|
| 1 | +From 85a047f20045580d3cfafaa7418f16a5a120cfa2 Mon Sep 17 00:00:00 2001 |
| 2 | +From: ain3sh <ainesh.chatterjee@gmail.com> |
| 3 | +Date: Thu, 2 Jul 2026 20:29:01 -0700 |
| 4 | +Subject: [PATCH] fuse: only invalidate STATX_BLOCKS on flush if pages were |
| 5 | + dirtied |
| 6 | + |
| 7 | +Since commit cf576c58b3a2 ("fuse: invalidate inode attr in writeback |
| 8 | +cache mode") fuse_flush() invalidates cached attributes so that |
| 9 | +st_blocks does not stay stale after buffered writes, since i_blocks is |
| 10 | +not maintained under writeback cache. Commit fa5eee57e33e ("fuse: |
| 11 | +selective attribute invalidation") narrowed this to STATX_BLOCKS. |
| 12 | + |
| 13 | +The invalidation is unconditional, however: every close(2) throws away |
| 14 | +cached STATX_BLOCKS even when the inode was only ever read. Because |
| 15 | +plain stat(2) requests the basic mask (which includes STATX_BLOCKS), |
| 16 | +any stat-after-close then forces a synchronous FUSE_GETATTR round trip |
| 17 | +that the attribute timeout was supposed to elide. Read-mostly |
| 18 | +workloads with open/read/close/stat patterns (build systems, git |
| 19 | +status style scanners) pay one GETATTR per file per cycle regardless |
| 20 | +of attr_timeout. |
| 21 | + |
| 22 | +i_blocks can only go stale through the page cache: every other write |
| 23 | +path (direct I/O, writethrough, copy_file_range, fallocate) already |
| 24 | +invalidates STATX_BLOCKS at write time via fuse_write_update_attr() |
| 25 | +(FUSE_STATX_MODSIZE includes STATX_BLOCKS). So track page-cache |
| 26 | +dirtying with a per-inode state bit, set in the buffered writeback |
| 27 | +write path and in fuse_page_mkwrite(), and only invalidate at flush |
| 28 | +time if the bit is set. The bit is tested-and-cleared, so a flush |
| 29 | +that writes back another fd's dirty pages still invalidates (the bit |
| 30 | +is per inode, not per file), and the motivating case of commit |
| 31 | +cf576c58b3a2 (du reading 0 blocks after a buffered write) is |
| 32 | +unaffected. |
| 33 | + |
| 34 | +On a FUSE filesystem with writeback cache and attr_timeout, a |
| 35 | +stat+open/read/close loop over 32 files x 32 iterations improves from |
| 36 | +18.7us to 8.5us per cycle, with GETATTR requests dropping from 1095 |
| 37 | +to 70. st_blocks remains correct after buffered and mmap writes. |
| 38 | +--- |
| 39 | + fs/fuse/file.c | 12 ++++++++++-- |
| 40 | + fs/fuse/fuse_i.h | 5 +++++ |
| 41 | + 2 files changed, 15 insertions(+), 2 deletions(-) |
| 42 | + |
| 43 | +diff --git a/fs/fuse/file.c b/fs/fuse/file.c |
| 44 | +index e052a0d44..086831ac5 100644 |
| 45 | +--- a/fs/fuse/file.c |
| 46 | ++++ b/fs/fuse/file.c |
| 47 | +@@ -510,9 +510,13 @@ static int fuse_flush(struct file *file, fl_owner_t id) |
| 48 | + inval_attr_out: |
| 49 | + /* |
| 50 | + * In memory i_blocks is not maintained by fuse, if writeback cache is |
| 51 | +- * enabled, i_blocks from cached attr may not be accurate. |
| 52 | ++ * enabled, i_blocks from cached attr may not be accurate. Only |
| 53 | ++ * invalidate if pages were dirtied through the page cache since the |
| 54 | ++ * last flush-time invalidation, so that read-only traffic does not |
| 55 | ++ * throw away the cached attributes on every close(2). |
| 56 | + */ |
| 57 | +- if (!err && fm->fc->writeback_cache) |
| 58 | ++ if (!err && fm->fc->writeback_cache && |
| 59 | ++ test_and_clear_bit(FUSE_I_BLOCKS_DIRTY, &get_fuse_inode(inode)->state)) |
| 60 | + fuse_invalidate_attr_mask(inode, STATX_BLOCKS); |
| 61 | + return err; |
| 62 | + } |
| 63 | +@@ -1550,6 +1554,9 @@ static ssize_t fuse_cache_write_iter(struct kiocb *iocb, struct iov_iter *from) |
| 64 | + &fuse_iomap_ops, |
| 65 | + &fuse_iomap_write_ops, |
| 66 | + file); |
| 67 | ++ if (written > 0) |
| 68 | ++ set_bit(FUSE_I_BLOCKS_DIRTY, |
| 69 | ++ &get_fuse_inode(inode)->state); |
| 70 | + } else { |
| 71 | + written = fuse_perform_write(iocb, from); |
| 72 | + } |
| 73 | +@@ -2392,6 +2399,7 @@ static vm_fault_t fuse_page_mkwrite(struct vm_fault *vmf) |
| 74 | + } |
| 75 | + |
| 76 | + folio_wait_writeback(folio); |
| 77 | ++ set_bit(FUSE_I_BLOCKS_DIRTY, &get_fuse_inode(inode)->state); |
| 78 | + return VM_FAULT_LOCKED; |
| 79 | + } |
| 80 | + |
| 81 | +diff --git a/fs/fuse/fuse_i.h b/fs/fuse/fuse_i.h |
| 82 | +index 85f738c53..fd763c749 100644 |
| 83 | +--- a/fs/fuse/fuse_i.h |
| 84 | ++++ b/fs/fuse/fuse_i.h |
| 85 | +@@ -257,6 +257,11 @@ enum { |
| 86 | + * or the fuse server has an exclusive "lease" on distributed fs |
| 87 | + */ |
| 88 | + FUSE_I_EXCLUSIVE, |
| 89 | ++ /* |
| 90 | ++ * Pages were dirtied through the page cache since the last flush-time |
| 91 | ++ * STATX_BLOCKS invalidation (writeback cache mode) |
| 92 | ++ */ |
| 93 | ++ FUSE_I_BLOCKS_DIRTY, |
| 94 | + }; |
| 95 | + |
| 96 | + struct fuse_conn; |
| 97 | +-- |
| 98 | +2.55.0 |
| 99 | + |
0 commit comments