[RFC PATCH v4 0/3] ext4: speed up fast commit on random writes
From: Daejun Park
Date: Wed Jul 29 2026 - 20:37:25 EST
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