You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
Severity: performance (biggest pause/latency lever in the runtime) Found by: audit fable-audit-perry-2.md.
What
The generational/copying GC machinery is structurally unreachable in production: every automatic collection is a stop-the-world, O(total-heap) mark-sweep, and each cycle rebuilds a validity structure over the entire heap and probes it per traced edge.
Why
registered_root_scanners_block_budgeted_gc() (crates/perry-runtime/src/gc/roots.rs:286-298) returns true if any registered scanner lacks a budgeted cursor; gc_init registers ~35 sync-only scanners (only ~8 are budgeted), so the sliced/budgeted stepper can never start and the fix(gc): compiled programs never GC'd on nursery pressure (P0) + copied-minor weak-slot semantics #5977direct fallback runs instead — under force_full_scan (conservative).
That conservative decision makes copied-minor ineligible (gc/copying.rs:746-750), so the copying nursery / survivor semispaces / incremental machinery are dead code; every trigger runs the synchronous fallback minor to completion.
The fallback rebuilds ValidPointerSet from scratch each cycle by walking every object in every arena (nursery + survivor + longlived + old) with a BTreeSet::insert per object (gc/trace.rs:172-330), then marks with a BTreeSet::contains per scanned root and per traced reference field, and sweeps every arena again. So a "minor" over a 5 MB nursery on a 500 MB old-gen still walks millions of objects twice with a BTree probe per edge.
Result: pauses scale with total live set, not allocation rate; first collection after the 128 MB trigger is hundreds of ms; steady-state servers get periodic full-heap STW.
Fix (either path)
Make minors nursery-scoped: per-block mark bitmaps / nursery-address-range checks so validation + sweep touch only nursery blocks + remembered-set pages; or
Unblock copied-minor in production: convert the ~35 sync-only scanners to budgeted/rewritable form and drop force_full_scan on the trigger path (shadow stack is precise; runtime frames are covered by RuntimeHandleScope).
Estimated 10–100× pause reduction for servers; removes the multi-hundred-ms first pause for large CLIs.
Confidence: high (the #5977 commit message itself documents the scanner-block; eligibility + trace code confirm the consequence). Also: CLAUDE.md's GC section says "64 MB initial threshold" — actual is 128 MB (gc/policy.rs:158).
Severity: performance (biggest pause/latency lever in the runtime)
Found by: audit
fable-audit-perry-2.md.What
The generational/copying GC machinery is structurally unreachable in production: every automatic collection is a stop-the-world, O(total-heap) mark-sweep, and each cycle rebuilds a validity structure over the entire heap and probes it per traced edge.
Why
registered_root_scanners_block_budgeted_gc()(crates/perry-runtime/src/gc/roots.rs:286-298) returns true if any registered scanner lacks a budgeted cursor;gc_initregisters ~35 sync-only scanners (only ~8 are budgeted), so the sliced/budgeted stepper can never start and the fix(gc): compiled programs never GC'd on nursery pressure (P0) + copied-minor weak-slot semantics #5977 direct fallback runs instead — underforce_full_scan(conservative).gc/copying.rs:746-750), so the copying nursery / survivor semispaces / incremental machinery are dead code; every trigger runs the synchronous fallback minor to completion.ValidPointerSetfrom scratch each cycle by walking every object in every arena (nursery + survivor + longlived + old) with aBTreeSet::insertper object (gc/trace.rs:172-330), then marks with aBTreeSet::containsper scanned root and per traced reference field, and sweeps every arena again. So a "minor" over a 5 MB nursery on a 500 MB old-gen still walks millions of objects twice with a BTree probe per edge.Result: pauses scale with total live set, not allocation rate; first collection after the 128 MB trigger is hundreds of ms; steady-state servers get periodic full-heap STW.
Fix (either path)
force_full_scanon the trigger path (shadow stack is precise; runtime frames are covered byRuntimeHandleScope).Estimated 10–100× pause reduction for servers; removes the multi-hundred-ms first pause for large CLIs.
Confidence: high (the #5977 commit message itself documents the scanner-block; eligibility + trace code confirm the consequence). Also: CLAUDE.md's GC section says "64 MB initial threshold" — actual is 128 MB (
gc/policy.rs:158).