btrfs: optimize fill_holes() to merge a new hole with both adjacent items

fill_holes() currently merges a punched hole with either the previous
or the next file extent item, but never both in the same call.  When
holes are punched in a non-sequential order this leaves consecutive
hole items in the inode's subvolume tree that should have been collapsed
into a single one.

This is a minor metadata optimization that reduces the number of file
extent items when holes are punched in non-sequential order. While
having extra file extent items is harmless and has no functional
impact, reducing metadata overhead can benefit workloads with heavily
fragmented hole patterns.

For example:

  fallocate -p -o 4K  -l 4K ${FILE}
  fallocate -p -o 12K -l 4K ${FILE}
  fallocate -p -o 8K  -l 4K ${FILE}

After the third punch the [4K, 8K) and [12K, 16K) holes become
adjacent to the new [8K, 12K) hole, but fill_holes() merges only one
side and leaves two separate hole items ([4K, 12K) and [12K, 16K))
instead of the expected single [4K, 16K) hole item.

Fix this by checking both path->slots[0] - 1 and path->slots[0] in one
pass:

  - If only the previous slot is mergeable, extend it forward as
    before.
  - If only the next slot is mergeable, extend it backward and update
    its key offset as before.
  - If both are mergeable, extend the previous item to cover the new
    hole plus the next item, and remove the redundant next item with
    btrfs_del_items().

Because the merge path may now delete an item, switch the initial
btrfs_search_slot() call from a plain lookup (ins_len = 0) to a
search-for-deletion (ins_len = -1), so the leaf is prepared for a
possible item removal.

Note: This optimization only applies to filesystems without the
NO_HOLES feature enabled. Since NO_HOLES is now the default, this
primarily benefits older filesystems or those explicitly created with
NO_HOLES disabled.

Signed-off-by: Dave Chen <davechen@synology.com>
Reviewed-by: Filipe Manana <fdmanana@suse.com>
Signed-off-by: Filipe Manana <fdmanana@suse.com>
Signed-off-by: David Sterba <dsterba@suse.com>
This commit is contained in:
Dave Chen 2026-05-04 09:43:56 +08:00 committed by David Sterba
parent 83f7e52b7e
commit 022568f8d5

View File

@ -2072,6 +2072,10 @@ static int fill_holes(struct btrfs_trans_handle *trans,
struct btrfs_file_extent_item *fi;
struct extent_map *hole_em;
struct btrfs_key key;
int modify_slot = -1;
int del_slot = -1;
bool update_offset = false;
u64 num_bytes = 0;
int ret;
if (btrfs_fs_incompat(fs_info, NO_HOLES))
@ -2081,7 +2085,7 @@ static int fill_holes(struct btrfs_trans_handle *trans,
key.type = BTRFS_EXTENT_DATA_KEY;
key.offset = offset;
ret = btrfs_search_slot(trans, root, &key, path, 0, 1);
ret = btrfs_search_slot(trans, root, &key, path, -1, 1);
if (ret <= 0) {
/*
* We should have dropped this offset, so if we find it then
@ -2094,33 +2098,44 @@ static int fill_holes(struct btrfs_trans_handle *trans,
leaf = path->nodes[0];
if (hole_mergeable(inode, leaf, path->slots[0] - 1, offset, end)) {
u64 num_bytes;
path->slots[0]--;
fi = btrfs_item_ptr(leaf, path->slots[0],
fi = btrfs_item_ptr(leaf, path->slots[0] - 1,
struct btrfs_file_extent_item);
num_bytes = btrfs_file_extent_num_bytes(leaf, fi) +
end - offset;
btrfs_set_file_extent_num_bytes(leaf, fi, num_bytes);
btrfs_set_file_extent_ram_bytes(leaf, fi, num_bytes);
btrfs_set_file_extent_offset(leaf, fi, 0);
btrfs_set_file_extent_generation(leaf, fi, trans->transid);
goto out;
modify_slot = path->slots[0] - 1;
}
if (hole_mergeable(inode, leaf, path->slots[0], offset, end)) {
u64 num_bytes;
key.offset = offset;
btrfs_set_item_key_safe(trans, path, &key);
fi = btrfs_item_ptr(leaf, path->slots[0],
struct btrfs_file_extent_item);
num_bytes = btrfs_file_extent_num_bytes(leaf, fi) + end -
offset;
if (modify_slot != -1) {
num_bytes += btrfs_file_extent_num_bytes(leaf, fi);
del_slot = path->slots[0];
} else {
num_bytes = btrfs_file_extent_num_bytes(leaf, fi) +
end - offset;
modify_slot = path->slots[0];
update_offset = true;
}
}
if (modify_slot >= 0) {
fi = btrfs_item_ptr(leaf, modify_slot,
struct btrfs_file_extent_item);
btrfs_set_file_extent_num_bytes(leaf, fi, num_bytes);
btrfs_set_file_extent_ram_bytes(leaf, fi, num_bytes);
if (update_offset) {
key.offset = offset;
btrfs_set_item_key_safe(trans, path, &key);
}
btrfs_set_file_extent_offset(leaf, fi, 0);
btrfs_set_file_extent_generation(leaf, fi, trans->transid);
if (del_slot >= 0) {
ret = btrfs_del_items(trans, root, path, del_slot, 1);
if (ret) {
btrfs_abort_transaction(trans, ret);
btrfs_release_path(path);
return ret;
}
}
goto out;
}
btrfs_release_path(path);