Skip to content

[perf] Generational GC unreachable in production → every collection is STW O(total heap) #6083

Description

@proggeramlug

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

  1. 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 #5977 direct fallback runs instead — under force_full_scan (conservative).
  2. 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.
  3. 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).

Metadata

Metadata

Assignees

No one assigned

    Labels

    performanceRuntime, compile-time, build-size, or memory performance

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions