perf(blob): small read-ahead for the local backend (read_prefetch 1 -> 2)

The local backend inherited the trait's conservative read_prefetch() = 1
(strictly sequential chunk reassembly), while S3/Azure already use 8. The
prior rationale was that concurrent opens over scattered content-addressed
chunk files turn one sequential read into competing random I/O ("slower
cold"). Benchmarked that assumption with examples/bench_blob_prefetch
(sweeps the buffered(N) depth over a real LocalBlobBackend under disk-bound
vs network-bound consumers and warm vs cold page cache).

Result on SSD-class storage (median MB/s vs N=1):
  warm  disk-bound   N=2 +11.8%   N=8 +3.9%   N=16 -4.4%
  cold  disk-bound   N=2  +7.2%   (no cold regression on SSD)
  network-bound (throttled)  ~0% at any N — the socket, not the disk, caps it

So N=8 is wrong for local (leaves gain on the table, risks HDD seek thrash)
and the network-bound win the analysis assumed doesn't materialize: buffered()
here overlaps the per-chunk File::open (cheap on local disk), not the data
read. N=2 captures most of the disk-bound gain — which covers localhost/LAN
downloads AND the internal blob reads that drain as fast as the disk delivers
(thumbnail render, transcode, ZIP export, content extraction) — at the lowest
fan-out. Env-tunable via OXICLOUD_LOCAL_READ_PREFETCH (set 1 on seek-bound
HDDs to restore the old behaviour; raise on fast NVMe). Signature unchanged,
so all ~16 LocalBlobBackend::new call sites are untouched.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01JG5yYZ9s868mJwqT2Qz7ez
This commit is contained in:
Claude
2026-06-22 08:10:46 +00:00
parent 73b7feeb0f
commit 5b8b740233
2 changed files with 52 additions and 6 deletions
+11 -6
View File
@@ -130,12 +130,17 @@ pub trait BlobStorageBackend: Send + Sync + 'static {
/// How many chunk fetches the CDC reader may run concurrently when
/// reassembling a file (`read_blob_stream`'s `buffered(N)` read-ahead).
///
/// The default is **1** — sequential, because for a local disk concurrent
/// opens turn one sequential read into several competing random-I/O streams
/// over content-addressed (scattered) chunk files, which is neutral on a
/// warm page cache and *slower* cold. Remote backends (S3/Azure) override
/// this with a higher value: there the dominant cost is per-chunk request
/// latency, and overlapping fetches hides it (≈ N× faster reassembly).
/// The trait default is a conservative **1** (strictly sequential) — the
/// safe fallback for any backend that doesn't know its own I/O profile.
/// Concrete backends override it:
/// • Local disk → a small benchmarked depth (default `2`, env-tunable):
/// overlapping the next chunk's `File::open` with the current chunk's
/// drain measured +7–12% on disk-bound reads with no cold regression on
/// SSDs; deeper queues buy little (the data read, not the open, is then
/// the cost) and risk competing random I/O over scattered content-
/// addressed chunk files on seek-bound HDDs. See `LocalBlobBackend`.
/// • Remote (S3/Azure) → `8`: there the dominant cost is per-chunk request
/// latency, and overlapping fetches hides it (≈ N× faster reassembly).
/// Wrapping backends delegate to the backend that actually serves the bytes.
fn read_prefetch(&self) -> usize {
1