Skip to content

JIT: Support general inlining in runtime async - #131538

Draft
jakobbotsch wants to merge 38 commits into
dotnet:mainfrom
jakobbotsch:async-inlining
Draft

JIT: Support general inlining in runtime async#131538
jakobbotsch wants to merge 38 commits into
dotnet:mainfrom
jakobbotsch:async-inlining

Conversation

@jakobbotsch

@jakobbotsch jakobbotsch commented Jul 29, 2026

Copy link
Copy Markdown
Member

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

jakobbotsch and others added 30 commits May 13, 2026 17:13
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
jakobbotsch and others added 4 commits July 29, 2026 17:25
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
Copilot AI review requested due to automatic review settings July 29, 2026 17:00
@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 Jul 29, 2026
@azure-pipelines

Copy link
Copy Markdown
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.

@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.

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

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 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 gate JitAsyncInlining).
  • Adds new async-focused tests and updates many async test projects to use Microsoft.NET.Sdk rather than Microsoft.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.

Comment on lines +24 to +36
public override void Post(SendOrPostCallback d, object? state)
{
SynchronizationContext currentCtx = Current;
SetSynchronizationContext(this);
try
{
d(state);
}
finally
{
SetSynchronizationContext(currentCtx);
}
}
Copilot AI review requested due to automatic review settings July 29, 2026 17:17

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

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 91 out of 91 changed files in this pull request and generated 1 comment.

Comment on lines +2640 to +2649
// 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));
}
jakobbotsch and others added 3 commits July 30, 2026 11:25
# 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
@jakobbotsch jakobbotsch added this to the 11.0.0 milestone Jul 30, 2026
@JulieLeeMSFT JulieLeeMSFT added the Priority:1 Work that is critical for the release, but we could probably ship without label Jul 30, 2026
Copilot AI review requested due to automatic review settings July 30, 2026 17:41

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

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 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

  • SwitchContext is an async helper that always suspends. In the resumption path, resumedCaller is only set in doneBlock, which executes after the SwitchContext call. If SwitchContext suspends (the expected behavior), suspension processing will observe resumedCaller == 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,

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 Priority:1 Work that is critical for the release, but we could probably ship without

Projects

None yet

Development

Successfully merging this pull request may close these issues.

JIT: Tracking issue for general runtime async inlining

3 participants