Repository navigation
gc-matrix: --pressure disables the very path #7019 added — the 'default' arm runs ZERO copying minors on all 22 corpus rows #7024
Description
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.
Activity
test(gc): report the two relocating collectors separately in the matr…
docs: GC knob kill-policy -- an unexercised mode is an unverified con…
style: clear two of the lint backlog items blocking every merge (fmt …
docs: the four ways a CI gate can be unable to fail (#7049)
fix(gc): measure the moving-defer allowance from the deferral point s…
fix(gc): run old-gen reclaim precisely at safepoints, stop scanning o…
fix(gc): root the array-store receiver across an allocating RHS (#7342)
test(gc): pin the sweep Eden census, and print the seed's verdict on …
test(gc): plant every pinned-fallback test through pin_object, and co…
test(gc): plant every pinned-fallback test through pin_object, and co…
perf(gc): stop traversing the young graph twice — skip the copying mi…
fix(net): make the listener GC rewrite test deterministic (#8712)
perf(gc): one proportional old-reclaim rule, and price the fulls it s…
perf(gc): one proportional old-reclaim rule, and price the fulls it s…
perf(gc): one proportional old-reclaim rule, and price the fulls it s…
perf(gc): one proportional old-reclaim rule, and price the fulls it s…