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
gc: incremental mode starts a budgeted cycle and never completes it — a small compiled program still performs ZERO automatic collections (#6950 residue) #6978
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":
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 —
— 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.
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.
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:PERRY_GC_HEAP_LIMIT=8(default/incremental)+ PERRY_GC_INCREMENTAL=0test_gap_repsel_canonical_i32test_gap_repsel_ptr_shape_localsA 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 onregistered_root_scanners_block_budgeted_gc(). Undergc_incremental_enabled()that predicate reduces to "is any copy-only scanner registered":A compiled program registers none (copy-only scanners are the FFI copy-only API plus tests — the comment in
gc/roots.rssays so). So the predicate isfalse, 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_BYTEScell instead ofeffective_next_arena_trigger()and therefore reported zero. For a malloc-triggered cycle the debt term is already correct —— 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
scripts/gc_repsel_matrix.sh'sdefault/verify_evac/gen_gc_off/wb_off/cons_scan_offarms requirecollect, 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 themovearms live by addingPERRY_GC_INCREMENTAL=0; thecollectarms 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=1must showcycles > 0fortest_gap_repsel_canonical_i32underPERRY_GC_HEAP_LIMIT=8with no other GC env var set.Related: #6950, #6977, #5476, #6180.