Skip to content

perf: RSS reductions with unchanged performance (idle trim, TLS cache cap, 4 KiB virgin stash, defer shrink) - #384

Merged
gg582 merged 4 commits into
devfrom
perf/rss-reductions
Oct 16, 2026
Merged

gg582 merged 4 commits into
devfrom
perf/rss-reductions

Conversation

@gg582

@gg582 gg582 commented Oct 16, 2026

Copy link
Copy Markdown
Member

Summary

Four independent "same performance, less RSS" patches from an RSS audit, one commit each (revertible individually). Every item was measured before and after; the perf gate was wrk A/B within noise (±3–5% band, P99.999 never worse), and scripts/ci/rss_gate.sh passes on the final tree (idle 36,696 KiB, round growth 232 KiB — slightly better than baseline's PASS at 36,892/324 KiB).

item mechanism measured RSS effect perf
perf(reactor) idle-streak malloc_trim(0) per reactor after CWIST_MALLOC_TRIM_IDLE_SEC (default 30 s, 0=off) of event-free wakes median 1224 KiB/worker + 1480 KiB master returned after a load round (gdb-forced measurement; auto-trim path verified end-to-end) none — runs only on zero-event wakes; also fixes the io_uring idle check being unreachable behind -ETIME
perf(tls) SSL_CTX_sess_set_cache_size at both SSL_CTX sites (CWIST_TLS_SESS_CACHE_SIZE, default 1024, 0=BoringSSL default) bounded retention: BoringSSL's default 20k-session cap is up to ~20 MiB/worker under pathological session-ID churn (measured real usage: 0 through wrk/ab/h2load/s_client churn) none — cap consulted on insert only
perf(http) virgin C1M stash starts at the 4 KiB idle floor instead of 16 KiB; doubling ladder unchanged 4000 young keep-alive conns: 24.8 → 12.9 KiB/conn (−11.7 KiB/conn, deterministic) within noise (8 KiB POST c64 +2.6%, c256 +2.9%)
perf(http) shrink stashes at CWIST_ASYNC_DEFER (anchor aged out before the hysteresis-gated helper) 3000 parked SSE/long-poll conns: 28.8 → 16.9 KiB/conn (−11.9 KiB/conn) within noise; one shrink→regrow pair per defer cycle, RX-uring invariant holds

Notes

Test plan

  • test_idle_reaper, test_http_fairness, test_rx_epoll, test_http_pipeline, test_async_defer, test_sse, test_reactor_wake, test_reactor_timer, test_reactor_drain_chunk — pass with default env and with knobs off/on
  • scripts/ci/rss_gate.sh PASS (candidate); check_env_knobs.py OK (both new knobs documented en+ko)
  • Perf A/B tables in the commit bodies

gg582 added 4 commits October 16, 2026 23:49
glibc never gives back the free top of the main arena unless asked, so a
mostly-idle deployment retains post-load heap forever. Measurements on
the rss_gate workload (gdb-forced malloc_trim(0) after one load round):
median 1224 KiB per worker returned, master 1632 KiB; stable 5 s later.

Track an idle streak per reactor (CLOCK_MONOTONIC anchor of the first
event-free wake) and call malloc_trim(0) on a zero-event wake once the
streak reaches CWIST_MALLOC_TRIM_IDLE_SEC seconds (default 30, cached
getter, 0 = off). Runs exclusively on wakes that dispatched nothing, so
there is no hot-path cost; weak malloc_trim declaration keeps non-glibc
Linux libcs (musl) linking with the trim degraded to a no-op. Also fixes
the io_uring idle check being unreachable: the timeout enter returns
-ETIME (not 0), which previously hit 'continue' before the head/tail
check. Verified end-to-end: workers 1248-1524 KiB and master 1480 KiB
returned after load on the auto-trim path; rss_gate.sh PASS (idle
36696 KiB, growth 232 KiB); wrk c64/c256 within noise.
Nothing called SSL_CTX_sess_set_cache_size, so the internal server
session cache kept BoringSSL's 1024*20-session default: under
pathological session-ID churn that is up to ~20 MiB per worker
retained forever. Measured real usage is zero (SSL_CTX_sess_number
stays 0 through wrk/ab/h2load/openssl-s_client churn, TLS 1.2 included
-- TLS 1.3 tickets are stateless), so this is bounded-retention
hardening with no hot-path cost (the cap is consulted on insert only).

Set SSL_CTX_sess_set_cache_size(ctx, N) at both SSL_CTX sites (https.c,
http3.c) with N from CWIST_TLS_SESS_CACHE_SIZE (default 1024, 0 =
BoringSSL default), via a cached getter in tls_chain.
The first arm of a new connection grew the rbuf stash straight to
CWIST_HTTP_READ_BUFFER_SIZE (16 KiB), and the doubling ladder in
http_async_stash_grow used the same base -- so every young connection
paid 16 KiB of rbuf (12 KiB beyond the 4 KiB idle floor it would be
shrunk to anyway after 10 s idle) even though the median request never
exceeds the floor. Grow virgin stashes to CWIST_ASYNC_STASH_IDLE and
make the doubling base the floor; any request above the floor regrows
through the unchanged ladder to the same cap as before.

Measured RSS win (1 worker, connections younger than the idle window,
deterministic across repeats): 4000 idle keep-alive connections retain
24.8 KiB/conn before, 12.9 KiB/conn after -- -11.7 KiB/conn (~47 MB
per 4000). Perf gate: wrk c64/c256 small requests and 8 KiB POST
within noise (8 KiB POST c64 +2.6%, c256 +2.9%, P99.999 never worse);
test_idle_reaper/fairness/rx_epoll/pipeline/async_defer pass with
default and override env.
A connection parked on cwist_async (SSE, long-poll) used to hold its
full rbuf+obuf stashes for the entire deferral -- the idle shrink only
ran at re-arm, which a parked connection never reaches. Shrink toward
the idle floor at defer: ~28 KiB/conn retained becomes ~KB for the
whole deferral (measured: 3000 parked connections retain 28.8 KiB/conn
before, 16.9 KiB/conn after, -11.9 KiB/conn).

The shared helper is hysteresis-gated (correct for keep-alive: a busy
connection must not shrink->regrow per request), and that hysteresis
is exactly wrong at defer -- the stash was just grown, yet the
connection now parks indefinitely. Age the anchor out before calling
the same helper: one shrink->regrow pair per defer cycle instead of
unbounded retention. RX-uring invariant holds (defer runs with no RECV
SQE in flight, same as re-arm). Perf A/B within noise; sse/reactor
tests pass.
@gg582
gg582 merged commit 9558473 into dev Oct 16, 2026
24 of 25 checks passed
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