Repository navigation
gc: shipped Perry runs at ~3x the RSS it needs — the nursery cap has never been on by default #7372
Description
Summary
Shipped Perry carries ~3× the RSS it needs to. The 16 MB nursery cap that #7056 identified as the entire footprint win is gated behind two knobs that both default to off, so it has never been active in a shipped build.
Measured on today's collector (statepoints default, 148f97bc2), 8 gc_ratchet probes, as a 2×2 — because the one-armed version of this measurement says something false:
| no scavenge | scavenge | |
|---|---|---|
| no cap (ships today) | 799,604,736 B | 799,604,736 B (+0%) |
| cap 16 MB | 537,165,824 B (−33%) | 245,006,336 B (−69%) |
Wall time over the same probes:
| arm | RSS | wall |
|---|---|---|
| ships today | baseline | 2017 ms |
| cap only | −33% | 2488 ms (+23%) |
| cap + scavenge | −69% | 2085 ms (+3%) |
| scavenge only | +0% | 2066 ms (+2%) |
Read the row and the column, not one cell. Scavenge alone buys nothing — the top row is identical to the byte. The cap alone trades a third of the footprint for a quarter of the wall time. Only together are they worth it: the cap makes collections frequent, and scavenge makes them evacuating (O(live) copying) rather than O(heap) sweeps, so frequency becomes cheap.
This is why #7056's recommendation — "decouple the 16 MB cap and keep it" — is not on its own sufficient. Acting on it literally ships the −33%/+23% arm.
Why now
Enabling scavenge also defers alloc-point collections to a precise safepoint instead of collecting behind a forced conservative scan. That was unreasonable while precise roots were opt-in; it became reasonable when #7370 made native roots the default.
What blocks it
I prototyped the change (cap unconditional + gc_scavenge_enabled() default-on) and it is not landable as-is:
- 11/11
gc_ratchetprobes stay byte-identical to Node, and the gap suite tracked the 447/19/13 baseline cleanly for the 200+ tests measured before I stopped. - But 26
gc::tests::*fail, against a flaky baseline of 1–4 (cargo-test: perry-runtime's suite fails a different number of tests on every run #7365). They are concentrated indebt_pacer,incremental_sweep_reclaim,temp_roots,root_words,triggers— the budgeted/incremental path that scavenge's deferral bypasses. A representative failure asserts a collection count of 1 and gets 0, which is the deferral doing exactly what it is supposed to.
One cause is already understood and fixed in the prototype: force_legacy_gc_pacing() un-capped the trigger by pinning gc_moving_loop_polls_enabled() off, which stops working once the cap is unconditional. The guard now suppresses the cap directly, and that fixed the tests which use it.
But mechanically pinning legacy pacing across incremental_sweep_reclaim fixed only 3 of 10. So the remaining failures are not a pacing-mode mismatch — each needs a judgement call on whether it asserts behaviour that legitimately changed, or catches a real problem. Rushing that is how a real regression gets pinned as "expected".
Suggested shape
- Land the
force_legacy_gc_pacingfix on its own — it is a latent bug today: the guard silently stops working the moment anyone makes the cap unconditional. - Triage the ~26 tests individually.
- Then flip both defaults together, never one alone.
- Per the kill-policy, the new
PERRY_GC_SCAVENGE=0off-state needs a CI arm; there is none today (grepover.github/workflowsreturns nothing).
Prototype is on perf/nursery-cap-by-default.
Activity
proggeramlug commentedon Aug 4, 2026
Prototype branch:
perf/nursery-cap-by-default(not a PR — see the blocking test work above).perf(gc): nursery cap + scavenge on by default — peak RSS -69% (#7377)