From mboxrd@z Thu Jan  1 00:00:00 1970
Received: from mail-yx1-f66.google.com (mail-yx1-f66.google.com [74.125.224.66])
	(using TLSv1.2 with cipher ECDHE-RSA-AES128-GCM-SHA256 (128/128 bits))
	(No client certificate requested)
	by smtp.subspace.kernel.org (Postfix) with ESMTPS id E6C3B3ACA7B
	for <linux-btrfs@vger.kernel.org>; Tue, 24 Feb 2026 19:22:51 +0000 (UTC)
Authentication-Results: smtp.subspace.kernel.org; arc=none smtp.client-ip=74.125.224.66
ARC-Seal:i=1; a=rsa-sha256; d=subspace.kernel.org; s=arc-20240116;
	t=1771960976; cv=none; b=XVW2xMfkoxvwXlH3k3WJTIZyB47vdlqaVKcoAVPSjDYe9u2Hm2wf0pUW2nKZFJtGoyrqmI2ogIUyy0rCHg0r0meUjX4Z8tugpGVZSSshYFdd5wQ76i+OgZSztDf86z2MlkcWA7ka4YcpHKA3jLm2otFFhKT43OUdd3JNiKwTNF0=
ARC-Message-Signature:i=1; a=rsa-sha256; d=subspace.kernel.org;
	s=arc-20240116; t=1771960976; c=relaxed/simple;
	bh=DgpYht4fUNuDZAz5MFfPHu9mwJHcXotoLQpfQcChW7U=;
	h=From:To:Subject:Date:Message-ID:MIME-Version; b=ccQ8OZP5q3ZsqyrFNBESrsVg9+zR0XTSaw7wplX3oVA4YfB4GQeRSIL/haymYXSk45e4hsxW7JdDoyOM0xLzooiDzHqtFTH6Xr2ujUSWY9DUA8JmuB8X3wz1uM+s2TFW+MH9+jNZX9CXT/0OQEUVjMOG6CZSJxaaonPt5PtxoVk=
ARC-Authentication-Results:i=1; smtp.subspace.kernel.org; dmarc=pass (p=none dis=none) header.from=gmail.com; spf=pass smtp.mailfrom=gmail.com; dkim=pass (2048-bit key) header.d=gmail.com header.i=@gmail.com header.b=ebL5qerw; arc=none smtp.client-ip=74.125.224.66
Authentication-Results: smtp.subspace.kernel.org; dmarc=pass (p=none dis=none) header.from=gmail.com
Authentication-Results: smtp.subspace.kernel.org; spf=pass smtp.mailfrom=gmail.com
Authentication-Results: smtp.subspace.kernel.org;
	dkim=pass (2048-bit key) header.d=gmail.com header.i=@gmail.com header.b="ebL5qerw"
Received: by mail-yx1-f66.google.com with SMTP id 956f58d0204a3-64aedd812baso5424653d50.3
        for <linux-btrfs@vger.kernel.org>; Tue, 24 Feb 2026 11:22:51 -0800 (PST)
DKIM-Signature: v=1; a=rsa-sha256; c=relaxed/relaxed;
        d=gmail.com; s=20230601; t=1771960970; x=1772565770; darn=vger.kernel.org;
        h=content-transfer-encoding:mime-version:message-id:date:subject:to
         :from:from:to:cc:subject:date:message-id:reply-to;
        bh=VANXF4HiSIEvY4DxHkIXWSh8Y6I7Ix6T14TADgiqlJE=;
        b=ebL5qerwjg78pFZMXE/T5J+XC1SFScjTkZULJ5whsOL+YruCaN1DTDDIyEwZNCk6Y7
         OVOL/LwXnUbXQOPuOTTgNrDDTM54JGkBzQnTZHMSxEnJbBScVyDC4bqWbpShloxYwM6U
         HZRcb5miaMrmCqb4QvRY98WxJ3qM4yIHnPQtnL7KXcQYlDfQtnz5WGYLtsplZ9cl7AVN
         0GxE2PAslJp3rCQz9JxNkmsh8XK+9PLSOcFMzaGok3ZvZnFcVKneSG69m79v6zkaMPMR
         xpuH88DJivfSappL77MfNhJgsMTyyMTv0NpdKaTeiyyUu4zZPKA7q7DSZOZHiv68ujwr
         ptMw==
X-Google-DKIM-Signature: v=1; a=rsa-sha256; c=relaxed/relaxed;
        d=1e100.net; s=20230601; t=1771960970; x=1772565770;
        h=content-transfer-encoding:mime-version:message-id:date:subject:to
         :from:x-gm-gg:x-gm-message-state:from:to:cc:subject:date:message-id
         :reply-to;
        bh=VANXF4HiSIEvY4DxHkIXWSh8Y6I7Ix6T14TADgiqlJE=;
        b=LMO48d3w1eWXueCQvQpXeO1ViHKC03l48p6DosqDZMPk6iOVVaPAB/k5xGGHW1/OXP
         vhe2GUU5c/GNcIvUACzV5qXp2ONwVpjQx5nFL5vs39568y4a8nYczFVuugxbK/6lOhk+
         qxdJjsfJk64t/E93Qs5NA4NEivSDKjnhZG6Ua23rPaUDCu52pHxkWoTqU/Q00qNeRN2R
         K3PtVr/I7xGRmdXo3xjiwR0hgYwfSK1NY0VptrhR+pKRQIH0c641DKRmm/2gTJL+9Tg0
         aw+6vJco5MULIryDd/MEFk2pUtIlHaUG/cqVKToG3zbPDYoQ5FXNDsS41YxPx7BvEPMz
         yJ5g==
X-Gm-Message-State: AOJu0YwcT01D3s5juqaeQg+j4OtyAdoeO9Xm9cBGar60j+hoMP1X8Mql
	WMOKe8hxh2FmVB4I7NSuLinpFil0H0mqB25+NMNqwagzG7tKTwPYYX/27nv14phvLKQ=
X-Gm-Gg: ATEYQzw5ZQwFgP/ndQWD/ixZ6xRTtgkrI7i0PSrOxJO9kNHG0pDAVGeWrW1eAsaMGMM
	ITcVKqs+Q5oXUCgXmPQ2asjusxR/Y/D3btO/dltM31Dm5BTj2VGKcwuy1cGj3UGy2MOZ55924nE
	2TG04EOE44jX3i2hAZZ9ll1hFe/DPgIYNmAGDq2317GlWSYSq73rRi11rj8RUai+FoCqL0I4ynx
	pGls0aT1YlSzNYxEqn4HHc0JOAwAPqM/azQYymJ5KUUWgz+E06dDncPuD1bwozFLytCcSPbWpch
	H366TPqfpJAkfd6WEFpwwRQ+pOrSjr0qnmVBTPQv4mU7vscyHa0RSN7wxU8Cmk6kSil0J/irGpe
	3u7z8f3vdaFpcNetHeDGz9QGeBDfkug/HIqaZ8I0e0s7xVP0cmy84bNfqq/bcu+oEIwRJEwLRV9
	v4/+8UCGpScNErAwP/5Q==
X-Received: by 2002:a05:690c:92:b0:798:25d:a4b2 with SMTP id 00721157ae682-79828f07424mr126145247b3.19.1771960970298;
        Tue, 24 Feb 2026 11:22:50 -0800 (PST)
Received: from localhost ([2a03:2880:25ff:56::])
        by smtp.gmail.com with ESMTPSA id 00721157ae682-7982dd81f97sm47052907b3.34.2026.02.24.11.22.49
        (version=TLS1_3 cipher=TLS_AES_256_GCM_SHA384 bits=256/256);
        Tue, 24 Feb 2026 11:22:49 -0800 (PST)
From: Leo Martins <loemra.dev@gmail.com>
To: linux-btrfs@vger.kernel.org,
	kernel-team@fb.com
Subject: [PATCH v3 0/3] btrfs: fix COW amplification under memory pressure
Date: Tue, 24 Feb 2026 11:22:31 -0800
Message-ID: <cover.1771884128.git.loemra.dev@gmail.com>
X-Mailer: git-send-email 2.47.3
Precedence: bulk
X-Mailing-List: linux-btrfs@vger.kernel.org
List-Id: <linux-btrfs.vger.kernel.org>
List-Subscribe: <mailto:linux-btrfs+subscribe@vger.kernel.org>
List-Unsubscribe: <mailto:linux-btrfs+unsubscribe@vger.kernel.org>
MIME-Version: 1.0
Content-Transfer-Encoding: 8bit

I've been investigating a pattern where COW amplification under memory
pressure exhausts the transaction block reserve, leading to spurious
ENOSPCs on filesystems with plenty of unallocated space.

The pattern is:

 1. btrfs_search_slot() begins tree traversal with cow=1
 2. Node at level N needs COW (old generation or WRITTEN flag set)
 3. btrfs_cow_block() allocates new block, copies data, updates
    parent pointer
 4. Traversal hits a condition requiring restart (node not cached,
    lock contention, need higher write_lock_level)
 5. btrfs_release_path() releases all locks and references
 6. Memory pressure triggers writeback on the COW'd block
 7. lock_extent_buffer_for_io() clears EXTENT_BUFFER_DIRTY and sets
    BTRFS_HEADER_FLAG_WRITTEN
 8. goto again - traversal restarts from root
 9. Traversal reaches the same tree node
 10. should_cow_block() sees WRITTEN flag, returns true
 11. btrfs_cow_block() allocates yet another new block, copies data,
     updates parent pointer again, consuming another reservation
 12. Steps 4-11 repeat under sustained memory pressure

This series fixes the problem with two complementary approaches:

Patch 1 implements Filipe's suggestion of overwriting in place. When
should_cow_block() encounters a WRITTEN buffer whose generation matches
the current transaction, it re-dirties the buffer and returns false
instead of requesting a COW. This is crash-safe because the committed
superblock does not reference buffers allocated in the current
transaction. Log trees, zoned devices, FORCE_COW, relocation, and
buffers mid-writeback are excluded. Beyond improving the amplification bug,
this is a general optimization for the entire transaction lifetime: any
time writeback runs during a transaction, revisiting the same path no
longer triggers unnecessary COW, reducing extent allocation overhead,
memory copies, and space usage per transaction.

Patch 2 inhibits writeback on COW'd buffers for the lifetime of the
transaction handle. This prevents WRITTEN from being set while a
handle holds a reference to the buffer, avoiding unnecessary re-COW
at its source. Only WB_SYNC_NONE (background/periodic flusher
writeback) is inhibited. WB_SYNC_ALL (data-integrity writeback from
btrfs_sync_log() and btrfs_write_and_wait_transaction()) always
proceeds, which is required for correctness in the fsync path where
log tree blocks must be written while the inhibiting handle is still
active.

Both approaches are independently useful. The overwrite path is a
general optimization that eliminates unnecessary COW across the entire
transaction, not just within a single handle. Writeback inhibition
prevents the problem from occurring in the first place and is the
only fix for log trees and zoned devices where overwrite does not
apply. Together they provide defense in depth against COW
amplification.

Patch 3 adds a tracepoint for tracking search slot restarts.

Note: Boris's AS_KERNEL_FILE changes prevent cgroup-scoped reclaim
from targeting extent buffer pages, making this harder to trigger via
per-cgroup memory limits. I was able to reproduce the amplification
using global memory pressure (drop_caches, root cgroup memory.reclaim,
memory hog) with concurrent filesystem operations.

Benchmark: concurrent filesystem operations under aggressive global
memory pressure. COW counts measured via bpftrace.

  Approach                   Max COW count   Num operations
  -------                    -------------   --------------
  Baseline                              35   -
  Overwrite only                        19   ~same
  Writeback inhibition only              5   ~2x
  Combined (this series)                 4   ~2x

Changes since v2:

Patch 1:
 - Add smp_mb__before_atomic() before FORCE_COW check (Filipe, Sun YangKai)
 - Hoist WRITEBACK, RELOC, FORCE_COW checks before WRITTEN block (Sun YangKai)
 - Update dirty_pages comment for qgroup_account_snapshot() (Filipe)
 - Relax btrfs_mark_buffer_dirty() lockdep assertion to accept read locks
 - Use btrfs_mark_buffer_dirty() instead of set_extent_buffer_dirty()
 - Remove folio_test_dirty() debug assertion (concurrent reader race)

Patch 2:
 - Fix comment style (Filipe)

Patch 3:
 - Replace counters with per-restart-site tracepoint (Filipe)

Changes since v1:

The v1 patch used a per-btrfs_search_slot() xarray to track COW'd
buffers. Filipe pointed out this was too complex and too narrow in
scope, and suggested overwriting in place instead. Qu raised the
concern that other btrfs_cow_block() callers outside of search_slot
would not be covered. Boris suggested transaction-handle-level scope
as an alternative.

v2 implemented both overwrite-in-place and writeback inhibition at
the transaction handle level. The per-search-slot xarray was replaced
by a per-transaction-handle xarray, which covers all
btrfs_force_cow_block() callers.

Leo Martins (3):
  btrfs: skip COW for written extent buffers allocated in current
    transaction
  btrfs: inhibit extent buffer writeback to prevent COW amplification
  btrfs: add tracepoint for search slot restart tracking

 fs/btrfs/ctree.c             | 70 +++++++++++++++++++++++++++++++-----
 fs/btrfs/disk-io.c           |  2 +-
 fs/btrfs/extent_io.c         | 67 +++++++++++++++++++++++++++++++---
 fs/btrfs/extent_io.h         |  6 ++++
 fs/btrfs/transaction.c       | 19 ++++++++++
 fs/btrfs/transaction.h       |  3 ++
 include/trace/events/btrfs.h | 24 +++++++++++++
 7 files changed, 176 insertions(+), 15 deletions(-)

-- 
2.47.3


From mboxrd@z Thu Jan  1 00:00:00 1970
Received: from mail-yx1-f68.google.com (mail-yx1-f68.google.com [74.125.224.68])
	(using TLSv1.2 with cipher ECDHE-RSA-AES128-GCM-SHA256 (128/128 bits))
	(No client certificate requested)
	by smtp.subspace.kernel.org (Postfix) with ESMTPS id 193C43ACA6E
	for <linux-btrfs@vger.kernel.org>; Tue, 24 Feb 2026 19:22:52 +0000 (UTC)
Authentication-Results: smtp.subspace.kernel.org; arc=none smtp.client-ip=74.125.224.68
ARC-Seal:i=1; a=rsa-sha256; d=subspace.kernel.org; s=arc-20240116;
	t=1771960977; cv=none; b=NylkVSfl6SgAQfNytaLQWaons5MEXnAkK/Nscqx3qSWcepjWP+XHulKKce6x0ADOEHLrvBG/0D5xJCqQhgW0ekuqlBCmqbzFD7QxDeC+Fv6nER0xSyxd5me/gesBGt2CVe2hKxn0bpQWKbg4omWBklkWYJ1RBXHeyqDmJAKT4rY=
ARC-Message-Signature:i=1; a=rsa-sha256; d=subspace.kernel.org;
	s=arc-20240116; t=1771960977; c=relaxed/simple;
	bh=gufBFGSI6wh5Vo6wxdgNEOu9rokSY9frOYuJC2UOYlM=;
	h=From:To:Subject:Date:Message-ID:In-Reply-To:References:
	 MIME-Version; b=nXKUcVO19vEpw92quQthmBPz/VRlWGcXOpgeEGztT4hRr7IBJBjy7wq4kdlz56GrP8sCYe10phSRb6xVWXYJumVOLo7L5dlGeVw8pUqCpc80M8zr6FD5ZsDVBpKM0qU2d4OuCCQcBXsDT1ezpgs6w9QTY6/kFe6djIGr6CFGdPA=
ARC-Authentication-Results:i=1; smtp.subspace.kernel.org; dmarc=pass (p=none dis=none) header.from=gmail.com; spf=pass smtp.mailfrom=gmail.com; dkim=pass (2048-bit key) header.d=gmail.com header.i=@gmail.com header.b=EBATxj2m; arc=none smtp.client-ip=74.125.224.68
Authentication-Results: smtp.subspace.kernel.org; dmarc=pass (p=none dis=none) header.from=gmail.com
Authentication-Results: smtp.subspace.kernel.org; spf=pass smtp.mailfrom=gmail.com
Authentication-Results: smtp.subspace.kernel.org;
	dkim=pass (2048-bit key) header.d=gmail.com header.i=@gmail.com header.b="EBATxj2m"
Received: by mail-yx1-f68.google.com with SMTP id 956f58d0204a3-649278a69c5so5924308d50.3
        for <linux-btrfs@vger.kernel.org>; Tue, 24 Feb 2026 11:22:52 -0800 (PST)
DKIM-Signature: v=1; a=rsa-sha256; c=relaxed/relaxed;
        d=gmail.com; s=20230601; t=1771960971; x=1772565771; darn=vger.kernel.org;
        h=content-transfer-encoding:mime-version:references:in-reply-to
         :message-id:date:subject:to:from:from:to:cc:subject:date:message-id
         :reply-to;
        bh=oD++iBj9IxDjwbbBgZSLqBIEox6IDYIGv8zNghuEH1I=;
        b=EBATxj2mjtgqj4wyddfp8s3KZrSzHayVfy1E53M7ZIGaSEkIIJjijNZRK27Smt/6Eg
         VA80nlh2yd9SVpV/WWo8IiKxfTRNJlbezj+S2d9BjRp+FyMhB8pCBXwFwuBBr5PddvaR
         ub5mIi7+HS2+uRQxTS7d5k8venhq1hpH5kGx3SUuRcc03EP9+6xl3c7d+exEYjNbcnLo
         OSjyz2UC4eF2Iv87XGZXnON9KX2AObKBPPk2M8Lv8Yow6KAyHzTnJiUQ/xkk8VLdw+mD
         uxhNN6XGZ7ouAeubbNiGpnYSzOznh/kqBmdrjcZt/U5+eK4OVltPNI66TMp7zAz8wzwd
         9trw==
X-Google-DKIM-Signature: v=1; a=rsa-sha256; c=relaxed/relaxed;
        d=1e100.net; s=20230601; t=1771960971; x=1772565771;
        h=content-transfer-encoding:mime-version:references:in-reply-to
         :message-id:date:subject:to:from:x-gm-gg:x-gm-message-state:from:to
         :cc:subject:date:message-id:reply-to;
        bh=oD++iBj9IxDjwbbBgZSLqBIEox6IDYIGv8zNghuEH1I=;
        b=aNRI21axThC7vQ4T3n4Rkm8rbxiG/sgyYGgXubUW2Cob5OZPHocvi75AtDX1Wv1NS4
         iA5tPKrJ5tBjcS0iOcsjk+LEMTcCsM7AZVPL+076E78CI97jIDqwalwQhVlvl8bJ0uLH
         oDCTje63KgRN0MB36MDdB+lhIPYQ/e3iDbsNCapeBHcvDXs4J7Y+HtO+GBzB89wLUEj/
         1wBdIfk1/gE+iGEZu6vm0v/qHPl/sWsl6Qel7AA0nSEnGIZ127jD9YIELROvNVryWR4m
         Y/kDLezF5D5aY21veTWpReQTxl9zmo7kKJ48xZ6dtPDgyELZUxqla4ZOIM8YqfilEu61
         jhZg==
X-Gm-Message-State: AOJu0YxSqvF7u2sXqsxUxmMieHIYx/axaqud1Mt447PsOWNXHVoblZYG
	+fydZCnHVQJzagl788qozsBNJkEewpqH0xV02Mw2wlukJBhzYd7r2GWY56iD7AME1W8=
X-Gm-Gg: ATEYQzx0sPKyH9YyxV1yowNciCoOi7kouPx0vjVOKRRBjEaJIDObtQrEO1ekZGdMWb2
	YwXIQotmFSLwOlKocBNL7lc4TZ/gFKNJCZ9EySBhxLKy1Z/hErlhlqX916UQKEvwgTxrqKPPDY1
	Gtvefua3aTrXQcWjdVDU8fJqtD56mQNiNGZo6CipfmzaNBOFzLLwNW/mtO02ICrFEgKh5072ytV
	dTxIz5VE8ahObNuxY9Y9SMVOnPvltVmT0y3/DyBTCUZmhcLFTFoNEiodcnuNTrfpMl8dlaC/FbM
	ibo9Os+OCCjntAzt8SrO6hgrIEJhrIohn6q7lWb5tkvKd9SJJY0Y+KQIpQypEvq4XjadGTg2csL
	mCqaSujerXlfeptrOMbt5Am+lhLFxoq2i3FTEBOJ3h5eYUtP0psty6UASLh2BevRFzQ7Uz+i0j9
	sP9+I5VcXlMFbDJ8PlccSGMMdEKVuO
X-Received: by 2002:a53:d008:0:b0:649:429a:4577 with SMTP id 956f58d0204a3-64c790a6274mr10830921d50.86.1771960971402;
        Tue, 24 Feb 2026 11:22:51 -0800 (PST)
Received: from localhost ([2a03:2880:25ff:74::])
        by smtp.gmail.com with ESMTPSA id 956f58d0204a3-64c7a35b41bsm4709512d50.14.2026.02.24.11.22.50
        (version=TLS1_3 cipher=TLS_AES_256_GCM_SHA384 bits=256/256);
        Tue, 24 Feb 2026 11:22:51 -0800 (PST)
From: Leo Martins <loemra.dev@gmail.com>
To: linux-btrfs@vger.kernel.org,
	kernel-team@fb.com
Subject: [PATCH v3 1/3] btrfs: skip COW for written extent buffers allocated in current transaction
Date: Tue, 24 Feb 2026 11:22:32 -0800
Message-ID: <4ce911a475b998ddf76951629ad203e6440ab0ca.1771884128.git.loemra.dev@gmail.com>
X-Mailer: git-send-email 2.47.3
In-Reply-To: <cover.1771884128.git.loemra.dev@gmail.com>
References: <cover.1771884128.git.loemra.dev@gmail.com>
Precedence: bulk
X-Mailing-List: linux-btrfs@vger.kernel.org
List-Id: <linux-btrfs.vger.kernel.org>
List-Subscribe: <mailto:linux-btrfs+subscribe@vger.kernel.org>
List-Unsubscribe: <mailto:linux-btrfs+unsubscribe@vger.kernel.org>
MIME-Version: 1.0
Content-Transfer-Encoding: 8bit

When memory pressure causes writeback of a recently COW'd buffer,
btrfs sets BTRFS_HEADER_FLAG_WRITTEN on it. Subsequent
btrfs_search_slot() restarts then see the WRITTEN flag and re-COW
the buffer unnecessarily, causing COW amplification that can exhaust
block reservations and degrade throughput.

Overwriting in place is crash-safe because the committed superblock
does not reference buffers allocated in the current (uncommitted)
transaction, so no on-disk tree points to this block yet.

When should_cow_block() encounters a WRITTEN buffer whose generation
matches the current transaction, instead of requesting a COW, re-dirty
the buffer and re-register its range in the transaction's dirty_pages.

Both are necessary because btrfs tracks dirty metadata through two
independent mechanisms. set_extent_buffer_dirty() sets the
EXTENT_BUFFER_DIRTY flag and the buffer_tree xarray PAGECACHE_TAG_DIRTY
mark, which is what background writeback (btree_write_cache_pages) uses
to find and write dirty buffers. The transaction's dirty_pages io tree
is a separate structure used by btrfs_write_and_wait_transaction() at
commit time to ensure all buffers allocated during the transaction are
persisted. The dirty_pages range was originally registered in
btrfs_init_new_buffer() when the block was first allocated. Normally
dirty_pages is only cleared at commit time by
btrfs_write_and_wait_transaction(), but if qgroups are enabled and
snapshots are being created, qgroup_account_snapshot() may have already
called btrfs_write_and_wait_transaction() and released the range before
the final commit-time call.

Keep BTRFS_HEADER_FLAG_WRITTEN set so that btrfs_free_tree_block()
correctly pins the block if it is freed later.

Relax the lockdep assertion in btrfs_mark_buffer_dirty() from
btrfs_assert_tree_write_locked() to lockdep_assert_held() so that it
accepts either a read or write lock. should_cow_block() may be called
from btrfs_search_slot() when only a read lock is held (nodes above
write_lock_level are read-locked). The write lock assertion previously
documented the caller convention that buffer content was being modified
under exclusive access, but btrfs_mark_buffer_dirty() and
set_extent_buffer_dirty() themselves only perform independently
synchronized operations: atomic bit ops on bflags, folio_mark_dirty()
(kernel-internal folio locking), xarray mark updates (xarray spinlock),
and percpu counter updates. The read lock is sufficient because it
prevents lock_extent_buffer_for_io() from acquiring the write lock and
racing on the dirty state. Since rw_semaphore permits concurrent
readers, multiple threads can enter btrfs_mark_buffer_dirty()
simultaneously for the same buffer; this is safe because
test_and_set_bit(EXTENT_BUFFER_DIRTY) ensures only one thread performs
the full dirty state transition.

Remove the CONFIG_BTRFS_DEBUG assertion in set_extent_buffer_dirty()
that checked folio_test_dirty() after marking the buffer dirty. This
assertion assumed exclusive access (only one thread in
set_extent_buffer_dirty() at a time), which held when the only caller
was btrfs_mark_buffer_dirty() under write lock. With concurrent readers
calling through should_cow_block(), a thread that loses the
test_and_set_bit race sees was_dirty=true and skips the folio dirty
marking, but the winning thread may not have called
btrfs_meta_folio_set_dirty() yet, causing the assertion to fire. This
is a benign race: the winning thread will complete the folio dirty
marking, and no writeback can clear it while readers hold their locks.

Hoist the EXTENT_BUFFER_WRITEBACK, BTRFS_HEADER_FLAG_RELOC, and
BTRFS_ROOT_FORCE_COW checks before the WRITTEN block since they apply
regardless of whether the buffer has been written back. This
consolidates the exclusion logic and simplifies the WRITTEN path to
only handle log trees and zoned devices. Moving the RELOC checks
before the smp_mb__before_atomic() barrier is safe because both
btrfs_root_id() (immutable) and BTRFS_HEADER_FLAG_RELOC (set at COW
time under tree lock) are stable values not subject to concurrent
modification; the barrier is only needed for BTRFS_ROOT_FORCE_COW
which is set concurrently by create_pending_snapshot().

Exclude cases where in-place overwrite is not safe:
 - EXTENT_BUFFER_WRITEBACK: buffer is mid-I/O
 - Zoned devices: require sequential writes
 - Log trees: log blocks are immediately referenced by a committed
   superblock via btrfs_sync_log(), so overwriting could corrupt the
   committed log
 - BTRFS_ROOT_FORCE_COW: snapshot in progress
 - BTRFS_HEADER_FLAG_RELOC: block being relocated

Signed-off-by: Leo Martins <loemra.dev@gmail.com>
---
 fs/btrfs/ctree.c     | 56 ++++++++++++++++++++++++++++++++++++++------
 fs/btrfs/disk-io.c   |  2 +-
 fs/btrfs/extent_io.c |  4 ----
 3 files changed, 50 insertions(+), 12 deletions(-)

diff --git a/fs/btrfs/ctree.c b/fs/btrfs/ctree.c
index 7267b2502665..0e02b7b14adc 100644
--- a/fs/btrfs/ctree.c
+++ b/fs/btrfs/ctree.c
@@ -599,9 +599,9 @@ int btrfs_force_cow_block(struct btrfs_trans_handle *trans,
 	return ret;
 }
 
-static inline bool should_cow_block(const struct btrfs_trans_handle *trans,
+static inline bool should_cow_block(struct btrfs_trans_handle *trans,
 				    const struct btrfs_root *root,
-				    const struct extent_buffer *buf)
+				    struct extent_buffer *buf)
 {
 	if (btrfs_is_testing(root->fs_info))
 		return false;
@@ -621,7 +621,11 @@ static inline bool should_cow_block(const struct btrfs_trans_handle *trans,
 	if (btrfs_header_generation(buf) != trans->transid)
 		return true;
 
-	if (btrfs_header_flag(buf, BTRFS_HEADER_FLAG_WRITTEN))
+	if (test_bit(EXTENT_BUFFER_WRITEBACK, &buf->bflags))
+		return true;
+
+	if (btrfs_root_id(root) != BTRFS_TREE_RELOC_OBJECTID &&
+	    btrfs_header_flag(buf, BTRFS_HEADER_FLAG_RELOC))
 		return true;
 
 	/* Ensure we can see the FORCE_COW bit. */
@@ -629,11 +633,49 @@ static inline bool should_cow_block(const struct btrfs_trans_handle *trans,
 	if (test_bit(BTRFS_ROOT_FORCE_COW, &root->state))
 		return true;
 
-	if (btrfs_root_id(root) == BTRFS_TREE_RELOC_OBJECTID)
-		return false;
+	if (btrfs_header_flag(buf, BTRFS_HEADER_FLAG_WRITTEN)) {
+		/*
+		 * The buffer was allocated in this transaction and has been
+		 * written back to disk (WRITTEN is set). Normally we'd COW
+		 * it again, but since the committed superblock doesn't
+		 * reference this buffer (it was allocated in this transaction),
+		 * we can safely overwrite it in place.
+		 *
+		 * We keep BTRFS_HEADER_FLAG_WRITTEN set. The block has been
+		 * persisted at this bytenr and will be again after the
+		 * in-place update. This is important so that
+		 * btrfs_free_tree_block() correctly pins the block if it is
+		 * freed later (e.g., during tree rebalancing or FORCE_COW).
+		 *
+		 * Log trees and zoned devices cannot use this optimization:
+		 * - Log trees: log blocks are written and immediately
+		 *   referenced by a committed superblock via
+		 *   btrfs_sync_log(), bypassing the normal transaction
+		 *   commit. Overwriting in place could corrupt the
+		 *   committed log.
+		 * - Zoned devices: require sequential writes.
+		 */
+		if (btrfs_root_id(root) == BTRFS_TREE_LOG_OBJECTID ||
+		    btrfs_is_zoned(root->fs_info))
+			return true;
 
-	if (btrfs_header_flag(buf, BTRFS_HEADER_FLAG_RELOC))
-		return true;
+		/*
+		 * Re-register this block's range in the current transaction's
+		 * dirty_pages so that btrfs_write_and_wait_transaction()
+		 * writes it. The range was originally registered when the
+		 * block was allocated. Normally dirty_pages is only cleared
+		 * at commit time by btrfs_write_and_wait_transaction(), but
+		 * if qgroups are enabled and snapshots are being created,
+		 * qgroup_account_snapshot() may have already called
+		 * btrfs_write_and_wait_transaction() and released the range
+		 * before the final commit-time call.
+		 */
+		btrfs_set_extent_bit(&trans->transaction->dirty_pages,
+				     buf->start,
+				     buf->start + buf->len - 1,
+				     EXTENT_DIRTY, NULL);
+		btrfs_mark_buffer_dirty(trans, buf);
+	}
 
 	return false;
 }
diff --git a/fs/btrfs/disk-io.c b/fs/btrfs/disk-io.c
index 32fffb0557e5..bee8f76fbfea 100644
--- a/fs/btrfs/disk-io.c
+++ b/fs/btrfs/disk-io.c
@@ -4491,7 +4491,7 @@ void btrfs_mark_buffer_dirty(struct btrfs_trans_handle *trans,
 #endif
 	/* This is an active transaction (its state < TRANS_STATE_UNBLOCKED). */
 	ASSERT(trans->transid == fs_info->generation);
-	btrfs_assert_tree_write_locked(buf);
+	lockdep_assert_held(&buf->lock);
 	if (unlikely(transid != fs_info->generation)) {
 		btrfs_abort_transaction(trans, -EUCLEAN);
 		btrfs_crit(fs_info,
diff --git a/fs/btrfs/extent_io.c b/fs/btrfs/extent_io.c
index dfc17c292217..ff1fc699a6ca 100644
--- a/fs/btrfs/extent_io.c
+++ b/fs/btrfs/extent_io.c
@@ -3791,10 +3791,6 @@ void set_extent_buffer_dirty(struct extent_buffer *eb)
 					 eb->len,
 					 eb->fs_info->dirty_metadata_batch);
 	}
-#ifdef CONFIG_BTRFS_DEBUG
-	for (int i = 0; i < num_extent_folios(eb); i++)
-		ASSERT(folio_test_dirty(eb->folios[i]));
-#endif
 }
 
 void clear_extent_buffer_uptodate(struct extent_buffer *eb)
-- 
2.47.3


From mboxrd@z Thu Jan  1 00:00:00 1970
Received: from mail-yx1-f66.google.com (mail-yx1-f66.google.com [74.125.224.66])
	(using TLSv1.2 with cipher ECDHE-RSA-AES128-GCM-SHA256 (128/128 bits))
	(No client certificate requested)
	by smtp.subspace.kernel.org (Postfix) with ESMTPS id 6116E277CA4
	for <linux-btrfs@vger.kernel.org>; Tue, 24 Feb 2026 19:22:54 +0000 (UTC)
Authentication-Results: smtp.subspace.kernel.org; arc=none smtp.client-ip=74.125.224.66
ARC-Seal:i=1; a=rsa-sha256; d=subspace.kernel.org; s=arc-20240116;
	t=1771960979; cv=none; b=KkEORR5VEfAo4IDdhjVn/r2kXplMiwTCs1Ainkd5xu1a2FJH6Sl4XWdraciHVfc9MmUN3mFz3UqMrhbeQD+YjU/NGJDIP84eYwKf7OtQKSb/fslZoy0O0ZTnnFc9YgP64IFADuV4cN8JHjWJtugTE3seweq1C6NyRjtiIJe6GMY=
ARC-Message-Signature:i=1; a=rsa-sha256; d=subspace.kernel.org;
	s=arc-20240116; t=1771960979; c=relaxed/simple;
	bh=0roapicKqypHJgsT5IdXyQqmsdIijnqhozmyb7JDgbc=;
	h=From:To:Cc:Subject:Date:Message-ID:In-Reply-To:References:
	 MIME-Version; b=d0ZXWjbNVK8xLmuqqL7IoXxdeR4hHKMQSuhL4fkJJozMPLj0WsBvRyoT1LC9bEIQdlqsEtdoJJeog/TGFZyg71WV95pKm4KUwgK44caFKry/y3TAVfHFTeV5YJRE3Zxbs7WUNOxE5XSwmyRCHmHl1JQVE71NfbuwkdtGnmerhWw=
ARC-Authentication-Results:i=1; smtp.subspace.kernel.org; dmarc=pass (p=none dis=none) header.from=gmail.com; spf=pass smtp.mailfrom=gmail.com; dkim=pass (2048-bit key) header.d=gmail.com header.i=@gmail.com header.b=naEQdBhE; arc=none smtp.client-ip=74.125.224.66
Authentication-Results: smtp.subspace.kernel.org; dmarc=pass (p=none dis=none) header.from=gmail.com
Authentication-Results: smtp.subspace.kernel.org; spf=pass smtp.mailfrom=gmail.com
Authentication-Results: smtp.subspace.kernel.org;
	dkim=pass (2048-bit key) header.d=gmail.com header.i=@gmail.com header.b="naEQdBhE"
Received: by mail-yx1-f66.google.com with SMTP id 956f58d0204a3-64ca1ba0089so764722d50.1
        for <linux-btrfs@vger.kernel.org>; Tue, 24 Feb 2026 11:22:54 -0800 (PST)
DKIM-Signature: v=1; a=rsa-sha256; c=relaxed/relaxed;
        d=gmail.com; s=20230601; t=1771960973; x=1772565773; darn=vger.kernel.org;
        h=content-transfer-encoding:mime-version:references:in-reply-to
         :message-id:date:subject:cc:to:from:from:to:cc:subject:date
         :message-id:reply-to;
        bh=lS4fGbsX+aJ0/7b54Jk6S20Gu9F+OEI9Oa2c8P64Axw=;
        b=naEQdBhEPZaxInMXr8aPk9c6IKHmQVtxgewCbmtY/gzq21yM54724SLpyqt4a/q9Dk
         Hc7Rlj0h5wHc3JdNEpQ3tTyLlVpno1DQzCijeQ7ntz3H1CRTyJpwtxHCjq3Z8srfqUSd
         2BVis9aqhOIFd6khSL4alK+TGPu/aiB3q0y+44wynPbAIZmIBKTsNHskrzyNDDlM9Abh
         gNDtKjYQUDf19r1B45J4s0dsw9xuYKahsqBtEpmzOtsVUYoWjgbulIIYhBftzFpwDek4
         F69WeuNYkN2y+023glxNDFO9dHOafcL1FPG1FPjnX1ueQW10mIIzjKrgCJl4t53d9g1v
         MqUA==
X-Google-DKIM-Signature: v=1; a=rsa-sha256; c=relaxed/relaxed;
        d=1e100.net; s=20230601; t=1771960973; x=1772565773;
        h=content-transfer-encoding:mime-version:references:in-reply-to
         :message-id:date:subject:cc:to:from:x-gm-gg:x-gm-message-state:from
         :to:cc:subject:date:message-id:reply-to;
        bh=lS4fGbsX+aJ0/7b54Jk6S20Gu9F+OEI9Oa2c8P64Axw=;
        b=Ps4hfdHxB2ckxKEifvcb6JqYWV6YkLQ7bTtPvr4XdfH7K/sxsGojRmE2H1wax+ytnt
         Fw0gJghC8Y8jGCC+L+mufSHXj8EvBjjCCSrQ3b+OTWVJHtfDcvbf0nDrBoDaPMvNlqM4
         SRAxoItyhkFKCJSo8/0RTlKgYaF3k5HgXIkC9qTmiJc02Yhr6noMuf7prcGRdDwL8FlY
         dAvErdSSuJmg5bTsZgWr2+peBavSKHu/23M0RqsO+kJJFnN3hTJboPpKY4tGxFCF+Bkl
         biVmt3v//tPWgo1Gu6rSQxIe98S2CJSD1g5yOWXy0U3LffinOag8v56begj/+mfgmiWm
         sGAw==
X-Gm-Message-State: AOJu0YzM/9fdXskNNbB4+PlSkwf7yYOY1USmYX1uNMxc5xwBqDt4J31e
	RlghSSbZ8tZojslW2FMCL2+3dGVHkhQUbvhdSX7YjYkQ4ytOiN7amDEILuVPVEj8tKI=
X-Gm-Gg: ATEYQzyEk5nKgszK5H8AXQrQiqmozn9gRqnYuF+yYHnlcF8RzBMANO2ZHpvvug1u6Rr
	szpZr7+/6UHqL1k2dnWeBb+DRz6pVScPaxwCEYwN95alasJxe8yXaiXIMT5T+ZDfkTZgT8tSSCh
	VhHTj7MCQ2Zkm4bBtm+WIJJaWEwoyUO7p3evnrY2MFTcPWPd735suJZNMkAyv3iWXyFQnZJRRmb
	SPucf7QoQRcCWfFES9Km4igEdAlLEe5hXisy7sicS3MiQ2R//Q8OE/7n5SVREF3icRyd1VmKKZT
	gd6LgLepbvlso99tVgvbfvl3/TD3plJD9kjc2XSqrN01A6DqHZrysR1enR5SBIv8/U90Stkbifs
	zdGgQHZ5RnpulU5twCvGRan/N7O+a2DyaeNiV54wDcyRAWnyosEbt+SIqF/sCh+DM5EsVNADlWT
	4MYbiYMXgEpjVyKHBpog==
X-Received: by 2002:a05:690c:6086:b0:795:1ad3:26be with SMTP id 00721157ae682-79829041d59mr122126357b3.52.1771960972637;
        Tue, 24 Feb 2026 11:22:52 -0800 (PST)
Received: from localhost ([2a03:2880:25ff:74::])
        by smtp.gmail.com with ESMTPSA id 00721157ae682-7982de0fd63sm46933897b3.46.2026.02.24.11.22.52
        (version=TLS1_3 cipher=TLS_AES_256_GCM_SHA384 bits=256/256);
        Tue, 24 Feb 2026 11:22:52 -0800 (PST)
From: Leo Martins <loemra.dev@gmail.com>
To: linux-btrfs@vger.kernel.org,
	kernel-team@fb.com
Cc: Filipe Manana <fdmanana@suse.com>
Subject: [PATCH v3 2/3] btrfs: inhibit extent buffer writeback to prevent COW amplification
Date: Tue, 24 Feb 2026 11:22:33 -0800
Message-ID: <cc847a35e26cc4dfad18c59e3c525cea507ff440.1771884128.git.loemra.dev@gmail.com>
X-Mailer: git-send-email 2.47.3
In-Reply-To: <cover.1771884128.git.loemra.dev@gmail.com>
References: <cover.1771884128.git.loemra.dev@gmail.com>
Precedence: bulk
X-Mailing-List: linux-btrfs@vger.kernel.org
List-Id: <linux-btrfs.vger.kernel.org>
List-Subscribe: <mailto:linux-btrfs+subscribe@vger.kernel.org>
List-Unsubscribe: <mailto:linux-btrfs+unsubscribe@vger.kernel.org>
MIME-Version: 1.0
Content-Transfer-Encoding: 8bit

Inhibit writeback on COW'd extent buffers for the lifetime of the
transaction handle, preventing background writeback from setting
BTRFS_HEADER_FLAG_WRITTEN and causing unnecessary re-COW.

COW amplification occurs when background writeback flushes an extent
buffer that a transaction handle is still actively modifying. When
lock_extent_buffer_for_io() transitions a buffer from dirty to
writeback, it sets BTRFS_HEADER_FLAG_WRITTEN, marking the block as
having been persisted to disk at its current bytenr. Once WRITTEN is
set, should_cow_block() must either COW the block again or overwrite
it in place, both of which are unnecessary overhead when the buffer
is still being modified by the same handle that allocated it. By
inhibiting background writeback on actively-used buffers, WRITTEN is
never set while a transaction handle holds a reference to the buffer,
avoiding this overhead entirely.

Add an atomic_t writeback_inhibitors counter to struct extent_buffer,
which fits in an existing 6-byte hole without increasing struct size.
When a buffer is COW'd in btrfs_force_cow_block(), call
btrfs_inhibit_eb_writeback() to store the eb in the transaction
handle's writeback_inhibited_ebs xarray (keyed by eb->start), take a
reference, and increment writeback_inhibitors. The function handles
dedup (same eb inhibited twice by the same handle) and replacement
(different eb at the same logical address). Allocation failure is
graceful: the buffer simply falls back to the pre-existing behavior
where it may be written back and re-COW'd.

In lock_extent_buffer_for_io(), when writeback_inhibitors is non-zero
and the writeback mode is WB_SYNC_NONE, skip the buffer. WB_SYNC_NONE
is used by the VM flusher threads for background and periodic
writeback, which are the only paths that cause COW amplification by
opportunistically writing out dirty extent buffers mid-transaction.
Skipping these is safe because the buffers remain dirty in the page
cache and will be written out at transaction commit time.

WB_SYNC_ALL must always proceed regardless of writeback_inhibitors.
This is required for correctness in the fsync path: btrfs_sync_log()
writes log tree blocks via filemap_fdatawrite_range() (WB_SYNC_ALL)
while the transaction handle that inhibited those same blocks is still
active. Without the WB_SYNC_ALL bypass, those inhibited log tree
blocks would be silently skipped, resulting in an incomplete log on
disk and corruption on replay. btrfs_write_and_wait_transaction()
also uses WB_SYNC_ALL via filemap_fdatawrite_range(); for that path,
inhibitors are already cleared beforehand, but the bypass ensures
correctness regardless.

Uninhibit in __btrfs_end_transaction() before atomic_dec(num_writers)
to prevent a race where the committer proceeds while buffers are still
inhibited. Also uninhibit in btrfs_commit_transaction() before writing
and in cleanup_transaction() for the error path.

Signed-off-by: Leo Martins <loemra.dev@gmail.com>
Reviewed-by: Filipe Manana <fdmanana@suse.com>
---
 fs/btrfs/ctree.c       |  4 +++
 fs/btrfs/extent_io.c   | 63 +++++++++++++++++++++++++++++++++++++++++-
 fs/btrfs/extent_io.h   |  6 ++++
 fs/btrfs/transaction.c | 19 +++++++++++++
 fs/btrfs/transaction.h |  3 ++
 5 files changed, 94 insertions(+), 1 deletion(-)

diff --git a/fs/btrfs/ctree.c b/fs/btrfs/ctree.c
index 0e02b7b14adc..d4da65bb9096 100644
--- a/fs/btrfs/ctree.c
+++ b/fs/btrfs/ctree.c
@@ -590,6 +590,10 @@ int btrfs_force_cow_block(struct btrfs_trans_handle *trans,
 		btrfs_tree_unlock(buf);
 	free_extent_buffer_stale(buf);
 	btrfs_mark_buffer_dirty(trans, cow);
+
+	/* Inhibit writeback on the COW'd buffer for this transaction handle. */
+	btrfs_inhibit_eb_writeback(trans, cow);
+
 	*cow_ret = cow;
 	return 0;
 
diff --git a/fs/btrfs/extent_io.c b/fs/btrfs/extent_io.c
index ff1fc699a6ca..e04e42a81978 100644
--- a/fs/btrfs/extent_io.c
+++ b/fs/btrfs/extent_io.c
@@ -1940,7 +1940,9 @@ static noinline_for_stack bool lock_extent_buffer_for_io(struct extent_buffer *e
 	 * of time.
 	 */
 	spin_lock(&eb->refs_lock);
-	if (test_and_clear_bit(EXTENT_BUFFER_DIRTY, &eb->bflags)) {
+	if ((wbc->sync_mode == WB_SYNC_ALL ||
+	     atomic_read(&eb->writeback_inhibitors) == 0) &&
+	    test_and_clear_bit(EXTENT_BUFFER_DIRTY, &eb->bflags)) {
 		XA_STATE(xas, &fs_info->buffer_tree, eb->start >> fs_info->nodesize_bits);
 		unsigned long flags;
 
@@ -2999,6 +3001,64 @@ static inline void btrfs_release_extent_buffer(struct extent_buffer *eb)
 	kmem_cache_free(extent_buffer_cache, eb);
 }
 
+/*
+ * btrfs_inhibit_eb_writeback - Inhibit writeback on buffer during transaction.
+ * @trans: transaction handle that will own the inhibitor
+ * @eb: extent buffer to inhibit writeback on
+ *
+ * Attempts 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.
+ */
+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;
+
+	/* 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;
+	}
+
+	/* Handle replacement of different eb at same index. */
+	if (old && old != eb) {
+		struct extent_buffer *old_eb = old;
+
+		atomic_dec(&old_eb->writeback_inhibitors);
+		free_extent_buffer(old_eb);
+	}
+
+	atomic_inc(&eb->writeback_inhibitors);
+}
+
+/*
+ * btrfs_uninhibit_all_eb_writeback - Uninhibit writeback on all buffers.
+ * @trans: transaction handle to clean up
+ */
+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);
+	}
+	xa_destroy(&trans->writeback_inhibited_ebs);
+}
+
 static struct extent_buffer *__alloc_extent_buffer(struct btrfs_fs_info *fs_info,
 						   u64 start)
 {
@@ -3009,6 +3069,7 @@ static struct extent_buffer *__alloc_extent_buffer(struct btrfs_fs_info *fs_info
 	eb->len = fs_info->nodesize;
 	eb->fs_info = fs_info;
 	init_rwsem(&eb->lock);
+	atomic_set(&eb->writeback_inhibitors, 0);
 
 	btrfs_leak_debug_add_eb(eb);
 
diff --git a/fs/btrfs/extent_io.h b/fs/btrfs/extent_io.h
index 73571d5d3d5a..fb68fbd4866c 100644
--- a/fs/btrfs/extent_io.h
+++ b/fs/btrfs/extent_io.h
@@ -102,6 +102,8 @@ struct extent_buffer {
 	/* >= 0 if eb belongs to a log tree, -1 otherwise */
 	s8 log_index;
 	u8 folio_shift;
+	/* Inhibits WB_SYNC_NONE writeback when > 0. */
+	atomic_t writeback_inhibitors;
 	struct rcu_head rcu_head;
 
 	struct rw_semaphore lock;
@@ -381,4 +383,8 @@ void btrfs_extent_buffer_leak_debug_check(struct btrfs_fs_info *fs_info);
 #define btrfs_extent_buffer_leak_debug_check(fs_info)	do {} while (0)
 #endif
 
+void btrfs_inhibit_eb_writeback(struct btrfs_trans_handle *trans,
+			       struct extent_buffer *eb);
+void btrfs_uninhibit_all_eb_writeback(struct btrfs_trans_handle *trans);
+
 #endif
diff --git a/fs/btrfs/transaction.c b/fs/btrfs/transaction.c
index f4cc9e1a1b93..a9a22629b49d 100644
--- a/fs/btrfs/transaction.c
+++ b/fs/btrfs/transaction.c
@@ -15,6 +15,7 @@
 #include "misc.h"
 #include "ctree.h"
 #include "disk-io.h"
+#include "extent_io.h"
 #include "transaction.h"
 #include "locking.h"
 #include "tree-log.h"
@@ -688,6 +689,8 @@ 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
@@ -1083,6 +1086,13 @@ static int __btrfs_end_transaction(struct btrfs_trans_handle *trans,
 	if (trans->type & __TRANS_FREEZABLE)
 		sb_end_intwrite(info->sb);
 
+	/*
+	 * Uninhibit extent buffer writeback before decrementing num_writers,
+	 * since the decrement wakes the committing thread which needs all
+	 * buffers uninhibited to write them to disk.
+	 */
+	btrfs_uninhibit_all_eb_writeback(trans);
+
 	WARN_ON(cur_trans != info->running_transaction);
 	WARN_ON(atomic_read(&cur_trans->num_writers) < 1);
 	atomic_dec(&cur_trans->num_writers);
@@ -2110,6 +2120,7 @@ static void cleanup_transaction(struct btrfs_trans_handle *trans, int err)
 	if (!test_bit(BTRFS_FS_RELOC_RUNNING, &fs_info->flags))
 		btrfs_scrub_cancel(fs_info);
 
+	btrfs_uninhibit_all_eb_writeback(trans);
 	kmem_cache_free(btrfs_trans_handle_cachep, trans);
 }
 
@@ -2556,6 +2567,14 @@ int btrfs_commit_transaction(struct btrfs_trans_handle *trans)
 	    fs_info->cleaner_kthread)
 		wake_up_process(fs_info->cleaner_kthread);
 
+	/*
+	 * Uninhibit writeback on all extent buffers inhibited during this
+	 * transaction before writing them to disk. Inhibiting prevented
+	 * writeback while the transaction was building, but now we need
+	 * them written.
+	 */
+	btrfs_uninhibit_all_eb_writeback(trans);
+
 	ret = btrfs_write_and_wait_transaction(trans);
 	if (unlikely(ret)) {
 		btrfs_err(fs_info, "error while writing out transaction: %d", ret);
diff --git a/fs/btrfs/transaction.h b/fs/btrfs/transaction.h
index 18ef069197e5..7d70fe486758 100644
--- a/fs/btrfs/transaction.h
+++ b/fs/btrfs/transaction.h
@@ -12,6 +12,7 @@
 #include <linux/time64.h>
 #include <linux/mutex.h>
 #include <linux/wait.h>
+#include <linux/xarray.h>
 #include "btrfs_inode.h"
 #include "delayed-ref.h"
 
@@ -162,6 +163,8 @@ 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;
 };
 
 /*
-- 
2.47.3


From mboxrd@z Thu Jan  1 00:00:00 1970
Received: from mail-yw1-f193.google.com (mail-yw1-f193.google.com [209.85.128.193])
	(using TLSv1.2 with cipher ECDHE-RSA-AES128-GCM-SHA256 (128/128 bits))
	(No client certificate requested)
	by smtp.subspace.kernel.org (Postfix) with ESMTPS id 51A553ACEE5
	for <linux-btrfs@vger.kernel.org>; Tue, 24 Feb 2026 19:22:55 +0000 (UTC)
Authentication-Results: smtp.subspace.kernel.org; arc=none smtp.client-ip=209.85.128.193
ARC-Seal:i=1; a=rsa-sha256; d=subspace.kernel.org; s=arc-20240116;
	t=1771960980; cv=none; b=kXNH9KBwuHF07LJ0PAu3JZnIbyzhKk340chdJqS7qb9FQOenhm+m5mi5ICD27vBlcZNhKudHcmsos0wPd7l76deRLUmSne1R3VXSkHH9Lc6y4ogd4lbH16A8m+kIq4bslCADse4Zl9Llm5LYlWVbzKmK2NGhBa8fD7LxsqlIPYQ=
ARC-Message-Signature:i=1; a=rsa-sha256; d=subspace.kernel.org;
	s=arc-20240116; t=1771960980; c=relaxed/simple;
	bh=RKxptFjOQg2ePxtrPFOMEGunm7brl19UYdjtsUqptSU=;
	h=From:To:Subject:Date:Message-ID:In-Reply-To:References:
	 MIME-Version; b=sHpCZgKCmfK3snM0npVTJj4ENW/Cvj0A5ljSSaaJWn5HXPrw36zI7D9eFfx1IO6BT8G36XMgHsCVbwDWmFPL394r147PJgn8ls8/iCUUq5lSrBM/XZwqbRkbRWNpaZ7uRT6XbR2YQX+o2X7hU96RxvZbuzccWNiWkwnrQf82ykI=
ARC-Authentication-Results:i=1; smtp.subspace.kernel.org; dmarc=pass (p=none dis=none) header.from=gmail.com; spf=pass smtp.mailfrom=gmail.com; dkim=pass (2048-bit key) header.d=gmail.com header.i=@gmail.com header.b=OS0z22Z2; arc=none smtp.client-ip=209.85.128.193
Authentication-Results: smtp.subspace.kernel.org; dmarc=pass (p=none dis=none) header.from=gmail.com
Authentication-Results: smtp.subspace.kernel.org; spf=pass smtp.mailfrom=gmail.com
Authentication-Results: smtp.subspace.kernel.org;
	dkim=pass (2048-bit key) header.d=gmail.com header.i=@gmail.com header.b="OS0z22Z2"
Received: by mail-yw1-f193.google.com with SMTP id 00721157ae682-7947cf097c1so55217627b3.2
        for <linux-btrfs@vger.kernel.org>; Tue, 24 Feb 2026 11:22:55 -0800 (PST)
DKIM-Signature: v=1; a=rsa-sha256; c=relaxed/relaxed;
        d=gmail.com; s=20230601; t=1771960974; x=1772565774; darn=vger.kernel.org;
        h=content-transfer-encoding:mime-version:references:in-reply-to
         :message-id:date:subject:to:from:from:to:cc:subject:date:message-id
         :reply-to;
        bh=j5blE1hym1Y/2JouN/dbaOiUOruy/Z7iUAuj7qCa+RA=;
        b=OS0z22Z2SgsT8pHI44vC1EdM4GfkKaombf7F5Jq8eXhsg9QGRM5R6EeMYEE8JHJPn8
         L+YQ+y6fi2F05QwBgTrFgyJKQ89WXPs6MgYd9evHj5RW09srb8nAkd6jI1MVytlsKkqS
         8CboCoAjBs3sjWQrg5gHt+J0jc2P4eDFnUgYX3DiXx/7AkgCLgV0yBtdxDD2CE4bWwtE
         RD0FoR+gP0PRq777s1Xo4MROyg5unmjWsv5an1eG7arDJz5dL9pGw3mqhO+mNV9Si8ij
         zY8ISZXrX5ZpS3qJAMugI1d70HtsRzvK7C1GfWvtljmXT9GaPLBNZ4USiDvPhjulziDh
         mM3Q==
X-Google-DKIM-Signature: v=1; a=rsa-sha256; c=relaxed/relaxed;
        d=1e100.net; s=20230601; t=1771960974; x=1772565774;
        h=content-transfer-encoding:mime-version:references:in-reply-to
         :message-id:date:subject:to:from:x-gm-gg:x-gm-message-state:from:to
         :cc:subject:date:message-id:reply-to;
        bh=j5blE1hym1Y/2JouN/dbaOiUOruy/Z7iUAuj7qCa+RA=;
        b=jm/Kts5ICk808WX3OTJIPw9Ykx+vEPzdsjsuPyq7Fh3zF/5dz9gF0hacjgsSHz33Yz
         oYjzeTfudmAO14Bix0TSzcb7PS0mEOTCCPzOv0vNbcQ13liO8WB2x4ZwaIfIWU7+/MFO
         LtEjRodKgMoU8UZuS5u6T7Zc+IDoxsdlMyVsI98iiUtXokl5Ks8holY5sI/FU4v0VlpK
         Cv1LlJZedMwUD/zwb+0Mr8safRNcfs38CjVrYvwrRGuxHQa3D9L9V83UMhCvpYbSM8+l
         Bco2XhbVzidDfa5hc2+FmBTCgwYGd2nVXqFr+a6fIyGmhDOlBrGp4tzwn/XI7VRS1pdz
         uW2w==
X-Gm-Message-State: AOJu0YyGsF2PghlZsNZEY92jAaSN304VF52OKOrWnhkC50uFuD/FRVqs
	5brttfSufQr9AKDN7tZ1M5sol+fU+sy5RAuJRiO9Z5mmzq0NzJ7v6aBd0gMbVWXLYoU=
X-Gm-Gg: ATEYQzzJ5x9Vholf+k5/oIvo+T3I2TCKc1mCvfBmXR8tg6/u8G3SrnSKcOUwoDD4Xgr
	30UhwlXZZRBX3qLzKHX9xYQ9hVREV60YwEKUJF6RYXNELvuHKz/ibjtIVNRxHe/aYJyGYW6pONQ
	aXVhltmYSN92NHgprzoLN6eF1Aya3K9JPf9S4EW9WW4F9NIBDlbKEBRlKNjKXFY2h0T6jUGeMVA
	apvyRsm0qnUGiaaQPnaAEsAE1jSIhlw91zf832kpOMAQNAeEPZcJWHVsK56AV1nVdlAAtCA1Vkr
	ZSMVO/MmOQ7qFl+Kx0zpqxw5UT4QZW2VryPz/e1ZcokfWC2ULedH67Upp6JJvT/FewOaSLxGPxB
	tTb9wrghGWQm/DezO7BXSzvvnVngbb1Nnp/PM4eOkPyx+Rwg81Wuw/o83/OAhJh96VDm9BH6/Eq
	UY1owRneM2I9l2QBJm
X-Received: by 2002:a05:690c:62c1:b0:797:d5f2:c64 with SMTP id 00721157ae682-79828cf722amr121678937b3.10.1771960973621;
        Tue, 24 Feb 2026 11:22:53 -0800 (PST)
Received: from localhost ([2a03:2880:25ff:9::])
        by smtp.gmail.com with ESMTPSA id 00721157ae682-7982dd88efcsm46952667b3.24.2026.02.24.11.22.53
        (version=TLS1_3 cipher=TLS_AES_256_GCM_SHA384 bits=256/256);
        Tue, 24 Feb 2026 11:22:53 -0800 (PST)
From: Leo Martins <loemra.dev@gmail.com>
To: linux-btrfs@vger.kernel.org,
	kernel-team@fb.com
Subject: [PATCH v3 3/3] btrfs: add tracepoint for search slot restart tracking
Date: Tue, 24 Feb 2026 11:22:34 -0800
Message-ID: <18c04d9a68f64fa5e36dde196306170d0fb437d9.1771884128.git.loemra.dev@gmail.com>
X-Mailer: git-send-email 2.47.3
In-Reply-To: <cover.1771884128.git.loemra.dev@gmail.com>
References: <cover.1771884128.git.loemra.dev@gmail.com>
Precedence: bulk
X-Mailing-List: linux-btrfs@vger.kernel.org
List-Id: <linux-btrfs.vger.kernel.org>
List-Subscribe: <mailto:linux-btrfs+subscribe@vger.kernel.org>
List-Unsubscribe: <mailto:linux-btrfs+unsubscribe@vger.kernel.org>
MIME-Version: 1.0
Content-Transfer-Encoding: 8bit

Add a btrfs_search_slot_restart tracepoint that fires at each restart
site in btrfs_search_slot(), recording the root, tree level, and
reason for the restart. This enables tracking search slot restarts
which contribute to COW amplification under memory pressure.

The four restart reasons are:
 - write_lock: insufficient write lock level, need to restart with
   higher lock
 - setup_nodes: node setup returned -EAGAIN
 - slot_zero: insertion at slot 0 requires higher write lock level
 - read_block: read_block_for_search returned -EAGAIN (block not
   cached or lock contention)

COW counts are already tracked by the existing trace_btrfs_cow_block()
tracepoint. The per-restart-site tracepoint avoids counter overhead
in the critical path when tracepoints are disabled, and provides
richer per-event information that bpftrace scripts can aggregate into
counts, histograms, and per-root breakdowns.

Signed-off-by: Leo Martins <loemra.dev@gmail.com>
---
 fs/btrfs/ctree.c             | 10 ++++++++--
 include/trace/events/btrfs.h | 24 ++++++++++++++++++++++++
 2 files changed, 32 insertions(+), 2 deletions(-)

diff --git a/fs/btrfs/ctree.c b/fs/btrfs/ctree.c
index d4da65bb9096..5447ee640190 100644
--- a/fs/btrfs/ctree.c
+++ b/fs/btrfs/ctree.c
@@ -2152,6 +2152,7 @@ int btrfs_search_slot(struct btrfs_trans_handle *trans, struct btrfs_root *root,
 			    p->nodes[level + 1])) {
 				write_lock_level = level + 1;
 				btrfs_release_path(p);
+				trace_btrfs_search_slot_restart(root, level, "write_lock");
 				goto again;
 			}
 
@@ -2214,8 +2215,10 @@ int btrfs_search_slot(struct btrfs_trans_handle *trans, struct btrfs_root *root,
 		p->slots[level] = slot;
 		ret2 = setup_nodes_for_search(trans, root, p, b, level, ins_len,
 					      &write_lock_level);
-		if (ret2 == -EAGAIN)
+		if (ret2 == -EAGAIN) {
+			trace_btrfs_search_slot_restart(root, level, "setup_nodes");
 			goto again;
+		}
 		if (ret2) {
 			ret = ret2;
 			goto done;
@@ -2231,6 +2234,7 @@ int btrfs_search_slot(struct btrfs_trans_handle *trans, struct btrfs_root *root,
 		if (slot == 0 && ins_len && write_lock_level < level + 1) {
 			write_lock_level = level + 1;
 			btrfs_release_path(p);
+			trace_btrfs_search_slot_restart(root, level, "slot_zero");
 			goto again;
 		}
 
@@ -2244,8 +2248,10 @@ int btrfs_search_slot(struct btrfs_trans_handle *trans, struct btrfs_root *root,
 		}
 
 		ret2 = read_block_for_search(root, p, &b, slot, key);
-		if (ret2 == -EAGAIN && !p->nowait)
+		if (ret2 == -EAGAIN && !p->nowait) {
+			trace_btrfs_search_slot_restart(root, level, "read_block");
 			goto again;
+		}
 		if (ret2) {
 			ret = ret2;
 			goto done;
diff --git a/include/trace/events/btrfs.h b/include/trace/events/btrfs.h
index 125bdc166bfe..abf1b73ee60f 100644
--- a/include/trace/events/btrfs.h
+++ b/include/trace/events/btrfs.h
@@ -1110,6 +1110,30 @@ TRACE_EVENT(btrfs_cow_block,
 		  __entry->cow_level)
 );
 
+TRACE_EVENT(btrfs_search_slot_restart,
+
+	TP_PROTO(const struct btrfs_root *root, int level,
+		 const char *reason),
+
+	TP_ARGS(root, level, reason),
+
+	TP_STRUCT__entry_btrfs(
+		__field(	u64,	root_objectid		)
+		__field(	int,	level			)
+		__string(	reason,	reason			)
+	),
+
+	TP_fast_assign_btrfs(root->fs_info,
+		__entry->root_objectid	= btrfs_root_id(root);
+		__entry->level		= level;
+		__assign_str(reason);
+	),
+
+	TP_printk_btrfs("root=%llu(%s) level=%d reason=%s",
+		  show_root_type(__entry->root_objectid),
+		  __entry->level, __get_str(reason))
+);
+
 TRACE_EVENT(btrfs_space_reservation,
 
 	TP_PROTO(const struct btrfs_fs_info *fs_info, const char *type, u64 val,
-- 
2.47.3


From mboxrd@z Thu Jan  1 00:00:00 1970
Received: from mail-wm1-f65.google.com (mail-wm1-f65.google.com [209.85.128.65])
	(using TLSv1.2 with cipher ECDHE-RSA-AES128-GCM-SHA256 (128/128 bits))
	(No client certificate requested)
	by smtp.subspace.kernel.org (Postfix) with ESMTPS id 6C4BC2D9EC4
	for <linux-btrfs@vger.kernel.org>; Wed, 25 Feb 2026 06:07:03 +0000 (UTC)
Authentication-Results: smtp.subspace.kernel.org; arc=none smtp.client-ip=209.85.128.65
ARC-Seal:i=1; a=rsa-sha256; d=subspace.kernel.org; s=arc-20240116;
	t=1771999625; cv=none; b=sCgiflZW9S0Q3dXVh/e0D4uGeeK/zDmlT0Su7/z/yiYzUeB+ggyRBZI8C9uURsEvE+ImyExYsNW+mvc2H9YIzl8Wg9StUr1yvuKCSM9cMw9TSWHZMvdnkLXd5sEKsbfk9npC3YZe2noc26V7UfDLve2dKgqPxdSbOFoHiu/of+U=
ARC-Message-Signature:i=1; a=rsa-sha256; d=subspace.kernel.org;
	s=arc-20240116; t=1771999625; c=relaxed/simple;
	bh=Q8MoV3esw6tldlzcn3PSTeLevvVyUBBS3Gu9GwUHGpg=;
	h=Message-ID:Date:MIME-Version:Subject:To:References:From:
	 In-Reply-To:Content-Type; b=AWoz+sJV1pCzcGklMJSyJ5ZCxJ72+l3ugV55zIkCnsbWLXY7XsNC0xJLy2Yos1p5F/0srFWRzU21XCyYEyUOITXfhhkP0voya/S3er+eGezH3AHZ4TPyzgPj3Hj9uBBMx/ZbacKIURs+rp2PeVmRHEJCSYhpy1eoxr0KeqHd5x4=
ARC-Authentication-Results:i=1; smtp.subspace.kernel.org; dmarc=pass (p=none dis=none) header.from=gmail.com; spf=pass smtp.mailfrom=gmail.com; dkim=pass (2048-bit key) header.d=gmail.com header.i=@gmail.com header.b=OIM5XuaQ; arc=none smtp.client-ip=209.85.128.65
Authentication-Results: smtp.subspace.kernel.org; dmarc=pass (p=none dis=none) header.from=gmail.com
Authentication-Results: smtp.subspace.kernel.org; spf=pass smtp.mailfrom=gmail.com
Authentication-Results: smtp.subspace.kernel.org;
	dkim=pass (2048-bit key) header.d=gmail.com header.i=@gmail.com header.b="OIM5XuaQ"
Received: by mail-wm1-f65.google.com with SMTP id 5b1f17b1804b1-4836cd6e0d4so7319585e9.3
        for <linux-btrfs@vger.kernel.org>; Tue, 24 Feb 2026 22:07:03 -0800 (PST)
DKIM-Signature: v=1; a=rsa-sha256; c=relaxed/relaxed;
        d=gmail.com; s=20230601; t=1771999622; x=1772604422; darn=vger.kernel.org;
        h=content-transfer-encoding:in-reply-to:from:content-language
         :references:to:subject:user-agent:mime-version:date:message-id:from
         :to:cc:subject:date:message-id:reply-to;
        bh=fuN9Dg3DNRfT5EYG1jOag3eiqgO92nSIK/H+MCfvhXY=;
        b=OIM5XuaQ8ysb0WvyEywDTqwk2S+xF6debv2izg0vb0wTvbBNfva1+42YxP07E6ICQM
         KYHwcejDEcQUcQYZy/xhbSk+TtClpuP8MgLOFsNobSfh27bPAZ7oC5ZkjDfYZl+ApXmW
         HJs8M8KbW6lxeJmD4xOuGpzDgTlTc6NghZFIDJ0kMviGCXyTSMIHCzFfZD5LnNJ+qokD
         rkz+VnJuO1vOsqVdihmUiFSjIdymo4vvehSbY/qlYXlcNN87eENt0FClAKWdpW4HZbDm
         fbfoHg/PqIoquwS5X2THhp65QZzy/+Y81WT8UnUu6+KWF8A91pI/S5zxmI0tM4iJ+1Z1
         eMkg==
X-Google-DKIM-Signature: v=1; a=rsa-sha256; c=relaxed/relaxed;
        d=1e100.net; s=20230601; t=1771999622; x=1772604422;
        h=content-transfer-encoding:in-reply-to:from:content-language
         :references:to:subject:user-agent:mime-version:date:message-id
         :x-gm-gg:x-gm-message-state:from:to:cc:subject:date:message-id
         :reply-to;
        bh=fuN9Dg3DNRfT5EYG1jOag3eiqgO92nSIK/H+MCfvhXY=;
        b=OYOw1t91aEz2NEtbj8i7yUJlPVcq01YpPgokinGpfBgsQKv6ClwEwnO7O2+PNXcOYG
         LsTN7VEhkNMtxyEdCRfgmISwAS02nDRKBYUK7dmvBIqlUgJEXzuIwklqDOjGfIjFR8sH
         YY5zSBa0i4iD435yy+Uzoc1U/c03nZ7g8soJbWXYB70iaWN/A6buaWUSYKOUBuhH34lL
         cHdrgeBZB4hNZa50ljk3XlUbKot9s1H5SOOgoeLlcQNHUPKlrOozXGZjEwvUE0h9h8A5
         Xcay5fsifTc7Q9qg23cKnIWnrOO7cZfEFmcZfN6XZr5cQVWPsYJCLGNV/VC+Ww1DU+Lz
         Ml1Q==
X-Forwarded-Encrypted: i=1; AJvYcCVXNK3uJ5+aibePEceSKu3vFNATx1At7R4sbm5ijeaTnxpINC5k72HnEpV30e2WeIQD50Z6cN0XgrrTFg==@vger.kernel.org
X-Gm-Message-State: AOJu0Yyv2K933Kj+lLor7BHoC9rEkux3xD77EcDCxmICDW4lhZEFUKqE
	o/c3qID55dAVxBamFJbEY33IB9Ebdye4lczjsBMT96ZscltdgoqI8+2W
X-Gm-Gg: ATEYQzyJhCYSkXKUN/toqvo2a55y1AssWhwwGfcTXOvkcQc/8i7eI6C4tWApYDLckf2
	lz2N9mfJrCBsVVzSv88FvjXhUAFzK7JvjFJsBs9M8bLnQQuWmD9Nv+TbcrvkUunHEWV+5Q7jumN
	/8HXnbgdDgDSRJJ6FRSdOTiCIGrtjwpR/gaE36wnI/7Dv1Jw6YZMD37SwTys9RmgR91BeXVDUZc
	/VB/WK0ZRZ0udrdEeKxEzwGpGHJD1PgTWzaRyA22/KZSE5INQuJWL9I5W4aHVzbn3U1QE80HG+V
	KuBBhgDBbY+DzAyxb4AsLIVVhJ747Zh6HU+Z/P9aMyyk3ijYTuHJ8mSVRze4E4IpEY9P/tG7BiQ
	0BixmvN/kjplLUZgpGeHJRIbz1RVQTkbkqmQXL1nfwmBQUCqxKFs1I0sM52/a8aGQGxgkhe5E1I
	g38Ft+p2iPaWCnfwSm9EiusRUrp+AQpzFTfpnabq3S+E4hyrlp9pErPyao8dSCVyqZJmk4xJA9p
	7RWag==
X-Received: by 2002:a05:600c:1988:b0:480:6612:253 with SMTP id 5b1f17b1804b1-483a962e4demr132745175e9.3.1771999621484;
        Tue, 24 Feb 2026 22:07:01 -0800 (PST)
Received: from ?IPV6:2408:8239:502:5512:18a9:8974:b792:9a40? ([140.238.217.67])
        by smtp.gmail.com with ESMTPSA id 5b1f17b1804b1-483bfcb318fsm5509575e9.6.2026.02.24.22.06.56
        (version=TLS1_3 cipher=TLS_AES_128_GCM_SHA256 bits=128/128);
        Tue, 24 Feb 2026 22:07:01 -0800 (PST)
Message-ID: <060df404-3e39-4637-88f4-ffd42fcddc26@gmail.com>
Date: Wed, 25 Feb 2026 14:06:51 +0800
Precedence: bulk
X-Mailing-List: linux-btrfs@vger.kernel.org
List-Id: <linux-btrfs.vger.kernel.org>
List-Subscribe: <mailto:linux-btrfs+subscribe@vger.kernel.org>
List-Unsubscribe: <mailto:linux-btrfs+unsubscribe@vger.kernel.org>
MIME-Version: 1.0
User-Agent: Mozilla Thunderbird
Subject: Re: [PATCH v3 1/3] btrfs: skip COW for written extent buffers
 allocated in current transaction
To: Leo Martins <loemra.dev@gmail.com>, linux-btrfs@vger.kernel.org,
 kernel-team@fb.com
References: <cover.1771884128.git.loemra.dev@gmail.com>
 <4ce911a475b998ddf76951629ad203e6440ab0ca.1771884128.git.loemra.dev@gmail.com>
Content-Language: en-US
From: Sun YangKai <sunk67188@gmail.com>
In-Reply-To: <4ce911a475b998ddf76951629ad203e6440ab0ca.1771884128.git.loemra.dev@gmail.com>
Content-Type: text/plain; charset=UTF-8; format=flowed
Content-Transfer-Encoding: 7bit



On 2026/2/25 03:22, Leo Martins wrote:
> When memory pressure causes writeback of a recently COW'd buffer,
> btrfs sets BTRFS_HEADER_FLAG_WRITTEN on it. Subsequent
> btrfs_search_slot() restarts then see the WRITTEN flag and re-COW
> the buffer unnecessarily, causing COW amplification that can exhaust
> block reservations and degrade throughput.
> 
> Overwriting in place is crash-safe because the committed superblock
> does not reference buffers allocated in the current (uncommitted)
> transaction, so no on-disk tree points to this block yet.
> 
> When should_cow_block() encounters a WRITTEN buffer whose generation
> matches the current transaction, instead of requesting a COW, re-dirty
> the buffer and re-register its range in the transaction's dirty_pages.
> 
> Both are necessary because btrfs tracks dirty metadata through two
> independent mechanisms. set_extent_buffer_dirty() sets the
> EXTENT_BUFFER_DIRTY flag and the buffer_tree xarray PAGECACHE_TAG_DIRTY
> mark, which is what background writeback (btree_write_cache_pages) uses
> to find and write dirty buffers. The transaction's dirty_pages io tree
> is a separate structure used by btrfs_write_and_wait_transaction() at
> commit time to ensure all buffers allocated during the transaction are
> persisted. The dirty_pages range was originally registered in
> btrfs_init_new_buffer() when the block was first allocated. Normally
> dirty_pages is only cleared at commit time by
> btrfs_write_and_wait_transaction(), but if qgroups are enabled and
> snapshots are being created, qgroup_account_snapshot() may have already
> called btrfs_write_and_wait_transaction() and released the range before
> the final commit-time call.
> 
> Keep BTRFS_HEADER_FLAG_WRITTEN set so that btrfs_free_tree_block()
> correctly pins the block if it is freed later.
> 
> Relax the lockdep assertion in btrfs_mark_buffer_dirty() from
> btrfs_assert_tree_write_locked() to lockdep_assert_held() so that it
> accepts either a read or write lock. should_cow_block() may be called
> from btrfs_search_slot() when only a read lock is held (nodes above
> write_lock_level are read-locked). The write lock assertion previously
> documented the caller convention that buffer content was being modified
> under exclusive access, but btrfs_mark_buffer_dirty() and
> set_extent_buffer_dirty() themselves only perform independently
> synchronized operations: atomic bit ops on bflags, folio_mark_dirty()
> (kernel-internal folio locking), xarray mark updates (xarray spinlock),
> and percpu counter updates. The read lock is sufficient because it
> prevents lock_extent_buffer_for_io() from acquiring the write lock and
> racing on the dirty state. Since rw_semaphore permits concurrent
> readers, multiple threads can enter btrfs_mark_buffer_dirty()
> simultaneously for the same buffer; this is safe because
> test_and_set_bit(EXTENT_BUFFER_DIRTY) ensures only one thread performs
> the full dirty state transition.
> 
> Remove the CONFIG_BTRFS_DEBUG assertion in set_extent_buffer_dirty()
> that checked folio_test_dirty() after marking the buffer dirty. This
> assertion assumed exclusive access (only one thread in
> set_extent_buffer_dirty() at a time), which held when the only caller
> was btrfs_mark_buffer_dirty() under write lock. With concurrent readers
> calling through should_cow_block(), a thread that loses the
> test_and_set_bit race sees was_dirty=true and skips the folio dirty
> marking, but the winning thread may not have called
> btrfs_meta_folio_set_dirty() yet, causing the assertion to fire. This
> is a benign race: the winning thread will complete the folio dirty
> marking, and no writeback can clear it while readers hold their locks.
> 
> Hoist the EXTENT_BUFFER_WRITEBACK, BTRFS_HEADER_FLAG_RELOC, and
> BTRFS_ROOT_FORCE_COW checks before the WRITTEN block since they apply
> regardless of whether the buffer has been written back. This
> consolidates the exclusion logic and simplifies the WRITTEN path to
> only handle log trees and zoned devices. Moving the RELOC checks
> before the smp_mb__before_atomic() barrier is safe because both
> btrfs_root_id() (immutable) and BTRFS_HEADER_FLAG_RELOC (set at COW
> time under tree lock) are stable values not subject to concurrent
> modification; the barrier is only needed for BTRFS_ROOT_FORCE_COW
> which is set concurrently by create_pending_snapshot().
> 
> Exclude cases where in-place overwrite is not safe:
>   - EXTENT_BUFFER_WRITEBACK: buffer is mid-I/O
>   - Zoned devices: require sequential writes
>   - Log trees: log blocks are immediately referenced by a committed
>     superblock via btrfs_sync_log(), so overwriting could corrupt the
>     committed log
>   - BTRFS_ROOT_FORCE_COW: snapshot in progress
>   - BTRFS_HEADER_FLAG_RELOC: block being relocated
> 
> Signed-off-by: Leo Martins <loemra.dev@gmail.com>

Thanks! Looks good now. Currently we only early return true in 
should_cow_block(), making the overall logic easier to follow.

And the commit message and comments are quite detailed and helpful.

Reviewed-by: Sun YangKai <sunk67188@gmail.com>

> ---
>   fs/btrfs/ctree.c     | 56 ++++++++++++++++++++++++++++++++++++++------
>   fs/btrfs/disk-io.c   |  2 +-
>   fs/btrfs/extent_io.c |  4 ----
>   3 files changed, 50 insertions(+), 12 deletions(-)
> 
> diff --git a/fs/btrfs/ctree.c b/fs/btrfs/ctree.c
> index 7267b2502665..0e02b7b14adc 100644
> --- a/fs/btrfs/ctree.c
> +++ b/fs/btrfs/ctree.c
> @@ -599,9 +599,9 @@ int btrfs_force_cow_block(struct btrfs_trans_handle *trans,
>   	return ret;
>   }
>   
> -static inline bool should_cow_block(const struct btrfs_trans_handle *trans,
> +static inline bool should_cow_block(struct btrfs_trans_handle *trans,
>   				    const struct btrfs_root *root,
> -				    const struct extent_buffer *buf)
> +				    struct extent_buffer *buf)
>   {
>   	if (btrfs_is_testing(root->fs_info))
>   		return false;
> @@ -621,7 +621,11 @@ static inline bool should_cow_block(const struct btrfs_trans_handle *trans,
>   	if (btrfs_header_generation(buf) != trans->transid)
>   		return true;
>   
> -	if (btrfs_header_flag(buf, BTRFS_HEADER_FLAG_WRITTEN))
> +	if (test_bit(EXTENT_BUFFER_WRITEBACK, &buf->bflags))
> +		return true;
> +
> +	if (btrfs_root_id(root) != BTRFS_TREE_RELOC_OBJECTID &&
> +	    btrfs_header_flag(buf, BTRFS_HEADER_FLAG_RELOC))
>   		return true;
>   
>   	/* Ensure we can see the FORCE_COW bit. */
> @@ -629,11 +633,49 @@ static inline bool should_cow_block(const struct btrfs_trans_handle *trans,
>   	if (test_bit(BTRFS_ROOT_FORCE_COW, &root->state))
>   		return true;
>   
> -	if (btrfs_root_id(root) == BTRFS_TREE_RELOC_OBJECTID)
> -		return false;
> +	if (btrfs_header_flag(buf, BTRFS_HEADER_FLAG_WRITTEN)) {
> +		/*
> +		 * The buffer was allocated in this transaction and has been
> +		 * written back to disk (WRITTEN is set). Normally we'd COW
> +		 * it again, but since the committed superblock doesn't
> +		 * reference this buffer (it was allocated in this transaction),
> +		 * we can safely overwrite it in place.
> +		 *
> +		 * We keep BTRFS_HEADER_FLAG_WRITTEN set. The block has been
> +		 * persisted at this bytenr and will be again after the
> +		 * in-place update. This is important so that
> +		 * btrfs_free_tree_block() correctly pins the block if it is
> +		 * freed later (e.g., during tree rebalancing or FORCE_COW).
> +		 *
> +		 * Log trees and zoned devices cannot use this optimization:
> +		 * - Log trees: log blocks are written and immediately
> +		 *   referenced by a committed superblock via
> +		 *   btrfs_sync_log(), bypassing the normal transaction
> +		 *   commit. Overwriting in place could corrupt the
> +		 *   committed log.
> +		 * - Zoned devices: require sequential writes.
> +		 */
> +		if (btrfs_root_id(root) == BTRFS_TREE_LOG_OBJECTID ||
> +		    btrfs_is_zoned(root->fs_info))
> +			return true;
>   
> -	if (btrfs_header_flag(buf, BTRFS_HEADER_FLAG_RELOC))
> -		return true;
> +		/*
> +		 * Re-register this block's range in the current transaction's
> +		 * dirty_pages so that btrfs_write_and_wait_transaction()
> +		 * writes it. The range was originally registered when the
> +		 * block was allocated. Normally dirty_pages is only cleared
> +		 * at commit time by btrfs_write_and_wait_transaction(), but
> +		 * if qgroups are enabled and snapshots are being created,
> +		 * qgroup_account_snapshot() may have already called
> +		 * btrfs_write_and_wait_transaction() and released the range
> +		 * before the final commit-time call.
> +		 */
> +		btrfs_set_extent_bit(&trans->transaction->dirty_pages,
> +				     buf->start,
> +				     buf->start + buf->len - 1,
> +				     EXTENT_DIRTY, NULL);
> +		btrfs_mark_buffer_dirty(trans, buf);
> +	}
>   
>   	return false;
>   }
> diff --git a/fs/btrfs/disk-io.c b/fs/btrfs/disk-io.c
> index 32fffb0557e5..bee8f76fbfea 100644
> --- a/fs/btrfs/disk-io.c
> +++ b/fs/btrfs/disk-io.c
> @@ -4491,7 +4491,7 @@ void btrfs_mark_buffer_dirty(struct btrfs_trans_handle *trans,
>   #endif
>   	/* This is an active transaction (its state < TRANS_STATE_UNBLOCKED). */
>   	ASSERT(trans->transid == fs_info->generation);
> -	btrfs_assert_tree_write_locked(buf);
> +	lockdep_assert_held(&buf->lock);
>   	if (unlikely(transid != fs_info->generation)) {
>   		btrfs_abort_transaction(trans, -EUCLEAN);
>   		btrfs_crit(fs_info,
> diff --git a/fs/btrfs/extent_io.c b/fs/btrfs/extent_io.c
> index dfc17c292217..ff1fc699a6ca 100644
> --- a/fs/btrfs/extent_io.c
> +++ b/fs/btrfs/extent_io.c
> @@ -3791,10 +3791,6 @@ void set_extent_buffer_dirty(struct extent_buffer *eb)
>   					 eb->len,
>   					 eb->fs_info->dirty_metadata_batch);
>   	}
> -#ifdef CONFIG_BTRFS_DEBUG
> -	for (int i = 0; i < num_extent_folios(eb); i++)
> -		ASSERT(folio_test_dirty(eb->folios[i]));
> -#endif
>   }
>   
>   void clear_extent_buffer_uptodate(struct extent_buffer *eb)


From mboxrd@z Thu Jan  1 00:00:00 1970
Received: from mail-wm1-f67.google.com (mail-wm1-f67.google.com [209.85.128.67])
	(using TLSv1.2 with cipher ECDHE-RSA-AES128-GCM-SHA256 (128/128 bits))
	(No client certificate requested)
	by smtp.subspace.kernel.org (Postfix) with ESMTPS id 8ACDC199FD3
	for <linux-btrfs@vger.kernel.org>; Wed, 25 Feb 2026 06:17:38 +0000 (UTC)
Authentication-Results: smtp.subspace.kernel.org; arc=none smtp.client-ip=209.85.128.67
ARC-Seal:i=1; a=rsa-sha256; d=subspace.kernel.org; s=arc-20240116;
	t=1772000260; cv=none; b=ABm3+DzW2JxTWdD6o371+TC2S/Am5JACwbtO+nWDZpvJ+wHOLOZz42ZKUUSHhUiQZGfsHl9IN2QjGf+pzGIj5lzFWnJsL3QyLJ/jg+uzQg2OqK4twx3MSfX6dhrfVKLmHlFjPd3ATy8WHdP96Nd6oZs/wR6tav2x17TPzb0NUyI=
ARC-Message-Signature:i=1; a=rsa-sha256; d=subspace.kernel.org;
	s=arc-20240116; t=1772000260; c=relaxed/simple;
	bh=pvr9qH2bqbP35Wd+owJmbTZ7Fg+AL1bHfKgrCXn3IRc=;
	h=Message-ID:Date:MIME-Version:Subject:To:Cc:References:From:
	 In-Reply-To:Content-Type; b=YEMxGZzfrusXQCURKr24Una/KsTHaHUwpb/if/h9zYsXfnpN+fXWmoFruJ4SupirE1gQKKinCLwungpgOLxdjdEWC4MOxs9PRrET2kNzKW72VkugM0Ycb2gN5+U+RK4S5nZ9IHptNlJgV9T/6HKYb/UUKa7adN8VhQjVBCHvMKU=
ARC-Authentication-Results:i=1; smtp.subspace.kernel.org; dmarc=pass (p=none dis=none) header.from=gmail.com; spf=pass smtp.mailfrom=gmail.com; dkim=pass (2048-bit key) header.d=gmail.com header.i=@gmail.com header.b=PnrLYDYC; arc=none smtp.client-ip=209.85.128.67
Authentication-Results: smtp.subspace.kernel.org; dmarc=pass (p=none dis=none) header.from=gmail.com
Authentication-Results: smtp.subspace.kernel.org; spf=pass smtp.mailfrom=gmail.com
Authentication-Results: smtp.subspace.kernel.org;
	dkim=pass (2048-bit key) header.d=gmail.com header.i=@gmail.com header.b="PnrLYDYC"
Received: by mail-wm1-f67.google.com with SMTP id 5b1f17b1804b1-483ad568d68so4500615e9.2
        for <linux-btrfs@vger.kernel.org>; Tue, 24 Feb 2026 22:17:38 -0800 (PST)
DKIM-Signature: v=1; a=rsa-sha256; c=relaxed/relaxed;
        d=gmail.com; s=20230601; t=1772000257; x=1772605057; darn=vger.kernel.org;
        h=content-transfer-encoding:in-reply-to:from:content-language
         :references:cc:to:subject:user-agent:mime-version:date:message-id
         :from:to:cc:subject:date:message-id:reply-to;
        bh=9Lml4s3WkJnBnwOnwr+d0MU68jowa/Sft8FTejev/Bo=;
        b=PnrLYDYCcq80oiRXhwVKJgW5hUNdOVMtJc2cxRUOA6lJs2p4wVsdtXZ6vbmpPsZqur
         hqoHjU+ep+UG3o1Iddq3in0IHoezViYXXwrLiD55Gu4ota/2ek3PsDvBF/RnePlyFlDj
         zTDBXzisCVqMuBzX277KGGEm4uR9Oj85hilz0OdbwD0owYr5sp0MncRBduQsLtuFR76Q
         oDIBVEDfYCvZl+LNF+a1SdCJetwR1MLi0vSKZSmaLCcUDOpMHuxp2fo++NHlBZeKnmbd
         SHK+PFPCV3I2wFn8Wok5ffIgp9aSzzbL+ZSbwBjxJFGnKG0rC8/c5xieL/C/LmNV57Wi
         lQAQ==
X-Google-DKIM-Signature: v=1; a=rsa-sha256; c=relaxed/relaxed;
        d=1e100.net; s=20230601; t=1772000257; x=1772605057;
        h=content-transfer-encoding:in-reply-to:from:content-language
         :references:cc:to:subject:user-agent:mime-version:date:message-id
         :x-gm-gg:x-gm-message-state:from:to:cc:subject:date:message-id
         :reply-to;
        bh=9Lml4s3WkJnBnwOnwr+d0MU68jowa/Sft8FTejev/Bo=;
        b=GYxciyB9QvI0TykKeUkaTnfJaJRnmsYiY4IH5PxuzvqZXH0LqRGhsWvFnI9AMkXeEG
         IwFhh2SpPHFnKh52uY2q+OZLRxUWA/OeDWKbh5NRPMThWrcqGmSSNGpOPi6PHmDDqyVI
         HatbENq1Wveh1c/GPPpkPZFVeBr7AByfvqkhRsA7F5tXcm1cE4nn9OP2Vsb0UljdVsfs
         R5s9orRlEQdaYj+C6CXzzbHklTORhdaELcoK/+GedWYPSv3KLPu/LLcKwQHJbYu/J1BL
         r3MnKX3nLrxq2TOGGAUySIzOjWYF2CijOkrjVfUegLBf2PzsewPCfOgoWW7au7Yzz+qg
         ABkQ==
X-Forwarded-Encrypted: i=1; AJvYcCVFCjS1jisONhgcazQtHSXndYbOwHwLL6oBbqgSLgnSJT1vyCqIOi5a889b90cf8zdy9O0U83BMjM/B4w==@vger.kernel.org
X-Gm-Message-State: AOJu0YwfRnQDfuJuSgoGIU6BlVS6Sbjli4X/n27ls1APRcXUZLf3AevW
	YHB4CcdWHIQIGv6BDM1ko28+1FIUDkPE1VQEBhyOBIwOn4iX06i+9G70+B+LdYvRV+XjiA==
X-Gm-Gg: ATEYQzxpXLrzVlEolFLZ8tOwtewfyqXbf4xQT5OcwSkRTyyZamBI6kx++j5aqXDM/ww
	tp38/Jb/hQctMHveGN/RFxbGnxsjdkjCxbQsnQARNfiE5p8enlF/QZkkFVS/vaP7lE0Tnzzh5AR
	IPCWf1NCXm/5hDsWZ+IzaXXk7rbQQQEzC019lByqTBKXQQZtYkkfKll46eDOoWLqq35+9ARGCv4
	Jor8XO6o4Dy9L8HjAblNNr7vmT445o0SKRL7p2yuzVi0Cs1KMueHyrVfPUYPjrVDJnii8jdXE10
	ds1oOAvTRTLxjsWVE16KPP766NKuGVv7gNYBxy99uuZhuX5hAlETzYBrK0ErrKPNHGjmG697VuV
	StW8vmUCiJDXOOaI8hzyStuWhVzqdlPuNQbhYZ3wFpvkf1hr67fF7zoA6wlOLUWBjNfP//3u6ue
	ek/17zSjpVM3Cw/JL207Ps7MHkTQ3G9CegirpSNZPqVu5NHazYMLAjKDnW48/kPPCkUqXfQBFIY
	mu4fA==
X-Received: by 2002:a05:6000:2008:b0:439:909f:c596 with SMTP id ffacd0b85a97d-439909fc8e6mr577356f8f.2.1772000256719;
        Tue, 24 Feb 2026 22:17:36 -0800 (PST)
Received: from ?IPV6:2408:8239:502:5512:18a9:8974:b792:9a40? ([140.238.217.67])
        by smtp.gmail.com with ESMTPSA id ffacd0b85a97d-43987f3ed03sm10811221f8f.16.2026.02.24.22.17.30
        (version=TLS1_3 cipher=TLS_AES_128_GCM_SHA256 bits=128/128);
        Tue, 24 Feb 2026 22:17:36 -0800 (PST)
Message-ID: <8b4f2be8-169d-4e10-a47f-b534ab0ebac6@gmail.com>
Date: Wed, 25 Feb 2026 14:17:24 +0800
Precedence: bulk
X-Mailing-List: linux-btrfs@vger.kernel.org
List-Id: <linux-btrfs.vger.kernel.org>
List-Subscribe: <mailto:linux-btrfs+subscribe@vger.kernel.org>
List-Unsubscribe: <mailto:linux-btrfs+unsubscribe@vger.kernel.org>
MIME-Version: 1.0
User-Agent: Mozilla Thunderbird
Subject: Re: [PATCH v3 2/3] btrfs: inhibit extent buffer writeback to prevent
 COW amplification
To: Leo Martins <loemra.dev@gmail.com>, linux-btrfs@vger.kernel.org,
 kernel-team@fb.com
Cc: Filipe Manana <fdmanana@suse.com>
References: <cover.1771884128.git.loemra.dev@gmail.com>
 <cc847a35e26cc4dfad18c59e3c525cea507ff440.1771884128.git.loemra.dev@gmail.com>
Content-Language: en-US
From: Sun YangKai <sunk67188@gmail.com>
In-Reply-To: <cc847a35e26cc4dfad18c59e3c525cea507ff440.1771884128.git.loemra.dev@gmail.com>
Content-Type: text/plain; charset=UTF-8; format=flowed
Content-Transfer-Encoding: 7bit



On 2026/2/25 03:22, Leo Martins wrote:
> Inhibit writeback on COW'd extent buffers for the lifetime of the
> transaction handle, preventing background writeback from setting
> BTRFS_HEADER_FLAG_WRITTEN and causing unnecessary re-COW.
> 
> COW amplification occurs when background writeback flushes an extent
> buffer that a transaction handle is still actively modifying. When
> lock_extent_buffer_for_io() transitions a buffer from dirty to
> writeback, it sets BTRFS_HEADER_FLAG_WRITTEN, marking the block as
> having been persisted to disk at its current bytenr. Once WRITTEN is
> set, should_cow_block() must either COW the block again or overwrite
> it in place, both of which are unnecessary overhead when the buffer
> is still being modified by the same handle that allocated it. By
> inhibiting background writeback on actively-used buffers, WRITTEN is
> never set while a transaction handle holds a reference to the buffer,
> avoiding this overhead entirely.
> 
> Add an atomic_t writeback_inhibitors counter to struct extent_buffer,
> which fits in an existing 6-byte hole without increasing struct size.
> When a buffer is COW'd in btrfs_force_cow_block(), call
> btrfs_inhibit_eb_writeback() to store the eb in the transaction
> handle's writeback_inhibited_ebs xarray (keyed by eb->start), take a
> reference, and increment writeback_inhibitors. The function handles
> dedup (same eb inhibited twice by the same handle) and replacement
> (different eb at the same logical address). Allocation failure is
> graceful: the buffer simply falls back to the pre-existing behavior
> where it may be written back and re-COW'd.
> 
> In lock_extent_buffer_for_io(), when writeback_inhibitors is non-zero
> and the writeback mode is WB_SYNC_NONE, skip the buffer. WB_SYNC_NONE
> is used by the VM flusher threads for background and periodic
> writeback, which are the only paths that cause COW amplification by
> opportunistically writing out dirty extent buffers mid-transaction.
> Skipping these is safe because the buffers remain dirty in the page
> cache and will be written out at transaction commit time.
> 
> WB_SYNC_ALL must always proceed regardless of writeback_inhibitors.
> This is required for correctness in the fsync path: btrfs_sync_log()
> writes log tree blocks via filemap_fdatawrite_range() (WB_SYNC_ALL)
> while the transaction handle that inhibited those same blocks is still
> active. Without the WB_SYNC_ALL bypass, those inhibited log tree
> blocks would be silently skipped, resulting in an incomplete log on
> disk and corruption on replay. btrfs_write_and_wait_transaction()
> also uses WB_SYNC_ALL via filemap_fdatawrite_range(); for that path,
> inhibitors are already cleared beforehand, but the bypass ensures
> correctness regardless.
> 
> Uninhibit in __btrfs_end_transaction() before atomic_dec(num_writers)
> to prevent a race where the committer proceeds while buffers are still
> inhibited. Also uninhibit in btrfs_commit_transaction() before writing
> and in cleanup_transaction() for the error path.
> 
> Signed-off-by: Leo Martins <loemra.dev@gmail.com>
> Reviewed-by: Filipe Manana <fdmanana@suse.com>
> ---
>   fs/btrfs/ctree.c       |  4 +++
>   fs/btrfs/extent_io.c   | 63 +++++++++++++++++++++++++++++++++++++++++-
>   fs/btrfs/extent_io.h   |  6 ++++
>   fs/btrfs/transaction.c | 19 +++++++++++++
>   fs/btrfs/transaction.h |  3 ++
>   5 files changed, 94 insertions(+), 1 deletion(-)
> 
> diff --git a/fs/btrfs/ctree.c b/fs/btrfs/ctree.c
> index 0e02b7b14adc..d4da65bb9096 100644
> --- a/fs/btrfs/ctree.c
> +++ b/fs/btrfs/ctree.c
> @@ -590,6 +590,10 @@ int btrfs_force_cow_block(struct btrfs_trans_handle *trans,
>   		btrfs_tree_unlock(buf);
>   	free_extent_buffer_stale(buf);
>   	btrfs_mark_buffer_dirty(trans, cow);
> +
> +	/* Inhibit writeback on the COW'd buffer for this transaction handle. */
> +	btrfs_inhibit_eb_writeback(trans, cow);
> +
>   	*cow_ret = cow;
>   	return 0;
>   
> diff --git a/fs/btrfs/extent_io.c b/fs/btrfs/extent_io.c
> index ff1fc699a6ca..e04e42a81978 100644
> --- a/fs/btrfs/extent_io.c
> +++ b/fs/btrfs/extent_io.c
> @@ -1940,7 +1940,9 @@ static noinline_for_stack bool lock_extent_buffer_for_io(struct extent_buffer *e
>   	 * of time.
>   	 */
>   	spin_lock(&eb->refs_lock);
> -	if (test_and_clear_bit(EXTENT_BUFFER_DIRTY, &eb->bflags)) {
> +	if ((wbc->sync_mode == WB_SYNC_ALL ||
> +	     atomic_read(&eb->writeback_inhibitors) == 0) &&
> +	    test_and_clear_bit(EXTENT_BUFFER_DIRTY, &eb->bflags)) {
>   		XA_STATE(xas, &fs_info->buffer_tree, eb->start >> fs_info->nodesize_bits);
>   		unsigned long flags;
>   
> @@ -2999,6 +3001,64 @@ static inline void btrfs_release_extent_buffer(struct extent_buffer *eb)
>   	kmem_cache_free(extent_buffer_cache, eb);
>   }
>   
> +/*
> + * btrfs_inhibit_eb_writeback - Inhibit writeback on buffer during transaction.
> + * @trans: transaction handle that will own the inhibitor
> + * @eb: extent buffer to inhibit writeback on
> + *
> + * Attempts 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.
> + */
> +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;
> +
> +	/* 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;
> +	}
> +
> +	/* Handle replacement of different eb at same index. */
> +	if (old && old != eb) {
> +		struct extent_buffer *old_eb = old;
> +
> +		atomic_dec(&old_eb->writeback_inhibitors);
> +		free_extent_buffer(old_eb);
> +	}
> +
> +	atomic_inc(&eb->writeback_inhibitors);
> +}
> +
> +/*
> + * btrfs_uninhibit_all_eb_writeback - Uninhibit writeback on all buffers.
> + * @trans: transaction handle to clean up
> + */
> +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);
> +	}
> +	xa_destroy(&trans->writeback_inhibited_ebs);
> +}
> +
>   static struct extent_buffer *__alloc_extent_buffer(struct btrfs_fs_info *fs_info,
>   						   u64 start)
>   {
> @@ -3009,6 +3069,7 @@ static struct extent_buffer *__alloc_extent_buffer(struct btrfs_fs_info *fs_info
>   	eb->len = fs_info->nodesize;
>   	eb->fs_info = fs_info;
>   	init_rwsem(&eb->lock);
> +	atomic_set(&eb->writeback_inhibitors, 0);
>   
>   	btrfs_leak_debug_add_eb(eb);
>   
> diff --git a/fs/btrfs/extent_io.h b/fs/btrfs/extent_io.h
> index 73571d5d3d5a..fb68fbd4866c 100644
> --- a/fs/btrfs/extent_io.h
> +++ b/fs/btrfs/extent_io.h
> @@ -102,6 +102,8 @@ struct extent_buffer {
>   	/* >= 0 if eb belongs to a log tree, -1 otherwise */
>   	s8 log_index;
>   	u8 folio_shift;
> +	/* Inhibits WB_SYNC_NONE writeback when > 0. */
> +	atomic_t writeback_inhibitors;

I might be missing something here, but I'm curious whether this atomic 
counter can ever go above 1. If not, and it's strictly binary, perhaps 
using atomic_set(1/0) instead of atomic_inc/dec would make the intent 
clearer?

Otherwise looks good. Thanks.

>   	struct rcu_head rcu_head;
>   
>   	struct rw_semaphore lock;
> @@ -381,4 +383,8 @@ void btrfs_extent_buffer_leak_debug_check(struct btrfs_fs_info *fs_info);
>   #define btrfs_extent_buffer_leak_debug_check(fs_info)	do {} while (0)
>   #endif
>   
> +void btrfs_inhibit_eb_writeback(struct btrfs_trans_handle *trans,
> +			       struct extent_buffer *eb);
> +void btrfs_uninhibit_all_eb_writeback(struct btrfs_trans_handle *trans);
> +
>   #endif
> diff --git a/fs/btrfs/transaction.c b/fs/btrfs/transaction.c
> index f4cc9e1a1b93..a9a22629b49d 100644
> --- a/fs/btrfs/transaction.c
> +++ b/fs/btrfs/transaction.c
> @@ -15,6 +15,7 @@
>   #include "misc.h"
>   #include "ctree.h"
>   #include "disk-io.h"
> +#include "extent_io.h"
>   #include "transaction.h"
>   #include "locking.h"
>   #include "tree-log.h"
> @@ -688,6 +689,8 @@ 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
> @@ -1083,6 +1086,13 @@ static int __btrfs_end_transaction(struct btrfs_trans_handle *trans,
>   	if (trans->type & __TRANS_FREEZABLE)
>   		sb_end_intwrite(info->sb);
>   
> +	/*
> +	 * Uninhibit extent buffer writeback before decrementing num_writers,
> +	 * since the decrement wakes the committing thread which needs all
> +	 * buffers uninhibited to write them to disk.
> +	 */
> +	btrfs_uninhibit_all_eb_writeback(trans);
> +
>   	WARN_ON(cur_trans != info->running_transaction);
>   	WARN_ON(atomic_read(&cur_trans->num_writers) < 1);
>   	atomic_dec(&cur_trans->num_writers);
> @@ -2110,6 +2120,7 @@ static void cleanup_transaction(struct btrfs_trans_handle *trans, int err)
>   	if (!test_bit(BTRFS_FS_RELOC_RUNNING, &fs_info->flags))
>   		btrfs_scrub_cancel(fs_info);
>   
> +	btrfs_uninhibit_all_eb_writeback(trans);
>   	kmem_cache_free(btrfs_trans_handle_cachep, trans);
>   }
>   
> @@ -2556,6 +2567,14 @@ int btrfs_commit_transaction(struct btrfs_trans_handle *trans)
>   	    fs_info->cleaner_kthread)
>   		wake_up_process(fs_info->cleaner_kthread);
>   
> +	/*
> +	 * Uninhibit writeback on all extent buffers inhibited during this
> +	 * transaction before writing them to disk. Inhibiting prevented
> +	 * writeback while the transaction was building, but now we need
> +	 * them written.
> +	 */
> +	btrfs_uninhibit_all_eb_writeback(trans);
> +
>   	ret = btrfs_write_and_wait_transaction(trans);
>   	if (unlikely(ret)) {
>   		btrfs_err(fs_info, "error while writing out transaction: %d", ret);
> diff --git a/fs/btrfs/transaction.h b/fs/btrfs/transaction.h
> index 18ef069197e5..7d70fe486758 100644
> --- a/fs/btrfs/transaction.h
> +++ b/fs/btrfs/transaction.h
> @@ -12,6 +12,7 @@
>   #include <linux/time64.h>
>   #include <linux/mutex.h>
>   #include <linux/wait.h>
> +#include <linux/xarray.h>
>   #include "btrfs_inode.h"
>   #include "delayed-ref.h"
>   
> @@ -162,6 +163,8 @@ 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;
>   };
>   
>   /*


From mboxrd@z Thu Jan  1 00:00:00 1970
Received: from smtp.kernel.org (aws-us-west-2-korg-mail-1.web.codeaurora.org [10.30.226.201])
	(using TLSv1.2 with cipher ECDHE-RSA-AES256-GCM-SHA384 (256/256 bits))
	(No client certificate requested)
	by smtp.subspace.kernel.org (Postfix) with ESMTPS id 0A9A22C17B3
	for <linux-btrfs@vger.kernel.org>; Wed, 25 Feb 2026 17:39:30 +0000 (UTC)
Authentication-Results: smtp.subspace.kernel.org; arc=none smtp.client-ip=10.30.226.201
ARC-Seal:i=1; a=rsa-sha256; d=subspace.kernel.org; s=arc-20240116;
	t=1772041171; cv=none; b=FQ0aCFFYNsT8bxojfubMblh6F8grEge95+Yno+RwbYbCGcuLrznCgm8piZuzqbH1fBYGh4fM6rXge3MAkxxjgyFxRqqV2oAfOzpKXDgAomaRmPTJYnSQz/CzPxgWTgF4Znoq1PF4xwopxtXXfEPP85v3DFmIrXG4g7PQ4K/ivC0=
ARC-Message-Signature:i=1; a=rsa-sha256; d=subspace.kernel.org;
	s=arc-20240116; t=1772041171; c=relaxed/simple;
	bh=swwpQKwnkQkPTQNBEKvWuoVM9KQC3eAiz7jBSp7zTE8=;
	h=MIME-Version:References:In-Reply-To:From:Date:Message-ID:Subject:
	 To:Cc:Content-Type; b=h5o5objXScKNd8ebWet7uG7DpabsGK8fDwsbArN9PyWLh40bEJhFfjwmSJADwl8TKUHBIGnUu1JXBH3h38MewQPdAPk3wVV9momUgfZdBtvTb5QUvOQWRe6GT5WgXWhjE5LA63E9viiGqbw3/PzBVuRW8FEubD5ODcYW2+dvJFk=
ARC-Authentication-Results:i=1; smtp.subspace.kernel.org; dkim=pass (2048-bit key) header.d=kernel.org header.i=@kernel.org header.b=tSih/S32; arc=none smtp.client-ip=10.30.226.201
Authentication-Results: smtp.subspace.kernel.org;
	dkim=pass (2048-bit key) header.d=kernel.org header.i=@kernel.org header.b="tSih/S32"
Received: by smtp.kernel.org (Postfix) with ESMTPSA id 898F4C19421
	for <linux-btrfs@vger.kernel.org>; Wed, 25 Feb 2026 17:39:30 +0000 (UTC)
DKIM-Signature: v=1; a=rsa-sha256; c=relaxed/simple; d=kernel.org;
	s=k20201202; t=1772041170;
	bh=swwpQKwnkQkPTQNBEKvWuoVM9KQC3eAiz7jBSp7zTE8=;
	h=References:In-Reply-To:From:Date:Subject:To:Cc:From;
	b=tSih/S32orSKV8D1HCphBnCUqKOA0GFwNa/QSV7VLuQ3JzY5+Qaqts+MjX3ReWXKd
	 OmtwjdOI8CIDjSF/WqlPKTacyXvu8sivDm0AaSmWvR/7jk5SRcM2BdSHia7Figxeo+
	 aVc8zwURCxm2IVTucS86d4wU+EOW9rDwBr7B4VgfHLWvmopnddcU7kBIp1tQXeWYUW
	 VakYcaDnXtwdiWSJmU2YOf06zA3ouQ+bmvjJnjfQqQf+clOuP3XC2JjOUa9MlTgGOn
	 y2APjEyu9MVZZ/QUR+CGKpBgY9I1mFvdapaBEtyq/3yq0pI9Kh+BypdUcMG6ao37aw
	 nciW3o/Oq3/+w==
Received: by mail-ej1-f53.google.com with SMTP id a640c23a62f3a-b8fb6ad3243so981122166b.1
        for <linux-btrfs@vger.kernel.org>; Wed, 25 Feb 2026 09:39:30 -0800 (PST)
X-Gm-Message-State: AOJu0YxjSXSnbTRJAhkY1mOqZwdoFMuJ1TP0k0GJ7zfB0rcs5sdlnTCq
	FAE+YYi5LHZjDfsmh03THqAX0a/NPjRm2/b/mVri9rL7NkGUlr/aHFlyIs0ayjvK7afA0/E7vxC
	M9FE3MalHZA6H0SL+OXAByq6F/JoIvM4=
X-Received: by 2002:a17:907:d0a:b0:b73:572d:3b07 with SMTP id
 a640c23a62f3a-b9081b3b176mr1236731066b.28.1772041169045; Wed, 25 Feb 2026
 09:39:29 -0800 (PST)
Precedence: bulk
X-Mailing-List: linux-btrfs@vger.kernel.org
List-Id: <linux-btrfs.vger.kernel.org>
List-Subscribe: <mailto:linux-btrfs+subscribe@vger.kernel.org>
List-Unsubscribe: <mailto:linux-btrfs+unsubscribe@vger.kernel.org>
MIME-Version: 1.0
References: <cover.1771884128.git.loemra.dev@gmail.com>
In-Reply-To: <cover.1771884128.git.loemra.dev@gmail.com>
From: Filipe Manana <fdmanana@kernel.org>
Date: Wed, 25 Feb 2026 17:38:52 +0000
X-Gmail-Original-Message-ID: <CAL3q7H5RAa72uLEktuTiQ-gWHQuZ8_q_zY7cKT5xBjVkyruELA@mail.gmail.com>
X-Gm-Features: AaiRm51ba5nAKe3bldGDsQX3XiY1sTSNr5DsQrcrIMjsgr16LbVhWZF4b1uwiaI
Message-ID: <CAL3q7H5RAa72uLEktuTiQ-gWHQuZ8_q_zY7cKT5xBjVkyruELA@mail.gmail.com>
Subject: Re: [PATCH v3 0/3] btrfs: fix COW amplification under memory pressure
To: Leo Martins <loemra.dev@gmail.com>
Cc: linux-btrfs@vger.kernel.org, kernel-team@fb.com
Content-Type: text/plain; charset="UTF-8"
Content-Transfer-Encoding: quoted-printable

On Tue, Feb 24, 2026 at 7:23=E2=80=AFPM Leo Martins <loemra.dev@gmail.com> =
wrote:
>
> I've been investigating a pattern where COW amplification under memory
> pressure exhausts the transaction block reserve, leading to spurious
> ENOSPCs on filesystems with plenty of unallocated space.
>
> The pattern is:
>
>  1. btrfs_search_slot() begins tree traversal with cow=3D1
>  2. Node at level N needs COW (old generation or WRITTEN flag set)
>  3. btrfs_cow_block() allocates new block, copies data, updates
>     parent pointer
>  4. Traversal hits a condition requiring restart (node not cached,
>     lock contention, need higher write_lock_level)
>  5. btrfs_release_path() releases all locks and references
>  6. Memory pressure triggers writeback on the COW'd block
>  7. lock_extent_buffer_for_io() clears EXTENT_BUFFER_DIRTY and sets
>     BTRFS_HEADER_FLAG_WRITTEN
>  8. goto again - traversal restarts from root
>  9. Traversal reaches the same tree node
>  10. should_cow_block() sees WRITTEN flag, returns true
>  11. btrfs_cow_block() allocates yet another new block, copies data,
>      updates parent pointer again, consuming another reservation
>  12. Steps 4-11 repeat under sustained memory pressure
>
> This series fixes the problem with two complementary approaches:
>
> Patch 1 implements Filipe's suggestion of overwriting in place. When
> should_cow_block() encounters a WRITTEN buffer whose generation matches
> the current transaction, it re-dirties the buffer and returns false
> instead of requesting a COW. This is crash-safe because the committed
> superblock does not reference buffers allocated in the current
> transaction. Log trees, zoned devices, FORCE_COW, relocation, and
> buffers mid-writeback are excluded. Beyond improving the amplification bu=
g,
> this is a general optimization for the entire transaction lifetime: any
> time writeback runs during a transaction, revisiting the same path no
> longer triggers unnecessary COW, reducing extent allocation overhead,
> memory copies, and space usage per transaction.
>
> Patch 2 inhibits writeback on COW'd buffers for the lifetime of the
> transaction handle. This prevents WRITTEN from being set while a
> handle holds a reference to the buffer, avoiding unnecessary re-COW
> at its source. Only WB_SYNC_NONE (background/periodic flusher
> writeback) is inhibited. WB_SYNC_ALL (data-integrity writeback from
> btrfs_sync_log() and btrfs_write_and_wait_transaction()) always
> proceeds, which is required for correctness in the fsync path where
> log tree blocks must be written while the inhibiting handle is still
> active.
>
> Both approaches are independently useful. The overwrite path is a
> general optimization that eliminates unnecessary COW across the entire
> transaction, not just within a single handle. Writeback inhibition
> prevents the problem from occurring in the first place and is the
> only fix for log trees and zoned devices where overwrite does not
> apply. Together they provide defense in depth against COW
> amplification.
>
> Patch 3 adds a tracepoint for tracking search slot restarts.
>
> Note: Boris's AS_KERNEL_FILE changes prevent cgroup-scoped reclaim
> from targeting extent buffer pages, making this harder to trigger via
> per-cgroup memory limits. I was able to reproduce the amplification
> using global memory pressure (drop_caches, root cgroup memory.reclaim,
> memory hog) with concurrent filesystem operations.
>
> Benchmark: concurrent filesystem operations under aggressive global
> memory pressure. COW counts measured via bpftrace.
>
>   Approach                   Max COW count   Num operations
>   -------                    -------------   --------------
>   Baseline                              35   -
>   Overwrite only                        19   ~same
>   Writeback inhibition only              5   ~2x
>   Combined (this series)                 4   ~2x
>
> Changes since v2:
>
> Patch 1:
>  - Add smp_mb__before_atomic() before FORCE_COW check (Filipe, Sun YangKa=
i)
>  - Hoist WRITEBACK, RELOC, FORCE_COW checks before WRITTEN block (Sun Yan=
gKai)
>  - Update dirty_pages comment for qgroup_account_snapshot() (Filipe)
>  - Relax btrfs_mark_buffer_dirty() lockdep assertion to accept read locks
>  - Use btrfs_mark_buffer_dirty() instead of set_extent_buffer_dirty()
>  - Remove folio_test_dirty() debug assertion (concurrent reader race)
>
> Patch 2:
>  - Fix comment style (Filipe)
>
> Patch 3:
>  - Replace counters with per-restart-site tracepoint (Filipe)
>
> Changes since v1:
>
> The v1 patch used a per-btrfs_search_slot() xarray to track COW'd
> buffers. Filipe pointed out this was too complex and too narrow in
> scope, and suggested overwriting in place instead. Qu raised the
> concern that other btrfs_cow_block() callers outside of search_slot
> would not be covered. Boris suggested transaction-handle-level scope
> as an alternative.
>
> v2 implemented both overwrite-in-place and writeback inhibition at
> the transaction handle level. The per-search-slot xarray was replaced
> by a per-transaction-handle xarray, which covers all
> btrfs_force_cow_block() callers.
>
> Leo Martins (3):
>   btrfs: skip COW for written extent buffers allocated in current
>     transaction
>   btrfs: inhibit extent buffer writeback to prevent COW amplification
>   btrfs: add tracepoint for search slot restart tracking

You can add to all the patches:

Reviewed-by: Filipe Manana <fdmanana@suse.com>

Thanks.

>
>  fs/btrfs/ctree.c             | 70 +++++++++++++++++++++++++++++++-----
>  fs/btrfs/disk-io.c           |  2 +-
>  fs/btrfs/extent_io.c         | 67 +++++++++++++++++++++++++++++++---
>  fs/btrfs/extent_io.h         |  6 ++++
>  fs/btrfs/transaction.c       | 19 ++++++++++
>  fs/btrfs/transaction.h       |  3 ++
>  include/trace/events/btrfs.h | 24 +++++++++++++
>  7 files changed, 176 insertions(+), 15 deletions(-)
>
> --
> 2.47.3
>
>

From mboxrd@z Thu Jan  1 00:00:00 1970
Received: from mail-yx1-f66.google.com (mail-yx1-f66.google.com [74.125.224.66])
	(using TLSv1.2 with cipher ECDHE-RSA-AES128-GCM-SHA256 (128/128 bits))
	(No client certificate requested)
	by smtp.subspace.kernel.org (Postfix) with ESMTPS id D8F412C859
	for <linux-btrfs@vger.kernel.org>; Thu, 26 Feb 2026 02:30:54 +0000 (UTC)
Authentication-Results: smtp.subspace.kernel.org; arc=none smtp.client-ip=74.125.224.66
ARC-Seal:i=1; a=rsa-sha256; d=subspace.kernel.org; s=arc-20240116;
	t=1772073056; cv=none; b=NtGouO5mp8Ju1PKi5bkbFFbOgvjyiSqhI9Rjcd/fOf5alUHwnvSgavDbQqzxAbNYKgoaCzGTQxc1zOToyn9/YaVYdu8rv+kVltY5nR69kimVoI2Zw/wEBYZyrrepH8Dw4BbCYovo4hWlPitETwBhwqqNNBWbGJVu+rr6yE79JIY=
ARC-Message-Signature:i=1; a=rsa-sha256; d=subspace.kernel.org;
	s=arc-20240116; t=1772073056; c=relaxed/simple;
	bh=f7FsqIVd9aJslHk5+ve1BohHw6jwbtkBLPLp3d3573c=;
	h=From:To:Cc:Subject:Date:Message-ID:In-Reply-To:References:
	 MIME-Version; b=gTo7FQpM7MG1cOReSgojkjzS6eX5ItTmOaCGxNGg2x7xbBxJGBkZpjTlZO/RjOWljXfsiPxiNc8BH/YUfrC6UIH+BQQu27n/3CAEiloF5xDSK7p6kPMSnc1XSFEh+nINaFpBWcoqRr/Psg3/RRLvKLxVgXii0r7sl66uY1hpSq4=
ARC-Authentication-Results:i=1; smtp.subspace.kernel.org; dmarc=pass (p=none dis=none) header.from=gmail.com; spf=pass smtp.mailfrom=gmail.com; dkim=pass (2048-bit key) header.d=gmail.com header.i=@gmail.com header.b=OuoWy9ft; arc=none smtp.client-ip=74.125.224.66
Authentication-Results: smtp.subspace.kernel.org; dmarc=pass (p=none dis=none) header.from=gmail.com
Authentication-Results: smtp.subspace.kernel.org; spf=pass smtp.mailfrom=gmail.com
Authentication-Results: smtp.subspace.kernel.org;
	dkim=pass (2048-bit key) header.d=gmail.com header.i=@gmail.com header.b="OuoWy9ft"
Received: by mail-yx1-f66.google.com with SMTP id 956f58d0204a3-64cb577e79cso330684d50.0
        for <linux-btrfs@vger.kernel.org>; Wed, 25 Feb 2026 18:30:54 -0800 (PST)
DKIM-Signature: v=1; a=rsa-sha256; c=relaxed/relaxed;
        d=gmail.com; s=20230601; t=1772073054; x=1772677854; darn=vger.kernel.org;
        h=content-transfer-encoding:mime-version:references:in-reply-to
         :message-id:date:subject:cc:to:from:from:to:cc:subject:date
         :message-id:reply-to;
        bh=Fniyo8nMFXcZyk2Mf81kadnn7U5/qe6oXznFmSRSkZA=;
        b=OuoWy9ftYdOpfvYerYOyeP17UkFWQPiUx5fwDbA2+I/zR8lSuc6eJBEn4aW7xTocyC
         tPBskj/JMqyNMSIoPL7wKnsD8ctFufDbzLkNhjll+R+M8wHaeR4w4x5qZdw7zkJwL38A
         REnt13XfAYIl+oCD5noAXy/83uh9hG48YtD+t6K6FjrdprkNR8F/uvm+ZaIlKFWb5KAx
         WC5ePv6/P+av6YQPDeaqZ6vKxiCgFbghLNv8qKE9Pu2Mj0D4OjFokewfSQQ1ajzJu0Ce
         /bVl0/c8kAFN6UTYpy88ln/B2RP7XNU1gElyrqEEkNCk2Ts5GaTXHB7dvlRIlcDPJWvl
         /jwg==
X-Google-DKIM-Signature: v=1; a=rsa-sha256; c=relaxed/relaxed;
        d=1e100.net; s=20230601; t=1772073054; x=1772677854;
        h=content-transfer-encoding:mime-version:references:in-reply-to
         :message-id:date:subject:cc:to:from:x-gm-gg:x-gm-message-state:from
         :to:cc:subject:date:message-id:reply-to;
        bh=Fniyo8nMFXcZyk2Mf81kadnn7U5/qe6oXznFmSRSkZA=;
        b=QIA+5zUUPPqkER7oBqjRK9O96JySYF6yO15yt8VmbewHvEiank+urWpy+0bMPFUMex
         xC0ZPu0g5DpgC2MWgYY6vg8TqESw0hh9y5Bl6g7OVFV2OZ4LIFU9FGfp0Giugmkzpni4
         gUQCX8EPq+YSk9EMhMOaEEvA4H2NHeibdzqCkdnzFORtcpcl1ux5ZUp+JESk4hRNTkMW
         j0+4MzsrG9vUxDDjOA34ptej0pIgX/Lev+C7cwSBRWolHMqn64cb2LIe37EOTVcHWWP1
         EYDWrlKaoOjGRFF5iNW/H5QGDnBdJ6zKG6STVVT5bWSFkEn37bw/Lwl1w1RZnvelng2y
         jnxA==
X-Gm-Message-State: AOJu0YzQOegmH3uZ+i8lde/PWr4GG4JE42YJkUV+euN6selGtp/WK0IG
	hTDMyD1E1taKITDU+ZYXxvK5vuIFnL0iLOADScFd/z0Zr+S1bV86irBQCPlqo6MX
X-Gm-Gg: ATEYQzzFR7QAefDCvrVxbVh6mB4bWH1byTRYQF07NoxP/fyY7W4Dfzcme4FNE4rPDDi
	zq8pPTGvESUlsAK8ziSxPcwNm8wfmRTHe0XK8JkODJn1hGQh40RzgAVFdtm5v+ZjvrGPf7BsiSJ
	FNpg2mb8yfTPdJS+OEWVHXFGmBHZwSMEFXSxv0ule7c3tweWVd6gz9ijgzapsn1zOfWRdv9pcur
	bRvv/RXHcX17RC3cdvN3Llc9DhEUSjk60aBmi05vWe7d5wXh4TfE6V/aHBLCWuIzTyHvjVUs3s1
	b4MXMYbOd7XX6JiQtS/Crj1Hes9ASzDRsMrG6HVRy2j62mNX5BylEuTQPvxMH9R3aS9YZcTR1uk
	JTlEtDTKF+Kps4gVXopSJwDabP6xAlDyVmQSE6350pu12SAem/Y0aIAlgAoDQ8lJ47wl743YVzC
	vbXcRRL+EykuaLuJW0ViM0vxBjXGsr
X-Received: by 2002:a53:c7c9:0:b0:649:b100:4c6e with SMTP id 956f58d0204a3-64c78d3f28emr10612364d50.60.1772073053674;
        Wed, 25 Feb 2026 18:30:53 -0800 (PST)
Received: from localhost ([2a03:2880:25ff:4f::])
        by smtp.gmail.com with ESMTPSA id 956f58d0204a3-64cb763d660sm381339d50.17.2026.02.25.18.30.52
        (version=TLS1_3 cipher=TLS_AES_256_GCM_SHA384 bits=256/256);
        Wed, 25 Feb 2026 18:30:53 -0800 (PST)
From: Leo Martins <loemra.dev@gmail.com>
To: Sun YangKai <sunk67188@gmail.com>
Cc: linux-btrfs@vger.kernel.org,
	kernel-team@fb.com,
	Filipe Manana <fdmanana@suse.com>
Subject: Re: [PATCH v3 2/3] btrfs: inhibit extent buffer writeback to prevent COW amplification
Date: Wed, 25 Feb 2026 18:30:47 -0800
Message-ID: <20260226023050.2138594-1-loemra.dev@gmail.com>
X-Mailer: git-send-email 2.47.3
In-Reply-To: <8b4f2be8-169d-4e10-a47f-b534ab0ebac6@gmail.com>
References: 
Precedence: bulk
X-Mailing-List: linux-btrfs@vger.kernel.org
List-Id: <linux-btrfs.vger.kernel.org>
List-Subscribe: <mailto:linux-btrfs+subscribe@vger.kernel.org>
List-Unsubscribe: <mailto:linux-btrfs+unsubscribe@vger.kernel.org>
MIME-Version: 1.0
Content-Transfer-Encoding: 8bit

On Wed, 25 Feb 2026 14:17:24 +0800 Sun YangKai <sunk67188@gmail.com> wrote:

> 
> 
> On 2026/2/25 03:22, Leo Martins wrote:
> > Inhibit writeback on COW'd extent buffers for the lifetime of the
> > transaction handle, preventing background writeback from setting
> > BTRFS_HEADER_FLAG_WRITTEN and causing unnecessary re-COW.
> > 
> > COW amplification occurs when background writeback flushes an extent
> > buffer that a transaction handle is still actively modifying. When
> > lock_extent_buffer_for_io() transitions a buffer from dirty to
> > writeback, it sets BTRFS_HEADER_FLAG_WRITTEN, marking the block as
> > having been persisted to disk at its current bytenr. Once WRITTEN is
> > set, should_cow_block() must either COW the block again or overwrite
> > it in place, both of which are unnecessary overhead when the buffer
> > is still being modified by the same handle that allocated it. By
> > inhibiting background writeback on actively-used buffers, WRITTEN is
> > never set while a transaction handle holds a reference to the buffer,
> > avoiding this overhead entirely.
> > 
> > Add an atomic_t writeback_inhibitors counter to struct extent_buffer,
> > which fits in an existing 6-byte hole without increasing struct size.
> > When a buffer is COW'd in btrfs_force_cow_block(), call
> > btrfs_inhibit_eb_writeback() to store the eb in the transaction
> > handle's writeback_inhibited_ebs xarray (keyed by eb->start), take a
> > reference, and increment writeback_inhibitors. The function handles
> > dedup (same eb inhibited twice by the same handle) and replacement
> > (different eb at the same logical address). Allocation failure is
> > graceful: the buffer simply falls back to the pre-existing behavior
> > where it may be written back and re-COW'd.
> > 
> > In lock_extent_buffer_for_io(), when writeback_inhibitors is non-zero
> > and the writeback mode is WB_SYNC_NONE, skip the buffer. WB_SYNC_NONE
> > is used by the VM flusher threads for background and periodic
> > writeback, which are the only paths that cause COW amplification by
> > opportunistically writing out dirty extent buffers mid-transaction.
> > Skipping these is safe because the buffers remain dirty in the page
> > cache and will be written out at transaction commit time.
> > 
> > WB_SYNC_ALL must always proceed regardless of writeback_inhibitors.
> > This is required for correctness in the fsync path: btrfs_sync_log()
> > writes log tree blocks via filemap_fdatawrite_range() (WB_SYNC_ALL)
> > while the transaction handle that inhibited those same blocks is still
> > active. Without the WB_SYNC_ALL bypass, those inhibited log tree
> > blocks would be silently skipped, resulting in an incomplete log on
> > disk and corruption on replay. btrfs_write_and_wait_transaction()
> > also uses WB_SYNC_ALL via filemap_fdatawrite_range(); for that path,
> > inhibitors are already cleared beforehand, but the bypass ensures
> > correctness regardless.
> > 
> > Uninhibit in __btrfs_end_transaction() before atomic_dec(num_writers)
> > to prevent a race where the committer proceeds while buffers are still
> > inhibited. Also uninhibit in btrfs_commit_transaction() before writing
> > and in cleanup_transaction() for the error path.
> > 
> > Signed-off-by: Leo Martins <loemra.dev@gmail.com>
> > Reviewed-by: Filipe Manana <fdmanana@suse.com>
> > ---
> >   fs/btrfs/ctree.c       |  4 +++
> >   fs/btrfs/extent_io.c   | 63 +++++++++++++++++++++++++++++++++++++++++-
> >   fs/btrfs/extent_io.h   |  6 ++++
> >   fs/btrfs/transaction.c | 19 +++++++++++++
> >   fs/btrfs/transaction.h |  3 ++
> >   5 files changed, 94 insertions(+), 1 deletion(-)
> > 
> > diff --git a/fs/btrfs/ctree.c b/fs/btrfs/ctree.c
> > index 0e02b7b14adc..d4da65bb9096 100644
> > --- a/fs/btrfs/ctree.c
> > +++ b/fs/btrfs/ctree.c
> > @@ -590,6 +590,10 @@ int btrfs_force_cow_block(struct btrfs_trans_handle *trans,
> >   		btrfs_tree_unlock(buf);
> >   	free_extent_buffer_stale(buf);
> >   	btrfs_mark_buffer_dirty(trans, cow);
> > +
> > +	/* Inhibit writeback on the COW'd buffer for this transaction handle. */
> > +	btrfs_inhibit_eb_writeback(trans, cow);
> > +
> >   	*cow_ret = cow;
> >   	return 0;
> >   
> > diff --git a/fs/btrfs/extent_io.c b/fs/btrfs/extent_io.c
> > index ff1fc699a6ca..e04e42a81978 100644
> > --- a/fs/btrfs/extent_io.c
> > +++ b/fs/btrfs/extent_io.c
> > @@ -1940,7 +1940,9 @@ static noinline_for_stack bool lock_extent_buffer_for_io(struct extent_buffer *e
> >   	 * of time.
> >   	 */
> >   	spin_lock(&eb->refs_lock);
> > -	if (test_and_clear_bit(EXTENT_BUFFER_DIRTY, &eb->bflags)) {
> > +	if ((wbc->sync_mode == WB_SYNC_ALL ||
> > +	     atomic_read(&eb->writeback_inhibitors) == 0) &&
> > +	    test_and_clear_bit(EXTENT_BUFFER_DIRTY, &eb->bflags)) {
> >   		XA_STATE(xas, &fs_info->buffer_tree, eb->start >> fs_info->nodesize_bits);
> >   		unsigned long flags;
> >   
> > @@ -2999,6 +3001,64 @@ static inline void btrfs_release_extent_buffer(struct extent_buffer *eb)
> >   	kmem_cache_free(extent_buffer_cache, eb);
> >   }
> >   
> > +/*
> > + * btrfs_inhibit_eb_writeback - Inhibit writeback on buffer during transaction.
> > + * @trans: transaction handle that will own the inhibitor
> > + * @eb: extent buffer to inhibit writeback on
> > + *
> > + * Attempts 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.
> > + */
> > +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;
> > +
> > +	/* 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;
> > +	}
> > +
> > +	/* Handle replacement of different eb at same index. */
> > +	if (old && old != eb) {
> > +		struct extent_buffer *old_eb = old;
> > +
> > +		atomic_dec(&old_eb->writeback_inhibitors);
> > +		free_extent_buffer(old_eb);
> > +	}
> > +
> > +	atomic_inc(&eb->writeback_inhibitors);
> > +}
> > +
> > +/*
> > + * btrfs_uninhibit_all_eb_writeback - Uninhibit writeback on all buffers.
> > + * @trans: transaction handle to clean up
> > + */
> > +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);
> > +	}
> > +	xa_destroy(&trans->writeback_inhibited_ebs);
> > +}
> > +
> >   static struct extent_buffer *__alloc_extent_buffer(struct btrfs_fs_info *fs_info,
> >   						   u64 start)
> >   {
> > @@ -3009,6 +3069,7 @@ static struct extent_buffer *__alloc_extent_buffer(struct btrfs_fs_info *fs_info
> >   	eb->len = fs_info->nodesize;
> >   	eb->fs_info = fs_info;
> >   	init_rwsem(&eb->lock);
> > +	atomic_set(&eb->writeback_inhibitors, 0);
> >   
> >   	btrfs_leak_debug_add_eb(eb);
> >   
> > diff --git a/fs/btrfs/extent_io.h b/fs/btrfs/extent_io.h
> > index 73571d5d3d5a..fb68fbd4866c 100644
> > --- a/fs/btrfs/extent_io.h
> > +++ b/fs/btrfs/extent_io.h
> > @@ -102,6 +102,8 @@ struct extent_buffer {
> >   	/* >= 0 if eb belongs to a log tree, -1 otherwise */
> >   	s8 log_index;
> >   	u8 folio_shift;
> > +	/* Inhibits WB_SYNC_NONE writeback when > 0. */
> > +	atomic_t writeback_inhibitors;
> 
> I might be missing something here, but I'm curious whether this atomic 
> counter can ever go above 1. If not, and it's strictly binary, perhaps 
> using atomic_set(1/0) instead of atomic_inc/dec would make the intent 
> clearer?

You're right! It looks like the counter will never go above 1 as we're
only increasing the counter when we actually COW the eb in
btrfs_force_cow_block and that will only ever happen once per eb.

This opens the door for:
 1. trans_handle A wants to modify eb Z, COWs and inhibits writeback
    on eb Z
 2. trans_handle B wants to modify eb Z, does not COW because same
    transaction generation and not WRITTEN, does not inhibit writeback
	on eb Z
 3. trans_handle A finishes, uninhibits eb Z
 4. trans_handle B still modifying eb Z with no inhibition

This leaves the door open for COW amplification. The correct thing to
do is inhibit writeback not only when the buffer is COW'd, but
whenever the handle resuses a COW'd buffer without re-COWing.

Will send out an updated v4 shortly, good catch!

> 
> Otherwise looks good. Thanks.
> 
> >   	struct rcu_head rcu_head;
> >   
> >   	struct rw_semaphore lock;
> > @@ -381,4 +383,8 @@ void btrfs_extent_buffer_leak_debug_check(struct btrfs_fs_info *fs_info);
> >   #define btrfs_extent_buffer_leak_debug_check(fs_info)	do {} while (0)
> >   #endif
> >   
> > +void btrfs_inhibit_eb_writeback(struct btrfs_trans_handle *trans,
> > +			       struct extent_buffer *eb);
> > +void btrfs_uninhibit_all_eb_writeback(struct btrfs_trans_handle *trans);
> > +
> >   #endif
> > diff --git a/fs/btrfs/transaction.c b/fs/btrfs/transaction.c
> > index f4cc9e1a1b93..a9a22629b49d 100644
> > --- a/fs/btrfs/transaction.c
> > +++ b/fs/btrfs/transaction.c
> > @@ -15,6 +15,7 @@
> >   #include "misc.h"
> >   #include "ctree.h"
> >   #include "disk-io.h"
> > +#include "extent_io.h"
> >   #include "transaction.h"
> >   #include "locking.h"
> >   #include "tree-log.h"
> > @@ -688,6 +689,8 @@ 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
> > @@ -1083,6 +1086,13 @@ static int __btrfs_end_transaction(struct btrfs_trans_handle *trans,
> >   	if (trans->type & __TRANS_FREEZABLE)
> >   		sb_end_intwrite(info->sb);
> >   
> > +	/*
> > +	 * Uninhibit extent buffer writeback before decrementing num_writers,
> > +	 * since the decrement wakes the committing thread which needs all
> > +	 * buffers uninhibited to write them to disk.
> > +	 */
> > +	btrfs_uninhibit_all_eb_writeback(trans);
> > +
> >   	WARN_ON(cur_trans != info->running_transaction);
> >   	WARN_ON(atomic_read(&cur_trans->num_writers) < 1);
> >   	atomic_dec(&cur_trans->num_writers);
> > @@ -2110,6 +2120,7 @@ static void cleanup_transaction(struct btrfs_trans_handle *trans, int err)
> >   	if (!test_bit(BTRFS_FS_RELOC_RUNNING, &fs_info->flags))
> >   		btrfs_scrub_cancel(fs_info);
> >   
> > +	btrfs_uninhibit_all_eb_writeback(trans);
> >   	kmem_cache_free(btrfs_trans_handle_cachep, trans);
> >   }
> >   
> > @@ -2556,6 +2567,14 @@ int btrfs_commit_transaction(struct btrfs_trans_handle *trans)
> >   	    fs_info->cleaner_kthread)
> >   		wake_up_process(fs_info->cleaner_kthread);
> >   
> > +	/*
> > +	 * Uninhibit writeback on all extent buffers inhibited during this
> > +	 * transaction before writing them to disk. Inhibiting prevented
> > +	 * writeback while the transaction was building, but now we need
> > +	 * them written.
> > +	 */
> > +	btrfs_uninhibit_all_eb_writeback(trans);
> > +
> >   	ret = btrfs_write_and_wait_transaction(trans);
> >   	if (unlikely(ret)) {
> >   		btrfs_err(fs_info, "error while writing out transaction: %d", ret);
> > diff --git a/fs/btrfs/transaction.h b/fs/btrfs/transaction.h
> > index 18ef069197e5..7d70fe486758 100644
> > --- a/fs/btrfs/transaction.h
> > +++ b/fs/btrfs/transaction.h
> > @@ -12,6 +12,7 @@
> >   #include <linux/time64.h>
> >   #include <linux/mutex.h>
> >   #include <linux/wait.h>
> > +#include <linux/xarray.h>
> >   #include "btrfs_inode.h"
> >   #include "delayed-ref.h"
> >   
> > @@ -162,6 +163,8 @@ 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;
> >   };
> >   
> >   /*

From mboxrd@z Thu Jan  1 00:00:00 1970
Received: from mail-wm1-f67.google.com (mail-wm1-f67.google.com [209.85.128.67])
	(using TLSv1.2 with cipher ECDHE-RSA-AES128-GCM-SHA256 (128/128 bits))
	(No client certificate requested)
	by smtp.subspace.kernel.org (Postfix) with ESMTPS id 60A8945C0B
	for <linux-btrfs@vger.kernel.org>; Thu, 26 Feb 2026 05:03:38 +0000 (UTC)
Authentication-Results: smtp.subspace.kernel.org; arc=none smtp.client-ip=209.85.128.67
ARC-Seal:i=1; a=rsa-sha256; d=subspace.kernel.org; s=arc-20240116;
	t=1772082220; cv=none; b=Wo/rOXxX5lZ5Hq4Y78lMVY0LXo49uNn3mOIIefeCS0CzVeLbEvpIv56hkR+U7Qw49OPkML5J0xkQFsNjr1dcj+lnxtLymV2X9IGS4T0V9V+hC8duNyS96svXyag4JO57uKHDQVcxZVFlXHAkGLiUZ5l1eow4WVcKxvgTwsmL6WM=
ARC-Message-Signature:i=1; a=rsa-sha256; d=subspace.kernel.org;
	s=arc-20240116; t=1772082220; c=relaxed/simple;
	bh=520tmXDBh7zxnpVuEg1KJZ/3/AplwuCOkmO4E+KewTc=;
	h=Message-ID:Date:MIME-Version:Subject:To:Cc:References:From:
	 In-Reply-To:Content-Type; b=jmibz0PigWHaQzq2NheBq1PnKyIGWZqiBfS7Wsqu63ZOHV2/Fs2KfK9wnB0EfJqOeds3+6VhKkl+XMkvMVLGOVUPNhaVWkidoPKwELx5SKOuWNw0m2iLY7KU9Hzy1uo41JWR2NkgosCZDCH2pxgr8LUi/oD0WoVmtPNBP/aT5ek=
ARC-Authentication-Results:i=1; smtp.subspace.kernel.org; dmarc=pass (p=none dis=none) header.from=gmail.com; spf=pass smtp.mailfrom=gmail.com; dkim=pass (2048-bit key) header.d=gmail.com header.i=@gmail.com header.b=EczCKAsf; arc=none smtp.client-ip=209.85.128.67
Authentication-Results: smtp.subspace.kernel.org; dmarc=pass (p=none dis=none) header.from=gmail.com
Authentication-Results: smtp.subspace.kernel.org; spf=pass smtp.mailfrom=gmail.com
Authentication-Results: smtp.subspace.kernel.org;
	dkim=pass (2048-bit key) header.d=gmail.com header.i=@gmail.com header.b="EczCKAsf"
Received: by mail-wm1-f67.google.com with SMTP id 5b1f17b1804b1-4836cd6e0d4so588685e9.3
        for <linux-btrfs@vger.kernel.org>; Wed, 25 Feb 2026 21:03:38 -0800 (PST)
DKIM-Signature: v=1; a=rsa-sha256; c=relaxed/relaxed;
        d=gmail.com; s=20230601; t=1772082217; x=1772687017; darn=vger.kernel.org;
        h=content-transfer-encoding:in-reply-to:from:content-language
         :references:cc:to:subject:user-agent:mime-version:date:message-id
         :from:to:cc:subject:date:message-id:reply-to;
        bh=I6agkUu1AJmePAHgXWPIGGibknIfKUrdVg8uwuOR4Jc=;
        b=EczCKAsfk24sh9vEP0o6a+Xk+XIs9aUNglsc+T4XMKrLyJeUfUUoWRA2o6MEWsCK1H
         F1JUwi/fP9gWZO4w9RwRpbYDWxJzm4LsraLfUIfWiRvCTygMeQIR7VPPpuTgQo0YFHMe
         mtkNT6AP8mMD+D0ed2uGCrLo2a/MeM8dLRG8tMQM4GnzJObYHyd9NjabdlWLG9+lCiSU
         eCQx80NLYz4Udt2KqIqz2beYdb+95GBkn2O5Oh3aYWk5uHhknwWXtWDsIAZZhHFShwQH
         fpcgF0Ru+9g97XJIvoOOXsfXnJrpAZ9D2Bq3QRk2PMaY15V046P0hSmSYE7Z4UciW1D+
         BiVA==
X-Google-DKIM-Signature: v=1; a=rsa-sha256; c=relaxed/relaxed;
        d=1e100.net; s=20230601; t=1772082217; x=1772687017;
        h=content-transfer-encoding:in-reply-to:from:content-language
         :references:cc:to:subject:user-agent:mime-version:date:message-id
         :x-gm-gg:x-gm-message-state:from:to:cc:subject:date:message-id
         :reply-to;
        bh=I6agkUu1AJmePAHgXWPIGGibknIfKUrdVg8uwuOR4Jc=;
        b=ppei1kRJdjr9pCEmfquQwE+ueknYbza7lH8jPisxZUi9YTcmLtma4S3aBh/76U/dIK
         nOmPz2eDzFzwmlNZ7bwfctfKFVUfxcfGPTHSGye1/9MVo+89yDElns+R0r4ECUNDZsWZ
         6hEiT7SqGBXGehVyNg0VjFj34oLmHU6N5vdkFamci1gfAGZKlcbls4jMS8mj6Zel1XsD
         sGuvYJQEG1/ccyK/z3wFS1w8EGTT0WHJb7ej67hks1lgm+9eJYZ9Yj9pso1vMSPqoYLm
         boB+iUpREEIQ8h4PYToxqCtBdYN25M7rWxCY3uPTBB4OUhnfihI6GHR1W+hKpHiOxN7s
         u3aA==
X-Gm-Message-State: AOJu0Ywgcm2KIXgaN+sLiqjQDTpvWwK7YtggIrram5f0J6ovyvunYX5I
	cDbBkwQPc4aBGTUXnMT3H6uva8nihffPSMMRm2RklGLi5GZ6YnopFWgR
X-Gm-Gg: ATEYQzysL3OAHmZOt4Ngpu5q//rvZD/cbIlnrI75g2b2xqUfzKTcPHCqwSK5be1mRcu
	/Qs/xMZTOoQU9qsc1lX0SaULKLGg1e8uVqvz7AWaFUSp4C9EIJgi3ORt9vp/BggSOTojAm+uN1j
	oDvnsw9Wy7hFq8dpR4FqqO/JCITVhzH3Yc9VbxxNVT8TTAuU9SWxh55nvEWE8ynZXhEGfsUOgUv
	+l0LVcMvM8LbNteoa1Ta0YiueExBJ0zgBZqLPwKHBf7YJrDVtn+//+1Qu8KGxhieMH83zE1Imnq
	5HmoSQtPPGUeu62XxDyNoLIWuktf6O9LqihdGBlxEUj1eQhqSXfuCTSBqQYSoQ5xQzvCO//bu9S
	Cljtyk+p0vBcvuQM6yAIE/+WrZiy9kIuWOc7HkhRTQb5b7F6g2MYcjmQZ5v65mrfctH7lsaAjct
	CRUDjB1gDq9eTVUOQMP1WaUB+ntNC3m2WfBGo=
X-Received: by 2002:a05:600c:1988:b0:480:6612:253 with SMTP id 5b1f17b1804b1-483a962e4demr170537225e9.3.1772082216121;
        Wed, 25 Feb 2026 21:03:36 -0800 (PST)
Received: from [192.168.1.13] ([193.122.84.74])
        by smtp.gmail.com with ESMTPSA id 5b1f17b1804b1-483bd765604sm103808285e9.15.2026.02.25.21.03.31
        (version=TLS1_3 cipher=TLS_AES_128_GCM_SHA256 bits=128/128);
        Wed, 25 Feb 2026 21:03:35 -0800 (PST)
Message-ID: <f5788abc-f4de-4f3a-9ab1-7aaa579c89c9@gmail.com>
Date: Thu, 26 Feb 2026 13:03:21 +0800
Precedence: bulk
X-Mailing-List: linux-btrfs@vger.kernel.org
List-Id: <linux-btrfs.vger.kernel.org>
List-Subscribe: <mailto:linux-btrfs+subscribe@vger.kernel.org>
List-Unsubscribe: <mailto:linux-btrfs+unsubscribe@vger.kernel.org>
MIME-Version: 1.0
User-Agent: Mozilla Thunderbird
Subject: Re: [PATCH v3 2/3] btrfs: inhibit extent buffer writeback to prevent
 COW amplification
To: Leo Martins <loemra.dev@gmail.com>
Cc: linux-btrfs@vger.kernel.org, kernel-team@fb.com,
 Filipe Manana <fdmanana@suse.com>
References: <20260226023050.2138594-1-loemra.dev@gmail.com>
Content-Language: en-US
From: Sun YangKai <sunk67188@gmail.com>
In-Reply-To: <20260226023050.2138594-1-loemra.dev@gmail.com>
Content-Type: text/plain; charset=UTF-8; format=flowed
Content-Transfer-Encoding: 7bit



On 2026/2/26 10:30, Leo Martins wrote:
> On Wed, 25 Feb 2026 14:17:24 +0800 Sun YangKai <sunk67188@gmail.com> wrote:
> 
>>
>>
>> On 2026/2/25 03:22, Leo Martins wrote:
>>> Inhibit writeback on COW'd extent buffers for the lifetime of the
>>> transaction handle, preventing background writeback from setting
>>> BTRFS_HEADER_FLAG_WRITTEN and causing unnecessary re-COW.
>>>
>>> COW amplification occurs when background writeback flushes an extent
>>> buffer that a transaction handle is still actively modifying. When
>>> lock_extent_buffer_for_io() transitions a buffer from dirty to
>>> writeback, it sets BTRFS_HEADER_FLAG_WRITTEN, marking the block as
>>> having been persisted to disk at its current bytenr. Once WRITTEN is
>>> set, should_cow_block() must either COW the block again or overwrite
>>> it in place, both of which are unnecessary overhead when the buffer
>>> is still being modified by the same handle that allocated it. By
>>> inhibiting background writeback on actively-used buffers, WRITTEN is
>>> never set while a transaction handle holds a reference to the buffer,
>>> avoiding this overhead entirely.
>>>
>>> Add an atomic_t writeback_inhibitors counter to struct extent_buffer,
>>> which fits in an existing 6-byte hole without increasing struct size.
>>> When a buffer is COW'd in btrfs_force_cow_block(), call
>>> btrfs_inhibit_eb_writeback() to store the eb in the transaction
>>> handle's writeback_inhibited_ebs xarray (keyed by eb->start), take a
>>> reference, and increment writeback_inhibitors. The function handles
>>> dedup (same eb inhibited twice by the same handle) and replacement
>>> (different eb at the same logical address). Allocation failure is
>>> graceful: the buffer simply falls back to the pre-existing behavior
>>> where it may be written back and re-COW'd.
>>>
>>> In lock_extent_buffer_for_io(), when writeback_inhibitors is non-zero
>>> and the writeback mode is WB_SYNC_NONE, skip the buffer. WB_SYNC_NONE
>>> is used by the VM flusher threads for background and periodic
>>> writeback, which are the only paths that cause COW amplification by
>>> opportunistically writing out dirty extent buffers mid-transaction.
>>> Skipping these is safe because the buffers remain dirty in the page
>>> cache and will be written out at transaction commit time.
>>>
>>> WB_SYNC_ALL must always proceed regardless of writeback_inhibitors.
>>> This is required for correctness in the fsync path: btrfs_sync_log()
>>> writes log tree blocks via filemap_fdatawrite_range() (WB_SYNC_ALL)
>>> while the transaction handle that inhibited those same blocks is still
>>> active. Without the WB_SYNC_ALL bypass, those inhibited log tree
>>> blocks would be silently skipped, resulting in an incomplete log on
>>> disk and corruption on replay. btrfs_write_and_wait_transaction()
>>> also uses WB_SYNC_ALL via filemap_fdatawrite_range(); for that path,
>>> inhibitors are already cleared beforehand, but the bypass ensures
>>> correctness regardless.
>>>
>>> Uninhibit in __btrfs_end_transaction() before atomic_dec(num_writers)
>>> to prevent a race where the committer proceeds while buffers are still
>>> inhibited. Also uninhibit in btrfs_commit_transaction() before writing
>>> and in cleanup_transaction() for the error path.
>>>
>>> Signed-off-by: Leo Martins <loemra.dev@gmail.com>
>>> Reviewed-by: Filipe Manana <fdmanana@suse.com>
>>> ---
>>>    fs/btrfs/ctree.c       |  4 +++
>>>    fs/btrfs/extent_io.c   | 63 +++++++++++++++++++++++++++++++++++++++++-
>>>    fs/btrfs/extent_io.h   |  6 ++++
>>>    fs/btrfs/transaction.c | 19 +++++++++++++
>>>    fs/btrfs/transaction.h |  3 ++
>>>    5 files changed, 94 insertions(+), 1 deletion(-)
>>>
>>> diff --git a/fs/btrfs/ctree.c b/fs/btrfs/ctree.c
>>> index 0e02b7b14adc..d4da65bb9096 100644
>>> --- a/fs/btrfs/ctree.c
>>> +++ b/fs/btrfs/ctree.c
>>> @@ -590,6 +590,10 @@ int btrfs_force_cow_block(struct btrfs_trans_handle *trans,
>>>    		btrfs_tree_unlock(buf);
>>>    	free_extent_buffer_stale(buf);
>>>    	btrfs_mark_buffer_dirty(trans, cow);
>>> +
>>> +	/* Inhibit writeback on the COW'd buffer for this transaction handle. */
>>> +	btrfs_inhibit_eb_writeback(trans, cow);
>>> +
>>>    	*cow_ret = cow;
>>>    	return 0;
>>>    
>>> diff --git a/fs/btrfs/extent_io.c b/fs/btrfs/extent_io.c
>>> index ff1fc699a6ca..e04e42a81978 100644
>>> --- a/fs/btrfs/extent_io.c
>>> +++ b/fs/btrfs/extent_io.c
>>> @@ -1940,7 +1940,9 @@ static noinline_for_stack bool lock_extent_buffer_for_io(struct extent_buffer *e
>>>    	 * of time.
>>>    	 */
>>>    	spin_lock(&eb->refs_lock);
>>> -	if (test_and_clear_bit(EXTENT_BUFFER_DIRTY, &eb->bflags)) {
>>> +	if ((wbc->sync_mode == WB_SYNC_ALL ||
>>> +	     atomic_read(&eb->writeback_inhibitors) == 0) &&
>>> +	    test_and_clear_bit(EXTENT_BUFFER_DIRTY, &eb->bflags)) {
>>>    		XA_STATE(xas, &fs_info->buffer_tree, eb->start >> fs_info->nodesize_bits);
>>>    		unsigned long flags;
>>>    
>>> @@ -2999,6 +3001,64 @@ static inline void btrfs_release_extent_buffer(struct extent_buffer *eb)
>>>    	kmem_cache_free(extent_buffer_cache, eb);
>>>    }
>>>    
>>> +/*
>>> + * btrfs_inhibit_eb_writeback - Inhibit writeback on buffer during transaction.
>>> + * @trans: transaction handle that will own the inhibitor
>>> + * @eb: extent buffer to inhibit writeback on
>>> + *
>>> + * Attempts 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.
>>> + */
>>> +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;
>>> +
>>> +	/* 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;
>>> +	}
>>> +
>>> +	/* Handle replacement of different eb at same index. */
>>> +	if (old && old != eb) {
>>> +		struct extent_buffer *old_eb = old;
>>> +
>>> +		atomic_dec(&old_eb->writeback_inhibitors);
>>> +		free_extent_buffer(old_eb);
>>> +	}
>>> +
>>> +	atomic_inc(&eb->writeback_inhibitors);
>>> +}
>>> +
>>> +/*
>>> + * btrfs_uninhibit_all_eb_writeback - Uninhibit writeback on all buffers.
>>> + * @trans: transaction handle to clean up
>>> + */
>>> +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);
>>> +	}
>>> +	xa_destroy(&trans->writeback_inhibited_ebs);
>>> +}
>>> +
>>>    static struct extent_buffer *__alloc_extent_buffer(struct btrfs_fs_info *fs_info,
>>>    						   u64 start)
>>>    {
>>> @@ -3009,6 +3069,7 @@ static struct extent_buffer *__alloc_extent_buffer(struct btrfs_fs_info *fs_info
>>>    	eb->len = fs_info->nodesize;
>>>    	eb->fs_info = fs_info;
>>>    	init_rwsem(&eb->lock);
>>> +	atomic_set(&eb->writeback_inhibitors, 0);
>>>    
>>>    	btrfs_leak_debug_add_eb(eb);
>>>    
>>> diff --git a/fs/btrfs/extent_io.h b/fs/btrfs/extent_io.h
>>> index 73571d5d3d5a..fb68fbd4866c 100644
>>> --- a/fs/btrfs/extent_io.h
>>> +++ b/fs/btrfs/extent_io.h
>>> @@ -102,6 +102,8 @@ struct extent_buffer {
>>>    	/* >= 0 if eb belongs to a log tree, -1 otherwise */
>>>    	s8 log_index;
>>>    	u8 folio_shift;
>>> +	/* Inhibits WB_SYNC_NONE writeback when > 0. */
>>> +	atomic_t writeback_inhibitors;
>>
>> I might be missing something here, but I'm curious whether this atomic
>> counter can ever go above 1. If not, and it's strictly binary, perhaps
>> using atomic_set(1/0) instead of atomic_inc/dec would make the intent
>> clearer?
> 
> You're right! It looks like the counter will never go above 1 as we're
> only increasing the counter when we actually COW the eb in
> btrfs_force_cow_block and that will only ever happen once per eb.
> 
> This opens the door for:
>   1. trans_handle A wants to modify eb Z, COWs and inhibits writeback
>      on eb Z
>   2. trans_handle B wants to modify eb Z, does not COW because same
>      transaction generation and not WRITTEN, does not inhibit writeback
> 	on eb Z
>   3. trans_handle A finishes, uninhibits eb Z
>   4. trans_handle B still modifying eb Z with no inhibition
> 
> This leaves the door open for COW amplification. The correct thing to
> do is inhibit writeback not only when the buffer is COW'd, but
> whenever the handle resuses a COW'd buffer without re-COWing.

Oh, I've just got the idea. It's inhibited per trans handle. Then it 
makes a lot of sense.

There might be some ebs that were inhibited due to tree rebalancing or 
tree walking and are no longer used, but I don't think this is a blocker 
for this patch.

Looking forward to your next patch version :)

> Will send out an updated v4 shortly, good catch!
> 
>>
>> Otherwise looks good. Thanks.

