[RFC PATCH v4 0/3] ext4: speed up fast commit on random writes

Daejun Park <[email protected]> Thu, 30 Jul 2026 09:37:15 +0900
Newsgroups org.kernel.vger.linux-ext4,org.kernel.vger.linux-kernel
Message-ID <20260730003715epcms2p54b7e3cc3409076cc9ee751f60951e167@epcms2p5>
ext4 fast commit tracks one coalesced [min,max] logical range per inode.  An
inode dirtied at several disjoint offsets between commits widens that span to
cover them all, and at commit time ext4_fc_snapshot_inode_data() walks the whole
span -- an ADD_RANGE per mapped segment, a DEL_RANGE per hole.  For scattered
writes that is hundreds to thousands of ranges for a handful of dirty regions,
overrunning EXT4_FC_SNAPSHOT_MAX_RANGES and falling back to a full commit (R=16
disjoint writes/fsync over a 1 GiB span: ~1095 ranges/commit, 76% fallback).

This tracks the actually-modified disjoint ranges and snapshots only those.

v3 [1] used a fixed-size array with a coalesce-on-overflow bound.  Andreas asked
to (a) reuse an existing dynamic structure instead of open-coding one, and (b)
find how many ranges it takes before tracking is worse than the fallback.  This
answers both; the array and the fc_max_ranges tunable are gone.

  1/3  track the disjoint ranges in a per-inode rbtree; merge-on-insert keeps
       the set disjoint.
  2/3  keep the first range inline in a union with the rbtree root, so a
       single-region inode allocates nothing; grow the tree only on the second
       disjoint range.
  3/3  cap the tracked ranges at EXT4_FC_SNAPSHOT_MAX_RANGES (2048); at the cap
       fall back to a full commit.

On (a): a private rbtree, reusing ext4's own machinery -- the extent status tree
already tracks per-inode lblk ranges as an rbtree, so it is idiomatic and adds no
dependency.  Not the shared es-tree: its shrinker reclaims mapped entries for
modified-but-not-yet-committed ranges (a probe evicted 448 under mild pressure),
which would lose modifications.  The node is a dedicated 32-byte struct with its
own slab cache, not extent_status -- which carries an unused 8-byte es_pblk and
lives in a SLAB_RECLAIM_ACCOUNT cache that would misaccount these non-reclaimable
nodes.  (A maple-tree version was also built and compared: behaviour-identical,
memory within ~15%, denser only at the range counts the cap already falls back
from -- so the dependency-free rbtree is the choice.)

On (b): the cap is EXT4_FC_SNAPSHOT_MAX_RANGES itself, reused directly.  Each
disjoint tracked range snapshots to at least one ADD_RANGE, so a tracked set
larger than that can never fit under the snapshot cap -- the fast commit would
overrun it and fall back regardless.  That count is exactly where tracking stops
paying off.  Verified: R=4096/fsync falls back there (fc_info "Insufficient
memory"), R=1024 tracks precisely.

The on-disk TLV format is unchanged; a GFP_ATOMIC failure under i_fc_lock falls
back to a full commit or coalesces into the inline range, so nothing is lost.

Tested on dev: crash recovery (power loss + replay + verify + e2fsck) 9600/9600
blocks clean at 512 dirty regions, and 200000/200000 clean with the cap firing
every round (2500 ranges/fsync); ext4/044 045, generic/456 482 pass (generic/455
fails identically unpatched); checkpatch --strict clean.  e2fsprogs needs no
update (format unchanged).

Still RFC -- feedback welcome.

Changes since v3 [1]:
  - fixed array + overflow-coalesce -> rbtree of disjoint ranges; tunable dropped.
  - first range kept inline (no alloc for single-region inodes).
  - bound memory by falling back at the snapshot cap.
  - documented why the shared es-tree can't be used (measured shrinker hazard).

[1] v3: https://lore.kernel.org/linux-ext4/20260722005922epcms2p566d8027c8d1dbb8d9c46e3d3b35b6fd0@epcms2p5/

Daejun Park (3):
  ext4: fast commit: track disjoint modified ranges in a private rbtree
  ext4: fast commit: keep the first range inline, allocate the tree
    lazily
  ext4: fast commit: bound the tracked range count

 fs/ext4/ext4.h        |  53 +++++-
 fs/ext4/fast_commit.c | 380 +++++++++++++++++++++++++++++++++++++-----
 fs/ext4/super.c       |   2 +
 3 files changed, 390 insertions(+), 45 deletions(-)


base-commit: 97e211ad1a34a5a979558a037619b9e633ca8620
-- 
2.43.0