Skip to content

gc: incremental mode starts a budgeted cycle and never completes it — a small compiled program still performs ZERO automatic collections (#6950 residue) #6978

Description

@proggeramlug

Split out of #6950. #6977 fixed the arena-triggered half of this; the malloc-triggered half survives, and it is why a small compiled program still performs ZERO automatic collections in the shipped (incremental) configuration.

Symptom

A compiled program in the default configuration reaches a due GC trigger, starts a budgeted cycle, and never finishes it. Measured on origin/main + #6977, release build, macOS arm64, pinned Node 26.5.0, PERRY_GC_TRACE=1 PERRY_GC_DIAG=1:

corpus test PERRY_GC_HEAP_LIMIT=8 (default/incremental) + PERRY_GC_INCREMENTAL=0
test_gap_repsel_canonical_i32 0 cycles 3 cycles
test_gap_repsel_ptr_shape_locals 0 cycles 4 cycles

A trigger is due in both columns — turning incremental off collects, so the arming path agrees there is work. The difference is entirely in who runs the collection.

Mechanism

gc_check_trigger's direct-collection arm (the #5476 patch) is gated on registered_root_scanners_block_budgeted_gc(). Under gc_incremental_enabled() that predicate reduces to "is any copy-only scanner registered":

if super::gc_incremental_enabled() {
    return has_copy_only;
}

A compiled program registers none (copy-only scanners are the FFI copy-only API plus tests — the comment in gc/roots.rs says so). So the predicate is false, the direct arm is skipped, and the trigger is handed to the budgeted stepper, which is driven only by bounded mutator assists.

For an arena-triggered cycle #6977 made those assists scale: the debt term was reading the raw GC_NEXT_TRIGGER_BYTES cell instead of effective_next_arena_trigger() and therefore reported zero. For a malloc-triggered cycle the debt term is already correct —

malloc_debt_objects: malloc_count.saturating_sub(next_malloc_trigger)

— it is just small on a small program, so gc_mutator_assist_scaled_work_units() stays near its 256-unit floor and the cycle still never reaches completion. Same outcome, different cause: the cycle is started and then starved.

Why it matters

  1. Correctness of the memory bound. "Started but never completed" is indistinguishable from "no collector" to RSS. This is the same shape as Memory not free in benchmark #5476 and as the debt-pacing regression fix(gc): a compiled program can now actually reach an evacuating minor (#6950) #6977 fixed; it keeps reappearing because the budgeted stepper has no completion guarantee, only a budget.
  2. Every GC gate that runs on small programs is inert. scripts/gc_repsel_matrix.sh's default / verify_evac / gen_gc_off / wb_off / cons_scan_off arms require collect, and on 18 of 20 corpus files they get zero cycles in the shipped configuration. fix(gc): a compiled program can now actually reach an evacuating minor (#6950) #6977 made the move arms live by adding PERRY_GC_INCREMENTAL=0; the collect arms deliberately were not changed, because they are supposed to describe the shipped configuration — and the shipped configuration genuinely does not collect. That honest UNVER is this issue.

Suggested direction (not prescriptive)

The budgeted stepper needs a completion guarantee, not a bigger budget. Something shaped like: if a cycle has been active across N assists while its arming trigger is still due, finish it synchronously — the graceful degradation the pacing doc already describes ("under extreme allocation pressure incremental degrades gracefully toward synchronous behavior rather than toward unbounded memory"), applied to elapsed assists rather than to measured debt alone. A starvation counter is cheap and cannot regress the case where the cycle does complete on budget.

Whatever the fix, the acceptance signal is liveness, not a green table: PERRY_GC_TRACE=1 must show cycles > 0 for test_gap_repsel_canonical_i32 under PERRY_GC_HEAP_LIMIT=8 with no other GC env var set.

Related: #6950, #6977, #5476, #6180.

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