Skip to content

Commit 46b72c7

Browse files
authored
Async profiler: V1 (TaskAsync) instrumentation + tests. (#129043)
### Summary Enables the async profiler for the V1 TaskAsync (state-machine-based) async path. Both V1 (TaskAsync) and V2 (RuntimeAsync) now emit a uniform, well-defined event stream that downstream tools can consume without knowing which async model produced a given chain. There are smaller variations to what events V1 can support, but all-important events are supported on both async models. The callstack events are also typed, since their data will end up slightly different (Native IP only on V2 and Method Start native IP and state on V1). PR includes extensive test suite covering both paths as well as refactoring. ### Motivation V1 async (the C#-compiler-generated state-machine IAsyncStateMachineBox model) is the dominant async path in the wild. Until now the async profiler only instrumented the V2 (RuntimeAsync) path, so V1 chains were invisible to consumers. This PR extends the instrumentation to V1 while keeping the event stream identical in shape between the two models. ### What's in this PR **Runtime V1 instrumentation** - AsyncStateMachineDispatcher — class deriving from Task<VoidTaskResult> that wraps the actual state-machine box, with per-invocation TLS-pushed state held in AsyncStateMachineDispatcherInfo (ref struct). - Per-dispatcher fields (LastContinuation, ReachedLastContinuation, InnerBox) track the cascade so we can emit accurate Resume, Suspend, Complete, and append events as chains grow. - Cooperative append mechanism: when a parent registers after a child has already started walking, the runtime emits AppendAsyncCallstack to backfill the visible chain. Three race outcomes are handled (parent-registers-before-child-completes, parent-registers-during-suspend, and the unrecoverable late-parent case which is a design limit). - StateMachineDiagnosticData / GetDiagnosticData plumbed through AsyncTaskMethodBuilderT, AsyncMethodBuilderCore, and IAsyncStateMachineBox to expose the walkable chain to the profiler. NativeAOT returns false from GetDiagnosticData due to lack of native method IP and state field access. - InstrumentCheckPoint guards at all V1 builder await-completion sites (TaskAwaiter, ValueTaskAwaiter, ConfiguredValueTaskAwaitable, YieldAwaitable, PoolingAsyncValueTaskMethodBuilderT), linked out when no event source support. **Async profiler V1 event model** The runtime emits Create + Resume + Suspend + Complete per dispatcher MoveNext. Create/suspend callstacks is not emitted on V1. The continuation chain is built and finalized after emitting a create/suspend event. Create/Suspend callstacks can be calculated when parsing the whole trace since the next resume for that context will carry the callstack. On V2, continuation chains are build and finalized before scheduled for execution. On V1 this happens in parallel and chains can end up truncated in case completion race between the thread yielding and the thread executing the resumed continuation chain. A continuation chain might also continue to build after a thread started to execute a continuation chain. To handle this a new event was added to async profiler, AppendAsyncCallstack, it can fire several times between a resume async context and its completion. This gives a parser the ability to recreate the full resumed async callstack at resume point, even if it didn't exist at that point during runtime. **Tests** Test files split by async model to keep each focused: - AsyncProfilerTests.cs — shared partial-class infrastructure: parsers, event listeners, scenario runners. - AsyncProfilerV1Tests.cs — tests under the TaskAsync_* prefix covering V1 scenarios. - AsyncProfilerV2Tests.cs — tests under the RuntimeAsync_* prefix covering V2 scenarios. Total of 116 tests covering both V1 and V2 scenarios. ### Out of scope (follow-ups) - Using the AsyncInstrumentation opens up for folding existing TPL and debugger checks into the same guard. This will be handled in a follow-up PR and could offer performance improvements on existing TPL and debugger paths where async profiler co-exists. - Code currently uses a dispatcher box put at the head of continuation chain to push/pop needed async dispatcher info on tls. There are some code paths that are known (thread pool, default sync context and scheduler), we could optimize those paths if we knew they are always taken at create location, removing the allocation. Having that said, every continuation in the chain is allocated, so might not be a big deal in the end anyways. - AsyncV1 have a late attach issue, same applies to TPL. Unless we always create/enable the async dispatcher info for AsyncV1, there will be potential blind spots in the stack when doing late attach and profiler have not been enabled at startup. - PoolingAsyncValueTaskMethodBuilderT instrumentation, can be added later, if needed. - NativeAOT V1 callstack support. NativeAOT async support is already limited in tooling. Since lack of async callstacks will void the majority of scenarios, NAOT is currently not supported on V1. Could be added in future if needed - Run V1 tests on Mono. AsyncProfiler + V1 instrumentation builds on Mono, but currently no tests are executed. Can be revisited later.
1 parent 65b2508 commit 46b72c7

21 files changed

Lines changed: 8010 additions & 2296 deletions

‎src/coreclr/System.Private.CoreLib/src/System/Runtime/CompilerServices/AsyncHelpers.CoreCLR.cs‎

Lines changed: 24 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -1043,6 +1043,8 @@ private unsafe void InstrumentedDispatchContinuations(AsyncInstrumentation.Flags
10431043
Continuation? nextContinuation = curContinuation.Next;
10441044
asyncDispatcherInfo.NextContinuation = nextContinuation;
10451045

1046+
RuntimeAsyncInstrumentationHelpers.SyncPointCheck(ref asyncDispatcherInfo, flags, curContinuation);
1047+
10461048
Debug.Assert(awaitState.CurrentThread != null);
10471049
if (curContinuation.TryGetExecutionContext(out ExecutionContext? execContext))
10481050
{
@@ -1228,7 +1230,7 @@ private static void InstrumentedFinalizeRuntimeAsyncTask<T>(RuntimeAsyncTask<T>
12281230
Continuation? nextContinuation = state.SentinelContinuation!.Next;
12291231
if (nextContinuation != null)
12301232
{
1231-
AsyncProfiler.CreateAsyncContext.Create((ulong)task.Id, nextContinuation);
1233+
AsyncProfiler.CreateAsyncContext.Create(task, nextContinuation);
12321234
}
12331235
}
12341236

@@ -1565,6 +1567,16 @@ public static bool InstrumentCheckPoint
15651567
get => AsyncInstrumentation.IsSupported && AsyncInstrumentation.ActiveFlags != AsyncInstrumentation.Flags.Disabled;
15661568
}
15671569

1570+
[MethodImpl(MethodImplOptions.AggressiveInlining)]
1571+
public static void SyncPointCheck(ref AsyncDispatcherInfo info, AsyncInstrumentation.Flags flags, Continuation curContinuation)
1572+
{
1573+
if (AsyncInstrumentation.IsEnabled.AsyncProfiler(flags))
1574+
{
1575+
info.AsyncProfilerInfo.CurrentContinuation = curContinuation;
1576+
AsyncProfiler.SyncPoint.Check(ref info.AsyncProfilerInfo);
1577+
}
1578+
}
1579+
15681580
[MethodImpl(MethodImplOptions.AggressiveInlining)]
15691581
public static void ResumeRuntimeAsyncContext(Task task, ref AsyncDispatcherInfo info, AsyncInstrumentation.Flags flags)
15701582
{
@@ -1626,7 +1638,7 @@ public static void CompleteRuntimeAsyncContext(ref AsyncDispatcherInfo info, Asy
16261638
{
16271639
if (AsyncInstrumentation.IsEnabled.AsyncProfiler(flags))
16281640
{
1629-
AsyncProfiler.CompleteAsyncContext.Complete(ref info.AsyncProfilerInfo);
1641+
AsyncProfiler.CompleteAsyncContext.Complete(ref info);
16301642
}
16311643

16321644
if (AsyncInstrumentation.IsEnabled.AsyncDebugger(flags))
@@ -1647,7 +1659,7 @@ public static void UnwindRuntimeAsyncMethodUnhandledException(ref AsyncDispatche
16471659
{
16481660
if (AsyncInstrumentation.IsEnabled.AsyncProfiler(flags))
16491661
{
1650-
AsyncProfiler.AsyncMethodException.Unhandled(ref info.AsyncProfilerInfo, unwindedFrames);
1662+
AsyncProfiler.AsyncMethodException.Unhandled(ref info, unwindedFrames);
16511663
}
16521664

16531665
if (AsyncInstrumentation.IsEnabled.AsyncDebugger(flags))
@@ -1668,7 +1680,7 @@ public static void UnwindRuntimeAsyncMethodHandledException(ref AsyncDispatcherI
16681680
{
16691681
if (AsyncInstrumentation.IsEnabled.AsyncProfiler(flags))
16701682
{
1671-
AsyncProfiler.AsyncMethodException.Handled(ref info.AsyncProfilerInfo, unwindedFrames);
1683+
AsyncProfiler.AsyncMethodException.Handled(ref info, unwindedFrames);
16721684
}
16731685

16741686
if (AsyncInstrumentation.IsEnabled.AsyncDebugger(flags))
@@ -1685,7 +1697,7 @@ public static void ResumeRuntimeAsyncMethod(ref AsyncDispatcherInfo info, AsyncI
16851697
{
16861698
if (AsyncInstrumentation.IsEnabled.AsyncProfiler(flags))
16871699
{
1688-
AsyncProfiler.ResumeAsyncMethod.Resume(ref info.AsyncProfilerInfo);
1700+
AsyncProfiler.ResumeAsyncMethod.Resume(ref info);
16891701
}
16901702

16911703
if (AsyncInstrumentation.IsEnabled.AsyncDebugger(flags))
@@ -1700,20 +1712,17 @@ public static void CompleteRuntimeAsyncMethod(ref AsyncDispatcherInfo info, Asyn
17001712
{
17011713
if (AsyncInstrumentation.IsEnabled.AsyncProfiler(flags))
17021714
{
1715+
if (AsyncInstrumentation.IsEnabled.CompleteAsyncMethod(flags))
1716+
{
1717+
AsyncProfiler.CompleteAsyncMethod.Complete(ref info);
1718+
}
1719+
17031720
AsyncProfiler.ContinuationWrapper.IncrementIndex(ref info.AsyncProfilerInfo);
17041721
}
17051722

1706-
if (AsyncInstrumentation.IsEnabled.CompleteAsyncMethod(flags))
1723+
if (AsyncInstrumentation.IsEnabled.AsyncDebugger(flags) && AsyncInstrumentation.IsEnabled.CompleteAsyncMethod(flags))
17071724
{
1708-
if (AsyncInstrumentation.IsEnabled.AsyncProfiler(flags))
1709-
{
1710-
AsyncProfiler.CompleteAsyncMethod.Complete(ref info.AsyncProfilerInfo);
1711-
}
1712-
1713-
if (AsyncInstrumentation.IsEnabled.AsyncDebugger(flags))
1714-
{
1715-
AsyncDebugger.CompleteAsyncMethod(curContinuation);
1716-
}
1725+
AsyncDebugger.CompleteAsyncMethod(curContinuation);
17171726
}
17181727
}
17191728

0 commit comments

Comments
 (0)