Skip to content

gc: shipped Perry runs at ~3x the RSS it needs — the nursery cap has never been on by default #7372

Description

@proggeramlug

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_ratchet probes 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 in debt_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

  1. Land the force_legacy_gc_pacing fix on its own — it is a latent bug today: the guard silently stops working the moment anyone makes the cap unconditional.
  2. Triage the ~26 tests individually.
  3. Then flip both defaults together, never one alone.
  4. Per the kill-policy, the new PERRY_GC_SCAVENGE=0 off-state needs a CI arm; there is none today (grep over .github/workflows returns nothing).

Prototype is on perf/nursery-cap-by-default.

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions