[wasm][R2R] Implement JIT_PollGC rare path - #131653
Conversation
The wasm JIT_PollGC helper was a PORTABILITY_ASSERT stub, so any GC poll emitted by crossgen2 into R2R code (e.g. a CoreLib class constructor at startup) aborted the runtime. Implement it to match the other architectures, whose asm stub tail-calls the managed Thread.PollGC. That helper's only effect is a p/invoke round-trip through the empty ThreadNative_PollGC QCall: the GC transition parks the thread at a safe point and the pinvoke rare path performs the suspension. The fast path returns immediately when no GC/ThreadAbort/suspend is pending; the rare path reuses the inline-pinvoke helpers (JIT_PInvokeBeginImpl seeds an InlinedCallFrame from the R2R caller's stack pointer and goes preemptive so a pending GC can walk the R2R frames, then JIT_PInvokeEndRarePath performs RareDisablePreemptiveGC, services any ThreadAbort, and pops the frame).
|
Azure Pipelines: Successfully started running 3 pipeline(s). 13 pipeline(s) were filtered out due to trigger conditions. There may be pipelines that require an authorized user to comment /azp run to run. |
|
Tagging subscribers to this area: @agocke |
There was a problem hiding this comment.
Pull request overview
Implements the WebAssembly VM JIT_PollGC helper (used at GC-safe poll points) so it no longer aborts at runtime, by adding a fast-path check on g_TrapReturningThreads and a rare-path transition sequence intended to allow GC/thread-suspend to proceed safely.
Changes:
- Replace the wasm
JIT_PollGCPORTABILITY_ASSERTstub with a functional implementation. - Add a fast path that returns immediately when no trap/suspend is pending.
- Add a rare path that sets up an
InlinedCallFrameand performs the GC-mode transition/suspension handshake.
Co-authored-by: Copilot Autofix powered by AI <175728472+Copilot@users.noreply.github.com>
There was a problem hiding this comment.
Pull request overview
Copilot reviewed 1 out of 1 changed files in this pull request and generated no new comments.
Suppressed comments (1)
src/coreclr/vm/wasm/helpers.cpp:808
- JIT_PollGC currently takes the inline-pinvoke rare path whenever g_TrapReturningThreads != 0, but it doesn’t replicate Thread.PollGC’s additional guard (CatchAtSafePoint). On other architectures the rare path tail-calls Thread.PollGC, which returns without doing the transition when the current thread isn’t actually in a catch-at-safe-point state. Without this check, wasm may execute JIT_PInvokeEndRarePath (and potentially block/suspend) in cases where other architectures would do nothing.
// Fast path: nothing to do unless a GC (or ThreadAbort / debugger suspend) is pending.
if (!g_TrapReturningThreads)
return;
// Rare path. On other architectures JIT_PollGC's asm stub tail-calls the managed Thread.PollGC,
| alignas(InlinedCallFrame) uint8_t inlinedCallFrameStorage[sizeof(InlinedCallFrame)]; | ||
| InlinedCallFrame* pInlinedCallFrame = reinterpret_cast<InlinedCallFrame*>(inlinedCallFrameStorage); | ||
| JIT_PInvokeBeginImpl(reinterpret_cast<void*>(callersStackPointer), pInlinedCallFrame); | ||
| JIT_PInvokeEnd(reinterpret_cast<void*>(callersStackPointer), pInlinedCallFrame, portableEntryPointContext); |
There was a problem hiding this comment.
Can we do the following to avoid the reinterpret_cast and stay away from lifetime-related UB?
| alignas(InlinedCallFrame) uint8_t inlinedCallFrameStorage[sizeof(InlinedCallFrame)]; | |
| InlinedCallFrame* pInlinedCallFrame = reinterpret_cast<InlinedCallFrame*>(inlinedCallFrameStorage); | |
| JIT_PInvokeBeginImpl(reinterpret_cast<void*>(callersStackPointer), pInlinedCallFrame); | |
| JIT_PInvokeEnd(reinterpret_cast<void*>(callersStackPointer), pInlinedCallFrame, portableEntryPointContext); | |
| InlinedCallFrame inlinedCallFrame; | |
| JIT_PInvokeBeginImpl(reinterpret_cast<void*>(callersStackPointer), &inlinedCallFrame); | |
| JIT_PInvokeEnd(reinterpret_cast<void*>(callersStackPointer), &inlinedCallFrame, portableEntryPointContext); |
| alignas(InlinedCallFrame) uint8_t inlinedCallFrameStorage[sizeof(InlinedCallFrame)]; | ||
| InlinedCallFrame* pInlinedCallFrame = reinterpret_cast<InlinedCallFrame*>(inlinedCallFrameStorage); | ||
| JIT_PInvokeBeginImpl(reinterpret_cast<void*>(callersStackPointer), pInlinedCallFrame); | ||
| JIT_PInvokeEnd(reinterpret_cast<void*>(callersStackPointer), pInlinedCallFrame, portableEntryPointContext); |
There was a problem hiding this comment.
Is the debug-only validation of stack pointer that's in JIT_PInvokeEnd going to work? I think the stack pointers are going to get messed up.
You may want to do a one-off test run without the g_TrapReturningThreads fast path to get proper coverage of the slow path.
There was a problem hiding this comment.
I do not think we need to go out of our way to try to reuse a few lines of code. A bit of duplication - that makes it easy to see what' going on - is fine.
| { | ||
| PORTABILITY_ASSERT("JIT_PollGC is not implemented on wasm"); | ||
| // Fast path: nothing to do unless a GC (or ThreadAbort / debugger suspend) is pending. | ||
| if (!g_TrapReturningThreads) |
There was a problem hiding this comment.
If we wanted to replicate the perf characteristics of other platforms, the fast path check would be done in inline wasm before saving the stackpointers.
Summary
Implements the WebAssembly
JIT_PollGChelper, which was aPORTABILITY_ASSERTstub.Problem
JIT_PollGC(CORINFO_HELP_POLL_GC) is emitted by RyuJIT/crossgen2 at GC-safe poll points (loop back-edges, call sites). On wasm it was unimplemented:Once CoreLib itself is R2R-compiled for
browser-wasm, its class constructors' GC polls call this helper during startup and abort the runtime (observed atSystem.AppContext.Setup→ a CoreLib cctor).Fix
Implement the helper to match the other architectures, whose
JIT_PollGCasm stub tail-calls the managedThread.PollGC. That managed helper's only effect is a p/invoke round-trip through the emptyThreadNative_PollGCQCall — the GC transition parks the thread at a safe point and the pinvoke rare path performs the actual suspension.This change reproduces that round-trip directly with the existing inline-pinvoke helpers:
g_TrapReturningThreadsis set (no GC /ThreadAbort/ debugger suspend pending) — the common case.JIT_PInvokeBeginImplpushes anInlinedCallFrameseeded from the R2R caller's stack pointer and switches to preemptive mode, so a pending GC can suspend and walk this thread's R2R frames at the safe point. The end sequence mirrorsJIT_PInvokeEnd: return to cooperative mode and, when a GC/abort is still pending,JIT_PInvokeEndRarePathperforms the actual suspension (RareDisablePreemptiveGC), services anyThreadAbort, and pops the frame.Notes / testing
clr.runtime,browser-wasm, Debug).g_TrapReturningThreadsset, which is hard to trigger deterministically on single-threaded wasm; it has not been exercised at runtime. A GC-stress run (or forcingg_TrapReturningThreads) would be worthwhile before relying on it.Note
This change and description were prepared with the assistance of GitHub Copilot.