Skip to content

gc: function-entry polls at indirect entries and recursive SCCs (RFC deferred collection S5, polls) - #11631

Draft
proggeramlug wants to merge 30 commits into
mainfrom
gc/s5-entry-polls
Draft

proggeramlug wants to merge 30 commits into
mainfrom
gc/s5-entry-polls

Conversation

@proggeramlug

@proggeramlug proggeramlug commented Sep 28, 2026 •

Copy link
Copy Markdown
Contributor

RFC deferred collection (#11528), step S5, the polls (decision 2). Stacked on #11630 (gc/s5-runtime-invariant): review that first. The diff here is the last commits.

Update (7daff50): class constructors no longer get an indirect-entry poll. new C() calls the constructor body directly, so an in-body poll made those call sites moving points, and the stale-register gate found 3 new stale uses (5 against a budget of 2, all new of AnonShape constructors). The stub design in the RFC is what makes constructor polls safe. Revalidation of this head is in progress (perrymaster:/root/s5/dom.log).

What it adds

  • Indirect-entry polls. These go in closure bodies, instance and static methods (including constructors and accessors, which enter through the class tables), and the __perry_wrap_* forwarders that make a top-level function a callback value.
  • Recursive-SCC polls. One entry poll per recursive SCC of the module's direct call graph, in the member with the most incoming edges from inside the SCC. See the refinement below.
  • No poll at a non-recursive direct-call function. The census's reason for rejecting polls at every function entry was 12.18 M vs 7.39 M relocations.

How a poll is placed.

  • Lowering emits a scaffold after parameters are rooted and before arguments materialisation (which can allocate). The scaffold is the armed-word load volatile, a branch, and call void @js_gc_entry_safepoint(). It is the back-edge poll under another symbol: same runtime body, same seeded-schedule candidacy, same PERRY_GC_SCHEDULE_ALLOC_KB pacing.
  • The whole-module pass entry_polls::finalize_module keeps a scaffold only in a function that the module leaf analysis, with entry polls ignored, already proves collecting. So every call to it was already a statepoint, and no call changes classification (S5 must not; S6 does that). A dropped scaffold's load becomes a constant and LLVM folds the branch away.
  • Both lowerings (RS4GC and shadow frames) use the same point, where the loop poll's rooting argument holds: parameters are rooted, and no temporary is live.
  • A value wrapper has no root slots of its own. On the armed path only, it spills its arguments (the closure pointer boxed as an object) to an entry buffer. js_gc_entry_safepoint_args roots them across the collection and writes the relocated values back.

Kill switch. It is the loop polls' own, PERRY_GC_MOVING_LOOP_POLLS=0. There is no new knob.

S1 tables. js_gc_entry_safepoint and js_gc_entry_safepoint_args are Reenters in all three gc_effects/*.tsv files, and declared poll seeds in seeds.txt. The wasm ABI table is regenerated.

Proposed refinement of decision 2, with evidence

Decision 2 says "one poll per recursive SCC". Implemented literally, that put a poll in benchmarks/suite/05_fibonacci: fib's numeric fast clone and its boxed fallback form an SCC. The run cost +59.98% instructions, a poll on every call of a recursion that allocates nothing.

This PR therefore polls a recursive SCC only when the recursion itself allocates without a poll: some member has a collecting edge of its own, or calls out of the SCC to a function that may collect and does not poll at its entry. SCCs are visited callees first, so a callee's poll is known in time.

  • An SCC whose only route to allocation goes through callees that bound their own allocation per call (they poll at entry) gets no second poll.
  • A pure SCC gets none; the RFC already admits pure recursive SCCs as leaves.

Result:

program literal placement refined placement
05_fibonacci +59.98% instructions +0.00%
rectree (loop-free recursive allocator) −52.59% −52.72% instructions, peak RSS −60…−74% (it drains at its SCC poll instead of waiting for the 64 MiB valve)

The rule is stated as a proposal on #11528. The checker (scripts/gc_poll_coverage_check.py) applies the same definition, so a violation of either reading shows up there.

Poll-coverage checker

scripts/gc_poll_coverage_check.py, run over emitted IR:

  • It reports every natural loop whose body may allocate but has no poll, and every allocating recursive SCC with no entry poll.
  • --max-uncovered-loops / --max-uncovered-sccs turn the counts into a ratchet.
  • --self-test proves it can fail.

It also answers the §2 open item: the specialized for / for-of / for-in lowerings named in the stmt/loops.rs comment are now measured rather than assumed.

Measurements

qb4, same base, same 32 programs as #11630, instructions as medians of 5.

Compute and RSS.

this PR vs main
largest instruction delta, any program +0.29% (probe_11)
05_fibonacci +0.00%
rectree (loop-free recursive allocator) −51.6% instructions, −62.6% peak RSS
worst single-program peak RSS +1.68% (shared with #11630)
ratchet median peak RSS +0.34% (same as #11630: the polls add none)

Poll counts and cost. From --statepoint-report=text, which now prints an entry polls: line:

program kept of scaffolds recursive-SCC polls
server fixture 6 of 10 0
09_method_calls 4 of 5 0
14_closure 1 of 2 0
05_fibonacci 2 of 3 1
rectree 9 of 10 4
  • None of these reported an allocating SCC without a candidate.
  • A kept poll's unarmed path is one volatile load, a compare and a not-taken branch. The programs with kept polls move ≤0.1% in instructions.

cc bundle (claude-code 2.1.112):

Δ
binary size +4.2 MB (+1.13%)
--version instructions +0.002%
--help instructions +0.002%
--version peak RSS +0.00%
--help peak RSS +0.60%

Compile wall (67 min) and compile peak RSS (37.8 GB) are equal to main. The cc poll count is not reported: that compile predates the report line.

Decision 3 (straight-line bodies). max_poll_wait_bytes is 0 on:

  • a 20,000-statement allocating top-level chunk;
  • a 20,000-statement retaining chunk;
  • a 100,000-string pool initialiser.

The valve never fired on any of them, so no statement-boundary polls were added. The cc bundle's init chunks under a real session remain unmeasured until #11559 lets cc run -p turns.

Tests

  • crates/perry/tests/gc_entry_polls_s5.rs (real TS → IR):
    • a self-recursive allocator polls;
    • a mutually recursive pair carries exactly one poll;
    • a non-recursive direct-call function (allocating or not) carries none;
    • an allocating method and closure poll, a non-allocating method does not;
    • the wrapper of an allocating function polls with spilled args, and the wrapper of a proven leaf does not;
    • the program's output is right;
    • the kill switch removes every entry poll;
    • runtime: a loop-free recursion reaches armed entry polls, drains at them, never fires the valve, and waits under 16 MiB for a poll.
  • entry_polls.rs unit tests: Tarjan on cycles and self-loops, a 50k-deep chain without recursion, callees-first order.
  • gc_poll_coverage_check.py --self-test.
  • perry-codegen: two shadow_inline tests pin exact register numbers; their pins moved by the scaffold's two registers.

@proggeramlug proggeramlug added the run-extended-tests Opt PR into compile-smoke/parity/doc-tests/drizzle-mysql-smoke label Sep 28, 2026
@coderabbitai

coderabbitai Bot commented Sep 28, 2026

Copy link
Copy Markdown

Important

Draft PR not reviewed

Draft PRs are not automatically reviewed by default.

  • Trigger a manual review

To automatically review draft PRs, update your CodeRabbit configuration:

reviews:
  auto_review:
    drafts: true

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.

❤️ Share

Comment @coderabbitai help to get the list of available commands.

proggeramlug pushed a commit that referenced this pull request Sep 28, 2026

This branch has not been deployed

No deployments
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

run-extended-tests Opt PR into compile-smoke/parity/doc-tests/drizzle-mysql-smoke

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant