Files
rustfs/crates
houseme 5f1a475c56 perf(ecstore): stop zeroing pooled shard buffers on the GET path (backlog#1159) (#4681)
`ShardBufferPool::take` handed out a `resize(len, 0)`-ed buffer, and the
reader then overwrote every byte of it. CPU profiling of a cached 1 MiB
GET (device reads = 0, so all cost is CPU) attributed 4.81% of the whole
server to that memset — a buffer pool exists to reuse an allocation, and
memsetting it gives the saving straight back.

The zeroing was load-bearing only because `BitrotReader::read` takes
`&mut [u8]`, which must be initialized. But the reader never reads what
the caller put there, and never returns a partially filled buffer: both
the hashed and the no-hash path either fill the whole shard or fail with
UnexpectedEof, and a hash mismatch is an error rather than a short read.
So the initialization bought nothing observable.

Add `BitrotReader::read_appending(&mut Vec<u8>, want)`, which appends into
the buffer's spare capacity instead of demanding initialized bytes:

  * hashed path — unchanged single copy, `extend_from_slice(data)` in place
    of `copy_from_slice` into a pre-zeroed buffer, and only after the hash
    verifies, so corrupt bytes never reach the caller's buffer;
  * no-hash path — `read_buf` writes straight into the spare capacity and
    advances the length only over bytes the reader actually wrote, so an
    uninitialized tail can never be exposed.

`ShardBufferPool::take` now yields an empty buffer with capacity, and
`read_shard` no longer needs to `truncate`. `read` keeps its old signature
for the remaining callers.

Four tests gate the contract rather than the call:
  * `read_appending` is byte-for-byte identical to `read` on both paths;
  * a truncated shard is UnexpectedEof, never a partially filled buffer;
  * bytes that fail the bitrot hash never reach the caller's buffer;
  * `want > shard_size` is rejected;
plus the pool test now asserts the allocation is reused (same pointer) and
never zeroed.

Verified: `erasure::` 213 passed, 0 failed; on a real Linux host
`erasure::` 209 and `disk::local::` 143 pass serially, and the failures
seen in a parallel full-suite run reproduce identically on unmodified main
(they are ENOSPC from a full root filesystem plus pre-existing flakes).

Not claimed: an end-to-end throughput number. The A/B on the bench host was
too noisy to attribute (one rep pair was not fully cached, and its root
filesystem filled mid-run); what is measured is that the removed memset was
4.81% of GET CPU in the pre-change profile.

Co-authored-by: heihutu <heihutu@gmail.com>
2026-07-10 12:34:45 +00:00
..