bench: streaming-read A/B harness for the 3c go/no-go (backlog#1144)#4
Conversation
…#1144) Add `examples/streaming_bench.rs` + `bench-streaming.sh` to answer whether ecstore's sequential streaming reads (open_read_stream/open_full_read) should be routed through io_uring, or keep delegating to StdBackend's buffered path (which rides kernel readahead). The example reads one file end-to-end with a chosen strategy and prints a CSV row (throughput is the decision metric); one measurement per invocation so the runner controls page-cache state. Strategies: std_buffered (baseline), std_odirect, and pipelined uring_read_at / uring_read_at_direct at a given queue depth — because io_uring's only lever on a sequential stream is depth (hiding per-op latency), a single-depth io_uring read is expected to lose and the fair comparison is deep-pipelined io_uring vs buffered+readahead. `bench-streaming.sh` sweeps strategy × size × chunk × queue-depth in two cache legs (warm: pre-read; cold: drop_caches before each timed run; cold needs root), pre-creating files so cold runs are genuinely cold. No library code changes; example is additive and does not affect the crate API. Verified: clippy -D warnings clean; all four strategies run end-to-end under real io_uring (byte counts match). Co-Authored-By: heihutu <heihutu@gmail.com>
38245d8 to
5561d3a
Compare
houseme
left a comment
There was a problem hiding this comment.
Review comments — please address these before merging.
I found three issues in the additive benchmark harness:
-
ensure_filesilently truncates an existing path whose size differs, andmetadata/File::create follow symlinks. The script is documented to run as root, so a wrongBENCH_DIRor direct CLI path can overwrite unrelated data. Refuse existing files, or create only inside a dedicated directory with no-follow semantics. -
The CLI accepts invalid parameters without validation.
chunk=0reachesstep_by(0),align=0reachesnext_multiple_of(0), and oversized queue-depth arithmetic can overflow or panic. Validate positive, power-of-two/alignment-compatible values and use checked arithmetic. -
The benchmark verifies only the total byte count. A strategy can return the wrong contents or wrong offsets and still pass
assert_eq!(total, size). Add an optional checksum/sample verification mode and cover partial-size/O_DIRECT boundary cases outside the timed measurement.
CI passing is useful, but it does not execute the sweep script or validate these runtime cases.
Two gaps in the harness as first written: - `ensure_file` used `File::create`, which truncates. The sweep runs as root with a caller-supplied BENCH_DIR, so a mistyped path would have destroyed data. It now creates only when absent (`create_new` + O_NOFOLLOW), and treats an existing wrong-size or non-regular path as a hard error instead of an overwrite. Reads use O_NOFOLLOW too, closing the stat/open window. - Throughput cannot distinguish a strategy that reads the right *number* of bytes from one that reads the wrong offsets or repeats a chunk. The file is now filled with an offset-addressable pattern and `BENCH_VERIFY=1` checks every delivered byte at its claimed offset. The runner replays the boundary geometries (partial tail, O_DIRECT tail inside a partly valid block, qd 1 and 4) under verification before any measurement is taken, so a wrong-offset strategy aborts the sweep instead of posting a fast number. Also validates the geometry up front (chunk/qd/align bounds, O_DIRECT chunk-vs-align) so bad input fails before any I/O rather than panicking in `step_by`/`align_offset`. Co-Authored-By: heihutu <heihutu@gmail.com>
|
All three addressed in f6911ec. Each fix was exercised on the bench host (Linux 6.17, real io_uring, NVMe), not just type-checked. 1. It no longer writes to an existing path at all. Observed: A directory at the target path is refused the same way, and 2. Unvalidated CLI geometry
3. Length-only assertion admitted wrong contents/offsets You are right that
That the check has teeth (flipping one byte at offset 700000, then re-running each strategy): Without Verification. On the bench host ( |
What
Adds the streaming-read A/B harness for the 3c go/no-go (rustfs/backlog#1144): should ecstore's sequential streaming reads (
open_read_stream/open_full_read) be routed through io_uring, or keep delegating to StdBackend's buffered path that rides kernel readahead?examples/streaming_bench.rs— reads one file end-to-end with a chosen strategy, prints a CSV row (throughput is the decision metric). One measurement per invocation so the runner controls page-cache state.bench-streaming.sh— sweeps strategy × size × chunk × queue-depth in two cache legs (warm: pre-read; cold:drop_cachesbefore each timed run), pre-creating files so cold runs are genuinely cold.Why pipelined io_uring
io_uring's only lever on a sequential stream is depth (keeping N reads in flight to hide per-op latency). A single-depth io_uring read is expected to lose to buffered+readahead, so the strategies take a queue depth and the fair comparison is deep-pipelined io_uring vs buffered+readahead. O_DIRECT variants additionally avoid page-cache pollution.
Scope
Library code unchanged — the example is additive and does not touch the crate API. This PR is the measurement harness; the go/no-go decision and results table land on backlog#1144 (and, if Go, a separate implementation PR).
Verification
clippy --example streaming_bench -- -D warningsclean.