Found while re-measuring the matrix after #7019 landed. This is a gate-methodology defect: the matrix's own pressure knob switches off the collector path #7019 added, so the arms that are supposed to describe the shipped configuration measure the pre-#7019 non-moving path instead.
Measurement
scripts/gc_repsel_matrix.sh --arms all --pressure 8, origin/main @ 4340bffc9, 22 corpus rows. Separating the two counters the harness currently sums together (see the sibling issue on that):
| arm |
files with a cycle |
files with [gc-copy-minor] ran |
Σ copied_objects |
default |
13/22 |
0/22 |
0 |
verify_evac |
13/22 |
0/22 |
0 |
gen_gc_off |
13/22 |
0/22 |
0 |
gen_off_verify |
13/22 |
0/22 |
0 |
cons_scan_off |
13/22 |
13/22 |
21 767 487 |
evac_minor |
21/22 |
21/22 |
3 628 777 |
shipped_default (no env at all) |
2/22 |
2/22 |
40 087 037 |
The default arm — "as-shipped GC configuration under allocation pressure" — runs zero copying minors on every corpus file. Its only movement evidence is moved_objects= from the older C4b mark-sweep evacuation policy, which is a different collector.
Mechanism
gc_check_trigger's defer arm (the one that hands the collection to js_gc_loop_safepoint → gc_safepoint_moving_minor, i.e. all of #7019) is guarded by
if gc_moving_loop_polls_enabled()
&& crate::arena::arena_total_bytes() < gc_moving_defer_hard_cap_dyn_bytes()
and gc_moving_defer_hard_cap_dyn_bytes() is budget_scaled(GC_MOVING_DEFER_HARD_CAP_BYTES, 1, 4, 2 MB) — the same 1/4-of-budget-with-a-2 MB-floor formula as gc_trigger_absolute_ceiling_bytes(), which sets the arena trigger. Under an explicit PERRY_GC_HEAP_LIMIT both collapse to the same value, so arena_total_bytes() < hard_cap is false at precisely the moment the trigger becomes due. The defer never happens; control falls through to the direct minor with ManualGcScanGuard::force_full_scan(), and the copying minor is reported ineligible:
[gc-copy-minor] eligible=false fallback=conservative_stack
PERRY_CONSERVATIVE_STACK_SCAN=off rescues it (that is why cons_scan_off shows 13/22 copy-minors) because an explicit env value beats the guard — but that is not the shipped configuration either.
There is no heap-limit setting that fixes it
test_gap_repsel_canonical_i32 / test_gap_repsel_ptr_shape_locals, sweeping the knob:
PERRY_GC_HEAP_LIMIT |
cycles |
[gc-copy-minor] ran |
| 2 |
1 / 2 |
0 |
8 (the matrix's --pressure 8) |
1 / 2 |
0 |
| 16, 32, 64, 128 |
0 |
0 |
Below the knee the hard cap blocks the defer; above it no trigger is ever due. The band is empty.
Why it matters
#7019 shipped moving minor-GC on by default. The matrix is the gate that is supposed to tell us whether the representations are safe under it. Today the arm named default proves the opposite of what its name suggests: it certifies the non-moving path. The only arms exercising #7019's own path are the ones that override the stack-scan mode, plus shipped_default, which is a requires=none control and reaches a trigger on just 2 of 22 rows.
Suggested direction (not prescriptive): decouple the moving-defer hard cap from the trigger ceiling — it is a safety valve for a mega-expression that reached no poll, so it should be a multiple of the trigger, not equal to it. Independently, the matrix probably wants a pressure arm that lowers the trigger without lowering the hard cap.
Environment: release build, macOS arm64, node 26.5.0 pinned.
Related: #7019, #6950, #6942, #7016.
Found while re-measuring the matrix after #7019 landed. This is a gate-methodology defect: the matrix's own pressure knob switches off the collector path #7019 added, so the arms that are supposed to describe the shipped configuration measure the pre-#7019 non-moving path instead.
Measurement
scripts/gc_repsel_matrix.sh --arms all --pressure 8,origin/main@4340bffc9, 22 corpus rows. Separating the two counters the harness currently sums together (see the sibling issue on that):[gc-copy-minor] rancopied_objectsdefaultverify_evacgen_gc_offgen_off_verifycons_scan_offevac_minorshipped_default(no env at all)The
defaultarm — "as-shipped GC configuration under allocation pressure" — runs zero copying minors on every corpus file. Its only movement evidence ismoved_objects=from the older C4b mark-sweep evacuation policy, which is a different collector.Mechanism
gc_check_trigger's defer arm (the one that hands the collection tojs_gc_loop_safepoint→gc_safepoint_moving_minor, i.e. all of #7019) is guarded byand
gc_moving_defer_hard_cap_dyn_bytes()isbudget_scaled(GC_MOVING_DEFER_HARD_CAP_BYTES, 1, 4, 2 MB)— the same 1/4-of-budget-with-a-2 MB-floor formula asgc_trigger_absolute_ceiling_bytes(), which sets the arena trigger. Under an explicitPERRY_GC_HEAP_LIMITboth collapse to the same value, soarena_total_bytes() < hard_capis false at precisely the moment the trigger becomes due. The defer never happens; control falls through to the direct minor withManualGcScanGuard::force_full_scan(), and the copying minor is reported ineligible:PERRY_CONSERVATIVE_STACK_SCAN=offrescues it (that is whycons_scan_offshows 13/22 copy-minors) because an explicit env value beats the guard — but that is not the shipped configuration either.There is no heap-limit setting that fixes it
test_gap_repsel_canonical_i32/test_gap_repsel_ptr_shape_locals, sweeping the knob:PERRY_GC_HEAP_LIMIT[gc-copy-minor] ran--pressure 8)Below the knee the hard cap blocks the defer; above it no trigger is ever due. The band is empty.
Why it matters
#7019 shipped moving minor-GC on by default. The matrix is the gate that is supposed to tell us whether the representations are safe under it. Today the arm named
defaultproves the opposite of what its name suggests: it certifies the non-moving path. The only arms exercising #7019's own path are the ones that override the stack-scan mode, plusshipped_default, which is arequires=nonecontrol and reaches a trigger on just 2 of 22 rows.Suggested direction (not prescriptive): decouple the moving-defer hard cap from the trigger ceiling — it is a safety valve for a mega-expression that reached no poll, so it should be a multiple of the trigger, not equal to it. Independently, the matrix probably wants a pressure arm that lowers the trigger without lowering the hard cap.
Environment: release build, macOS arm64, node 26.5.0 pinned.
Related: #7019, #6950, #6942, #7016.