rewrite.read-size sets the buffer for source-side reads in the
materialize copy loop and the block-map hashing pass. It was validated
only for being a positive multiple of the sectorsize, so nothing stopped
an operator from asking for a buffer larger than any transfer that could
usefully be made with it.
btrfs will not create a data extent larger than 128 MiB, so a copy
buffer bigger than that cannot produce one larger extent: the write is
split into 128 MiB extents regardless, and the surplus only holds dirty
memory, once per worker thread. Preallocated extents can reach 256 MiB
(BLOCK_SIZE_MAX_EXTENT, for the fallocate limit introduced by kernel
commit
24542bf7ea5e4fdfdb5157ff544c093fa4dcb536), but a prealloc extent
cannot be replaced by a single write either, so that larger number is
not a useful buffer size.
Add BLOCK_SIZE_MAX_DATA_EXTENT for the 128 MiB figure, reject anything
above it at config load next to the existing alignment check, state the
maximum in the built-in config comment, and explain in docs/config-file.md
why the useful ceiling is the smaller of the two extent limits.
The cap also keeps the copy loop's writes under BLOCK_SIZE_MAX_TRANSFER,
which pwrite_or_die() enforces.
Assisted-by: Claude-Code:claude-opus-5
the partial block when its bytes are pwritten to the temp file,
which is wasteful enough to be worth rejecting at config load
rather than diagnosing in production.
+ * Maximum `128M`. btrfs will not create a data extent larger than
+ 128 MiB, so a write bigger than that cannot produce a single
+ larger extent — it is split into 128 MiB extents regardless, and
+ the extra buffer only holds more dirty memory, once per worker
+ thread. Preallocated extents can reach 256 MiB, but a prealloc
+ extent cannot be replaced by one write either, so 256 MiB is not
+ a useful size here. Values above the maximum are rejected at
+ config load.
* **`restart-max`**
Maximum hash-conflict restarts per extent. Hash-conflict restarts
# Buffer size for source-side reads in the materialize copy
# loop and the block-map hashing pass. Must be a positive
- # multiple of the filesystem sectorsize.
+ # multiple of the filesystem sectorsize. Maximum 128M, the
+ # largest extent a single write can replace.
# Size value (e.g. 128K, 1M, 4M).
# See docs/config-file.md for sizing tradeoffs.
read-size = 1M
<< " is not a positive multiple of filesystem sectorsize "
<< BeesContext::s_sectorsize);
}
+ // Cap the buffer at the largest extent a single write can
+ // replace. Beyond that the copy loop cannot produce a bigger
+ // extent, so the extra bytes buy nothing and cost RSS in every
+ // worker thread at once. The cap is also what keeps the copy
+ // loop's pwrite below BLOCK_SIZE_MAX_TRANSFER, which
+ // pwrite_or_die() enforces.
+ if (rv.m_read_size > ranged_cast<uint64_t>(BLOCK_SIZE_MAX_DATA_EXTENT)) {
+ THROW_ERROR(invalid_argument,
+ "rewrite.read-size: " << pretty(rv.m_read_size)
+ << " exceeds the maximum " << pretty(BLOCK_SIZE_MAX_DATA_EXTENT)
+ << ", the largest extent a single write can replace");
+ }
BEESLOGINFO("rewrite.read-size = " << pretty(rv.m_read_size) << " [rewrite.read-size]");
}
m_rewrite_policy = rv;
/// where preallocated extents may be 256 MiB instead of the normal 128 MiB limit.
const off_t BLOCK_SIZE_MAX_EXTENT = 256 * 1024 * 1024;
+/// Largest data extent btrfs will create, and so the largest write that can
+/// replace an extent in one operation (128 MiB). Buffering more than this
+/// for a copy cannot produce a larger extent, it only holds more dirty
+/// memory. Note this is smaller than @ref BLOCK_SIZE_MAX_EXTENT: a
+/// preallocated extent may be 256 MiB, but it cannot be replaced by a single
+/// write, so 256 MiB is not a useful copy-buffer size either.
+const off_t BLOCK_SIZE_MAX_DATA_EXTENT = 128 * 1024 * 1024;
+
/// @}
/// @name Block alignment masks