fix(gc): measure the moving-defer allowance from the deferral point so the copying minor is reachable under a heap budget - #7057
Conversation
The alloc-point deferral that hands a nursery trigger to the precise-root safepoint -- and therefore to the copying minor -- was guarded by an absolute arena cap derived from budget_scaled(128 MB, 1, 4, 2 MB), the same formula as gc_trigger_absolute_ceiling_bytes(). Under any heap budget small enough for the ceiling to reach the nursery cap the two collapse to one number, making 'a trigger is due' (arena_total >= trigger) and 'the deferral is allowed' (arena_total < cap) exact complements: the deferral was unreachable and the copying minor never ran. Make the allowance a slack measured from the deferral point instead. The first deferral of a cycle is unconditional; the safety valve fires once the arena has grown a slack past it, and retires the pending request so the baseline cannot go stale.
…t now reaches it (#7024, #6993) Adds a `scavenge` liveness requirement -- the copying minor's own `[gc-copy-minor] ran copied_objects=` counter, never the sum that also counts the C4b mark-sweep evacuation (#7025) -- and gives it to the four arms whose subject is the relocating young-gen minor: default, verify_evac, cons_scan_off, cons_scan_off_force. Adds a compiled-program regression test for the observable that distinguishes the two worlds under an explicit PERRY_GC_HEAP_LIMIT: copied_objects > 0. Asserting 'a cycle happened' does not; the broken build collected on 13 of 22 corpus rows while relocating nothing.
|
No actionable comments were generated in the recent review. 🎉 ℹ️ Recent review info⚙️ Run configurationConfiguration used: defaults Review profile: CHILL Plan: Pro Plus Run ID: 📒 Files selected for processing (6)
📝 WalkthroughWalkthroughMoving-GC deferral now uses a baseline-relative slack window instead of an absolute cap. Runtime tests, a heap-limited copying-minor regression test, and GC matrix gating updates verify that relocating minors execute and are measured independently. ChangesMoving GC deferral and validation
Estimated code review effort: 3 (Moderate) | ~20 minutes Sequence Diagram(s)sequenceDiagram
participant gc_check_trigger
participant moving_defer_within_slack
participant gc_safepoint_moving_minor
gc_check_trigger->>moving_defer_within_slack: evaluate arena growth against defer baseline
moving_defer_within_slack-->>gc_check_trigger: allow or retire deferral
gc_check_trigger->>gc_safepoint_moving_minor: route owed collection to moving minor
Possibly related issues
Possibly related PRs
Suggested labels: 🚥 Pre-merge checks | ✅ 5✅ Passed checks (5 passed)
✨ Finishing Touches📝 Generate docstrings
🧪 Generate unit tests (beta)
Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out. Comment |
Filed granularly rather than smoothed over
One gate observation this PR does not fix
Local verification summary
|
Closes #7024. Closes #6993.
The defect
gc_check_trigger's deferral arm — the one that hands an allocation-point nursery trigger to the next precise-root safepoint, and therefore to the copying minor that #7019 made default-on — was guarded by an absolute committed-arena cap:gc_moving_defer_hard_cap_dyn_bytes()wasbudget_scaled(128 MB, 1, 4, 2 MB)— byte-for-byte the formula behindgc_trigger_absolute_ceiling_bytes(). Andgc_budgeted_due_trigger()reports the arena trigger due exactly whenUnder any heap budget small enough for the ceiling to reach the 16 MB nursery cap — every
PERRY_GC_HEAP_LIMIT≤ 64, every device budget a small container or watch derives, every--pressuresetting the GC stress matrix uses — the two collapse to the same number. "A trigger is due" and "the deferral is allowed" become exact complements: the deferral is refused at precisely the arena size that makes the trigger due. Control falls through to the alloc-point minor underManualGcScanGuard::force_full_scan(), the collector reports[gc-copy-minor] eligible=false fallback=conservative_stack, and a heap-limited deployment silently runs the pre-#7019 non-moving collector.Reproduced on
e279b2d54before touching anything,--pressure 8, pinned Node 26.5.0:defaultverify_evaccons_scan_offshipped_defaultThe fix — a formula change, no new knob
The allowance is now a slack measured from the deferral point, not an absolute arena size (
GC_MOVING_DEFER_SLACK_BYTES/gc_moving_defer_slack_dyn_bytes(),budget_scaled(64 MB, 1, 3, 1 MB)):No env knob was added, so nothing new is owed an OFF arm under the GC knob kill-policy.
Proof the copying minor now runs
PERRY_GC_HEAP_LIMIT=8 PERRY_GC_DIAG=1— thedefaultarm's exact configuration, no other GC env:(In an auto-optimized link the runtime staticlib carries no
diagnosticsfeature, soPERRY_GC_TRACE=1emits only[gc] cycle (diagnostics feature disabled …)markers and the copy counter comes fromPERRY_GC_DIAG. Both were set for every run below, exactly as the harness does.)Matrix delta —
--arms all --pressure 8, 440 cellsLiveness:
default0/22 → 12/22 copy-minor,verify_evac0/22 → 12/22.Every one of the 14 remaining FAILs is the same cell —
test_gap_repsel_p4a3_numarray_barriers,exit=139 cycles=1 evacuated=0 scavenged=3594— on the 14 arms that now run a copying minor. No corpus row broke that was not already red. The two rows that moved are the two pre-existing #6981p4a3rows:default × p4a3_numarray_barrierscycles=1 scavenged=0)exit=139 scavenged=3594)verify_evac × p4a3_numarray_barrierscycles=1 scavenged=0)exit=139 scavenged=3594)p4a3_ptr_numarray × 12 precise-root armsThe two new reds are not softened, and they are the point: that SIGSEGV is #6981's, previously producible only by
cons_scan_offand the%E%arms. It now reaches the arm named after the shipped configuration.p4a3_ptr_numarraygoing green on 12 arms is a real behaviour change, not a mask: it now relocates at a safepoint (scavenged=3585) instead of at the register-imprecise allocation point, and its output matches the oracle byte-for-byte.#6993 — the per-PR subset can now see the class
That is what
default × p4a3_numarray_barriersflipping colour demonstrates. The whole "raw reference held across a relocating collection" class (#6951, #6972, #6982, #6991, #6992) was invisible to--arms prbecausedefault/verify_evacran the non-moving minor andcons_scan_offrelocated only with incremental mode on. It is now reproducible per-PR, by the shipped configuration, without adding a red arm.evac_minor/force_verifystay out ofPR_ARMSfor their original, unchanged reason (#6981), and theWHEN #6981 CLOSESnote stays.Gate teeth
scavengeliveness requirement, given todefault,verify_evac,cons_scan_off,cons_scan_off_force— it reads the copying minor's own[gc-copy-minor] ran copied_objects=counter, never the sum that also counts the C4b mark-sweep evacuation (gc-matrix: the 'moved' liveness counter sums the C4b mark-sweep evacuation with the copying minor, so a move-arm can be green with zero scavenged objects #7025). A verifier arm that relocates nothing verifies nothing, and used to report PASS for it.gc/tests/triggers.rs) sweep every plausible heap budget and assert the deferral is satisfiable at the exact arena size that makes the trigger due, and that the safety valve still refuses past one slack. They also assert the collapse premise itself (due_at >= legacy_capwhenever the ceiling reaches the nursery cap), so the reason is recorded and not just the fix.crates/perry/tests/gc_copy_minor_under_heap_limit.rspins the end-to-end observable: a compiled program underPERRY_GC_HEAP_LIMIT=8must reportcopied_objects > 0. Exit 0 does not distinguish the two worlds — the broken build exited 0 and collected.Measured consequences worth knowing
test_gap_specabi_reassignstops collecting under the pressure knob (PASS → UNVER on every arm that used to take the alloc-point fallback). It is a 20-line program with no loop, so there is no back-edge poll to drain the deferral and it exits before reaching any safepoint. Previously the collapsed cap forced a non-moving minor at the allocation point. Growth stays bounded by the slack, and the harness reports it honestly as UNVER rather than green.PERRY_GC_HEAP_LIMIT=8). That is the price of the copying minor being reachable there at all; the previous behaviour bought its lower ceiling by never relocating.Verification
Local box, pinned Node 26.5.0 (
node --versionasserted; the harness refuses to run otherwise), release profile, isolatedCARGO_TARGET_DIR. Baseline and fixed runtimes were built from the same worktree by checking the three runtime files back and forth, so the A/B differs in nothing else; the auto-optimize archive is source-fingerprinted overperry-runtime, and was observed rebuilding on the switch.Summary by CodeRabbit
Bug Fixes
Tests