v2.2.2
--closed runs 17-19% faster, and paced runs shed the same overhead. Nothing
about what zrk measures changes: pacing, coordinated-omission correction and
every number in the report mean exactly what they did in 2.2.1.
What was slow
Every request armed its own wire timeout by spawning a concurrent task that
slept to the bound, then cancelling and joining it once the response landed.
--timeout defaults to 2s and --closed never turned it off, so the
pacing-free mode paid for a task spawn, a cancel and a join on every single
request — to enforce a deadline that, on a healthy server, never came close to
firing.
Measured against nginx at 256 connections over 4 threads, that cost 0.173
context switches and 1.42us of user CPU per request. Running with
--timeout 0 — no wire timeout at all — was 16-22% faster purely by not doing
it.
What changed
Requests are strictly sequential on a connection: at most one is ever on the
wire. So one watcher per connection can enforce every request's deadline, and
the request loop only publishes the in-flight deadline and withdraws it.
The connection now holds that deadline alongside a counter bumped on each arm
and disarm. The watcher sleeps to the deadline and, finding the counter
unchanged, knows the request it timed never left the wire and is safe to abort.
Keying on the counter rather than on the deadline value keeps that sound when
two sends land in the same nanosecond.
A busy connection wakes the watcher about once per timeout period instead of
once per request. Per-request cost becomes four atomic stores: 0.00086 context
switches and 0.70us of user CPU, a 200x drop in the former.
Throughput rises 17.0% at 256 connections and 18.8% at 512 — all of the
headroom --timeout 0 showed at 256, and 85% of it at 512, with the timeout
still enforced.
The timeout still works
This was a mechanism change, not a feature removal, and that distinction is
the point. In --closed there is no send schedule behind a stalled request,
so a server that accepts connections and never answers has nothing else to
give it away: without the timeout those connections would simply stop issuing
requests, and the run would report a lower rate with zero errors — a hung
server reading as a slightly slower one, and --max-error-rate and
--slo-p99 passing on it.
Verified against a peer that accepts and never responds: timeouts are still
raised, still recorded as timeout errors, and still tear the connection
down.
Also
Two bounds became one. The wire timeout runs from the actual send and the
--deadline-abort bound from the scheduled send, but both are absolute
timestamps — so the earlier one binds, a single deadline replaces the two
concurrent timers, and which one fired is recovered by comparison. And since
the timeout runs from the actual send, only a send that paced re-reads the
clock; a send that fired immediately, which is every send in --closed,
reuses the timestamp it already had.
zio moves to 38206678. Most of what that picks up is select and channel
protocol work zrk's connection path never touches, but one commit matters
here: the runtime's cached clock snapshot refreshed before its backend poll,
so a duration timer armed after a poll that slept was backdated by the whole
wait and fired on the next scan instead of at its deadline. The new watcher
arms exactly such a timer. Its own deadline re-check meant no request was ever
wrongly aborted, but an idle connection's watcher could re-arm in a spin until
the snapshot caught up. It measures as no throughput change, as expected for
select and channel work.
zig build test covers 243 tests, all passing, plus
zig fmt --check clean across four cross-compiled targets.