Skip to content

fix(codegen): keep RS4GC off the inline-asm loop barrier - #8123

Closed
proggeramlug wants to merge 2 commits into
mainfrom
fix/8121-rs4gc-inline-asm-statepoint
Closed

fix(codegen): keep RS4GC off the inline-asm loop barrier#8123
proggeramlug wants to merge 2 commits into
mainfrom
fix/8121-rs4gc-inline-asm-statepoint

Conversation

@proggeramlug

Copy link
Copy Markdown
Contributor

The bug

Perry emits call void asm sideeffect "", ""() as the issue-#74 loop-preservation barrier. rewrite-statepoints-for-gc rewrites every non-leaf call in a gc "statepoint-example" function into a gc.statepoint — and for inline asm that means using the InlineAsm itself as the statepoint's callee operand. That is invalid IR:

Cannot take the address of an inline asm!
  %statepoint_token26 = call token (i64, i32, ptr, i32, i32, ...)
      @llvm.experimental.gc.statepoint.p0(i64 2882400000, i32 0,
      ptr elementtype(void ()) asm sideeffect "", "", i32 0, i32 0, i32 0, i32 0)
      [ "gc-live"(ptr addrspace(1) %.1181, ...) ]
LLVM ERROR: Broken module found, compilation aborted!

inprocess::optimize_and_emit verified the module before the rewrite but not after, so the broken module went straight to SelectionDAG, which dereferenced the bogus callee and killed the compiler with SIGBUS in AArch64TargetLowering::LowerCall. Symbolized:

#0 llvm::AArch64TargetLowering::LowerCall(...)
#1 llvm::TargetLowering::LowerCallTo(...)
#3 llvm::SelectionDAGBuilder::LowerAsSTATEPOINT(...)
#4 llvm::SelectionDAGBuilder::LowerStatepoint(...)
#5 llvm::SelectionDAGBuilder::visitIntrinsicCall(...)

This blocked #8040: compiling next@16.3.0's bundled jsonwebtoken died 100 modules into a 104-module production App Route.

The fix

  • Mark the barrier "gc-leaf-function" on the native and text emission paths and in generic call_asm. That is the documented way to tell RS4GC a call cannot trigger a collection — true by construction for a barrier that emits no instructions.
  • Verify after RS4GC, not only before, so this class of bug is a clean compile error naming the pass instead of a crash that takes the compiler process down. Same reasoning as the neighbouring rs4gc_funclet_refusal (Windows: native-root stack walker so PERRY_RS4GC=1 works there (#7173) #7354).

Tests

Three, split so no half can pass hollowly:

test fails if
rs4gc_breaks_an_unmarked_inline_asm_barrier RS4GC stops breaking unmarked barriers, i.e. the fix became unnecessary and its sibling vacuous
a_gc_leaf_inline_asm_barrier_survives_rs4gc the attribute stops working, or RS4GC rewrote nothing (asserts gc.statepoint is present first)
perry_emits_the_loop_barrier_as_a_gc_leaf Perry stops emitting the attribute — the two above use hand-written IR and would stay green through a full revert of the fix

An earlier version of the second test was vacuous and is worth recording: it built its fixture with enable_shadow_frame, so the function carried no gc "statepoint-example" strategy, RS4GC rewrote nothing (statepoint count = 0), and it passed with the fix reverted. The gc.statepoint assertion is what closes that hole.

Validation on the real workload

Reproduction (~2.5 min on a quiet host; the ">90 min" in #8121 was build contention):

npm pack next@16.3.0
tar -xzf next-16.3.0.tgz package/dist/compiled/jsonwebtoken/index.js
cp package/dist/compiled/jsonwebtoken/index.js ./jsonwebtoken-index.js   # sha256 056c2ddd…a6b9
printf 'const jwt = require("./jsonwebtoken-index.js");\nconsole.log(typeof jwt);\n' > main.js
perry compile main.js --output-type dylib --no-auto-optimize --no-cache -o min.dylib

Must run outside a Next directory, or .next/server auto-discovery pulls in all 104 modules.

The barriers live in exactly one codegen unit — grep -c "asm sideeffect" gives 4 in unit 2/2 and 0 in unit 1/2 — and that is precisely the unit stock opt rejects. With this change, LLVM unit 2/2 finished (228.4s) and the compile has run 49+ minutes with no crash against a 2:30 crash point.

Why this is a draft

The crash is fixed and unit-tested, but the full #8034 104-module gate has not been re-run with PERRY_RS4GC=1 yet — the remaining unit is slow for an unrelated reason (below). Promoting out of draft once that gate result is in.

Adjacent, not fixed here

RS4GC expands this module 21x: perry_closure_jsonwebtoken_index_js__227 (the webpack module factory, emitted as one 4.93 MB function — 8,471 basic blocks, 8,044 calls, 13,369 addrspace(1) GC pointers) turns 19 MB of IR into 413 MB, with 21,774 statepoints and 1,502,654 gc.relocates. That is the compile-time blowup reported in #8121, now unmasked because the crash no longer preempts it. It deserves its own issue — reducing values live across safepoints, or not emitting an 8k-safepoint monolith, shrinks it everywhere.

Refs #8040. Fixes #8121.

Ralph Küpper added 2 commits August 15, 2026 04:22
The issue-#74 loop-preservation barrier is `call void asm sideeffect "", ""()`.
rewrite-statepoints-for-gc rewrites every non-leaf call in a `gc "statepoint"`
function into a gc.statepoint, and for inline asm that means using the InlineAsm
itself as the statepoint's callee operand — invalid IR, rejected by the verifier
with "Cannot take the address of an inline asm!".

optimize_and_emit verified the module before the rewrite but not after, so the
broken module reached SelectionDAG and killed the compiler with SIGBUS inside
AArch64TargetLowering::LowerCall. next@16.3.0's bundled jsonwebtoken died this
way 100 modules into a 104-module production App Route.

Mark the barrier "gc-leaf-function" on both emission paths, and verify after
RS4GC so this class is a clean error naming the pass rather than a crash.

Fixes #8121. Refs #8040.
Three tests, split so no half can pass hollowly:

- `rs4gc_breaks_an_unmarked_inline_asm_barrier` pins the defect itself: without
  "gc-leaf-function", RS4GC produces IR the verifier rejects with "Cannot take
  the address of an inline asm!". If this ever stops failing, the marking has
  become unnecessary and its sibling is vacuous.
- `a_gc_leaf_inline_asm_barrier_survives_rs4gc` covers the cure, and asserts
  `gc.statepoint` is present first so a fixture RS4GC never touched cannot pass
  by doing nothing. (An earlier attempt did exactly that: it used
  enable_shadow_frame, so the function carried no gc strategy, RS4GC rewrote
  nothing, and the test passed with the fix reverted.)
- `perry_emits_the_loop_barrier_as_a_gc_leaf` covers the emission on both the
  text and native paths. The two above use hand-written IR and would stay green
  if Perry stopped emitting the attribute, so this is the half that catches a
  revert of the fix.

Refs #8121.
@coderabbitai

coderabbitai Bot commented Aug 15, 2026

Copy link
Copy Markdown

Important

Review skipped

Draft detected.

Please check the settings in the CodeRabbit UI or the .coderabbit.yaml file in this repository. To trigger a single review, invoke the @coderabbitai review command.

⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro Plus

Run ID: bec91d0e-fa91-4ce9-aaa8-c0f87dea4b65

You can disable this status message by setting the reviews.review_status to false in the CodeRabbit configuration file.

Use the checkbox below for a quick retry:

  • 🔍 Trigger review

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

Copy link
Copy Markdown
Contributor Author

Validation upgraded: a completed compile with PERRY_RS4GC=1

Earlier the evidence was "runs far past the crash point without dying". It is now a finished artifact. Same input (next@16.3.0's bundled jsonwebtoken), same build of this branch, RS4GC enabled:

arm result
PERRY_LL_SIZE_OPT=0 (forced -O0) completed, 2m46.5s real / 3m29.5s user, 40 MB dylib
default (-Os) still running at 60+ min, no crash

Before this branch the same command SIGBUS'd in ~2m30s. So the crash is fixed, and the compile genuinely finishes rather than merely surviving longer.

This also isolates the compile-time blowup

The two arms differ only in the opt level applied to the RS4GC-expanded IR, so the >20x gap is the -Os pipeline on statepoint output — not RS4GC itself, and not the fix.

That points at a threshold that is arguably mistuned for this shape. oversized_opt_flag sends a unit to -O0 when its widest function exceeds ll_o0_threshold_bytes() (6 MB). The offending function here — perry_closure_jsonwebtoken_index_js__227, the webpack module factory Perry emits as a single body — is 4.93 MB, just under, so it takes -Os. Its profile is 8,471 basic blocks, 8,044 calls, 13,369 addrspace(1) GC pointers, which RS4GC turns into 21,774 statepoints and 1,502,654 gc.relocates (19 MB -> 413 MB of IR).

The sibling constant ll_size_opt_max_fn_bytes() (256 KB) is the one documented as "the average IR bytes-per-function below which we size-optimize", and the comment there explicitly describes the pathological case as "megabytes-per-function". A 4.93 MB function is that case.

I have not changed the threshold in this PR. Retuning it is a real tradeoff — any unit containing one oversized function would drop to -O0 wholesale, including its many ordinary functions, and -Os is documented as emitting 30-50% less __text. That deserves its own change with a size measurement behind it, not a drive-by edit inside a crash fix.

Flagging it here because it has a direct consequence for #8040: at hours-per-compile the #8034 fixture cannot serve as a CI gate regardless of correctness, and the definition of done there needs ten cold starts plus a 100-iteration 20-way concurrent run.

@proggeramlug

Copy link
Copy Markdown
Contributor Author

Closing as superseded — main already carries this fix, landed independently through #8082's sibling work while this branch was in validation. I checked before rebasing rather than force-pushing over it.

origin/main has all three pieces, and two of them in a better form than this PR:

  • native pathdialect/mod.rs adds "gc-leaf-function" to the inline-asm call site.
  • text pathinst.rs emits call void asm sideeffect "", ""() "gc-leaf-function" as an inline string attribute. Cleaner than this PR, which introduced an attributes #5 group and had to emit it conditionally from module.rs.
  • post-RS4GC verifyinprocess.rs verifies the rewritten module with a more precise message than mine ("this is a Perry codegen bug — the input shape must be exempted or fixed").

It also has something this PR does not: demote_relocation_bloated_functions, which re-checks opt tier against POST-rewrite sizes and opts just the exploded functions out of -Os. That directly addresses the relocation fan-out measured in #8132.

The independent diagnosis agreed on every particular, including the numbers — that comment records "one 51k-line minified-bundle closure grew 40x to 2.1M instructions, and a single -Os function pass then ran for over an hour", against my measurement of 19 MB → 413 MB of IR with 21,774 statepoints and 1,502,654 gc.relocates on the jsonwebtoken unit.

Nothing here is worth salvaging on top. The tests in this branch (rs4gc_breaks_an_unmarked_inline_asm_barrier, a_gc_leaf_inline_asm_barrier_survives_rs4gc, perry_emits_the_loop_barrier_as_a_gc_leaf) may still be useful if main's version does not already pin the behaviour both ways — the first of those asserts RS4GC does break an unmarked barrier, so it fails if the exemption ever stops being necessary. Happy to re-open just the test half if that coverage is missing.

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

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

[Next.js/dylib] perry compile SIGBUSes in LLVM codegen of next/dist/compiled/jsonwebtoken (100/104 modules in)

1 participant