Re: [PATCH] btrfs: replace writeback inhibition xarray with a fixed inline buffer

Sun YangKai <[email protected]>
Newsgroups org.kernel.vger.linux-btrfs
Message-ID <[email protected]>

On 2026/6/25 01:57, Leo Martins wrote:
> Commit f9a48549a15a ("btrfs: inhibit extent buffer writeback to prevent
> COW amplification") tracks the extent buffers a transaction handle has
> inhibited in a per-handle xarray. Keying the tracking to the transaction
> handle is correct, but using an xarray for it causes two problems in
> production.
> 
> First, a write_iops regression. Every COW calls
> btrfs_inhibit_eb_writeback() from btrfs_force_cow_block() and
> should_cow_block(), which does an xa_store() keyed by eb->start. The
> kernel test robot reported a 22.6% fio.write_iops regression on a
> single-task 4k randwrite workload (ftruncate ioengine, buffered IO) on
> btrfs. The cost is the per-COW xarray store done on every COW'd block.
> Replacing it with a non-allocating fixed buffer recovers the lost
> throughput, and that buffer does more per-COW bookkeeping yet still
> recovers, so the cost is the xarray operation itself rather than the
> extra tracking work.
> 
> Second, an unbounded cleanup walk. btrfs_uninhibit_all_eb_writeback()
> iterates every eb the handle inhibited with xa_for_each(). A single
> handle that COWs a very large number of blocks (inode eviction, or
> truncate of a file with many extents, where btrfs_truncate_inode_items()
> loops over many search_again descents under one handle) makes that walk
> arbitrarily long. It runs in __btrfs_end_transaction() before
> num_writers is dropped, so it blocks the committing thread; this shows up
> as multi-second stalls and RCU stall reports.
> 
> Replace the xarray with a fixed inline array on btrfs_trans_handle,
> managed with a CLOCK (second-chance) eviction policy. Inhibiting a buffer
> becomes an array append with no allocation and no tree walk, and the
> end-of-handle cleanup is bounded by the array size.
> 
> The set that actually needs protection is the working set the handle
> revisits across search_again descents, the search path frontier, which is
> on the order of the tree height. It is not every block the handle ever
> COWs. should_cow_block() re-inhibiting an already tracked buffer marks it
> referenced, so revisited buffers survive eviction while write-once buffers
> are reclaimed first. A small fixed buffer is therefore enough where a
> non-evicting array would either overflow or have to grow without bound.
> BTRFS_INHIBITED_EBS_SLOTS is 8 and the reference bits pack into a u32.
> 
> The CLOCK eviction is what justifies the extra complexity over a plain
> non-evicting array. On a workload built to stress amplification, removing
> 16 heavily fragmented 64 MiB files in one transaction while background
> writeback keeps writing out in-use metadata, counting re-COW events (a
> buffer already COWed in the running transaction, written back, then COWed
> again) against first-COW events and summing across the eviction (n=5),
> re-COWs outnumber first COWs about 6.1 to 1 with no inhibition, 3.8 to 1
> with a non-evicting array of 32 slots, 1.6 to 1 with this 8 slot CLOCK
> array, and 1.4 to 1 with the reverted unbounded xarray. The non-evicting
> array fills with write-once buffers and stops covering the buffers the
> handle keeps revisiting, so even at four times the slots it leaves most of
> the amplification. CLOCK evicts the cold buffers and keeps the revisited
> ones, which recovers almost all of the unbounded benefit. The eviction
> policy, not the buffer size, is what closes the gap.
> 
> eb->writeback_inhibitors and the WB_SYNC_ALL bypass in
> lock_extent_buffer_for_io() are unchanged, so fsync and commit behavior
> are unaffected. A reference is taken on each tracked buffer so it cannot
> be freed while the array points at it; eviction drops that reference and
> the inhibitor count.
> 
> Fixes: f9a48549a15a ("btrfs: inhibit extent buffer writeback to prevent COW amplification")
> Reported-by: kernel test robot <[email protected]>
> Closes: https://lore.kernel.org/oe-lkp/[email protected]
> Signed-off-by: Leo Martins <[email protected]>

Reviewed-by: Sun YangKai <[email protected]>

Thanks for your great work. The overall direction looks nice and the 
design is clean. I've got some non‑blocking suggestions, though.


> ---
>   fs/btrfs/extent_io.c   | 78 +++++++++++++++++++++++++-----------------
>   fs/btrfs/transaction.c |  2 --
>   fs/btrfs/transaction.h | 15 ++++++--
>   3 files changed, 59 insertions(+), 36 deletions(-)
> 
> diff --git a/fs/btrfs/extent_io.c b/fs/btrfs/extent_io.c
> index 9d7ca80477fd..1845a617fb10 100644
> --- a/fs/btrfs/extent_io.c
> +++ b/fs/btrfs/extent_io.c
> @@ -2988,41 +2988,58 @@ static inline void btrfs_release_extent_buffer(struct extent_buffer *eb)
>    * @trans:  transaction handle that will own the inhibitor
>    * @eb:      extent buffer to inhibit writeback on
>    *
> - * Attempt to track this extent buffer in the transaction's inhibited set.  If
> - * memory allocation fails, the buffer is simply not tracked. It may be written
> - * back and need re-COW, which is the original behavior.  This is acceptable
> - * since inhibiting writeback is an optimization.
> + * Attempt to track this extent buffer in the transaction's inhibited set.  When
> + * the set is full the coldest tracked buffer is evicted instead.  An untracked
> + * buffer may be written back and need re-COW, which is the original behavior.
> + * This is acceptable since inhibiting writeback is an optimization.
>    */
> -void btrfs_inhibit_eb_writeback(struct btrfs_trans_handle *trans, struct extent_buffer *eb)
> +void btrfs_inhibit_eb_writeback(struct btrfs_trans_handle *trans,
> +				struct extent_buffer *eb)
>   {
> -	unsigned long index = eb->start >> trans->fs_info->nodesize_bits;
> -	void *old;
> +	u32 slot;
>   
>   	lockdep_assert_held(&eb->lock);
> -	/* Check if already inhibited by this handle. */
> -	old = xa_load(&trans->writeback_inhibited_ebs, index);
> -	if (old == eb)
> -		return;
> -
> -	/* Take reference for the xarray entry. */
> -	refcount_inc(&eb->refs);
>   
> -	old = xa_store(&trans->writeback_inhibited_ebs, index, eb, GFP_NOFS);
> -	if (xa_is_err(old)) {
> -		/* Allocation failed, just skip inhibiting this buffer. */
> -		free_extent_buffer(eb);
> -		return;
> +	/* Already tracked: set its reference bit (second chance) and return. */
> +	for (u32 i = 0; i < trans->nr_inhibited_ebs; i++) {
> +		if (trans->inhibited_ebs[i] == eb) {
> +			trans->inhibited_ebs_referenced |= 1U << i;
> +			return;
> +		}
>   	}
>   
> -	/* Handle replacement of different eb at same index. */
> -	if (old && old != eb) {
> -		struct extent_buffer *old_eb = old;
> +	if (trans->nr_inhibited_ebs < BTRFS_INHIBITED_EBS_SLOTS) {
> +		slot = trans->nr_inhibited_ebs++;
> +	} else {
> +		/*
> +		 * Full: advance the CLOCK hand, clearing reference bits until a
> +		 * slot without one is found and evicted. Clearing a bit per step
> +		 * bounds this to BTRFS_INHIBITED_EBS_SLOTS iterations.
> +		 */
> +		while (trans->inhibited_ebs_referenced &
> +		       (1U << trans->inhibited_ebs_hand)) {
> +			trans->inhibited_ebs_referenced &=
> +				~(1U << trans->inhibited_ebs_hand);
> +			trans->inhibited_ebs_hand =
> +				(trans->inhibited_ebs_hand + 1) %
> +				BTRFS_INHIBITED_EBS_SLOTS;

I think we can using overflowing add with ` & (SLOTS - 1)` as the index 
as long as SLOTS is power of 2, instead of `% SLOTS`. And the higher 
bits can parsed as number of "rounds" if we keep them, and I'm not sure 
if the extra info is useful for any purpose.

> +		}
> +		slot = trans->inhibited_ebs_hand;
> +		trans->inhibited_ebs_hand =
> +			(trans->inhibited_ebs_hand + 1) % BTRFS_INHIBITED_EBS_SLOTS;

the same math here.

>   
> -		atomic_dec(&old_eb->writeback_inhibitors);
> -		free_extent_buffer(old_eb);
> +		atomic_dec(&trans->inhibited_ebs[slot]->writeback_inhibitors);
> +		free_extent_buffer(trans->inhibited_ebs[slot]);
>   	}

And personally I'd love to wrap this part of logic into a helper 
function like

static inline u8 btrfs_inhibit_prepare_slot(struct btrfs_trans_handle 
*trans) {...}

Or something with a better name.

>   
> +	/*
> +	 * Pin the eb while the array holds a raw pointer to it; the counter is
> +	 * what lock_extent_buffer_for_io() checks.
> +	 */
> +	refcount_inc(&eb->refs);
>   	atomic_inc(&eb->writeback_inhibitors);
> +	trans->inhibited_ebs[slot] = eb;
> +	trans->inhibited_ebs_referenced |= 1U << slot;
>   }
>   
>   /*
> @@ -3030,14 +3047,13 @@ void btrfs_inhibit_eb_writeback(struct btrfs_trans_handle *trans, struct extent_
>    */
>   void btrfs_uninhibit_all_eb_writeback(struct btrfs_trans_handle *trans)
>   {
> -	struct extent_buffer *eb;
> -	unsigned long index;
> -
> -	xa_for_each(&trans->writeback_inhibited_ebs, index, eb) {
> -		atomic_dec(&eb->writeback_inhibitors);
> -		free_extent_buffer(eb);
> +	for (u32 i = 0; i < trans->nr_inhibited_ebs; i++) {
> +		atomic_dec(&trans->inhibited_ebs[i]->writeback_inhibitors);
> +		free_extent_buffer(trans->inhibited_ebs[i]);
>   	}
> -	xa_destroy(&trans->writeback_inhibited_ebs);
> +	trans->nr_inhibited_ebs = 0;
> +	trans->inhibited_ebs_referenced = 0;
> +	trans->inhibited_ebs_hand = 0;
>   }
>   
>   static struct extent_buffer *__alloc_extent_buffer(struct btrfs_fs_info *fs_info,
> diff --git a/fs/btrfs/transaction.c b/fs/btrfs/transaction.c
> index 4358f4b63057..b4b8d587effb 100644
> --- a/fs/btrfs/transaction.c
> +++ b/fs/btrfs/transaction.c
> @@ -697,8 +697,6 @@ start_transaction(struct btrfs_root *root, unsigned int num_items,
>   		goto alloc_fail;
>   	}
>   
> -	xa_init(&h->writeback_inhibited_ebs);
> -
>   	/*
>   	 * If we are JOIN_NOLOCK we're already committing a transaction and
>   	 * waiting on this guy, so we don't need to do the sb_start_intwrite
> diff --git a/fs/btrfs/transaction.h b/fs/btrfs/transaction.h
> index 7d70fe486758..8f628d79d4f0 100644
> --- a/fs/btrfs/transaction.h
> +++ b/fs/btrfs/transaction.h
> @@ -12,7 +12,6 @@
>   #include <linux/time64.h>
>   #include <linux/mutex.h>
>   #include <linux/wait.h>
> -#include <linux/xarray.h>
>   #include "btrfs_inode.h"
>   #include "delayed-ref.h"
>   
> @@ -23,6 +22,7 @@ struct btrfs_fs_info;
>   struct btrfs_root_item;
>   struct btrfs_root;
>   struct btrfs_path;
> +struct extent_buffer;
>   
>   /*
>    * Signal that a direct IO write is in progress, to avoid deadlock for sync
> @@ -136,6 +136,12 @@ enum {
>   
>   #define TRANS_EXTWRITERS	(__TRANS_START | __TRANS_ATTACH)
>   
> +/*
> + * Must stay <= 32: the CLOCK reference bits pack into the u32
> + * inhibited_ebs_referenced.
> + */
> +#define BTRFS_INHIBITED_EBS_SLOTS	8
> +
>   struct btrfs_trans_handle {
>   	u64 transid;
>   	u64 bytes_reserved;
> @@ -163,8 +169,11 @@ struct btrfs_trans_handle {
>   	struct btrfs_fs_info *fs_info;
>   	struct list_head new_bgs;
>   	struct btrfs_block_rsv delayed_rsv;
> -	/* Extent buffers with writeback inhibited by this handle. */
> -	struct xarray writeback_inhibited_ebs;
> +	/* Extent buffers this handle has inhibited writeback on. */
> +	struct extent_buffer *inhibited_ebs[BTRFS_INHIBITED_EBS_SLOTS];

This takes 64 bytes, which is necessary, fine.

> +	u32 inhibited_ebs_referenced;	/* CLOCK reference bit per slot */
> +	u32 nr_inhibited_ebs;

Given the 8-slot limit for inhibited_ebs (4 bits (8) for capacity, 8 
bits for bitmap), using u8 for these two fields is a clear and 
straightforward choice IMO. Even if we later extend the limit to 16, 
only the bitmap field (inhibited_ebs_referenced) would need to be 
widened to u16.

Thanks,
Sun YangKai

> +	u8 inhibited_ebs_hand;		/* CLOCK hand */
>   };
>   
>   /*
lmpx.com only provides a reader for public news (NNTP) servers. It is not affiliated with the servers or forums shown here and is not responsible for the content of articles, which is written by their respective authors.