Skip to content

perf(http): gate the keep-alive stash shrink on an idle window - #381

Merged
gg582 merged 2 commits into
devfrom
perf/stash-idle-hysteresis
Oct 16, 2026
Merged

gg582 merged 2 commits into
devfrom
perf/stash-idle-hysteresis

Conversation

@gg582

@gg582 gg582 commented Oct 16, 2026

Copy link
Copy Markdown
Member

Summary

  • What: the C1M async path no longer shrinks an idle rbuf/obuf stash to the 4 KiB floor at every keep-alive re-arm. The shrink is gated on capacity demand: conn->stash_grown_sec (new field, stamped at every rbuf/obuf growth) must be older than CWIST_ASYNC_STASH_IDLE_SEC (default 10 s, 0 = legacy immediate shrink) before the re-arm shrinks anything.
  • Why: shrink-at-every-rearm + regrow-on-next-large-request is a realloc pair per request on busy large-request keep-alive connections — glibc arena lock, possible buffer move, fresh page touches: a rare-but-large P99.999 stall source. The two shrink sites also disagreed (REARM shrank to the floor; cwist_http_async_rearm freed rbuf only >64 KiB and never touched obuf) — both now share one helper with a single documented policy.
  • RSS story preserved: a quiet connection still converges to the small floor one window after its last growth (glibc retains freed heap anyway, so convergence is in live allocation — verified).

A/B (8 KiB POST keep-alive — the path the contract benchmark doesn't exercise; 3 interleaved rounds, medians; base-vs-base noise RPS ±2–4%, P99.999 19.8–83 ms)

leg RPS base → cand P99.999 base → cand
c64 190.8k → 222.0k (+16.3%) 32.9 → 18.5 ms (−44%)
c256 262.3k → 294.9k (+12.4%) 70.4 → 41.7 ms (−41%)

Both beyond noise by the merge rule. Regression legs (standard small-request cwist_bench, c256/c400): within noise, no regression (c256 slightly favorable).

Path-firing proof: instrumented counters showed baseline = 4 GROW + 3 SHRINK per 3-request session vs candidate = 1 GROW + 0 SHRINK checks passed (instrumentation reverted before commit).

Notes

  • Anchor is stash_grown_sec, not last_active_sec — the latter is stamped immediately before both shrink sites, so a literal check would never shrink (failing the RSS goal).
  • CWIST_ASYNC_STASH_IDLE_SEC=0 is byte-identical at the REARM site; the continue-path's legacy asymmetry (no obuf handling, different rbuf policy) is intentionally fixed by the unification.

Test plan

  • test_idle_reaper (also with knob 0 and 1), test_http_fairness, test_async_defer, test_rx_epoll, test_http_pipeline, test_abi_layout — pass
  • check_env_knobs.py (incl. doc-twin check), check_test_wiring.py — OK
  • Local independent re-verify: clean build + test_idle_reaper / test_http_fairness / test_async_defer pass

gg582 added 2 commits October 16, 2026 21:28
The C1M async path shrank an empty rbuf/obuf stash to the 4 KiB floor at
every keep-alive re-arm, and the next request above the floor regrew it
(http_async_stash_grow doubling realloc): a shrink->regrow realloc pair
per request on busy large-request connections -- glibc arena lock, a
possible buffer move with memcpy, fresh page touches -- a
rare-but-large P99.999 stall source. The two shrink sites also disagreed
(http_async_dispatch shrank to the floor; cwist_http_async_rearm freed
rbuf only above 64 KiB and never touched obuf).

Gate the shrink on capacity demand instead of time-since-activity:
conn->stash_grown_sec (stamped at every rbuf/obuf growth, the >=16 KiB
zero-copy handover, and connection setup) must be older than
CWIST_ASYNC_STASH_IDLE_SEC (default 10 s, cached getter, 0 = legacy
immediate shrink) before the re-arm shrinks anything. Busy keep-alive
connections keep their capacity and pay zero reallocs; a quiet
connection still converges to the small floor one window after its last
growth, preserving the C1M idle-RSS goal (delayed by the window; glibc
retains freed heap anyway, so RSS convergence is in live allocation).
Both re-arm sites now share one helper with a single documented policy.

A/B (8 KiB POST keep-alive, the path the contract benchmark does not
exercise; 3 interleaved rounds, medians, base-vs-base noise RPS 2-4% /
P99.999 19.8-83 ms):
  c64:  RPS 190.8k -> 222.0k (+16.3%),  P99.999 32.9 -> 18.5 ms (-44%)
  c256: RPS 262.3k -> 294.9k (+12.4%),  P99.999 70.4 -> 41.7 ms (-41%)
Standard small-request legs (c256/c400 cwist_bench): within noise, no
regression. Tests: test_idle_reaper (also knob 0/1), test_http_fairness,
test_async_defer, test_rx_epoll, test_http_pipeline, test_abi_layout
pass; check_env_knobs and check_test_wiring OK.
The hysteresis anchor appends one field to cwist_http_async_conn_t;
regenerate the baseline so the CI drift gate passes. The baseline diff is
the PR's record of the layout change (pre-v4 guarantee check stays
skipped until the v4 tag exists).
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