gc: allocation never begins a precise collection phase (RFC deferred collection S5, runtime) - #11630
Draft
proggeramlug wants to merge 15 commits into
Draft
proggeramlug wants to merge 15 commits into
proggeramlug wants to merge 15 commits into
Conversation
added 11 commits
September 28, 2026 13:25
|
Important Draft PR not reviewedDraft PRs are not automatically reviewed by default.
To automatically review draft PRs, update your CodeRabbit configuration: reviews:
auto_review:
drafts: trueThanks 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 |
This branch has not been deployed
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
RFC deferred collection (#11528), step S5, runtime half: an allocation never begins a precise or moving collection phase (D2), plus the unmapped-frame safety net and the valve gate (decisions 5 and 10). The new polls are the follow-up PR (
gc/s5-entry-polls). This PR changes no call's safepoint classification.Allocation-point inventory and routing
An allocation point is the dynamic extent of
gc_check_trigger. Every slow path funnels into it:arena_cell_alloc→gc_check_trigger, everygc_malloc→gc_check_trigger_inlined, the JSON mid-parse checks (json/parse_api.rs,parse_inline_object.rs), and the root-lock flush of a deferred trigger check.AllocationPointGuard(gc/alloc_point.rs) marks that extent.gc/policy.rsgc_check_trigger_evaluatepolicy.rsgc_budgeted_start_or_step→cycle.rsstep_root_scan/FinalRootRemarkframe_root_phase_refused), the poll is armed, and the next declared poll serves it (gc_safepoint_moving_minor→serve_budgeted_root_phase). Heap-only phases still advance from assistsgc/mod.rsgc_try_emergency_reclaimpolicy.rsflush_deferred_gc_requestgc()at the lock exitGC_POLL_OWED_REQUEST). This is option 2 of #11523assert_d2_synchronous_collection()runs at both synchronous chokepoints (gc_collect_minor_with_trigger_inner,gc_collect_full_mark_sweep_with_trigger). It panics in every build if a collection begins at an allocation point without requesting the conservative scan. It checks the request (ManualGcScanGuard), because the test isolation guards andPERRY_CONSERVATIVE_STACK_SCAN=offoverride the decision on purpose.PERRY_GC_SAFEPOINT_ONLYruntime contract (heal/strict), and with itConservativeScanSite::SafepointContractHeal. D2 is now the default. Codegen still reads the variable for its researchAllocNoReentryswitch; S6 replaces that with the table.New, and an owner question: the parked-cycle valve. An active budgeted cycle blocks every other collection, the nursery valve included. So a parked cycle with no poll in reach would grow without bound. After the nursery slack past the park point, the phase is served at the allocation point, and that firing is counted (
parked_valve_fires, gated to zero). This is precise at an allocation point:This is raised on #11528.
Safety net (layer 3)
gc/roots/stack_maps_frame_verify.rs: a precise collection that finds a frame whose function is a generated statepoint function, but whose return address matches no record, panics.Membership.
_Unwind_GetRegionStart._Unwind_Find_FDEon the x29-chain walker.RUNTIME_FUNCTION.BeginAddresson Windows.Each result is looked up in the GC map's function list. No range table was needed.
Zero-record functions. An instrumented compile (
PERRY_GC_INSTRUMENTS=1,PERRY_GC_VERIFY_FRAMESor a seed at compile time) also lists statepoint functions that have zero records, i.e. functions whose every call is a leaf. v6 runtimes already skip such entries, so the format is unchanged.Not checked. Collections that requested the conservative scan.
Armed by
PERRY_GC_VERIFY_FRAMES=1(a new instrument knob), byPERRY_GC_SCHEDULE_SEED, and indebug_assertions/gcauditbuilds.Release status: off. Every unmatched frame costs an FDE lookup, and the zero-record listing costs map bytes. Instrumented CI arms cover it.
Liveness.
frames_verified=on[gc-schedule] doneand[gc-verify-frames].gc_instrument_smoke.shgains arm 8, which fails onframes_verified=0.Gates
PERRY_GC_VALVE_LEDGER=<file>makes every process append one line at exit.scripts/gc_valve_ledger_check.py(with a self-test) fails on anyvalve_fires/parked_valve_fires/d2_violationsabove zero, and on fewer lines than passing tests, which proves the check ran.scripts/gc_valve_ratchet_probes.sh).GC_UNSAFE_ZONESis held, on[gc-alloc-point]. Diagnostic only.max_poll_wait_bytes: the most the arena grew between arming a collection and draining it at a poll (or giving it to the valve).Measurements
All on qb4 (EPYC 9275F), release builds,
PERRY_NO_AUTO_OPTIMIZE=1, same basebe39bbf3c, arms interleaved. Instructions areperf stat instructions:umedians of 5 runs; peak RSS is theru_maxrssmedian. The box is shared, so wall time is not used.Compute (32 programs): benchmarks/suite, bench_*, the 14 GC ratchet probes, the event-loop server fixture and the decision-3 fixtures.
bench_string_heavybench_string_heavywas +3.6% until the unsafe-zone note moved off the inlinedarena_cell_alloconto the out-of-line block-reserve path.RSS, decision 8.
Owner call: the median delta is positive, inside the harness's documented 0.41% spread, and matches the 93 KB (+0.6%) of added text (verifier and counters). If "no median regression" means exactly zero, this does not meet it.
cc bundle (claude-code 2.1.112, compiled with PR #11631, which contains this PR, 5 paired runs, private HOME;
-pturns are blocked by #11559):--version--helpThe compile itself is 67 min and 37.8 GB peak in both arms.
Latency (decision 6). Event-loop server fixture: 30k requests over
setImmediate, a 200-object response graph each.PERRY_GC_MOVING_LOOP_POLLS=0 PERRY_GC_SCAVENGE=0, 5 rounds). No valve fired. Serving the root scan at the poll in one step first madestep_max_us40–53 ms against 34–35 ms. The poll now serves one host-sized slice per poll, like a host step, and stays armed until the phase is done. After that change:step_max_usfinal_remark_max_usTests
gc/tests/alloc_point_invariant.rs:stack_maps_frame_verify.rs: sabotage. An unmatched frame in a listed generated function panics under a precise collection, and a runtime frame or a conservative collection does not.roots.rs,noncollecting_root_lock.rs). The assist-drives-the-cycle tests call the loop's back-edge poll (support::back_edge_poll).RUST_TEST_THREADS=1 cargo test --release -p perry-runtime --lib, final head): 4712 passed, 0 failed.test_gap_gc*, run on the stacked entry-polls build, which contains this PR): 57/57, and the valve ledger has 57 lines, all zero. The full gap suite and gc-stress run in CI.