Skip to content

[Wasm RyuJit] emit virtual IP ranges in the unwind info#128382

Open
AndyAyersMS wants to merge 6 commits into
dotnet:mainfrom
AndyAyersMS:WasmReportVirtualIPRanges
Open

[Wasm RyuJit] emit virtual IP ranges in the unwind info#128382
AndyAyersMS wants to merge 6 commits into
dotnet:mainfrom
AndyAyersMS:WasmReportVirtualIPRanges

Conversation

@AndyAyersMS
Copy link
Copy Markdown
Member

@AndyAyersMS AndyAyersMS commented May 19, 2026

Extend the per-funclet unwind info to record the length of the virtual IP range. The lowest Virtual IP for a funclet can be found by summing the lengths of all the prior funclets.

The unwind info previously just recorded the size of the fixed part of the frame.

Virtual IP ranges for funclets (and main method) are disjoint.

Data is encoded as ULEB128.

Extend the per-funclet unwind info to record the starting Virtual IP
for the funclet and the length of the virtual IP range.

The unwind info previously just recorded the size of the fixed part
of the frame.

Virtual IP ranges for funclets (and main method) are disjoint.

All data is encoded as ULEB128.
Copilot AI review requested due to automatic review settings May 19, 2026 18:44
@github-actions github-actions Bot added the area-CodeGen-coreclr CLR JIT compiler in src/coreclr/src/jit and related components such as SuperPMI label May 19, 2026
@dotnet-policy-service
Copy link
Copy Markdown
Contributor

Tagging subscribers to this area: @JulieLeeMSFT, @jakobbotsch
See info in area-owners.md if you want to be subscribed.

@AndyAyersMS
Copy link
Copy Markdown
Member Author

@davidwrighton PTAL
fyi @dotnet/wasm-contrib

@AndyAyersMS
Copy link
Copy Markdown
Member Author

AndyAyersMS commented May 19, 2026

Sample output (from jit dump), for a method with a simple try/finally:

Unwind info for main 0: VIP range [0, 2); frame size 28
Unwind info for funclet 1: VIP range [2, 4); frame size 16

Also note you currently must pass --codegenopt:JitWasmFunclets=1 to crossgen2 to see any funclet codegen reach the host. This is temporary until the host can perform funclet extraction.

Copy link
Copy Markdown
Contributor

Copilot AI left a comment

Choose a reason for hiding this comment

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

Pull request overview

This PR updates the Wasm RyuJIT unwind-info payload to include a per-method/per-funclet Virtual IP (VIP) range alongside the existing frame-size data, and records those VIP ranges during the Wasm virtual-IP phase.

Changes:

  • Extend FuncInfoDsc (Wasm-only) with startVirtualIP / endVirtualIP fields.
  • Record startVirtualIP / endVirtualIP for each function/funclet during fgWasmVirtualIP().
  • Emit unwind info as ULEB128-encoded { frameSize, startVirtualIP, (endVirtualIP - startVirtualIP) } in unwindEmitFunc().

Reviewed changes

Copilot reviewed 3 out of 3 changed files in this pull request and generated 1 comment.

File Description
src/coreclr/jit/unwindwasm.cpp Emits Wasm unwind payload including VIP range (start + delta) encoded as ULEB128.
src/coreclr/jit/fgwasm.cpp Captures per-funclet VIP range boundaries while assigning VIPs.
src/coreclr/jit/compiler.h Stores per-funclet VIP range endpoints in FuncInfoDsc under TARGET_WASM.

Comment thread src/coreclr/jit/unwindwasm.cpp Outdated
Comment thread src/coreclr/jit/unwindwasm.cpp
Comment thread src/coreclr/jit/unwindwasm.cpp
Copilot AI review requested due to automatic review settings May 20, 2026 16:15
Copy link
Copy Markdown
Contributor

Copilot AI left a comment

Choose a reason for hiding this comment

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

Pull request overview

Copilot reviewed 4 out of 4 changed files in this pull request and generated 2 comments.

Comments suppressed due to low confidence (1)

src/coreclr/jit/codegenwasm.cpp:3472

  • This changes the GC header's code length to maxVirtualIP, but the subsequent gcMakeRegPtrTable calls still pass codeSize/prologSize (native code byte offsets) to define interruptible ranges and call sites. Mixing VIP and byte-offset coordinate systems risks producing invalid GC info (e.g., offsets beyond the reported code length). Make the GC header and all reported offsets use the same units.
    unsigned callCnt = 0;

    // First we figure out the encoder ID's for the stack slots and registers.
    gcInfo.gcMakeRegPtrTable(gcInfoEncoder, codeSize, prologSize, GCInfo::MAKE_REG_PTR_MODE_ASSIGN_SLOTS, &callCnt);

Comment thread src/coreclr/jit/unwindwasm.cpp Outdated
Comment on lines +3462 to 3466
}

// Follow the code pattern of the x86 gc info encoder (genCreateAndStoreGCInfoJIT32).
gcInfo.gcInfoBlockHdrSave(gcInfoEncoder, codeSize, prologSize);
gcInfo.gcInfoBlockHdrSave(gcInfoEncoder, maxVirtualIP, /* prologSize */ 0);

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

Actually.. upon reflection, we should report the "size" here, which is maxVirtualIP + 1, I believe. Also the copilot comment is probably on point, but if it doesn't assert I'm ok with fixing that later.

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

And so we should probably start with maxVirtualIP being set to 0, so the extra +1 at the end isn't doing too much.

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

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

The end virtual IP in a region is greater than any virtual IP used, except when there are no funclets, so I think we end up at the same place.

If reporting prolog size 0 is a problem, I can report it as size 1, and make the lowest observable VIP be 2, since the runtime will never see execution in the prolog. Then for method with no EH the "length" would be reported as 3.

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

The prolog size this is only a problem if the GCEncoder gets upset. I don't understand your math here though.

If the prolog is size 1, then the min observable vip would be 1, since the next vip after 0 would be 1, and I would expect the length to be reported as 2. Why would it end up as 3?

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

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

Yeah I suppose that will work... in a method with no EH all call sites report virtual IP of 1, prolog length is 1, and method length is 2.

Comment thread src/coreclr/jit/fgwasm.cpp
Comment thread src/coreclr/jit/unwindwasm.cpp Outdated
uint8_t buffer[15];
size_t index = 0;
assert(func->endVirtualIP >= func->startVirtualIP);
index += GetEmitter()->emitOutputULEB128(buffer, func->funWasmFrameSize);
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

nit: either index = instead of index += or do buffer + index here instead of buffer. as written it looks like a bug even though it's not

Comment thread src/coreclr/jit/unwindwasm.cpp Outdated
Copy link
Copy Markdown
Member

@kg kg left a comment

Choose a reason for hiding this comment

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

LGTM aside from copilot and davidw's concerns

Comment thread src/coreclr/jit/unwindwasm.cpp Outdated
size_t index = 0;
assert(func->endVirtualIP >= func->startVirtualIP);
index += GetEmitter()->emitOutputULEB128(buffer, func->funWasmFrameSize);
index += GetEmitter()->emitOutputULEB128(buffer + index, func->startVirtualIP);
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

There is no reason to store the startVirtualIP. It isn't actually useful for anything. Only the size matters. Notably I would expect that we should be able to compute the startVirtualIP by summing the sizes of the previous unwind data from the previously encoded funclets/main method. (Main method startVirtualIP should be 0).

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

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

Ok, easy enough to leave it out.

Copilot AI review requested due to automatic review settings May 21, 2026 00:55
Copy link
Copy Markdown
Contributor

Copilot AI left a comment

Choose a reason for hiding this comment

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

Pull request overview

Copilot reviewed 4 out of 4 changed files in this pull request and generated 2 comments.

Comment thread src/coreclr/jit/unwindwasm.cpp
Comment thread src/coreclr/jit/unwindwasm.cpp
@AndyAyersMS
Copy link
Copy Markdown
Member Author

Looks like there is some other length field in GC info that needs fixing.

@AndyAyersMS
Copy link
Copy Markdown
Member Author

Looks like there is some other length field in GC info that needs fixing.

This is going to be more complicated than I thought. We need to track the Virtual IPs per instruction group so we can properly describe no-GC regions, and we need to ensure we have appropriate Virtual IP updates for these regions.

Or else we need to ensure we don't create any no-GC regions.

Or else convince ourselves that these regions cannot span calls and so we can just ignore them for GC reporting purposes on Wasm, at least for now, since the only viable GC safepoints are at calls.

@jkotas
Copy link
Copy Markdown
Member

jkotas commented May 21, 2026

convince ourselves that these regions cannot span calls and so we can just ignore them for GC reporting purposes on Wasm, at least for now,

I think this is valid simplifying assumption to make for wasm.

We should be able to omit reporting fully interruptible GC information (including no-GC regions) on any platform that requires explicit GC polls and does not support suspension via execution "redirection".

@AndyAyersMS
Copy link
Copy Markdown
Member Author

I started down the path of disabling fully interruptible GC for Wasm, but there are some missing parts that will require more work. Perhaps we can defer it a bit to unblock consumption of the data we're preoducing here

  • We removed the ability for the JIT to insert general GC polls in Remove code for GC Poll marking and insertion. #42664. We'll need to revive this for Wasm, and think about whether we can tolerate call per iteration for tight (call-free) loops or will want to fix them so we're not polling every iteration.
  • The JIT code indicates that GC reporting for methods with EH requires fully interruptible GC (as execution can stop most anywhere for implicit exceptions). That is not true on Wasm but there are perhaps corresponding changes needed on the runtime side if we relax this. Or maybe it falls out since the funclet and main method virtual IPs will always be at safe points.

@AndyAyersMS
Copy link
Copy Markdown
Member Author

Locally I hit an assert in debug SPC which I thought we had fixed:

Single method repro args:--singlemethodtypename "System.BitConverter" --singlemethodname "SingleToInt32Bits" --singlemethodindex 1
C:\repos\runtime3\src\coreclr\jit\codegenwasm.cpp:1813
Assertion failed 'NYI_WASM: Contained bitcast operands' in 'System.BitConverter:SingleToInt32Bits(float):int' during 'Generate code' (IL size 7; hash 0x810ee8cc; MinOpts)

working around this (and any subsequent NYIs) via

--codegenopt:JitWasmNyiToR2RUnsupported=1

SPC finishes crossgen cleanly.

@jkotas
Copy link
Copy Markdown
Member

jkotas commented May 21, 2026

We'll need to revive this for Wasm, and think about whether we can tolerate call per iteration for tight (call-free) loops or will want to fix them so we're not polling every iteration.

We may be able to get by without explicit gc polls for single threaded wasm, for MVP at least. The explicit gc polls should not be require for single-threaded runtime to work. The gc can be only triggered from the one thread and there are no other threads to suspend for the gc.

The explicit gc polls should be only required for scenarios like managed debugger attach to a process that is stuck inside a long running loop in AOT compiled code. I think it is P2 - I believe that it does not work with Mono either.

@kg
Copy link
Copy Markdown
Member

kg commented May 22, 2026

Locally I hit an assert in debug SPC which I thought we had fixed:

Single method repro args:--singlemethodtypename "System.BitConverter" --singlemethodname "SingleToInt32Bits" --singlemethodindex 1
C:\repos\runtime3\src\coreclr\jit\codegenwasm.cpp:1813
Assertion failed 'NYI_WASM: Contained bitcast operands' in 'System.BitConverter:SingleToInt32Bits(float):int' during 'Generate code' (IL size 7; hash 0x810ee8cc; MinOpts)

working around this (and any subsequent NYIs) via

--codegenopt:JitWasmNyiToR2RUnsupported=1

SPC finishes crossgen cleanly.

I'll look into this bitcast issue.

Copy link
Copy Markdown
Contributor

@adamperlin adamperlin left a comment

Choose a reason for hiding this comment

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

This LGTM from what I understand. I don't have enough context to comment on the GC-related discussion here.

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

Labels

area-CodeGen-coreclr CLR JIT compiler in src/coreclr/src/jit and related components such as SuperPMI

Projects

None yet

Development

Successfully merging this pull request may close these issues.

6 participants