btrfs: prepare btrfs_punch_hole_lock_range() for large data folios
authorQu Wenruo <wqu@suse.com>
Thu, 27 Mar 2025 08:50:01 +0000 (19:20 +1030)
committerDavid Sterba <dsterba@suse.com>
Thu, 15 May 2025 12:30:40 +0000 (14:30 +0200)
The function btrfs_punch_hole_lock_range() needs to make sure there is
no other folio in the range, thus it goes with filemap_range_has_page(),
which works pretty fine.

But if we have large folios, under the following case
filemap_range_has_page() will always return true, forcing
btrfs_punch_hole_lock_range() to do a very time consuming busy loop:

        start                            end
        |                                |
  |//|//|//|//|  |  |  |  |  |  |  |  |//|//|
   \         /                         \   /
    Folio A                            Folio B

In the above case, folio A and B contain our start/end indexes, and there
are no other folios in the range.  Thus we do not need to retry inside
btrfs_punch_hole_lock_range().

To prepare for large data folios, introduce a helper,
check_range_has_page(), which will:

- Shrink the search range towards page boundaries
  If the rounded down end (exclusive, otherwise it can underflow when @end
  is inside the folio at file offset 0) is no larger than the rounded up
  start, it means the range contains no other pages other than the ones
  covering @start and @end.

  Can return false directly in that case.

- Grab all the folios inside the range

- Skip any large folios that cover the start and end indexes

- If any other folios are found return true

- Otherwise return false

This new helper is going to handle both large folios and regular ones.

Reviewed-by: Filipe Manana <fdmanana@suse.com>
Signed-off-by: Qu Wenruo <wqu@suse.com>
Signed-off-by: David Sterba <dsterba@suse.com>
fs/btrfs/file.c

index 5959d66ff6b420de4fd3422ec34c13512e6446b7..48fc6561c33aa3d4087a3a42902454563cd0d2df 100644 (file)
@@ -2157,11 +2157,29 @@ static int find_first_non_hole(struct btrfs_inode *inode, u64 *start, u64 *len)
        return ret;
 }
 
-static void btrfs_punch_hole_lock_range(struct inode *inode,
-                                       const u64 lockstart,
-                                       const u64 lockend,
-                                       struct extent_state **cached_state)
+/*
+ * Check if there is no folio in the range.
+ *
+ * We cannot utilize filemap_range_has_page() in a filemap with large folios
+ * as we can hit the following false positive:
+ *
+ *        start                            end
+ *        |                                |
+ *  |//|//|//|//|  |  |  |  |  |  |  |  |//|//|
+ *   \         /                         \   /
+ *    Folio A                            Folio B
+ *
+ * That large folio A and B cover the start and end indexes.
+ * In that case filemap_range_has_page() will always return true, but the above
+ * case is fine for btrfs_punch_hole_lock_range() usage.
+ *
+ * So here we only ensure that no other folios is in the range, excluding the
+ * head/tail large folio.
+ */
+static bool check_range_has_page(struct inode *inode, u64 start, u64 end)
 {
+       struct folio_batch fbatch;
+       bool ret = false;
        /*
         * For subpage case, if the range is not at page boundary, we could
         * have pages at the leading/tailing part of the range.
@@ -2172,17 +2190,45 @@ static void btrfs_punch_hole_lock_range(struct inode *inode,
         *
         * And do not decrease page_lockend right now, as it can be 0.
         */
-       const u64 page_lockstart = round_up(lockstart, PAGE_SIZE);
-       const u64 page_lockend = round_down(lockend + 1, PAGE_SIZE);
+       const u64 page_lockstart = round_up(start, PAGE_SIZE);
+       const u64 page_lockend = round_down(end + 1, PAGE_SIZE);
+       const pgoff_t start_index = page_lockstart >> PAGE_SHIFT;
+       const pgoff_t end_index = (page_lockend - 1) >> PAGE_SHIFT;
+       pgoff_t tmp = start_index;
+       int found_folios;
+
+       /* The same page or adjacent pages. */
+       if (page_lockend <= page_lockstart)
+               return false;
+
+       folio_batch_init(&fbatch);
+       found_folios = filemap_get_folios(inode->i_mapping, &tmp, end_index, &fbatch);
+       for (int i = 0; i < found_folios; i++) {
+               struct folio *folio = fbatch.folios[i];
 
+               /* A large folio begins before the start. Not a target. */
+               if (folio->index < start_index)
+                       continue;
+               /* A large folio extends beyond the end. Not a target. */
+               if (folio->index + folio_nr_pages(folio) > end_index)
+                       continue;
+               /* A folio doesn't cover the head/tail index. Found a target. */
+               ret = true;
+               break;
+       }
+       folio_batch_release(&fbatch);
+       return ret;
+}
+
+static void btrfs_punch_hole_lock_range(struct inode *inode,
+                                       const u64 lockstart, const u64 lockend,
+                                       struct extent_state **cached_state)
+{
        while (1) {
                truncate_pagecache_range(inode, lockstart, lockend);
 
                lock_extent(&BTRFS_I(inode)->io_tree, lockstart, lockend,
                            cached_state);
-               /* The same page or adjacent pages. */
-               if (page_lockend <= page_lockstart)
-                       break;
                /*
                 * We can't have ordered extents in the range, nor dirty/writeback
                 * pages, because we have locked the inode's VFS lock in exclusive
@@ -2193,8 +2239,7 @@ static void btrfs_punch_hole_lock_range(struct inode *inode,
                 * locking the range check if we have pages in the range, and if
                 * we do, unlock the range and retry.
                 */
-               if (!filemap_range_has_page(inode->i_mapping, page_lockstart,
-                                           page_lockend - 1))
+               if (!check_range_has_page(inode, lockstart, lockend))
                        break;
 
                unlock_extent(&BTRFS_I(inode)->io_tree, lockstart, lockend,