Skip to content

perf(reactor): serve events queued past the tail threshold first - #375

Closed
gg582 wants to merge 1 commit into
devfrom
perf/tail-latency-heuristic
Closed

gg582 wants to merge 1 commit into
devfrom
perf/tail-latency-heuristic

Conversation

@gg582

@gg582 gg582 commented Oct 13, 2026 •

Copy link
Copy Markdown
Member

Summary

Stamps every armed reactor event with CLOCK_MONOTONIC and, at delivery in both the io_uring and epoll paths, serves events whose queue age exceeds CWIST_REACTOR_TAIL_US (default 3500 us, 0 disables) first, reusing the existing late-round bypass path in the HTTP layer. The latency probe reports the elevated count as tail_elevations.

A/B measurement

Workload: wrk -t4 -c256 -d30s -s scripts/ci/tail_latency.lua against benchmarks/web-frameworks/cwist_bench on port 3000; medians over 8 runs (io_uring) / 3 runs (epoll). "off" = CWIST_REACTOR_TAIL_US=3600000 (heuristic effectively disabled).

io_uring (default backend):

TAIL_US RPS p50 (ms) p99 (ms) p99.999 (ms)
off 281,155 0.334 4.716 11.83
2500 281,718 0.329 4.671 12.73
3500 (default) 280,551 0.334 4.672 12.24
5000 279,776 0.329 4.731 13.60

epoll (CWIST_REACTOR_BACKEND=epoll):

mode RPS p50 (ms) p99 (ms) p99.999 (ms)
off 300,882 0.344 4.401 10.55
on (3500) 302,787 0.340 4.380 10.89

High-concurrency validation (c512 / c1024, wrk -t8, 3 runs per cell, heuristic on = 3500 us vs off)

backend conc mode RPS p50 (ms) p99 (ms) p99.999 (ms)
io_uring 512 off 359,956 0.553 10.52 54.6
io_uring 512 on 363,627 0.543 9.86 56.8
io_uring 1024 off 366,030 1.499 15.74 48.1
io_uring 1024 on 365,231 1.481 15.70 56.5
epoll 512 off 388,013 0.548 8.55 41.1
epoll 512 on 385,887 0.562 8.96 43.3
epoll 1024 off 384,782 1.394 14.31 46.9
epoll 1024 on 384,075 1.397 13.87 51.6

Latency-probe confirmation at c1024 (io_uring, CWIST_LATENCY_PROBE=1): the heuristic fires only a handful of times per worker (tail_elevations ≈ 3–4 per worker per 30 s run, vs ~1 with the heuristic off, out of ~10 M requests), while ~100–130 k queue_delay samples exceed 5 ms in both states and the queue_delay histogram (p50 ≈ 2500 us, p99 ≈ 25 ms) is unchanged. The age-at-arm stamp is refreshed too often for events to cross the 3500 us threshold at delivery, so dispatch order is measurably unaffected — which matches the flat wrk numbers.

Conclusion

Neutral up to c1024 on this harness: no p99/p99.999 improvement beyond noise at any tested threshold or concurrency, and no RPS regression beyond ±1%. The mechanism is wired correctly (proven by test_reactor_tail and the probe counter) but rarely triggers under this workload, so it is a safe, opt-out-by-env no-op rather than a measured win. Keeping the 3500 us default; results reported honestly rather than claimed as a speedup.

Stamp every armed event with CLOCK_MONOTONIC at arm time (one clock read
and store) and check queue age at delivery in both reactor backends: a
round-start scan pulls already-aged events to the front of the dispatch
batch, and a per-delivery check catches events that age behind a slow
earlier callback. An event past CWIST_REACTOR_TAIL_US (default 3500 us,
0 disables) is dispatched first and sees cwist_reactor_dispatch_late()
true, so the HTTP layer serves its buffered pipeline without yielding --
the same bypass late-round mode (CWIST_REACTOR_LATE_US) uses, so both
backends share one code path. RX-uring/RX-epoll completions carry their
arm stamp on the HTTP side; the RX completion handler runs the same age
check and raises the bypass. Aged-event counter is reported by the
latency probe as tail_elevations.
@gg582

gg582 commented Oct 14, 2026

Copy link
Copy Markdown
Member Author

Closing: neutral on every A/B in the description (no p99/p99.999 gain beyond noise up to c1024). The late-round heuristic it builds on is being removed in a follow-up PR.

@gg582

gg582 commented Oct 14, 2026

Copy link
Copy Markdown
Member Author

Closing: neutral on every A/B in the description. The existing late-round heuristic (CWIST_REACTOR_LATE_US) is removed in a follow-up PR.

@gg582 gg582 closed this Oct 14, 2026
gg582 added a commit that referenced this pull request Oct 15, 2026
The late-round mode from bc358b4 (issue #319 experiment) switched the rest
of an io_uring dispatch round into a no-yield, flush-per-callback mode once
the round had run past 3 ms. It never showed a tail improvement beyond
noise in the same-runner A/B, and the follow-up age-based variant (#375)
was neutral too. Drop it:

- reactor.c: reactor_late_ns(), cwist_reactor_dispatch_late(), the
  per-callback clock read and late flush, and the late_rounds /
  late_callbacks probe counters;
- reactor_rx.h: the cwist_reactor_dispatch_late() declaration and stub;
- app.c: the HTTP handler yields every yield_batch requests again, with
  no late bypass; the now unused reactor_rx.h include goes too;
- tests/test_reactor_late.c and its Makefile target;
- the CWIST_REACTOR_LATE_US rows in docs/runtime-config.md (en, ko);
- the "CWIST C1M (late mode off)" benchmark leg.

The round-budget flush (CWIST_REACTOR_ROUND_BUDGET_US) and the post-drain
chunking (CWIST_REACTOR_DRAIN_CHUNK) are unchanged. Past cwist_c1m_lateoff
rows in benchmarks/webserver.json stay as history.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant