Patch 1 keeps the disjoint-range set in a fixed EXT4_FC_MAX_RANGES+1 array. Embedding that in every ext4_inode_info costs ~140 bytes on inodes that never use fast commit or only ever touch a single contiguous range. Keep the first range inline in i_fc_range and allocate the array only when a second disjoint range appears; free it when the inode is evicted. The tracking path runs under i_fc_lock, so the array is allocated with GFP_ATOMIC; on allocation failure we coalesce into the inline range -- i.e. the original single-range behaviour -- so memory pressure never forces a full commit. The per-inode fast-commit footprint drops to 20 bytes. Signed-off-by: Daejun Park --- fs/ext4/ext4.h | 23 +++++++++++----- fs/ext4/fast_commit.c | 64 ++++++++++++++++++++++++++++++++++++++++--- fs/ext4/super.c | 1 + 3 files changed, 77 insertions(+), 11 deletions(-) diff --git a/fs/ext4/ext4.h b/fs/ext4/ext4.h index 8e93d30766fd..d93cc52cd01e 100644 --- a/fs/ext4/ext4.h +++ b/fs/ext4/ext4.h @@ -1106,14 +1106,23 @@ struct ext4_inode_info { */ /* - * Disjoint lblk ranges modified in this fast commit. Tracking the + * Logical block ranges modified in this fast commit. Tracking the * actual modified ranges (instead of one coalesced [min,max]) avoids * snapshotting the whole spanned extent map for scattered allocations. - * Sorted by start, mutually disjoint. Bounded by EXT4_FC_MAX_RANGES; - * the extra slot is transient room used while inserting before an - * overflow merge. Protected by i_fc_lock. + * + * The first range is kept inline in i_fc_range, so the common case of a + * single contiguous dirty region needs no allocation. When a second + * disjoint range appears the inode is upgraded to the i_fc_ranges array + * (EXT4_FC_MAX_RANGES + 1 entries, sorted and mutually disjoint; the + * extra slot is transient room used while inserting before an overflow + * merge), allocated then and freed when the inode is evicted. If that + * allocation fails we fall back to coalescing into i_fc_range, i.e. the + * original single coalesced-range behaviour. i_fc_nr_ranges counts the + * valid ranges; while i_fc_ranges is NULL it is 0 or 1. Protected by + * i_fc_lock. */ - struct ext4_fc_lblk_range i_fc_ranges[EXT4_FC_MAX_RANGES + 1]; + struct ext4_fc_lblk_range i_fc_range; + struct ext4_fc_lblk_range *i_fc_ranges; unsigned int i_fc_nr_ranges; /* @@ -1135,8 +1144,8 @@ struct ext4_inode_info { spinlock_t i_raw_lock; /* protects updates to the raw inode */ /* - * Protect concurrent accesses on i_fc_ranges, i_fc_nr_ranges - * and inode's EXT4_FC_STATE_COMMITTING state bit. + * Protect concurrent accesses on i_fc_range, i_fc_ranges, + * i_fc_nr_ranges and inode's EXT4_FC_STATE_COMMITTING state bit. */ spinlock_t i_fc_lock; diff --git a/fs/ext4/fast_commit.c b/fs/ext4/fast_commit.c index 1ea3742a55b1..9d36365eae02 100644 --- a/fs/ext4/fast_commit.c +++ b/fs/ext4/fast_commit.c @@ -230,6 +230,7 @@ void ext4_fc_init_inode(struct inode *inode) struct ext4_inode_info *ei = EXT4_I(inode); ext4_fc_reset_inode(inode); + ei->i_fc_ranges = NULL; ext4_clear_inode_state(inode, EXT4_STATE_FC_COMMITTING); ext4_clear_inode_state(inode, EXT4_STATE_FC_REQUEUE); INIT_LIST_HEAD(&ei->i_fc_list); @@ -691,6 +692,8 @@ static int __track_range(handle_t *handle, struct inode *inode, void *arg, struct ext4_inode_info *ei = EXT4_I(inode); struct __track_range_args *__arg = (struct __track_range_args *)arg; + ext4_lblk_t start = __arg->start, end = __arg->end; + ext4_lblk_t s0, e0; if (inode->i_ino < EXT4_FIRST_INO(inode->i_sb)) { ext4_debug("Special inode %llu being modified\n", inode->i_ino); @@ -701,12 +704,61 @@ static int __track_range(handle_t *handle, struct inode *inode, void *arg, * A sub-block punch hole rounds up the start and down the end, passing * end == start - 1: no whole block changed, so there is nothing to * track. (ext4_fc_track_template has already reset the range set for a - * new transaction.) + * new transaction, so we need not do it here.) */ - if (__arg->end < __arg->start) + if (end < start) return 0; - ext4_fc_range_add(ei, __arg->start, __arg->end); + /* Already upgraded to the heap array: full multi-interval tracking. */ + if (ei->i_fc_ranges) { + ext4_fc_range_add(ei, start, end); + return 0; + } + + /* First range of this commit stays inline, no allocation needed. */ + if (ei->i_fc_nr_ranges == 0) { + ei->i_fc_range.start = start; + ei->i_fc_range.len = end - start + 1; + ei->i_fc_nr_ranges = 1; + return 0; + } + + /* One inline range so far. */ + s0 = ei->i_fc_range.start; + e0 = s0 + ei->i_fc_range.len - 1; + + /* Disjoint from it: try to upgrade to the array for exact tracking. */ + if (start > e0 + 1 || end + 1 < s0) { + struct ext4_fc_lblk_range *heap; + + /* + * GFP_ATOMIC: we hold i_fc_lock. __GFP_NOWARN: failure is not + * fatal -- we fall back to the single coalesced range below -- + * so it must not splat under memory pressure. + */ + heap = kmalloc_array(EXT4_FC_MAX_RANGES + 1, sizeof(*heap), + GFP_ATOMIC | __GFP_NOWARN); + if (heap) { + heap[0] = ei->i_fc_range; + ei->i_fc_ranges = heap; + ext4_fc_range_add(ei, start, end); + return 0; + } + /* + * Out of memory: fall back to the original single coalesced + * range by absorbing the gap below. This over-logs the spanned + * extents but stays a valid fast commit (no full-commit + * fallback), so there is nothing to mark ineligible. + */ + } + + /* Overlapping/adjacent, or array allocation failed: coalesce inline. */ + if (start < s0) + s0 = start; + if (end > e0) + e0 = end; + ei->i_fc_range.start = s0; + ei->i_fc_range.len = e0 - s0 + 1; return 0; } @@ -1178,7 +1230,11 @@ static int ext4_fc_snapshot_inode_data(struct inode *inode, *nr_rangesp = 0; return 0; } - memcpy(tracked, ei->i_fc_ranges, nr_tracked * sizeof(tracked[0])); + if (ei->i_fc_ranges) + memcpy(tracked, ei->i_fc_ranges, + nr_tracked * sizeof(tracked[0])); + else + tracked[0] = ei->i_fc_range; /* inline single-range mode */ ei->i_fc_nr_ranges = 0; spin_unlock(&ei->i_fc_lock); diff --git a/fs/ext4/super.c b/fs/ext4/super.c index f2c52cc74676..cfa7e6bec385 100644 --- a/fs/ext4/super.c +++ b/fs/ext4/super.c @@ -1455,6 +1455,7 @@ static void ext4_free_in_core_inode(struct inode *inode) pr_warn("%s: inode %llu still in fc list", __func__, inode->i_ino); } + kfree(EXT4_I(inode)->i_fc_ranges); kmem_cache_free(ext4_inode_cachep, EXT4_I(inode)); } -- 2.43.0