]> git.hungrycats.org Git - linux/commit
btrfs: resume the delayed-ref merge walk instead of restarting it 6.18/topics/delayed-ref-fixes
authorZygo Blaxell <ce3g8jdj@umail.furryterror.org>
Fri, 18 Sep 2026 18:40:54 +0000 (14:40 -0400)
committerZygo Blaxell <ce3g8jdj@umail.furryterror.org>
Sat, 19 Sep 2026 00:18:17 +0000 (20:18 -0400)
commit8b7d314dce0cd56c2686bcedfc2f32a93e998315
treeca05aba9405fb0ef06af02227a544eaaf719e920
parent8f3741e6feb045da5b406df0a80b42a1adfb289b
btrfs: resume the delayed-ref merge walk instead of restarting it

btrfs_merge_delayed_refs() restarts its walk of a head's ref tree from
rb_first_cached() every time merge_ref() reports a merge:

again:
for (node = rb_first_cached(&head->ref_tree); node;
     node = rb_next(node)) {
ref = rb_entry(node, ...);
if (seq && ref->seq >= seq)
continue;
if (merge_ref(fs_info, delayed_refs, head, ref, seq))
goto again;
}

The restart is there because merge_ref() returns true only when it has
freed the caller's cursor node, so the caller cannot call rb_next() on it.
That makes the walk quadratic in the number of refs on the head, and it
runs with head->lock held -- a spinlock -- so it cannot be broken up.

On a filesystem doing continuous backref resolution this is not
theoretical. Each backref walk advances fs_info->tree_mod_seq, and
init_delayed_ref_common() stamps that seq into every new fs-tree ref, so
refs to the same block and root stop comparing equal and stop being merged
at insert time by insert_delayed_ref().  They accumulate as separate nodes
in one contiguous group instead, and the merge walk restarts across the
whole group on every cancelling pair.

Measured with the ftrace function profiler on a filesystem running a
dedupe agent and a verifier that both walk backrefs continuously, during a
stall, over a 60 second window:

  btrfs_merge_delayed_refs()         115,166 calls
  comp_refs()                  1,171,273,171 calls

comp_refs() is called once per node the merge walk compares, so that is
10,170 comparisons per walk.  Solving the restart structure's N^2/2 puts
roughly 143 refs on a head.  comp_refs() is a leaf, so its own total is
not distorted by nesting: 38.9 CPU-seconds of that 60 second window went
to comparing delayed refs and nothing else, with the transaction thread
pinned at 100% of a core and zero tasks anywhere in the machine waiting on
IO.

The cost is not subtle.  Every other task queued behind the commit;
relocation of a single block group made no progress for three and a half
hours; snapshot deletion, which had been clearing about 49 subvolumes a
minute, stopped entirely.  An earlier instance on the same machine
produced a 26 second soft lockup with the stack inside the merge walk.

merge_ref() does not actually need the caller to restart.  It advances its
own cursor past a node before freeing that node:

next = rb_entry(node, ...);
node = rb_next(node);
...
drop_delayed_ref(fs_info, delayed_refs, head, next);

so it always holds a position that survives the frees, and there are only
three ways it can end having freed the caller's node:

 - it swapped, and the survivor keeps a non-zero ref_mod.  The caller's node is
   freed; the survivor is still in the tree and may still merge with what
   follows, so the walk resumes at the survivor.

 - the caller's ref_mod reached zero, with or without a swap.  Both nodes are
   gone, and the local cursor -- already advanced past the freed one -- is the
   resume point, or NULL to end the walk.

Return whether @ref was freed, and hand the position back through an out
parameter.  The two cannot be folded into one pointer: the resume position
is legitimately NULL when the freed node was last in the tree, and a NULL
return would then tell the caller its own node is still live. The walk becomes strictly forward and linear, and nothing is left
unmerged.

Skipping the re-examination that the restart performed does not lose
merges. comp_refs() keys on type and root-or-parent, neither of which any
merge changes, so merging cannot make two previously incomparable refs
comparable. Mergeable refs are contiguous, because the tree is sorted by
comp_refs(..., check_seq = true) and the merge key is the same comparison
without the seq -- a prefix of the sort key.  And a ref skipped by the seq
test stays skipped, since seq is read once per btrfs_merge_delayed_refs()
call.

The restart has had this shape since commit 0e0adbcfdc90 ("btrfs: track
refs in a rb_tree instead of a list") in v4.15.

Assisted-by: Claude:claude-opus-5[1m]
fs/btrfs/delayed-ref.c