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
{{ message }}
Repository navigation
gc: reduce safepoint density (safepoint polling instead of every-call statepoints) #8596
Perry treats every call as a GC safepoint: rewrite-statepoints-for-gc wraps each non-leaf call in a gc.statepoint and relocates every live root across it. That is correct (any callee might collect) but it makes the safepoint count of a function equal to its call count, which is the × safepoints term in the RS4GC relocation fan-out live_roots × safepoints behind #8583 (the Claude Code @main has ~106k call sites).
Many production GC'd VMs instead use safepoint polling: collection can only begin at a bounded set of poll points — loop back-edges, function returns/entries, and allocation sites — and ordinary calls are not safepoints unless the callee itself polls. This shrinks the safepoint set dramatically without losing the guarantee that a collection can always make progress, because a thread reaches a poll in bounded time.
Proposal (exploratory — needs design)
Move from "every call is a safepoint" toward a polling model:
take safepoints at loop back-edges, allocation sites, and returns, rather than at every call;
a non-allocating / non-polling call is a gc-leaf-style edge that does not relocate roots;
keep a mechanism to bring a thread to a safepoint on demand (poll flag the callee checks), which perry already has pieces of (GC_SAFEPOINT_PENDING, js_gc_loop_safepoint, the loop-poll path).
Why it's worth it
Unlike #8595 (outlining), which fixes the giant-function symptom, reducing safepoint density shrinks the × safepoints factor for every function in the codebase, not just the outlier — lowering RS4GC cost and stack-map size across the board, and reducing the number of points at which roots must be kept live (better register allocation).
Scope / risk
This is the largest of the three and a genuine GC-architecture change: it affects collection latency/progress guarantees, the interaction with the moving scavenge (#7019), write barriers, and the conservative-scan fallback. It should be scoped and prototyped carefully — filed here so the option is tracked rather than lost, not as ready work.
Context: surfaced while fixing #8583 (RS4GC relocation fan-out). Complements #8595 (outlining) and the root-spilling stopgap in #8583. Root-lowering background: #7370 (native roots default), #7280/#7173/#7174.
Four provably-non-collecting helpers reclassified as gc-leaf-function (verified against their runtime bodies + already in the NONCOLLECTING authority): js_typed_feedback_closure_direct_call_guard, js_implicit_this_get, js_tdz_suppress_begin/_end. Zero semantic risk, trims safepoints across all functions. Not the fan-out fix — that's the polling model below.
Design notes — the real lever (safepoint polling)
The × safepoints term shrinks materially only by moving from "every non-leaf call is a safepoint" to safepoint polling: collection may begin only at a bounded set of poll points; ordinary calls stop being safepoints, so RS4GC statepoints (and relocates) collapse to the poll/alloc sites.
Pieces perry already has:GC_SAFEPOINT_PENDING + js_gc_loop_safepoint (the loop back-edge poll), and gc/pressure.rs already defers a pressure-triggered collection to the next poll when a generated frame is live. Today that coexists with statepoints-at-every-call; polling would make the poll points the only safepoints.
What it touches: (1) codegen — emit poll checks at loop back-edges, allocation sites, and returns, and stop treating calls as safepoints in function/precise_roots.rs; (2) the moving scavenge (gc: default-on evacuating young-gen scavenge (moving minor at precise safepoints) #7019) — a moving collection may run only where all live frames' roots are precisely known, i.e. a poll (a non-moving one can be conservative anywhere); (3) the conservative-scan fallback for a thread caught between polls; (4) the write-barrier / AllocNoReentry "alloc-point valve" contract, which already assumes deferral-to-safepoint semantics; (5) invoke/unwind rooting (codegen: lower try/catch to LLVM invoke/landingpad, delete the setjmp/longjmp path #7302).
Why it's separate + large: it changes collection-latency and progress guarantees (every loop back-edge must poll; no unbounded poll-free call chain), so it needs a flagged prototype validated against the gc-stress suite before it can be a default. This is where most of gc: reduce safepoint density (safepoint polling instead of every-call statepoints) #8596's value — and its risk — lives.
Bonus finding (checker over-inclusion — not a live bug)
While auditing, two helpers listed in scripts/gc_root_dominance_check.py's NONCOLLECTING set actually allocate; the Rust classify_direct_callee correctly leaves both Unknown, so there is no miscompile, but the checker could pass a build with stale roots across such a call:
js_closure_unbox_callee_checked (closure/unbox.rs:25) — non-callable path is throw_not_callable() → js_throw_type_error_not_a_function, which allocates the TypeError and unwinds.
js_class_object_pin_parent (object/class_registry/parent_static.rs:289) — calls js_string_from_bytes and js_object_set_field_by_name; both allocate.
Worth tightening the checker's set (or documenting why they're admitted there). js_object_mark_class is undetermined — it calls transition_object_shape_to_class, which needs a shape-descriptor-allocation audit.
Closed by #8689, landed via #8700 (squash 8224d879a).
The implementing PR was landed through a batch wrapper PR rather than merged directly, so GitHub's Closes #8596 keyword never fired at merge time. Closing manually.
Activity
proggeramlug commentedon Aug 22, 2026
Safe increment filed: #8598
Four provably-non-collecting helpers reclassified as
gc-leaf-function(verified against their runtime bodies + already in theNONCOLLECTINGauthority):js_typed_feedback_closure_direct_call_guard,js_implicit_this_get,js_tdz_suppress_begin/_end. Zero semantic risk, trims safepoints across all functions. Not the fan-out fix — that's the polling model below.Design notes — the real lever (safepoint polling)
The
× safepointsterm shrinks materially only by moving from "every non-leaf call is a safepoint" to safepoint polling: collection may begin only at a bounded set of poll points; ordinary calls stop being safepoints, so RS4GC statepoints (and relocates) collapse to the poll/alloc sites.GC_SAFEPOINT_PENDING+js_gc_loop_safepoint(the loop back-edge poll), andgc/pressure.rsalready defers a pressure-triggered collection to the next poll when a generated frame is live. Today that coexists with statepoints-at-every-call; polling would make the poll points the only safepoints.function/precise_roots.rs; (2) the moving scavenge (gc: default-on evacuating young-gen scavenge (moving minor at precise safepoints) #7019) — a moving collection may run only where all live frames' roots are precisely known, i.e. a poll (a non-moving one can be conservative anywhere); (3) the conservative-scan fallback for a thread caught between polls; (4) the write-barrier /AllocNoReentry"alloc-point valve" contract, which already assumes deferral-to-safepoint semantics; (5) invoke/unwind rooting (codegen: lower try/catch to LLVM invoke/landingpad, delete the setjmp/longjmp path #7302).Bonus finding (checker over-inclusion — not a live bug)
While auditing, two helpers listed in
scripts/gc_root_dominance_check.py'sNONCOLLECTINGset actually allocate; the Rustclassify_direct_calleecorrectly leaves bothUnknown, so there is no miscompile, but the checker could pass a build with stale roots across such a call:js_closure_unbox_callee_checked(closure/unbox.rs:25) — non-callable path isthrow_not_callable()→js_throw_type_error_not_a_function, which allocates the TypeError and unwinds.js_class_object_pin_parent(object/class_registry/parent_static.rs:289) — callsjs_string_from_bytesandjs_object_set_field_by_name; both allocate.Worth tightening the checker's set (or documenting why they're admitted there).
js_object_mark_classis undetermined — it callstransition_object_shape_to_class, which needs a shape-descriptor-allocation audit.perf(gc): mark four more non-collecting helpers as gc-leaf (#8596) (#…
proggeramlug commentedon Aug 24, 2026
Closed by #8689, landed via #8700 (squash
8224d879a).The implementing PR was landed through a batch wrapper PR rather than merged directly, so GitHub's
Closes #8596keyword never fired at merge time. Closing manually.