]> git.ipfire.org Git - thirdparty/linux.git/commitdiff
btrfs: optimize fill_holes() to merge a new hole with both adjacent items
authorDave Chen <davechen@synology.com>
Mon, 4 May 2026 01:43:56 +0000 (09:43 +0800)
committerDavid Sterba <dsterba@suse.com>
Mon, 8 Jun 2026 13:53:31 +0000 (15:53 +0200)
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>
fs/btrfs/file.c

index a63d2ac676594dc28b74340b140318acdab5827f..43792d5a792c166c6c449b1cb1ac5810730d3394 100644 (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);