Conversation
The runtime counts what explains a benchmark result rather than merely states it — buffer_ring_empty, recv_parked, recv_fallback, forward_throttled, recv_arm_failures — but nothing in the workspace read them back out. A sweep over buffer geometry could report that an arm was slower without being able to say whether it starved its provided ring, degraded to the fallback recv, or neither. ringline-rs#416 is blocked on this. `bench-server --metrics-out <path>` walks the metriken registry and writes it as JSON at shutdown, and the interesting counters print to stderr for whoever is reading a job log. Two things the tests caught: A counter nobody has incremented reads back as `None`, not `Some(0)`, so the first version dumped nothing at all for a healthy run — identical to what a broken harness produces. Declared entries now report 0. And `ringline/bytes` was incremented in ten places on io_uring and none on mio, so every mio arm would have read zero bytes and any per-byte figure derived from the dump would have silently divided by it. The mio recv (plaintext and TLS ciphertext) and send paths now count too. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01FDHdYwqsnPN4EkhRBvbDgE
`run_ringline` hardcoded `recv_buffer(256, msg_size-derived)` while `--recv-buffer-bytes` and `--recv-ring-size` were read only by the proxy arm. Every echo arm of a buffer-geometry sweep would therefore have run the *same* configuration under different labels and reported a confident "no effect" — 27 arms of Phase A in ringline-rs#416, and any earlier echo-path buffer comparison anyone believes. Found by the Phase 0 gate rather than by reading: a deliberately starved 8x4KiB ring counted zero starvation, which is only possible if the flags did nothing. It now runs what it is told, and the ready line prints the effective geometry so a log proves which config ran: ready (backend=io_uring, echo_mode=direct, recv_buffer=8x4096 = 0 MiB/worker) ringline/pool/buffer_ring_empty = 11950066 ringline/pool/recv_parked = 11950066 against zero starvation at 256x65536, with throughput 12.4 GB vs 28.4 GB across the same window. The arguments move into an `EchoCfg`, as the proxy arm's already had to. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01FDHdYwqsnPN4EkhRBvbDgE
…ghput Joins each chunk's client results with its server metrics dump by arm id. An arm whose server never dumped prints `?`, never 0 — silence is not a clean ring, and the two must not read the same. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01FDHdYwqsnPN4EkhRBvbDgE
Intent recorded before building, per the journal's ground rules. The question: provided-buffer size and ring depth are one knob at a fixed memory budget, so every workload picks which of the two to be wrong about. ringline-rs#415 measured the size edge — ~6,600 instructions per completion cycle on the forward path, worth 25% between 16 KiB and 64 KiB buffers — and ringline-rs#416 Phase A chunk 1 measured the depth edge, where the same 1 MiB buffer starves 3,066,467 times at a 4-deep ring and zero times at 256 deep, the deep one being the fastest arm in its group. `IOU_PBUF_RING_INC` (Linux 6.12) is the mechanism that would sever the tie. Recorded honestly: I recall a prior 200 GbE sweep finding INC not a win, and that result is not checked in anywhere in this repo. It is marked unverified rather than cited. The adjacent 2026-07 NO-GO on *adaptive* sizing stands and does not cover this, whose criteria A-D none of ringline-rs#415's finding matches — so this would add a criterion E rather than quietly contradict it. Also recorded: why the kernel floor must be a runtime probe (build.rs gates at 6.1; gating at 6.12 would drop every 6.1-6.11 host), and the bid-lifecycle sites that assume one bid is one discrete segment, which INC breaks. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01FDHdYwqsnPN4EkhRBvbDgE
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01FDHdYwqsnPN4EkhRBvbDgE
The collector read the percentiles from a nested `latency` object; bench-client emits them at the top level, so every arm came back with p99 = None and the decision rule's latency half would have been silently absent from the table. Accept both shapes, print p99, and warn loudly if an echo arm still has none. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01FDHdYwqsnPN4EkhRBvbDgE
The first criteria asked INC to win on a tuned homogeneous workload, which is the case it is least likely to win: if you know the message size you can already pick the geometry for it. The value is robustness without tuning, and mixed traffic is where fixed geometry hurts — owner's framing, and the right one. Criterion 1 becomes an oracle comparison: the shipped default vs the best geometry chosen with hindsight vs a plausible misconfiguration, on a size distribution. The default-to-hindsight gap is the tuning burden INC would remove, and if it is small there is nothing here however elegant the mechanism. Also recorded: Phase A is entirely homogeneous, so its flat copy-path surface is evidence about tuned workloads only — the same omission that left the 2026-07 NO-GO's mixed-workload gate unmet. And the memory question is sharpened: the ring backing is not mlocked and not charged to RLIMIT_MEMLOCK (provided.rs:54), so over-provisioning is virtual up front — but alloc_zeroed pages go resident when touched and stay, so the 2026-07 RSS figure describes what a steady benchmark touched, not what a bursty production workload will. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01FDHdYwqsnPN4EkhRBvbDgE
… feature A per-connection size mix in bench-client would have meant threading a distribution through ClientState and the per-worker connection slices. Two concurrent client processes against one server give the same property — one ring, two size populations — with no client change, and report each population separately, which is the more informative experiment: the failure worth finding is the small messages starving while the large ones hold the ring, and an aggregate would hide exactly that. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01FDHdYwqsnPN4EkhRBvbDgE
… a precondition The first Phase A run was not server-bound in any arm — ~8 of 24 cores on the echo arms, ~9 on the forward arms, every forward arm from 16 KiB to 1 MiB pinned at the ~22 Gbit/s wire ceiling, including a 1 MiB x 4 arm that starved 441,351 times and still tied the clean arms. A flat surface from an unsaturated server is indistinguishable from geometry not mattering, so that run decides nothing. --workers and --conns are now generator flags with the reason recorded next to them, and the design doc states saturation as a precondition to reading any arm as a result. Client threads scale with connection count, since one blocking writer per connection round-robined by too few threads makes the load generator the limiter; the connect wait grows to 120s for high fan-in. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01FDHdYwqsnPN4EkhRBvbDgE
`ConfigBuilder::prefault_buffers(bool)`, default off while the trade is measured (ringline-rs#416). `vec![0u8; n]` goes through `alloc_zeroed`, so a large allocation is mapped but untouched: every page is the shared zero page until something writes to it. On the recv path that first write is the *kernel* copying an skb in, so the minor fault lands on the completion path rather than at startup. Prefaulting walks the provided recv ring and the send copy pool once, on the worker thread that owns them. What it buys is predictability, not throughput: faults stop appearing as ramp-phase p99 outliers, and RSS after startup equals RSS under load — so a ring the machine cannot back fails at launch rather than during a traffic burst, which is how RLIMIT_NOFILE and RLIMIT_MEMLOCK are already handled. What it costs is that over-provisioning stops being free, which is the point but is a behaviour change, hence default off. NUMA is why this lives in the driver constructor rather than anywhere convenient: `worker.rs` pins the thread before building the driver, so the pages fault in on the owning worker's node. Prefaulting from the launching thread would pull every worker's buffers onto one node. The write is volatile because a plain store of 0 to an already-zeroed page that nothing later reads is exactly the store an optimizer may delete, and deleting it would leave the page cold. Tests assert the property, not the call: `mincore` residency before and after (the "before" half matters — without it a platform that eagerly populated mappings would pass while prefaulting did nothing), and an end-to-end RSS check that a 32 MiB pool moves RSS by >=90% of its size and was not already resident from allocation. A follow-up could use MADV_POPULATE_WRITE (Linux 5.14+) instead of the touch loop; the loop is portable across both backends and macOS, which is what a prototype wants. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01FDHdYwqsnPN4EkhRBvbDgE
Per workload: the shipped default, the best arm inside the default 4 MiB budget, and the best arm at any budget. The equal-memory column is the honest comparison; the any-budget column says what more memory buys. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01FDHdYwqsnPN4EkhRBvbDgE
Two saturated passes, 2 workers, 64 vs 1024 connections, 108 arms. The shipped `recv_buffer(256, 16384)` is minimax-optimal across the measured space: worst case -34%, median -6%, against -36/-59/-70/-83% worst for every alternative that fits the same 4 MiB budget. It never wins a workload and never loses badly, which is what a default is for. The reason no geometry wins is that the optimum moves along two axes in opposite directions. Message size pulls toward bigger buffers (payload per completion amortises a fixed ~6,600-instruction cost, ringline-rs#415); fan-in pulls toward deeper rings (depth bounds concurrent arrivals). At fixed memory those trade directly, and six different geometries win the twelve workloads. 1 MiB x 4 wins four of them outright and is 83% down on another, with a 1.03-second p99 at 256 B x 1024 connections from 137,895,774 ring starvations. The one systematic exception is forwarding: -34% at 64 connections, -18% at 1024. `forward_to`'s sizing note is load-bearing rather than advisory, and now gives a range with the fan-in dependence explained — 1 MiB x 4 is best at low fan-in and collapses at high, so 256 KiB x 16 is the safer proxy default. Cross-validation: the forward arms reproduce ringline-rs#415 on an independently built harness, 11.32 vs 11.2 Gbit/s at the default and 14.26 vs 14.0 at 64 KiB. This also narrows ringline-rs#416's INC case, recorded in that journal: the tuning burden a geometry-aware runtime could remove is ~6% for request/response — below criterion 1's bar — and 18-34% only for forwarding. Criterion 1 now targets a mixed workload on the forwarding path specifically. Raw per-arm results and the regret analysis are checked in beside the generator. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01FDHdYwqsnPN4EkhRBvbDgE
ringline-rs#416 Phase A found the recv geometry default sound everywhere except forwarding (-34% at 64 conns, -18% at 1024). Instructions per byte says that gap is not about buffer size: at the same 16 KiB default, direct echo runs 0.985 instr/byte against Mode A's 1.26, over a ~0.86 kernel floor. Echo at the default buffer is already as efficient as forwarding at four times the buffer. The difference is that Mode A wakes a task per provided buffer, where run_direct_echo submits from inside the CQE handler. The fix is to move the forward state into the driver -- the shape the mio backend already has since ringline-rs#415 -- and submit the next write from the write-completion handler. Also recorded: buffers stop filling past ~100 KB (a multishot recv completes with what TCP has queued, not a full buffer), so bigger buffers are not a substitute; that ceiling is what made a 4-deep 1 MiB ring post a 1.03-second p99. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01FDHdYwqsnPN4EkhRBvbDgE
Mode A woke a task for every provided buffer: recv CQE -> hold push -> wake_recv -> collect_wakeups -> poll_ready_tasks -> future poll -> SQE build -> write CQE -> replenish -> wake_recv. `run_direct_echo` has never done this, and ringline-rs#416 measured the difference: 1.26 instructions per byte for Mode A against direct echo's 0.985 at the same 16 KiB buffer, over a ~0.86 kernel floor. Echo at the default buffer was already as efficient as forwarding at four times the buffer. Move the forward state (target, len, forwarded) out of ForwardToFuture and into the driver as ForwardProgress -- the shape the mio backend has had since ringline-rs#415 -- so `handle_forward_write` and the segmented recv branch can pop the next held buffer and submit it themselves. `advance_forward` is the one driver method both call; `finish_forward_if_done` is the one place a terminal result is recorded and the task woken. Ordering is unchanged: one write in flight per connection, held buffers in arrival order. Only who issues the write changes. The future still drives `advance_forward` on every poll, and that is not redundant. The first attempt made it park-only, on the assumption that the handlers covered every path -- they cover the ones carrying data. EOF does not: `handle_recv_multi` notes the FIN and wakes, the future parked again, and a truncated forward hung forever. 47 of 60 proxy runs. Driving it from every poll means any wake, from any path, can finish the forward; the handlers remain the reason the common path never needs one. Verified on io_uring: clippy -D warnings clean, full suite green, and the ringline-rs#415 proxy regression test 0/60 (47/60 before the EOF fix). Performance not yet claimed -- the A/B is next. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01FDHdYwqsnPN4EkhRBvbDgE
Built, correct (proxy gate 0/60 on io_uring), and ~3% slower. The diagnostic that matters: instructions per byte is unchanged, 1.236 baseline vs 1.243 with the change. Removing a task wake per provided buffer moved nothing, so the ~6,600 instructions per completion from ringline-rs#415 does not live in the scheduler round-trip. That leaves exactly one difference from run_direct_echo, the one this entry deferred: direct echo gathers a drain's worth of buffers into one send (ringline-rs#397), Mode A writes one buffer per write. Per N buffers that is N+1 completions against 2N, which is ~4,300 instructions per buffer at 16 KiB and is now the only remaining explanation. Gathering is the next attempt. The prototype stays on a branch rather than landing: it is a correct regression, and its only value is as a prerequisite for gathering, which has not been shown to work yet. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01FDHdYwqsnPN4EkhRBvbDgE
Mode A wrote one held buffer per write; `run_direct_echo` has coalesced a drain's worth into a single send since ringline-rs#397. ringline-rs#416 measured what that costs: 1.24 instructions/byte against direct echo's 0.985 at the same 16 KiB buffer, over a ~0.86 kernel floor. Removing the per-buffer task wake recovered none of it (the direct-forward NO-GO), which left the completion count as the only remaining explanation: per N buffers, N recv + N write CQEs against direct echo's N + 1. So a forward write now takes up to MAX_FORWARD_IOV (16) held buffers as one `sendmsg` (sockets, keeping MSG_WAITALL, which a bare writev would lose) or `writev` (files). Ordering is unchanged: iovecs in hold order, still one write in flight per connection. SQE memory must outlive the operation, and here that is three things — the buffers, the iovec array, and the msghdr pointing at it. All live in the driver's ForwardWriteState, and the state is installed *before* pointers into it are taken, since moving it afterwards would leave msg_iov's owner dangling even though the iovec heap block survives. Bid release is now batch-wide, on completion, submit failure, and fail_forward_write. A short write rebuilds the iovec array from `written` rather than re-slicing: whole consumed buffers drop out and the first survivor starts mid-buffer. The rebuild only ever runs between operations. The cap is 16 because a batch's bids all return together, so a large batch delays them and coarsens the hold-cap throttle -- and 16 x 16 KiB is already past the ~100 KB where ringline-rs#416 measured socket queues stop filling recv completions. Two unit tests asserted the single-buffer shape (`total == 5`, hold dropping by one) and now assert the batch, strengthened rather than merely renumbered: gathered iovec counts, the rebuilt array covering exactly the bytes still owed, a fully-written buffer dropping out of the iovecs, and no bid returning until the batch completes -- that last one guards the new way to break "exactly one replenish per bid". Verified on io_uring: clippy -D warnings clean, full suite green, the ringline-rs#415 proxy regression test 0/60. Performance not yet claimed. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01FDHdYwqsnPN4EkhRBvbDgE
Three interleaved reps each at the default geometry: 11.23 -> 14.60 Gbit/s, instructions per byte 1.236 -> 0.906 (past direct echo's 0.985, within 5% of the ~0.86 kernel floor). Per-buffer overhead falls from 0.38 to 0.05 instr/byte, so 87% of Mode A's avoidable cost was the completion count. Ranges do not overlap. With the failed direct-forward attempt this decomposes the ~6,600 instructions per completion: the scheduler round-trip was ~0, the completion count was ~all of it. The NO-GO earned its cost by eliminating the wrong explanation. It also closes ringline-rs#416's one exception. At equal memory the default now measures 14.60 against 15.13 at 64 KiB and 14.22 at 256 KiB -- within 3.5% of best, the same order as its deficit on every echo workload, and 256 KiB is now worse than the default. So 's sizing section is retired rather than revised: the answer to 'what geometry should I use' is the default, for everything measured. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01FDHdYwqsnPN4EkhRBvbDgE
This was referenced Sep 18, 2026
Contributor
Author
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Phase 0 of #416. Two bugs, both of which would have turned the sweep into a confident null.
The echo arm ignored the geometry flags
run_ringlinehardcodedrecv_buffer(256, msg_size-derived), while--recv-buffer-bytesand--recv-ring-sizewere read only by the proxy arm. Every echo arm of a buffer-geometry sweep would have run the same configuration under different labels — 27 arms of #416's Phase A — and reported "no effect" with a straight face. It also means any earlier echo-path buffer comparison run throughbench-servermeasured nothing.It now runs what it is told, and the ready line prints the effective geometry so a log proves which config ran rather than which was requested.
Nothing read the runtime counters back out
BUFFER_RING_EMPTY,RECV_PARKED,RECV_FALLBACK,FORWARD_THROTTLED,RECV_ARM_FAILURESare all counted and none were readable — no exporter anywhere in the workspace. A sweep could report that an arm was slower without being able to say whether it starved its provided ring, degraded to the fallback recv, or neither.bench-server --metrics-out <path>walks the metriken registry to JSON at shutdown; the interesting counters also print to stderr for whoever is reading a job log.Two things the tests caught while writing it:
None, notSome(0), so the first version dumped nothing at all for a healthy run — byte-identical to what a broken harness produces. Declared entries now report 0, so "no starvation" and "no dump" look different.ringline/byteswas incremented in ten places on io_uring and none on mio, so every mio arm would have read zero bytes and any per-byte figure derived from the dump would have silently divided by it. The mio recv (plaintext and TLS ciphertext) and send paths now count too.Gate
The spec requires a deliberately starved ring to actually show starvation, or the dump is not wired to anything. On hv02, io_uring, 64 connections at 256 KiB:
The counters discriminate, and the 2.3× throughput gap is the starvation being paid for. This gate is what found the ignored-flags bug — the first run reported zero starvation on the tiny ring, which is only possible if the flags did nothing.
Also included:
experiments/recv-buffer-sweep/generate.py, which generates the sweep as barrier-per-arm two-guest experiments (one guest boot per chunk of 12 arms, server foreground undertimeout -s TERMso each arm dumps its counters and rezolus scopes to that arm). Validated: 54 arms in 5 chunks for Phase A, all 24 barriers per chunk paired across both jobs.Verified:
cargo test --alland clippy-D warningson both backends; the gate above on real io_uring hardware.🤖 Generated with Claude Code
https://claude.ai/code/session_01FDHdYwqsnPN4EkhRBvbDgE