Skip to content

wasm: inline CA fast paths and fully GC-managed PyFrame#522

Merged
youknowone merged 10 commits into
mainfrom
wasm-jit
Jul 13, 2026
Merged

wasm: inline CA fast paths and fully GC-managed PyFrame#522
youknowone merged 10 commits into
mainfrom
wasm-jit

Conversation

@youknowone

@youknowone youknowone commented Jul 13, 2026

Copy link
Copy Markdown
Owner

Two related performance cycles on the wasm CALL_ASSEMBLER path, plus the diagnostic tooling used to find them. Cumulative interleaved A/B on fib(32) with PYRE_WASM_CA=1: 0.70s → 0.37s (medians; each cycle measured against its own saved pre-cycle baseline under identical load).

Part 1 — wasm CA residual levers (follow-up to #501)

  • Inline CA frame allocation and shadow-stack updates (59f275fe24f): the direct CA arm previously made six helper call_indirects per call (alloc, reload ×4, pop), each through a thread-local RefCell. Now the nursery bump, GcHeader/JitFrame init, memory.fill payload clear, and jf shadow-stack push/pop/reload are emitted inline (assembler.py:1122-1136 _call_header/footer_shadowstack, rewrite.py nursery fast path). Gated off under gc_stress, non-plain types, and over-size frames.
  • Decline CA traces with host trampoline calls (c27531164d0, cf1b5e42204): the host jit_call trampoline writes results through the pre-call frame pointer and never reloads local 0, so a non-i64-ABI residual call (CallF/void/Newstr) on a movable CA frame could use a stale frame after a collecting call. CA compilation is now declined when the source loop, the CA bridge, or any chained bridge (cumulative census, OR-ed at bridge registration) uses the trampoline. Diag slot 15 (decl_ca_trampoline) + regression bench wasm_ca_trampoline_decline.py.
  • Shrink CA callee frame prefix (88b967cdcb1): tail-call-area layout [value slots | dispatch key | Ref homes | call area] — the full geometry keeps the arena/bridge-compatible size, while CA callee allocation stops at the homes (544 → 392 bytes at the frozen floors). Sound because the trampoline decline guarantees no trampoline op runs on a CA frame. fib(32) minors 1243 → 987.
  • Inline trace frame reload (eee449661a1): the generic post-collecting-call frame reloads (helper call_indirect, ~14M/run on fib(32)) now emit the jf-top load inline when the top-cell address is baked.
  • GuestProfiler flag (c5586739ba7): PYRE_WASM_GUEST_PROFILE=<out.json> drives a wasmtime epoch-based sampling profiler (200µs ticker, Firefox processed format). This is what attributed the remaining GC subtree cost to the PyFrame destructor complex below.

Part 2 — fully GC-managed PyFrame (destructor removal)

Profiling showed ~30% of the fib(32) CA run inside the PyFrame destructor complex: every frame allocation was pushed onto young_objects_with_destructors and swept per minor (~7M dead frames/run), and the destructor made PYFRAME ineligible for type_alloc_is_plain, forcing every JIT-built frame through the runtime alloc helper. Upstream PyFrame(W_Root) has no destructor: its locals list, FrameDebugData, and block-stack nodes are plain GC objects. The destructor existed only to free the three malloc'd resources of interpreter/generator call frames:

  • 89491b1ba67 — call-frame locals arrays move from std::alloc blocks to old-gen GC arrays (PY_OBJECT_ARRAY), same generation and lifetime as the owning frame, so the existing frame-level barrier discipline is preserved unchanged. Tracer-private snapshot frames keep std::alloc arrays (they are not GC-traced). The frame custom trace visits the array field slot and walks old-gen items in place (interpreter stores do not write-barrier the array).
  • 06200a87645FrameDebugData becomes an old-gen GC object (tid 103) and block-stack nodes become nursery GC objects (tid 104, previous is construction-only and always strictly older, so it needs no write barrier). Representation follows the owning frame's regime; snapshot clones stay boxed. hidden_operationerr is now traced (no writer can store a heap ref today; traced defensively).
  • 74b4a720a41 — with all frame-owned resources GC-managed, the destructor freed nothing; it is deleted. PYFRAME now satisfies type_alloc_is_plain: no destructor-list bookkeeping per frame, and the wasm/dynasm/cranelift plain-allocation fast paths inline-bump frame allocation.
  • 19473814add — removes the collection-phase thread-local the first slice introduced: RPython custom tracers are phase-agnostic (jitframe.py:104 jitframe_trace; the collection kind lives in the callback, base.py:335), so the in-place item walk now runs in both collection kinds — at majors it is idempotent duplicate marking.

Verification

Per slice and at tip: wasm / dynasm / cranelift synthetic suites 153/153 each; fib(32) output 2178309 with gc_minors=987, gc_majors=0 invariant; trampoline-decline bench exact output with decl_ca_trampoline=1; generator/try-finally parity benches; MAJIT_GC_STRESS=1 cargo test -p pyre-jit --features dynasm,gc_stress -- --test-threads=1 (serial; forced collections shake out the moving-GC paths).

🤖 Generated with Claude Code

Summary by CodeRabbit

  • New Features

    • Added optional WebAssembly guest sampling profiling, configurable through an environment variable.
    • Improved support for optimized recursive calls and inline execution paths.
  • Bug Fixes

    • Improved garbage-collection handling for nursery objects and interpreter frame data.
    • Prevented unsafe optimized call chaining when host trampolines are required.
    • Improved frame allocation and shadow-stack management for more reliable execution.
  • Tests

    • Added regression coverage for optimized recursive calls and frame layout behavior.

@coderabbitai

coderabbitai Bot commented Jul 13, 2026

Copy link
Copy Markdown

Review Change Stack

Warning

Review limit reached

@youknowone, you've reached your PR review limit, so we couldn't start this review.

Next review available in: 36 minutes

Enable usage-based reviews in Billing to review now. Otherwise, wait until the next included review is available.
You're only billed for reviews past your plan's rate limits ($0.25/file).

How can I continue?

After more reviews become available, a review can be triggered using the @coderabbitai review command as a PR comment. Alternatively, push new commits to this PR.

To avoid repeated limits, reduce automatic review volume by pausing incremental auto-reviews earlier, using label-based review opt-in, excluding WIP or generated PR titles, or requesting reviews manually when the PR is ready. If your team needs uninterrupted high-volume reviews, an organization admin can enable usage-based reviews.

How do review limits work?

CodeRabbit enforces per-developer PR review limits for each organization. Most developers receive the normal plan review availability.

For paid Pro and Pro+ PR reviews, CodeRabbit uses adaptive limits for sustained high-volume activity. When a developer's recent PR review activity reaches the 95th percentile or higher among CodeRabbit users, additional reviews become available more gradually as earlier reviews age out of the rolling window.

Please refer docs for additional details.

Review details
⚙️ Run configuration

Configuration used: Organization UI

Review profile: ASSERTIVE

Plan: Pro

Run ID: 0c3c490a-9b8c-4302-80f3-14ce95f8a68c

📥 Commits

Reviewing files that changed from the base of the PR and between 1947381 and 9cf9959.

📒 Files selected for processing (19)
  • majit/majit-backend-cranelift/src/compiler.rs
  • majit/majit-backend-dynasm/src/runner.rs
  • majit/majit-backend-wasm/src/codegen.rs
  • majit/majit-backend-wasm/src/failguard.rs
  • majit/majit-backend-wasm/src/lib.rs
  • majit/majit-gc/src/collector.rs
  • majit/majit-gc/src/lib.rs
  • majit/majit-gc/src/shadow_stack.rs
  • pyre/bench/synth/wasm_ca_trampoline_decline.py
  • pyre/pyre-interpreter/src/call.rs
  • pyre/pyre-interpreter/src/eval.rs
  • pyre/pyre-interpreter/src/function.rs
  • pyre/pyre-interpreter/src/pyframe.rs
  • pyre/pyre-jit-trace/src/descr.rs
  • pyre/pyre-jit-trace/src/state.rs
  • pyre/pyre-jit-trace/src/trace.rs
  • pyre/pyre-jit-trace/src/trace_opcode.rs
  • pyre/pyre-jit/src/eval.rs
  • pyre/pyre-wasm-runner/src/main.rs

Walkthrough

The PR adds nursery-object queries and shadow-stack limit addresses, introduces inline CA frame allocation and trampoline-aware bridge decisions, moves PyFrame auxiliary storage into explicit GC allocation regimes, updates GC tracing and registrations, and adds wasm guest profiling plus a CA regression benchmark.

Changes

Runtime nursery and shadow-stack plumbing

Layer / File(s) Summary
Nursery hooks and shadow-stack limits
majit/majit-gc/..., majit/majit-backend-cranelift/src/compiler.rs, majit/majit-backend-dynasm/src/runner.rs
GC allocators expose nursery membership, active-runtime hooks are installed, and the JIT frame shadow stack exposes a stable limit-cell address validated across growth.
CA frame geometry and inline codegen
majit/majit-backend-wasm/src/codegen.rs
Compact frames separate the CA prefix from the tail call area; CA codegen adds inline nursery allocation, shadow-stack reload/pop operations, and centralized trampoline-call census logic.
CA bridge eligibility and census
majit/majit-backend-wasm/src/failguard.rs, majit/majit-backend-wasm/src/lib.rs
Loops and bridges track trampoline usage, decline unsafe CA chaining, pass inline parameters, and use updated frame sizing.
GC-managed PyFrame ownership
pyre/pyre-interpreter/..., pyre/pyre-jit/..., pyre/pyre-jit-trace/...
Frame locals, debug data, and block chains gain explicit allocation regimes, GC registration, write barriers, ownership-aware tracing, and updated construction paths.
Profiling and regression coverage
pyre/bench/synth/wasm_ca_trampoline_decline.py, pyre/pyre-wasm-runner/src/main.rs
A CA trampoline regression benchmark and optional epoch-driven wasm guest profiler are added, and bridge diagnostic labels are updated.

Estimated code review effort: 5 (Critical) | ~120 minutes

Possibly related issues

Possibly related PRs

Sequence Diagram(s)

sequenceDiagram
  participant Interpreter
  participant WasmLoop
  participant GC
  participant ShadowStack
  Interpreter->>WasmLoop: execute CA trace
  WasmLoop->>GC: query nursery and allocate frame
  WasmLoop->>ShadowStack: push JitFrame
  ShadowStack-->>WasmLoop: provide top and limit state
  WasmLoop-->>Interpreter: reload frame and continue
Loading

Poem

I’m a rabbit with frames in a burrow so bright,
Nursery pointers now hop into sight.
CA calls leap, then safely land,
Shadow-stack limits held close at hand.
GC-tended PyFrames bloom in the night.

🚥 Pre-merge checks | ✅ 5
✅ Passed checks (5 passed)
Check name Status Explanation
Docstring Coverage ✅ Passed Docstring coverage is 100.00% which is sufficient. The required threshold is 80.00%.
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title clearly captures the two main themes of the PR: wasm CA fast-path inlining and moving PyFrame to fully GC-managed storage.
✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch wasm-jit

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.

@github-actions

github-actions Bot commented Jul 13, 2026

Copy link
Copy Markdown

🤖 Codex parity review

Static analysis of this diff vs the local RPython/PyPy sources (commit 292bce7).

Files in the reviewed diff
majit/majit-backend-cranelift/src/compiler.rs
majit/majit-backend-dynasm/src/runner.rs
majit/majit-backend-wasm/src/codegen.rs
majit/majit-backend-wasm/src/failguard.rs
majit/majit-backend-wasm/src/lib.rs
majit/majit-gc/src/collector.rs
majit/majit-gc/src/lib.rs
majit/majit-gc/src/shadow_stack.rs
pyre/bench/synth/wasm_ca_trampoline_decline.py
pyre/pyre-interpreter/src/call.rs
pyre/pyre-interpreter/src/eval.rs
pyre/pyre-interpreter/src/function.rs
pyre/pyre-interpreter/src/pyframe.rs
pyre/pyre-jit-trace/src/descr.rs
pyre/pyre-jit-trace/src/state.rs
pyre/pyre-jit-trace/src/trace.rs
pyre/pyre-jit-trace/src/trace_opcode.rs
pyre/pyre-jit/src/eval.rs
pyre/pyre-wasm-runner/src/main.rs

1. Regressions to PyPy parity introduced by this patch

  • pyre/pyre-interpreter/src/pyframe.rs:2307-2312 ↔ rpython/memory/gc/minimark.py:1078-1114append_block now allocates FrameBlock in the moving nursery, then stores it into an old-gen PyFrame.lastblock without a write barrier. Main used non-GC allocation here. After the frame’s prior remembered-set entry is consumed, a later block push can leave the nursery node unreachable during minor collection. RPython’s translated old-object field store uses remember_young_pointer; this patch must similarly remember the frame.

  • pyre/pyre-interpreter/src/pyframe.rs:604-631 ↔ rpython/memory/gc/minimark.py:1293-1318clone_block_chain(..., OldGenGc) constructs moving nursery nodes, but after the final node is allocated it returns an unrooted raw pointer. snapshot_for_generator then performs FrameBox::new, an old-gen allocation that may collect before the returned PyFrame is installed into a rooted GC object. The temporary FrameBlockRoot roots only the previous chain during each allocation, not the completed head across that later allocation; the copied block chain can be reclaimed or forwarded without updating the snapshot.

2. Other mismatches introduced by this patch

  • majit/majit-backend-cranelift/src/compiler.rs:1676-1688 ↔ rpython/memory/gc/minimark.py:1282-1318 — the new nursery-membership hook returns false when the production GC is installed through gc_sync rather than the TLS test box. Unlike the adjacent ownership hook, it has no gc_sync::gc_query_reentrant fallback. Consequently pyframe_object_custom_trace misclassifies every production nursery child as old-gen and manually scans its stale pre-forwarding payload.

  • majit/majit-backend-dynasm/src/runner.rs:620-632 ↔ rpython/memory/gc/minimark.py:1282-1318 — same missing production gc_sync fallback in the dynasm hook. The global hook is installed by install_gc_standalone, but this implementation only queries DYNASM_ACTIVE_GC / its raw TLS pointer.

3. Pre-existing mismatches (already present before this patch)

None.

4. Structural adaptations

  • majit/majit-backend-wasm/src/codegen.rs:953-984 ↔ rpython/jit/backend/llsupport/jitframe.py:48-52 — wasm’s host jit_call trampoline and linear-memory call area have no RPython/PyPy analogue. The patch’s trampoline census and CA decline are a wasm host/guest adaptation, not a line-for-line PyPy port.

  • majit/majit-backend-wasm/src/lib.rs:2278-2325 ↔ rpython/jit/backend/llsupport/jitframe.py:61-89 — allocating wasm execution state as a GC-managed JitFrame with a wasm32 frame geometry is required by the Rust/wasm embedding model; RPython directly allocates its JITFRAME GC struct.

  • pyre/pyre-wasm-runner/src/main.rs:215-256 ↔ rpython/jit/backend/llsupport/jitframe.py:48-52wasmtime::GuestProfiler, epoch interruption, and the profiling worker thread are runner diagnostics with no PyPy interpreter counterpart.

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: 19473814ad

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

// and always targets the strictly older node. Thus an old-gen block
// never points to a younger block and needs no write barrier.
unsafe { (*node).previous = previous };
self.lastblock = node;

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P1 Badge Add a write barrier for new frame blocks

When self is a GC-managed old-gen frame, alloc_frame_block(..., OldGenGc) normally returns a nursery FRAME_BLOCK and this assignment creates an old-to-young edge without remembering the frame. Active frames are scanned as roots, but after a generator or escaped frame suspends inside a try/finally/with block, a later minor collection can miss lastblock once the frame’s earlier remembered-set entry has been consumed, allowing the block stack node to be collected or left stale. Call the frame write barrier after publishing a GC-owned node here.

Useful? React with 👍 / 👎.

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 1

🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@pyre/pyre-wasm-runner/src/main.rs`:
- Around line 306-313: Update the guest profiling cleanup block around
guest_profiler.take() so File::create and p.finish errors are logged and ignored
instead of propagated with ?. Preserve processing and returning run_result even
when profile output fails, matching the error-tolerant behavior of the JIT stats
section.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: ASSERTIVE

Plan: Pro

Run ID: bd866115-33a3-4437-9835-305c32549133

📥 Commits

Reviewing files that changed from the base of the PR and between cb91531 and 1947381.

📒 Files selected for processing (19)
  • majit/majit-backend-cranelift/src/compiler.rs
  • majit/majit-backend-dynasm/src/runner.rs
  • majit/majit-backend-wasm/src/codegen.rs
  • majit/majit-backend-wasm/src/failguard.rs
  • majit/majit-backend-wasm/src/lib.rs
  • majit/majit-gc/src/collector.rs
  • majit/majit-gc/src/lib.rs
  • majit/majit-gc/src/shadow_stack.rs
  • pyre/bench/synth/wasm_ca_trampoline_decline.py
  • pyre/pyre-interpreter/src/call.rs
  • pyre/pyre-interpreter/src/eval.rs
  • pyre/pyre-interpreter/src/function.rs
  • pyre/pyre-interpreter/src/pyframe.rs
  • pyre/pyre-jit-trace/src/descr.rs
  • pyre/pyre-jit-trace/src/state.rs
  • pyre/pyre-jit-trace/src/trace.rs
  • pyre/pyre-jit-trace/src/trace_opcode.rs
  • pyre/pyre-jit/src/eval.rs
  • pyre/pyre-wasm-runner/src/main.rs

Comment on lines +306 to +313
if let Some(path) = &guest_profile_out {
if let Some(p) = store.data_mut().guest_profiler.take() {
let f = std::fs::File::create(path).context("create guest profile output")?;
p.finish(std::io::BufWriter::new(f))
.context("write guest profile")?;
eprintln!("[guest-profile] wrote {path}");
}
}

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🎯 Functional Correctness | 🟡 Minor | ⚡ Quick win

Profile write failure suppresses the guest run result.

File::create and finish() use ? to propagate errors. If the profile write fails, run() returns Err and the guest output stored in run_result (line 299) is never processed. This is inconsistent with the JIT stats section below, which tolerates errors via unwrap_or. Since profiling is diagnostic, its failure shouldn't lose the main result.

🛡️ Suggested fix: log profile errors instead of propagating
     if let Some(path) = &guest_profile_out {
         if let Some(p) = store.data_mut().guest_profiler.take() {
-            let f = std::fs::File::create(path).context("create guest profile output")?;
-            p.finish(std::io::BufWriter::new(f))
-                .context("write guest profile")?;
-            eprintln!("[guest-profile] wrote {path}");
+            match std::fs::File::create(path).context("create guest profile output") {
+                Ok(f) => match p.finish(std::io::BufWriter::new(f)) {
+                    Ok(()) => eprintln!("[guest-profile] wrote {path}"),
+                    Err(e) => eprintln!("[guest-profile] write failed: {e:#}"),
+                },
+                Err(e) => eprintln!("[guest-profile] create output failed: {e:#}"),
+            }
         }
     }
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
if let Some(path) = &guest_profile_out {
if let Some(p) = store.data_mut().guest_profiler.take() {
let f = std::fs::File::create(path).context("create guest profile output")?;
p.finish(std::io::BufWriter::new(f))
.context("write guest profile")?;
eprintln!("[guest-profile] wrote {path}");
}
}
if let Some(path) = &guest_profile_out {
if let Some(p) = store.data_mut().guest_profiler.take() {
match std::fs::File::create(path).context("create guest profile output") {
Ok(f) => match p.finish(std::io::BufWriter::new(f)) {
Ok(()) => eprintln!("[guest-profile] wrote {path}"),
Err(e) => eprintln!("[guest-profile] write failed: {e:#}"),
},
Err(e) => eprintln!("[guest-profile] create output failed: {e:#}"),
}
}
}
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@pyre/pyre-wasm-runner/src/main.rs` around lines 306 - 313, Update the guest
profiling cleanup block around guest_profiler.take() so File::create and
p.finish errors are logged and ignored instead of propagated with ?. Preserve
processing and returning run_result even when profile output fails, matching the
error-tolerant behavior of the JIT stats section.

Keep frozen frame geometry complete with a tail call area while allocating CA callee JitFrames only through their Ref homes.

Assisted-by: Claude
Assisted-by: Claude
Gate a wasmtime GuestProfiler behind PYRE_WASM_GUEST_PROFILE=<out.json>:
epoch interruption drives sample() from the deadline callback with a
200us ticker thread, and the profile is written in the Firefox processed
format after the run. Main-module symbols only; samples land on epoch
checkpoints, so bulk ops are attributed to the following checkpoint.

Assisted-by: Claude
Frame auxiliary resources now follow their owning frame regime: GC frames use old-gen debug data and nursery block nodes, while tracer-private snapshots retain Box allocations.

Transitional try_gc_owns_object gates keep the PyFrame destructor and explicit clearing paths from freeing managed resources until the destructor is removed.

Assisted-by: Claude
All frame-owned resources are GC-managed as of the two prior commits, so the PYFRAME destructor freed nothing. PYFRAME_GC_TYPE_ID now satisfies type_alloc_is_plain.

Assisted-by: Claude
Tracer callbacks now enumerate slots identically in minor and major collections, matching the phase-agnostic custom-trace contract. Old-generation payload item walks at majors are idempotent duplicate marking.

Assisted-by: Claude

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: 9cf9959a5e

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

// `previous` is written only here (and in clone/unpickle construction)
// and always targets the strictly older node. Thus an old-gen block
// never points to a younger block and needs no write barrier.
unsafe { (*node).previous = previous };

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P1 Badge Remember fallback old-gen block nodes before linking previous

When the nursery is full, alloc_frame_block can return an old-gen FRAME_BLOCK via the no-collect allocation fallback. In that case this store can put a still-nursery older block into an old-gen node without remembering the node; a later minor collection that reaches the frame's lastblock edge to this old node will not scan previous, so nested try/with block stacks can lose the rest of the chain. Add a write barrier for node after storing previous when it is GC-owned; the same fallback shape also applies to the clone-chain construction.

Useful? React with 👍 / 👎.

// it immediately with w_globals or a freshly allocated mapping.
// Remembering the completed object is harmless for Box fallback
// and keeps the old-gen debug payload visible to the next minor.
remember_frame_debug_data(self.debugdata);

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge Remember existing debugdata before mutating its GC refs

This barrier only runs when debugdata is first created, but FrameDebugData is now a GC-managed old-gen object. After a minor collection resets its TRACK_YOUNG_PTRS flag and removes it from the remembered set, existing-debugdata writers such as setdictscope, fset_f_trace, and descr_clear mutate fields through getorcreate_debug_data without going through getorcreatedebug; storing a fresh locals dict or trace function then creates an unremembered old-to-young edge that a later minor can reclaim. Remember the debugdata object before returning it for mutation, or after each field write, not just on creation.

Useful? React with 👍 / 👎.

@youknowone youknowone merged commit a951de2 into main Jul 13, 2026
30 checks passed
@youknowone youknowone deleted the wasm-jit branch July 13, 2026 10:41
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.

1 participant