JIT: Support general inlining in runtime async - #131538
Conversation
We will need this to enable inlining.
Invariant nodes and LCL_VARs do not need to be lifted across async calls.
Consolidate the duplicated non-null assert in FinishContextHandlingAndSuspensionWithHelper into a single assert covering all three well-known args. Update the stale comment in InsertFinishContextHandlingCall that still described the resumed placeholder as being replaced by a 'continuation != null' computation; it is now replaced by the already-computed resumed value. No functional change. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> Copilot-Session: d86cad55-abc0-42d6-8369-bc6bc619326f
Value numbering can fold the AsyncResumedUse argument to a constant when the resumed indicator is provably zero at the call, for example when two awaits sit on mutually exclusive paths. IsReusableSuspension only accepted locals, so it bailed out with 'argument is too complex' and gave up on merging the suspension points. That produced a duplicated CORINFO_HELP_ALLOC_CONTINUATION sequence, an extra resumption block and an extra readonly data slot. In smoke_tests.nativeaot this showed up as +59 bytes (17.15%) on Generics+TestAsyncGVMScenarios:AsyncGvm1[System.__Canon] and +57 bytes (16.52%) on RunAsync. Accept invariant nodes as well, matching the predicate already used for invariantResumed in CreateResumptionsAndSuspensions. Invariant nodes are constants, LCL_ADDR or FTN_ADDR, so comparing them and removing them from the call is safe. Both methods now improve by 15 bytes relative to the base instead of regressing. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> Copilot-Session: d86cad55-abc0-42d6-8369-bc6bc619326f
…ning # Conflicts: # src/coreclr/inc/jiteeversionguid.h # src/coreclr/jit/async.cpp # src/coreclr/jit/async.h # src/coreclr/tools/superpmi/superpmi-shared/methodcontext.h # src/tests/async/pgo/pgo.csproj
StoreAsyncAwaiter removed the awaiter node from the call block after it had already been spliced into the suspension block's LIR range, corrupting the range and tripping 'prev == m_lastNode'. Remove the node from the call block before building the store instead, and remove the FIELD_LIST node itself in the multi-field case. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> Copilot-Session: 9ed3e552-b907-4815-9683-56a7fed2cdf3
Folds in dotnet#127800. Renames GT_CONTINUATION_FIELD_OFFSET to GT_CONTINUATION_MEMBER_OFFSET to match the node actually implemented. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> Copilot-Session: 9ed3e552-b907-4815-9683-56a7fed2cdf3
…ontext to the JIT General runtime async inlining needs to check, when an inlined callee logically returns to its caller after having been resumed, whether it is already running in the continuation context the caller's continuation captured, and to restore that continuation's ExecutionContext. IsOnRightContext mirrors the 'can inline' conditions in RuntimeAsyncTaskContinuation.QueueIfNecessary; the two must be kept in sync. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> Copilot-Session: 9ed3e552-b907-4815-9683-56a7fed2cdf3
Suspends and resumes in a specified continuation context, rather than one captured at the suspension point. General runtime async inlining uses this when an inlined callee logically returns to its caller after having been resumed and IsOnRightContext reports the current context does not match the one the caller's continuation captured. It suspends on an already completed task so that the dispatcher re-dispatches immediately. That dispatch runs with canInline: false, so it always posts or schedules onto the requested context instead of running inline. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> Copilot-Session: 9ed3e552-b907-4815-9683-56a7fed2cdf3
Extends ContinuationMember with slots for the ExecutionContext, continuation context and continuation flags that an inlined async frame captures when it logically returns to its caller. These are keyed by inline depth rather than by individual inline frame. Two frames at the same depth can share storage because their live ranges cannot overlap: the value is written at a frame's first suspension and consumed by that same frame's post-inline IR, and one frame at a given depth must be left before another can be entered. This bounds the extra continuation members by the maximum inline depth instead of by the number of inlined async frames. No functional change yet; nothing requests these members. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> Copilot-Session: 9ed3e552-b907-4815-9683-56a7fed2cdf3
…exts Adds the JitAsyncInlining config knob, off by default, which lifts the CALLEE_AWAIT restriction and allows inlining async callees that may suspend. When an await in an inlinee may suspend it no longer inherits context handling from the inlining call. Instead it is treated exactly like an await in a non-inlined method and picks up the inlinee's own resumed/ExecutionContext/ SynchronizationContext locals, which the inlinee's own SaveAsyncContexts phase already creates. The cheap paths (async versions of synchronous methods and tail awaits) keep inheriting from the caller and are unchanged. With the knob off behavior is unchanged: the restructured gate reports the same CALLEE_AWAIT observation, and inlinees do not get their own context args. The knob is not yet functional when enabled; the post-inline IR and the nested capture chain at suspensions are still missing. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> Copilot-Session: 9ed3e552-b907-4815-9683-56a7fed2cdf3
…al async inlining SaveAsyncContexts now records whether the body contains any async call. For an inlinee without one the resumed indicator is provably always false, so the post-inline async frame IR can be skipped entirely, keeping the cheap shape for what is by far the common case. Also bails out of general async inlining in OSR methods. An OSR method is entered with the tier0 method's continuation, whose layout differs from the one the OSR method computes, so an inlined frame's members cannot be read from it. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> Copilot-Session: 9ed3e552-b907-4815-9683-56a7fed2cdf3
…Context RestoreExecutionContext takes the Thread whose contexts were captured on method entry. The restore an inlined async frame needs runs only after a resumption, so it must target the thread we were resumed on instead. Adds RestoreInlinedFrameExecutionContext, which reads the current thread itself, and points the JIT-EE handle at it. This also keeps the JIT side to a single argument. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> Copilot-Session: 9ed3e552-b907-4815-9683-56a7fed2cdf3
SaveAsyncContexts runs for inlinees too, and JIT_FLAG_OSR is not cleared for an inlinee, so opts.IsOSR() is true while importing an inlinee of an OSR method. The OSR-specific handling was therefore applied to inlinees, where all three parts of it are wrong: - the resumed indicator was initialized from the continuation argument, but an inlinee has not resumed just because the OSR method was entered via a resumption - CaptureContexts was skipped, leaving the inlinee's context locals unassigned for its own RestoreContexts - the inlinee's fresh temps were marked lvIsOSRLocal, though they have no home in the tier0 frame An inlinee inside an OSR method starts a fresh logical frame, so it needs the same treatment as in a non-OSR method. With this, general async inlining works in OSR methods: an inlinee's resumed indicator can only become true at a resumption point belonging to that inlinee's own async calls, which are resumption points of the OSR method's continuation layout, so the continuation is never the tier0 one where it is read. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> Copilot-Session: 9ed3e552-b907-4815-9683-56a7fed2cdf3
…y region Suspending inside a protected region additionally requires catching the exception and rethrowing it around the post-inline handling, per the design document. That is not implemented, so bail out for now. Inlinees with EH are only inlined when compInlineMethodsWithEH is enabled, so this is reachable but not the common case. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> Copilot-Session: 9ed3e552-b907-4815-9683-56a7fed2cdf3
…caller
When an inlined async callee is resumed inside its own body, the async
infrastructure only restores the context of the suspension point itself. The
work it would otherwise have done when the callee's frame returned to its caller
now has to be performed by the JIT:
if (resumed_F)
{
RestoreInlinedFrameExecutionContext(continuation.ExecutionContextFor<caller>);
if (!IsOnRightContext(continuation.ContinuationContextFor<caller>,
continuation.FlagsFor<caller>))
{
await SwitchContext(...);
}
resumed_caller = true;
}
This is emitted as a real control flow diamond at the join point all of the
inlinee's returns converge on, alongside the other post-inline IR. IsOnRightContext
stays visible in the IR and is an inline candidate, so it needs to be a statement
root with its value consumed through a GT_RET_EXPR.
Two bugs this exposed are fixed here as well:
- Continuation members were registered on whichever Compiler instance was
importing, so an awaiter inside an inlinee got an index into the inlinee's
vector while its offset node was resolved against the root's layout. Members
describe the root method's continuation, so they are now always registered
there.
- GT_CONTINUATION_MEMBER_OFFSET was always bashed to a TYP_INT constant. Offsets
used in address arithmetic are TYP_I_IMPL, so the node's type is now preserved.
Also records each inlined frame's async locals on its InlineContext, since the
inlinee's Compiler is discarded once it has been spliced in.
The nested capture chain at suspensions is still missing, so enabling
JitAsyncInlining is not yet correct.
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Copilot-Session: 9ed3e552-b907-4815-9683-56a7fed2cdf3
When an async call inside an inlined frame suspends, every enclosing frame that has not yet resumed needs its contexts captured into the continuation and restored onto the thread, as if its physical frame had returned. The IR for that is created by the async transformation, long after the optimizer has run, so those values have to be kept live and GC reported at the call until then. That is exactly what the existing per-frame context args do, so the enclosing frames just add more of them: one set of AsyncResumedUse/AsyncExecutionContext/ AsyncSynchronizationContext per frame, appended outward when the inlinee is spliced in. These are pseudo-args, so duplicates take no registers and are expanded out later. The innermost frame stays first. Only uses are needed for the enclosing frames. AsyncResumedDef stays innermost-only, since the caller's indicator is set by the post-inline IR rather than by a resumption point. Adjusts the two places that assumed these args were unique: IsReusableSuspension now requires all of them to agree, and HandleReusedSuspension removes all of them. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> Copilot-Session: 9ed3e552-b907-4815-9683-56a7fed2cdf3
Completes general runtime async inlining. When an async call inside an inlined frame suspends, every enclosing frame that has not yet resumed must capture the contexts it would hand to its caller and restore its caller's contexts onto the thread, as if its physical frame had returned. A frame having resumed implies its caller has too, so the frames can be walked outward as a straight line rather than as the nested conditionals of the design document: once one frame has resumed, the helpers for it and every frame outside it no-op. Each suspension jumps to a tail block that does this, shared by all suspensions in the same frame. Adds AsyncHelpers.CaptureInlinedFrameTransition for the capture, which no-ops when the frame has already resumed. The chain of frames is recorded on AsyncCallInfo rather than being recovered from the call's context args, since optimizations may rewrite the arg nodes; the args remain purely to keep the values live and GC reported until here. Also fixes impInheritAsyncContextsFromInliner, which propagated only the innermost frame's contexts to an inlined callee's async calls. An async call carrying a frame chain can itself be an inline candidate, and dropping the rest of the chain silently lost the handling for every frame outside the immediate one. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> Copilot-Session: 9ed3e552-b907-4815-9683-56a7fed2cdf3
The worked examples from the design document. Inlining removes a call site and a callee, and with them the context handling the async infrastructure would otherwise perform at each frame boundary, so these pin down the observable results: each frame resumes on the synchronization context its own continuation captured, and sees the AsyncLocal values from the ExecutionContext it captured. The callees are marked AggressiveInlining because the profitability heuristic otherwise rejects them, and the test runs with tiering disabled, since at tier 0 nothing is inlined and the tests would not exercise the feature at all. Verified to exercise a three frame chain, and to fail if the frame transition IR is removed. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> Copilot-Session: 9ed3e552-b907-4815-9683-56a7fed2cdf3
Adds a test covering runtime async inlining in methods rejitted via OSR, including resuming inside an inlined frame there. Verified to exercise an inlined frame with a suspension tail inside an OSR variant. Also corrects the change from 'Apply OSR async context handling to the root frame only'. That commit claimed opts.IsOSR() was true while importing an inlinee, so the OSR-specific handling was being wrongly applied to inlinees. It is not: compInitOptions clears JIT_FLAG_OSR for inlinees (compiler.cpp), so the added condition was redundant and the bugs it described did not exist. The condition is removed again and replaced with a comment recording why the handling only ever applies to the root frame. Removing the earlier bail on inlining into OSR methods was still correct, which the new test demonstrates. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> Copilot-Session: 9ed3e552-b907-4815-9683-56a7fed2cdf3
It is a GenTreeVal like GT_RECORD_ASYNC_RESUME and GT_ASYNC_RESUME_INFO, but was missing from gtCloneExpr, so cloning one hit 'Cloning of node not supported'. Reachable whenever such a node ends up somewhere that gets cloned, for example a loop body that is unrolled. Found by Fuzzlyn. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> Copilot-Session: 9ed3e552-b907-4815-9683-56a7fed2cdf3
# Conflicts: # src/coreclr/inc/jiteeversionguid.h
|
Azure Pipelines: Successfully started running 6 pipeline(s). 10 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: @JulieLeeMSFT, @jakobbotsch |
There was a problem hiding this comment.
Pull request overview
This PR extends CoreCLR “runtime async” support to enable general inlining of async callees (including those that may suspend), by plumbing additional async frame/context state through the JIT, adding new runtime helper entry points, and introducing tests/docs that pin down observable context behavior under inlining (including OSR).
Changes:
- Adds new runtime helpers / handles and a new JIT↔EE API (
getAwaitAwaiterInContinuationCall) to support awaiter handling via continuation storage and to support post-inline context fixes (restore EC + optional context switch). - Extends JIT async infrastructure (new
GT_CONTINUATION_MEMBER_OFFSET, additional well-known args, inline context async-frame metadata, and general-inlining logic + config gateJitAsyncInlining). - Adds new async-focused tests and updates many async test projects to use
Microsoft.NET.Sdkrather thanMicrosoft.NET.Sdk.IL, plus a design doc for runtime async inlining.
Reviewed changes
Copilot reviewed 91 out of 91 changed files in this pull request and generated 1 comment.
Show a summary per file
| File | Description |
|---|---|
| src/tests/JIT/Regression/JitBlue/GitHub_130437/GitHub_130437.csproj | Switch test project SDK while retaining IL language targets. |
| src/tests/async/without-yields/without-yields.csproj | Switch test project SDK to Microsoft.NET.Sdk. |
| src/tests/async/with-yields/with-yields.csproj | Switch test project SDK to Microsoft.NET.Sdk. |
| src/tests/async/void/void.csproj | Switch test project SDK to Microsoft.NET.Sdk. |
| src/tests/async/varying-yields/varying-yields.csproj | Switch test project SDK to Microsoft.NET.Sdk. |
| src/tests/async/varying-yields/varying-yields-valuetask.csproj | Switch test project SDK to Microsoft.NET.Sdk. |
| src/tests/async/varying-yields/varying-yields-task.csproj | Switch test project SDK to Microsoft.NET.Sdk. |
| src/tests/async/valuetask/valuetask.csproj | Switch test project SDK to Microsoft.NET.Sdk. |
| src/tests/async/valuetask-source/valuetask-source.csproj | Switch test project SDK to Microsoft.NET.Sdk. |
| src/tests/async/valuetask-source/valuetask-source-stub.csproj | Switch test project SDK to Microsoft.NET.Sdk. |
| src/tests/async/synchronization-context/synchronization-context.csproj | Switch test project SDK to Microsoft.NET.Sdk. |
| src/tests/async/struct/struct.csproj | Switch test project SDK to Microsoft.NET.Sdk. |
| src/tests/async/struct/struct.cs | Removes a stray blank line in the test source. |
| src/tests/async/staticvirtual/staticvirtual.csproj | Switch test project SDK to Microsoft.NET.Sdk. |
| src/tests/async/shared-generic/shared-generic.csproj | Switch test project SDK to Microsoft.NET.Sdk. |
| src/tests/async/returns/returns.csproj | Switch test project SDK to Microsoft.NET.Sdk. |
| src/tests/async/regression/managed-thread-id.csproj | Switch test project SDK to Microsoft.NET.Sdk. |
| src/tests/async/regression/128566.csproj | Switch test project SDK to Microsoft.NET.Sdk. |
| src/tests/async/regression/125805.csproj | Switch test project SDK to Microsoft.NET.Sdk. |
| src/tests/async/regression/125615.csproj | Switch test project SDK to Microsoft.NET.Sdk. |
| src/tests/async/regression/125042.csproj | Switch test project SDK to Microsoft.NET.Sdk. |
| src/tests/async/pinvoke/pinvoke.csproj | Switch test project SDK to Microsoft.NET.Sdk. |
| src/tests/async/pgo/pgo.csproj | Switch test project SDK to Microsoft.NET.Sdk (plus formatting tweak). |
| src/tests/async/override/override.csproj | Switch test project SDK to Microsoft.NET.Sdk. |
| src/tests/async/osr-inlined-contexts/osr-inlined-contexts.csproj | New OSR-focused test project with env-var configuration to force OSR. |
| src/tests/async/osr-inlined-contexts/osr-inlined-contexts.cs | New OSR/inlining coverage validating context correctness across resumes. |
| src/tests/async/objects-captured/objects-captured.csproj | Switch test project SDK to Microsoft.NET.Sdk. |
| src/tests/async/object/object.csproj | Switch test project SDK to Microsoft.NET.Sdk. |
| src/tests/async/mincallcost-microbench/mincallcost-microbench.csproj | Switch test project SDK to Microsoft.NET.Sdk. |
| src/tests/async/inst-unbox-thunks/inst-unbox-thunks.csproj | Switch test project SDK to Microsoft.NET.Sdk. |
| src/tests/async/inlined-frame-contexts/inlined-frame-contexts.csproj | New test project to validate inlined-frame context semantics (tiering disabled). |
| src/tests/async/inlined-frame-contexts/inlined-frame-contexts.cs | New test cases covering SyncContext/ExecutionContext restoration under inlining. |
| src/tests/async/implement/implement.csproj | Switch test project SDK to Microsoft.NET.Sdk. |
| src/tests/async/gc-roots-scan/gc-roots-scan.csproj | Switch test project SDK to Microsoft.NET.Sdk. |
| src/tests/async/execution-context/execution-context.csproj | Switch test project SDK to Microsoft.NET.Sdk. |
| src/tests/async/eh-microbench/eh-microbench.csproj | Switch test project SDK to Microsoft.NET.Sdk. |
| src/tests/async/custom-struct-awaiters/custom-struct-awaiters.csproj | New test project validating custom struct awaiter handling. |
| src/tests/async/custom-struct-awaiters/custom-struct-awaiters.cs | New test exercising safe/unsafe struct awaiters without boxing. |
| src/tests/async/collectible-alc/collectible-alc.csproj | Switch test project SDK to Microsoft.NET.Sdk. |
| src/tests/async/cancellation/cancellation.csproj | Switch test project SDK to Microsoft.NET.Sdk. |
| src/tests/async/byref-param/byref-param.csproj | Switch test project SDK to Microsoft.NET.Sdk. |
| src/tests/async/awaitingnotasync/awaitingnotasync.csproj | Switch test project SDK to Microsoft.NET.Sdk. |
| src/tests/async/awaitingnoasyncgeneric/awaitingnoasyncgeneric.csproj | Switch test project SDK to Microsoft.NET.Sdk. |
| src/libraries/System.Private.CoreLib/src/System/Runtime/CompilerServices/AsyncHelpers.cs | Adds awaiter-in-continuation helper paths used by the JIT for struct awaiters. |
| src/coreclr/vm/jitinterface.h | Adds EE-side helper for awaiter-in-continuation runtime lookup computation. |
| src/coreclr/vm/jitinterface.cpp | Implements getAwaitAwaiterInContinuationCall and its runtime lookup support. |
| src/coreclr/vm/corelib.h | Adds CoreLib binder entries for new AsyncHelpers methods. |
| src/coreclr/tools/superpmi/superpmi/icorjitinfo.cpp | Plumbs new ICorJitInfo entrypoint through SuperPMI interface. |
| src/coreclr/tools/superpmi/superpmi-shim-simple/icorjitinfo_generated.cpp | Adds shim forwarding for new API. |
| src/coreclr/tools/superpmi/superpmi-shim-counter/icorjitinfo_generated.cpp | Adds shim counting + forwarding for new API. |
| src/coreclr/tools/superpmi/superpmi-shim-collector/icorjitinfo.cpp | Adds collector recording for new API. |
| src/coreclr/tools/superpmi/superpmi-shared/methodcontext.h | Adds recording/replay surface + packet id for new API. |
| src/coreclr/tools/superpmi/superpmi-shared/methodcontext.cpp | Implements record/dump/replay for new API call. |
| src/coreclr/tools/superpmi/superpmi-shared/lwmlist.h | Adds LWM entry for new API. |
| src/coreclr/tools/superpmi/superpmi-shared/agnostic.h | Adds agnostic key struct + async info extensions for SuperPMI. |
| src/coreclr/tools/Common/JitInterface/ThunkGenerator/ThunkInput.txt | Adds new ICorJitInfo method to thunk generator input. |
| src/coreclr/tools/Common/JitInterface/CorInfoTypes.cs | Extends CORINFO_ASYNC_INFO with new helper method handles. |
| src/coreclr/tools/Common/JitInterface/CorInfoImpl.cs | Implements new API + async helper handle population in managed JIT interface. |
| src/coreclr/tools/Common/JitInterface/CorInfoImpl_generated.cs | Adds unmanaged callback plumbing for new API. |
| src/coreclr/tools/aot/jitinterface/jitinterface_generated.h | Adds new callback + wrapper implementation for AOT JIT interface. |
| src/coreclr/tools/aot/ILCompiler.ReadyToRun/Compiler/ReadyToRunCodegenCompilation.cs | Ensures new helper methods are referenced for R2R scenarios. |
| src/coreclr/System.Private.CoreLib/src/System/Runtime/CompilerServices/AsyncHelpers.CoreCLR.cs | Adds runtime support for inlined-frame context restore/switch + awaiter continuation indirection. |
| src/coreclr/jit/wellknownargs.h | Adds new well-known pseudo-args for async awaiter + resumed tracking. |
| src/coreclr/jit/valuenum.cpp | Treats GT_CONTINUATION_MEMBER_OFFSET as unknown for VN purposes. |
| src/coreclr/jit/promotionliveness.cpp | Generalizes LCL_ADDR definition size discovery via call local-def enumeration. |
| src/coreclr/jit/morph.cpp | Extends struct-arg morphing to handle pseudo args like AsyncAwaiter. |
| src/coreclr/jit/liveness.cpp | Generalizes tracked call-definition detection beyond retbuf-only case. |
| src/coreclr/jit/lclvars.cpp | Minor cleanup: uses INDEBUG wrapper for debug-only state. |
| src/coreclr/jit/lclmorph.cpp | Generalizes call-local-definition handling (retbuf + async resumed def). |
| src/coreclr/jit/jitconfigvalues.h | Replaces continuation reuse config with JitAsyncInlining gate. |
| src/coreclr/jit/inline.h | Records per-inlinee async frame locals + computes async frame depth. |
| src/coreclr/jit/inline.def | Adds new inline observation for await inside try region. |
| src/coreclr/jit/importercalls.cpp | Adds awaiter optimization to read struct awaiters from continuation; adds general async inlining rules and frame-context inheritance extensions. |
| src/coreclr/jit/importer.cpp | Adjusts ordering to mark await call async before inheriting inliner context args. |
| src/coreclr/jit/ICorJitInfo_wrapper_generated.hpp | Adds wrapper implementation for new ICorJitInfo API. |
| src/coreclr/jit/ICorJitInfo_names_generated.h | Adds API name entry for new ICorJitInfo method. |
| src/coreclr/jit/gtstructs.h | Adds GT_CONTINUATION_MEMBER_OFFSET node to GenTreeVal union set. |
| src/coreclr/jit/gtlist.h | Defines new IR node kind GT_CONTINUATION_MEMBER_OFFSET. |
| src/coreclr/jit/gentree.h | Adds async inlined-frame metadata to AsyncCallInfo + non-const accessor. |
| src/coreclr/jit/gentree.cpp | Adds debug display / clone / iterator support and call-defined async-resumed lcl-addr detection. |
| src/coreclr/jit/fginline.cpp | Adds async inlined-frame post-inline IR construction and propagation of enclosing frame args. |
| src/coreclr/jit/fgdiagnostic.cpp | Updates debug linked-local validation to account for multiple call-defined locals. |
| src/coreclr/jit/compiler.hpp | Extends call local-def enumeration to include async resumed def (plus retbuf). |
| src/coreclr/jit/compiler.h | Adds state for resumed indicator / may-suspend tracking, continuation member indexing, and general async inlining config gate. |
| src/coreclr/jit/compiler.cpp | Updates OSR-local filtering to include resumed indicator. |
| src/coreclr/jit/async.h | Extends async continuation layout support for multiple “continuation member” kinds and inlined frame tails. |
| src/coreclr/inc/jiteeversionguid.h | Updates JIT-EE version GUID for interface change. |
| src/coreclr/inc/icorjitinfoimpl_generated.h | Adds new method to EE’s ICorJitInfo impl declaration. |
| src/coreclr/inc/corinfo.h | Extends CORINFO_ASYNC_INFO and adds ICorStaticInfo method getAwaitAwaiterInContinuationCall. |
| docs/design/coreclr/jit/runtime-async-inlining.md | New design doc describing general runtime async inlining semantics and pitfalls. |
| public override void Post(SendOrPostCallback d, object? state) | ||
| { | ||
| SynchronizationContext currentCtx = Current; | ||
| SetSynchronizationContext(this); | ||
| try | ||
| { | ||
| d(state); | ||
| } | ||
| finally | ||
| { | ||
| SetSynchronizationContext(currentCtx); | ||
| } | ||
| } |
| // TODO-Async: when this call suspends, the frames enclosing the caller need their | ||
| // context handling run too. Those come from the enclosing frame chain, which is added | ||
| // to async calls as additional pseudo-args. | ||
| call->gtArgs.PushFront(this, NewCallArg::Primitive(gtNewLclVarNode(syncCtx, TYP_REF)) | ||
| .WellKnown(WellKnownArg::AsyncSynchronizationContext)); | ||
| call->gtArgs.PushFront(this, NewCallArg::Primitive(gtNewLclVarNode(execCtx, TYP_REF)) | ||
| .WellKnown(WellKnownArg::AsyncExecutionContext)); | ||
| call->gtArgs.PushFront(this, NewCallArg::Primitive(gtNewLclVarNode(resumed, TYP_INT)) | ||
| .WellKnown(WellKnownArg::AsyncResumedUse)); | ||
| } |
# Conflicts: # src/coreclr/jit/async.cpp # src/coreclr/jit/async.h # src/coreclr/jit/compiler.h # src/coreclr/jit/importercalls.cpp
The context pseudo-args on an async call are the source of truth for what the suspension stores. The whole optimizer runs between adding them and the async transformation, so those nodes can be rewritten: 'resumed' indicators in particular get const folded, and contexts can be copy propagated to other locals. Reconstructing the values from the locals recorded at inline time ignored all of that and stored whatever the original locals happened to hold. Take the actual nodes off the call instead, spilling to a temp only when a node is neither invariant nor a local, exactly as the existing shared finish-context handling does when it needs to reference an arg from another block. The nodes also become the identity used for sharing a tail, so suspensions share one only when they agree on every value. That is now observable: with two awaits in the same inlined frame, the first has its resumed indicator folded to the constant zero while the second still reads the local, and they correctly get separate tails. Sharing them would have stored the wrong value at one of them. This removes the need for the side table, so AsyncCallInfo::InlineFrameLocals and the AsyncFrameLocals type go away. InlineContext keeps its frame locals, which are still the right source when creating the args during inlining. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> Copilot-Session: 9ed3e552-b907-4815-9683-56a7fed2cdf3
Each suspension now gets its own tail. Sharing required the suspensions to agree on every value the tail stores, which in practice they do not: the first await in a frame typically has its resumed indicator folded to a constant while later ones still read the local, so the tails were never actually shared. Left as a TODO; it is an optimization that saves cold code and can come back separately. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> Copilot-Session: 9ed3e552-b907-4815-9683-56a7fed2cdf3
There was a problem hiding this comment.
Pull request overview
Copilot reviewed 84 out of 84 changed files in this pull request and generated no new comments.
Comments suppressed due to low confidence (1)
src/coreclr/jit/fginline.cpp:2853
SwitchContextis an async helper that always suspends. In the resumption path,resumedCalleris only set indoneBlock, which executes after theSwitchContextcall. IfSwitchContextsuspends (the expected behavior), suspension processing will observeresumedCaller == 0, which can cause the switch-call suspension to be treated like a first-time (non-resumed) suspension for the caller frame.
// switchBlock: get back onto the continuation context the caller's continuation
// captured. This is an await, so it may suspend.
{
GenTreeCall* const switchCall = gtNewUserCallNode(asyncInfo->switchContextMethHnd, TYP_VOID);
switchCall->gtArgs.PushFront(this,
Built on #128152 and #131342.
Currently just validating. I am hoping to get this in .NET 11, perhaps disabled by default, depending on how validation goes.
Fix #127865