Repository navigation
perf: RSS reductions with unchanged performance (idle trim, TLS cache cap, 4 KiB virgin stash, defer shrink) - #384
Merged
Merged
Conversation
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.
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.
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.shpasses on the final tree (idle 36,696 KiB, round growth 232 KiB — slightly better than baseline's PASS at 36,892/324 KiB).perf(reactor)malloc_trim(0)per reactor afterCWIST_MALLOC_TRIM_IDLE_SEC(default 30 s,0=off) of event-free wakes-ETIMEperf(tls)SSL_CTX_sess_set_cache_sizeat both SSL_CTX sites (CWIST_TLS_SESS_CACHE_SIZE, default 1024,0=BoringSSL default)perf(http)perf(http)CWIST_ASYNC_DEFER(anchor aged out before the hysteresis-gated helper)Notes
rss-gate.yml,c100k.yml) and the soak RSS leg are the CI backstop; soak already runs on this PR via ci(soak): gate the HTTP soak on push/PR, dispatch for release candidates #379's gate.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/onscripts/ci/rss_gate.shPASS (candidate);check_env_knobs.pyOK (both new knobs documented en+ko)