Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
46 commits
Select commit Hold shift + click to select a range
41bfc51
JIT: Use an indicator variable "did resume?" in runtime async
jakobbotsch May 13, 2026
80c8b10
Fix
jakobbotsch May 13, 2026
ca5ef77
Comment
jakobbotsch May 13, 2026
cca3fc5
Copilot feedback
jakobbotsch May 13, 2026
6564c12
Generalize
jakobbotsch May 15, 2026
73187af
Fixes
jakobbotsch May 15, 2026
d13f065
JIT: Move invariant nodes and LCL_VARs in LiftLIREdges
jakobbotsch May 15, 2026
e5b707e
Fix
jakobbotsch May 15, 2026
6d2556d
Run jit-format
jakobbotsch May 15, 2026
f448161
Squash change with GT_CONTINUATION_MEMBER_OFFSET
jakobbotsch Jul 27, 2026
8442fea
Merge branch 'main' of github.com:dotnet/runtime into async-indicator
jakobbotsch Jul 27, 2026
7df6426
Merge branch 'main' of github.com:dotnet/runtime into async-indicator
jakobbotsch Jul 27, 2026
0330364
Run jit-format
jakobbotsch Jul 27, 2026
dc1941d
JIT: Address async transformation review feedback
jakobbotsch Jul 28, 2026
3a37708
JIT: Allow invariant async pseudo-args when reusing suspensions
jakobbotsch Jul 28, 2026
695e4c0
Merge remote-tracking branch 'origin/async-indicator' into async-inli…
jakobbotsch Jul 29, 2026
545d8bd
JIT: Fix LIR corruption when storing async awaiter into continuation
jakobbotsch Jul 29, 2026
673390a
JIT: Add design document for general runtime async inlining
jakobbotsch Jul 29, 2026
dd130c3
Add AsyncHelpers.IsOnRightContext and expose it and RestoreExecutionC…
jakobbotsch Jul 29, 2026
7623742
Add AsyncHelpers.SwitchContext and expose it to the JIT
jakobbotsch Jul 29, 2026
27edc8f
JIT: Add continuation members for inlined async frames
jakobbotsch Jul 29, 2026
80bbc3d
JIT: Add JitAsyncInlining knob and give inlinee awaits their own cont…
jakobbotsch Jul 29, 2026
fe0ae8b
JIT: Detect whether an async body may suspend, and skip OSR for gener…
jakobbotsch Jul 29, 2026
2ed700c
Use a purpose-built helper for restoring an inlined frame's Execution…
jakobbotsch Jul 29, 2026
5ca846e
JIT: Apply OSR async context handling to the root frame only
jakobbotsch Jul 29, 2026
110df7e
JIT: Do not generally inline async calls that may suspend inside a tr…
jakobbotsch Jul 29, 2026
65fdb57
JIT: Emit post-inline IR for an inlined async frame returning to its …
jakobbotsch Jul 29, 2026
117822b
JIT: Carry enclosing inlined frames' context values on async calls
jakobbotsch Jul 29, 2026
8103025
JIT: Run enclosing inlined frames' context handling on suspension
jakobbotsch Jul 29, 2026
d6c80dd
Add tests for context handling across inlined async frames
jakobbotsch Jul 29, 2026
9911625
Add OSR coverage for async inlining, and correct an earlier OSR claim
jakobbotsch Jul 29, 2026
34a8784
JIT: Support cloning GT_CONTINUATION_MEMBER_OFFSET
jakobbotsch Jul 29, 2026
f3a00bf
Merge remote-tracking branch 'upstream/main' into async-inlining
jakobbotsch Jul 29, 2026
30078a6
Enable by default for testing
jakobbotsch Jul 29, 2026
096e368
Fix
jakobbotsch Jul 29, 2026
b7e30ed
Merge remote-tracking branch 'upstream/main' into async-inlining
jakobbotsch Jul 30, 2026
0d86ed6
JIT: Take inlined frame suspension values from the IR, not a side table
jakobbotsch Jul 30, 2026
3447a52
JIT: Remove sharing of inlined frame suspension tails
jakobbotsch Jul 30, 2026
036133c
JIT: Emit the inlined async frame transition as calls rather than IR
jakobbotsch Jul 31, 2026
ce2aec6
JIT: Merge the inlined async frame transition into a single helper call
jakobbotsch Jul 31, 2026
6134f4e
Make RestoreInlinedFrameContexts inlinable
jakobbotsch Jul 31, 2026
38e42b5
JIT: Make the inlined async frame transition call an inline candidate
jakobbotsch Jul 31, 2026
6962878
Remove AggressiveInlining from RestoreInlinedFrameContexts
jakobbotsch Jul 31, 2026
9263050
JIT: Do not add context pseudo-args to the inlined frame transition call
jakobbotsch Jul 31, 2026
285d6d9
Drop redundant casts in RestoreInlinedFrameContexts
jakobbotsch Jul 31, 2026
27aac9f
JIT: Derive the enclosing async frame chain from the inlining call
jakobbotsch Jul 31, 2026
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
348 changes: 348 additions & 0 deletions docs/design/coreclr/jit/runtime-async-inlining.md

Large diffs are not rendered by default.

Original file line number Diff line number Diff line change
Expand Up @@ -223,6 +223,8 @@ private ref struct RuntimeAsyncStackState
public INotifyCompletion? Notifier;
public ValueTaskSourceContinuation? ValueTaskSourceContinuation;
public RuntimeAsyncTaskContinuation? TaskContinuation;
public delegate*<Continuation, int, Action, void> AwaiterContinuation;
public int AwaiterOffset;

// When we suspend in the leaf, the contexts are captured into these fields.
public ExecutionContext? LeafExecutionContext;
Expand Down Expand Up @@ -265,6 +267,8 @@ public void CaptureContexts()
[NonVersionable]
public void Push(RuntimeAsyncStackState* stackState)
{
stackState->AwaiterContinuation = null;
stackState->AwaiterOffset = 0;
stackState->Next = StackState;
StackState = stackState;
CurrentThread ??= Thread.CurrentThread;
Expand Down Expand Up @@ -805,7 +809,15 @@ internal unsafe bool HandleSuspended(ref RuntimeAsyncAwaitState state)

try
{
if (stackState->CriticalNotifier is { } critNotifier)
if (stackState->AwaiterContinuation != null)
{
// The awaiter is stored in the continuation for the caller of
// AwaitAwaiterInContinuation or UnsafeAwaitAwaiterInContinuation.
Debug.Assert((headContinuation.Flags & ContinuationFlags.AllContinuationFlags) == 0);
stackState->AwaiterContinuation(
headContinuation, stackState->AwaiterOffset, GetContinuationAction());
}
else if (stackState->CriticalNotifier is { } critNotifier)
{
// Result of async call to AwaitAwaiter or UnsafeAwaitAwaiter.
// These never have special continuation context handling.
Expand Down Expand Up @@ -1477,6 +1489,131 @@ private static void CaptureContinuationContextFlags(ref ContinuationFlags flags,
flags |= ContinuationFlags.ContinueOnThreadPool;
}

// Restore the contexts that an inlined async frame captured when it logically returned to
// its caller, after that frame was resumed inside its own body.
//
// Used by the JIT when inlining runtime async calls. The JIT emits the check of whether
// the frame was resumed at all and calls this when it was; everything the async
// infrastructure would otherwise have done at that frame boundary happens here.
//
// Unlike the synchronous restore at the end of a method, the ExecutionContext restore runs
// only after a resumption, so it must target the thread we were resumed on rather than the
// one whose contexts were captured on entry.
//
// The continuation context check determines whether resuming the caller's continuation here
// would be dispatched inline. It mirrors the "can inline" conditions in
// RuntimeAsyncTaskContinuation.QueueIfNecessary; the two must be kept in sync.
//
// 'flags' must contain only ContinuationFlags.AllContinuationFlags bits.
[BypassReadyToRun]
[MethodImpl(MethodImplOptions.Async)]
private static void RestoreInlinedFrameContexts(ExecutionContext? previousExecCtx, object? continuationContext, ContinuationFlags flags)
{
Debug.Assert((flags & ~ContinuationFlags.AllContinuationFlags) == 0);

// We are inside a runtime async chain, so the thread has already been cached. Use it
// instead of Thread.CurrentThreadAssumedInitialized to keep this to one TLS lookup.
ref RuntimeAsyncAwaitState state = ref t_runtimeAsyncAwaitState;
Thread? currentThread = state.CurrentThread;
Debug.Assert(currentThread != null);

RestoreExecutionContext(currentThread, previousExecCtx);

if ((flags & ContinuationFlags.ContinueOnThreadPool) != 0)
{
SynchronizationContext? syncCtx = currentThread._synchronizationContext;
if (syncCtx is null || syncCtx.GetType() == typeof(SynchronizationContext))
{
TaskScheduler? sched = TaskScheduler.InternalCurrent;
if (sched is null || sched == TaskScheduler.Default)
{
return;
}
}
}
else if ((flags & ContinuationFlags.ContinueOnCapturedSynchronizationContext) != 0)
{
Debug.Assert(continuationContext is SynchronizationContext);
if (continuationContext == currentThread._synchronizationContext)
{
return;
}
}
else if ((flags & ContinuationFlags.ContinueOnCapturedTaskScheduler) != 0)
{
Debug.Assert(continuationContext is TaskScheduler);
if (continuationContext == TaskScheduler.InternalCurrent)
{
return;
}
}
else
{
// No continuation context was captured, so there is nothing to switch to.
return;
}

TailAwait();
SwitchToContinuationContext(ref state, continuationContext, flags);
}

// Suspend and resume in the specified continuation context.
//
// Suspending on an already completed task makes the dispatcher re-dispatch the continuation
// immediately. Because that dispatch happens with canInline: false, it always posts or
// schedules onto the requested context rather than running inline here, which is what we
// want -- we only get here when we are known to be on the wrong context.
[BypassReadyToRun]
[MethodImpl(MethodImplOptions.Async)]
private static unsafe void SwitchToContinuationContext(ref RuntimeAsyncAwaitState state, object? continuationContext, ContinuationFlags flags)
{
Continuation? sentinelContinuation = state.SentinelContinuation ??= new Continuation();

RuntimeAsyncTaskContinuation? taskCont = state.CachedTaskContinuation;
if (taskCont != null)
{
state.CachedTaskContinuation = null;
}
else
{
taskCont = new RuntimeAsyncTaskContinuation();
}

taskCont.Initialize(Task.CompletedTask);
taskCont.ContinuationContext = continuationContext;
taskCont.Flags |= flags;

sentinelContinuation.Next = taskCont;
state.StackState->TaskContinuation = taskCont;

state.CaptureContexts();
AsyncSuspend(taskCont);
}

// Capture the contexts an inlined async frame hands to its caller when it logically
// returns during a suspension, i.e. what the caller's continuation would have captured
// had the callee's frame been physically present.
//
// No-ops when the frame has already resumed: in that case the caller's continuation
// already exists and keeps the values it captured when the frame first suspended.
//
// The suspension walks the inlined frames outward, and a frame having resumed implies
// its caller has too, so these can be emitted as a straight line: once one frame has
// resumed, this and every subsequent restore for the frames outside it no-op.
private static void CaptureInlinedFrameTransition(bool resumed,
ref object? continuationContext,
ref ContinuationFlags flags,
ref ExecutionContext? execContext)
{
if (resumed)
{
return;
}

CaptureContinuationContext(ref continuationContext, ref flags);
execContext = CaptureExecutionContext();
}

// Finish suspension in the common case of a custom await or for a ConfigureAwait(false) task await:
// - Capture current ExecutionContext into the continuation
// - Restore ExecutionContext and SynchronizationContext to the current Thread object
Expand Down
14 changes: 14 additions & 0 deletions src/coreclr/inc/corinfo.h
Original file line number Diff line number Diff line change
Expand Up @@ -1836,6 +1836,12 @@ struct CORINFO_ASYNC_INFO
CORINFO_METHOD_HANDLE restoreContextsMethHnd;
// Method handle for AsyncHelpers.RestoreContextsOnSuspension, used before suspending in async methods
CORINFO_METHOD_HANDLE restoreContextsOnSuspensionMethHnd;
// Method handle for AsyncHelpers.RestoreInlinedFrameContexts, used when an inlined
// async callee logically returns to its caller after having been resumed
CORINFO_METHOD_HANDLE restoreInlinedFrameContextsMethHnd;
// Method handle for AsyncHelpers.CaptureInlinedFrameTransition, used on suspension to capture
// the contexts each inlined async frame hands to its caller
CORINFO_METHOD_HANDLE captureInlinedFrameTransitionMethHnd;
// Finish suspension without saving continuation context (i.e. custom awaiter or ConfigureAwait(false))
CORINFO_METHOD_HANDLE finishSuspensionNoContinuationContextMethHnd;
// Finish suspension with saving continuation context (i.e. normal task await)
Expand Down Expand Up @@ -3168,6 +3174,14 @@ class ICorStaticInfo
// instantiation argument that must be passed to the await call.
virtual CORINFO_METHOD_HANDLE getAwaitReturnCall(CORINFO_METHOD_HANDLE callerHandle, CORINFO_CONTEXT_HANDLE* contextHandle, CORINFO_LOOKUP* instArg) = 0;

virtual CORINFO_METHOD_HANDLE getAwaitAwaiterInContinuationCall(
CORINFO_METHOD_HANDLE callerHandle,
CORINFO_SIG_INFO* callSig,
bool isUnsafe,
CORINFO_CONTEXT_HANDLE* contextHandle,
CORINFO_LOOKUP* instArg
) = 0;

/*********************************************************************************/
//
// Diagnostic methods
Expand Down
7 changes: 7 additions & 0 deletions src/coreclr/inc/icorjitinfoimpl_generated.h
Original file line number Diff line number Diff line change
Expand Up @@ -506,6 +506,13 @@ CORINFO_METHOD_HANDLE getAwaitReturnCall(
CORINFO_CONTEXT_HANDLE* contextHandle,
CORINFO_LOOKUP* instArg) override;

CORINFO_METHOD_HANDLE getAwaitAwaiterInContinuationCall(
CORINFO_METHOD_HANDLE callerHandle,
CORINFO_SIG_INFO* callSig,
bool isUnsafe,
CORINFO_CONTEXT_HANDLE* contextHandle,
CORINFO_LOOKUP* instArg) override;

mdMethodDef getMethodDefFromMethod(
CORINFO_METHOD_HANDLE hMethod) override;

Expand Down
10 changes: 5 additions & 5 deletions src/coreclr/inc/jiteeversionguid.h
Original file line number Diff line number Diff line change
Expand Up @@ -37,11 +37,11 @@

#include <minipal/guid.h>

constexpr GUID JITEEVersionIdentifier = { /* 134b051a-e1ec-4f52-a1bf-f919908aa33c */
0x134b051a,
0xe1ec,
0x4f52,
{0xa1, 0xbf, 0xf9, 0x19, 0x90, 0x8a, 0xa3, 0x3c}
constexpr GUID JITEEVersionIdentifier = { /* a41245a2-74c0-47ac-87d7-9375cb833125 */
0xa41245a2,
0x74c0,
0x47ac,
{0x87, 0xd7, 0x93, 0x75, 0xcb, 0x83, 0x31, 0x25}
};

#endif // JIT_EE_VERSIONING_GUID_H
1 change: 1 addition & 0 deletions src/coreclr/jit/ICorJitInfo_names_generated.h
Original file line number Diff line number Diff line change
Expand Up @@ -126,6 +126,7 @@ DEF_CLR_API(runWithSPMIErrorTrap)
DEF_CLR_API(getEEInfo)
DEF_CLR_API(getAsyncInfo)
DEF_CLR_API(getAwaitReturnCall)
DEF_CLR_API(getAwaitAwaiterInContinuationCall)
DEF_CLR_API(getMethodDefFromMethod)
DEF_CLR_API(printMethodName)
DEF_CLR_API(getMethodNameFromMetadata)
Expand Down
13 changes: 13 additions & 0 deletions src/coreclr/jit/ICorJitInfo_wrapper_generated.hpp
Original file line number Diff line number Diff line change
Expand Up @@ -1199,6 +1199,19 @@ CORINFO_METHOD_HANDLE WrapICorJitInfo::getAwaitReturnCall(
return temp;
}

CORINFO_METHOD_HANDLE WrapICorJitInfo::getAwaitAwaiterInContinuationCall(
CORINFO_METHOD_HANDLE callerHandle,
CORINFO_SIG_INFO* callSig,
bool isUnsafe,
CORINFO_CONTEXT_HANDLE* contextHandle,
CORINFO_LOOKUP* instArg)
{
API_ENTER(getAwaitAwaiterInContinuationCall);
CORINFO_METHOD_HANDLE temp = wrapHnd->getAwaitAwaiterInContinuationCall(callerHandle, callSig, isUnsafe, contextHandle, instArg);
API_LEAVE(getAwaitAwaiterInContinuationCall);
return temp;
}

mdMethodDef WrapICorJitInfo::getMethodDefFromMethod(
CORINFO_METHOD_HANDLE hMethod)
{
Expand Down
Loading
Loading