Btrfs: incremental send, fix unnecessary hole writes for sparse files
authorFilipe Manana <fdmanana@suse.com>
Tue, 14 Feb 2017 17:56:32 +0000 (17:56 +0000)
committerFilipe Manana <fdmanana@suse.com>
Fri, 24 Feb 2017 00:39:21 +0000 (00:39 +0000)
When using the NO_HOLES feature, during an incremental send we often issue
write operations for holes when we should not, because that range is already
a hole in the destination snapshot. While that does not change the contents
of the file at the receiver, it avoids preservation of file holes, leading
to wasted disk space and extra IO during send/receive.

A couple examples where the holes are not preserved follows.

 $ mkfs.btrfs -O no-holes -f /dev/sdb
 $ mount /dev/sdb /mnt
 $ xfs_io -f -c "pwrite -S 0xaa 0 4K" /mnt/foo
 $ xfs_io -f -c "pwrite -S 0xaa 0 4K" -c "pwrite -S 0xbb 1028K 4K" /mnt/bar
 $ btrfs subvolume snapshot -r /mnt /mnt/snap1

 # Now add one new extent to our first test file, increasing its size and
 # leaving a 1Mb hole between the first extent and this new extent.
 $ xfs_io -c "pwrite -S 0xbb 1028K 4K" /mnt/foo

 # Now overwrite the last extent of our second test file.
 $ xfs_io -c "pwrite -S 0xcc 1028K 4K" /mnt/bar

 $ btrfs subvolume snapshot -r /mnt /mnt/snap2

 $ xfs_io -r -c "fiemap -v" /mnt/snap2/foo
 /mnt/snap2/foo:
 EXT: FILE-OFFSET      BLOCK-RANGE      TOTAL FLAGS
   0: [0..7]:          25088..25095         8 0x2000
   1: [8..2055]:       hole              2048
   2: [2056..2063]:    24576..24583         8 0x2001

 $ xfs_io -r -c "fiemap -v" /mnt/snap2/bar
 /mnt/snap2/bar:
 EXT: FILE-OFFSET      BLOCK-RANGE      TOTAL FLAGS
   0: [0..7]:          25096..25103         8 0x2000
   1: [8..2055]:       hole              2048
   2: [2056..2063]:    24584..24591         8 0x2001

  $ btrfs send /mnt/snap1 -f /tmp/1.snap
  $ btrfs send -p /mnt/snap1 /mnt/snap2 -f /tmp/2.snap

  $ umount /mnt
  # It's not relevant to enable no-holes in the new filesystem.
  $ mkfs.btrfs -O no-holes -f /dev/sdc
  $ mount /dev/sdc /mnt
  $ btrfs receive /mnt -f /tmp/1.snap
  $ btrfs receive /mnt -f /tmp/2.snap

  $ xfs_io -r -c "fiemap -v" /mnt/snap2/foo
  /mnt/snap2/foo:
  EXT: FILE-OFFSET      BLOCK-RANGE      TOTAL FLAGS
    0: [0..7]:          24576..24583         8 0x2000
    1: [8..2063]:       25624..27679      2056   0x1

  $ xfs_io -r -c "fiemap -v" /mnt/snap2/bar
  /mnt/snap2/bar:
  EXT: FILE-OFFSET      BLOCK-RANGE      TOTAL FLAGS
    0: [0..7]:          24584..24591         8 0x2000
    1: [8..2063]:       27680..29735      2056   0x1

The holes do not exist in the second filesystem and they were replaced
with extents filled with the byte 0x00, making each file take 1032Kb of
space instead of 8Kb.

So fix this by not issuing the write operations consisting of buffers
filled with the byte 0x00 when the destination snapshot already has a
hole for the respective range.

A test case for fstests will follow soon.

Signed-off-by: Filipe Manana <fdmanana@suse.com>
fs/btrfs/send.c

index 712922ea64d27824aaba81f75480753c702e174e..456c8901489b6c6b468901854bcdcbc53cb5cf13 100644 (file)
@@ -5306,6 +5306,81 @@ out:
        return ret;
 }
 
+static int range_is_hole_in_parent(struct send_ctx *sctx,
+                                  const u64 start,
+                                  const u64 end)
+{
+       struct btrfs_path *path;
+       struct btrfs_key key;
+       struct btrfs_root *root = sctx->parent_root;
+       u64 search_start = start;
+       int ret;
+
+       path = alloc_path_for_send();
+       if (!path)
+               return -ENOMEM;
+
+       key.objectid = sctx->cur_ino;
+       key.type = BTRFS_EXTENT_DATA_KEY;
+       key.offset = search_start;
+       ret = btrfs_search_slot(NULL, root, &key, path, 0, 0);
+       if (ret < 0)
+               goto out;
+       if (ret > 0 && path->slots[0] > 0)
+               path->slots[0]--;
+
+       while (search_start < end) {
+               struct extent_buffer *leaf = path->nodes[0];
+               int slot = path->slots[0];
+               struct btrfs_file_extent_item *fi;
+               u64 extent_end;
+
+               if (slot >= btrfs_header_nritems(leaf)) {
+                       ret = btrfs_next_leaf(root, path);
+                       if (ret < 0)
+                               goto out;
+                       else if (ret > 0)
+                               break;
+                       continue;
+               }
+
+               btrfs_item_key_to_cpu(leaf, &key, slot);
+               if (key.objectid < sctx->cur_ino ||
+                   key.type < BTRFS_EXTENT_DATA_KEY)
+                       goto next;
+               if (key.objectid > sctx->cur_ino ||
+                   key.type > BTRFS_EXTENT_DATA_KEY ||
+                   key.offset >= end)
+                       break;
+
+               fi = btrfs_item_ptr(leaf, slot, struct btrfs_file_extent_item);
+               if (btrfs_file_extent_type(leaf, fi) ==
+                   BTRFS_FILE_EXTENT_INLINE) {
+                       u64 size = btrfs_file_extent_inline_len(leaf, slot, fi);
+
+                       extent_end = ALIGN(key.offset + size,
+                                          root->fs_info->sectorsize);
+               } else {
+                       extent_end = key.offset +
+                               btrfs_file_extent_num_bytes(leaf, fi);
+               }
+               if (extent_end <= start)
+                       goto next;
+               if (btrfs_file_extent_disk_bytenr(leaf, fi) == 0) {
+                       search_start = extent_end;
+                       goto next;
+               }
+               ret = 0;
+               goto out;
+next:
+               path->slots[0]++;
+       }
+       ret = 1;
+out:
+       btrfs_free_path(path);
+       return ret;
+}
+
 static int maybe_send_hole(struct send_ctx *sctx, struct btrfs_path *path,
                           struct btrfs_key *key)
 {
@@ -5350,8 +5425,17 @@ static int maybe_send_hole(struct send_ctx *sctx, struct btrfs_path *path,
                        return ret;
        }
 
-       if (sctx->cur_inode_last_extent < key->offset)
-               ret = send_hole(sctx, key->offset);
+       if (sctx->cur_inode_last_extent < key->offset) {
+               ret = range_is_hole_in_parent(sctx,
+                                             sctx->cur_inode_last_extent,
+                                             key->offset);
+               if (ret < 0)
+                       return ret;
+               else if (ret == 0)
+                       ret = send_hole(sctx, key->offset);
+               else
+                       ret = 0;
+       }
        sctx->cur_inode_last_extent = extent_end;
        return ret;
 }