[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,
I did, but on ST system, this doesn't do anything. There is no GC thread to kick in. Should we rather leave the rare path implementation empty until then ? |
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:816
JIT_PollGCRarePathconstructs anInlinedCallFrameas a normal C++ local and then callsJIT_PInvokeBeginImpl, which placement-news anInlinedCallFrameinto that same storage. IfInlinedCallFramehas a non-trivial constructor (e.g., underFEATURE_INTERPRETER), this results in starting the object lifetime twice and is undefined behavior. Also, the wasm implementation currently always performs the pinvoke transition wheneverg_TrapReturningThreads != 0, but the canonical path on other architectures tail-callsThread.PollGC, which first gates the transition onCatchAtSafePoint()(seeThread.CoreCLR.cs). Adding theCatchAtSafePoint()gate here keeps semantics aligned and avoids unnecessary rare-path work for cases where the global is non-zero but the thread shouldn't trap.
EXTERN_C void JIT_PollGCRarePath(void* sp)
{
InlinedCallFrame inlinedCallFrame;
JIT_PInvokeBeginImpl(sp, &inlinedCallFrame);
| "i32.load %[g_TrapReturningThreads]\n" | ||
| "if\n" | ||
| " global.get __stack_pointer\n" | ||
| " local.set 1\n" /* save previous __stack_pointer into the unused pep local */ |
There was a problem hiding this comment.
The FCall infrastructure stores the __stack_pointer in the callersStackPointer local.
runtime/src/coreclr/vm/fcall.h
Lines 347 to 359 in 367f831
Is there an advantage in doing one or the other way - does the wasm JIT generate better code for one vs. the other?
There was a problem hiding this comment.
local.0 and local.1 are both identical scratch variables.
I don't know if the V8 JIT can re-order the if g_TrapReturningThreads before global.get __stack_pointer if we use WASM_WRAPPER_FUNC_INITIAL. I estimate that it can. Is that what you are asking ?
I slightly prefer the hand rolled asm block over the WASM_WRAPPER_FUNC_INITIAL reuse.
This will be used by managed debugger break-in. The debugger break-in is going to set the flag that triggers the rare path that will end up in calling into the debugger to report that we have stopped. |
Co-authored-by: Jan Kotas <jkotas@microsoft.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:816
JIT_PollGCRarePathduplicates theJIT_PInvokeEndlogic (disable preemptive GC, checkg_TrapReturningThreads, then eitherJIT_PInvokeEndRarePath()or pop the frame). This creates a divergence risk if the P/Invoke end sequence changes (debug asserts, additional bookkeeping, etc.). Consider delegating toJIT_PInvokeEndafterJIT_PInvokeBeginImplinstead of re-implementing the end sequence here.
Thread* pThread = (Thread*)inlinedCallFrame.m_pThread;
pThread->m_fPreemptiveGCDisabled.StoreWithoutBarrier(1);
if (g_TrapReturningThreads)
{
JIT_PInvokeEndRarePath();

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.