diff --git a/src/coreclr/System.Private.CoreLib/System.Private.CoreLib.csproj b/src/coreclr/System.Private.CoreLib/System.Private.CoreLib.csproj
index c72920560ecaa7..34eaf7557a6e46 100644
--- a/src/coreclr/System.Private.CoreLib/System.Private.CoreLib.csproj
+++ b/src/coreclr/System.Private.CoreLib/System.Private.CoreLib.csproj
@@ -206,6 +206,7 @@
+
diff --git a/src/coreclr/System.Private.CoreLib/src/System/Runtime/CompilerServices/AsyncHelpers.CoreCLR.cs b/src/coreclr/System.Private.CoreLib/src/System/Runtime/CompilerServices/AsyncHelpers.CoreCLR.cs
index 81d9f982c4e7ea..0df8865300398a 100644
--- a/src/coreclr/System.Private.CoreLib/src/System/Runtime/CompilerServices/AsyncHelpers.CoreCLR.cs
+++ b/src/coreclr/System.Private.CoreLib/src/System/Runtime/CompilerServices/AsyncHelpers.CoreCLR.cs
@@ -139,6 +139,13 @@ internal unsafe ref struct AsyncDispatcherInfo
// to match an inflight Task to the corresponding Continuation chain.
public Task? CurrentTask;
+#if TARGET_64BIT
+ [FieldOffset(24)]
+#else
+ [FieldOffset(12)]
+#endif
+ public AsyncProfiler.Info AsyncProfilerInfo;
+
// Information about current task dispatching, to be used for async
// stackwalking.
[ThreadStatic]
@@ -498,7 +505,7 @@ internal void InstrumentedHandleSuspended(AsyncInstrumentation.Flags flags, ref
{
if (AsyncInstrumentation.IsEnabled.AsyncDebugger(flags))
{
- Continuation? nextContinuation = t_runtimeAsyncAwaitState.SentinelContinuation!.Next;
+ Continuation? nextContinuation = state.SentinelContinuation!.Next;
AsyncDebugger.HandleSuspended(nextContinuation, newContinuation);
@@ -659,12 +666,13 @@ private unsafe void InstrumentedDispatchContinuations(AsyncInstrumentation.Flags
ref byte resultLoc = ref nextContinuation != null ? ref nextContinuation.GetResultStorageOrNull() : ref GetResultStorage();
RuntimeAsyncInstrumentationHelpers.ResumeRuntimeAsyncMethod(ref asyncDispatcherInfo, flags, curContinuation);
- Continuation? newContinuation = curContinuation.ResumeInfo->Resume(curContinuation, ref resultLoc);
+ Continuation? newContinuation = RuntimeAsyncInstrumentationHelpers.ResumeContinuation(ref asyncDispatcherInfo, flags, curContinuation, ref resultLoc);
if (newContinuation != null)
{
newContinuation.Next = nextContinuation;
- RuntimeAsyncInstrumentationHelpers.SuspendRuntimeAsyncContext(flags, curContinuation, newContinuation);
+
+ RuntimeAsyncInstrumentationHelpers.AwaitSuspendedRuntimeAsyncContext(ref asyncDispatcherInfo, flags, curContinuation, newContinuation, awaitState.SentinelContinuation!.Next);
InstrumentedHandleSuspended(flags, ref awaitState, newContinuation);
awaitState.Pop();
@@ -672,7 +680,7 @@ private unsafe void InstrumentedDispatchContinuations(AsyncInstrumentation.Flags
return;
}
- RuntimeAsyncInstrumentationHelpers.CompleteRuntimeAsyncMethod(flags, curContinuation);
+ RuntimeAsyncInstrumentationHelpers.CompleteRuntimeAsyncMethod(ref asyncDispatcherInfo, flags, curContinuation);
}
catch (Exception ex)
{
@@ -698,7 +706,7 @@ private unsafe void InstrumentedDispatchContinuations(AsyncInstrumentation.Flags
return;
}
- RuntimeAsyncInstrumentationHelpers.UnwindRuntimeAsyncMethodHandledException(flags, curContinuation, unwindedFrames);
+ RuntimeAsyncInstrumentationHelpers.UnwindRuntimeAsyncMethodHandledException(ref asyncDispatcherInfo, flags, curContinuation, unwindedFrames);
handlerContinuation.SetException(ex);
asyncDispatcherInfo.NextContinuation = handlerContinuation;
@@ -723,7 +731,7 @@ private unsafe void InstrumentedDispatchContinuations(AsyncInstrumentation.Flags
if (QueueContinuationFollowUpActionIfNecessary(asyncDispatcherInfo.NextContinuation))
{
- RuntimeAsyncInstrumentationHelpers.SuspendRuntimeAsyncContext(ref asyncDispatcherInfo, flags, curContinuation);
+ RuntimeAsyncInstrumentationHelpers.QueueSuspendedRuntimeAsyncContext(ref asyncDispatcherInfo, flags, curContinuation, asyncDispatcherInfo.NextContinuation);
awaitState.Pop();
refDispatcherInfo = asyncDispatcherInfo.Next;
@@ -837,6 +845,15 @@ private static void InstrumentedFinalizeRuntimeAsyncTask(RuntimeAsyncTask
{
if (AsyncInstrumentation.IsEnabled.CreateAsyncContext(flags))
{
+ if (AsyncInstrumentation.IsEnabled.AsyncProfiler(flags))
+ {
+ Continuation? nextContinuation = state.SentinelContinuation!.Next;
+ if (nextContinuation != null)
+ {
+ AsyncProfiler.CreateAsyncContext.Create((ulong)task.Id, nextContinuation);
+ }
+ }
+
if (AsyncInstrumentation.IsEnabled.AsyncDebugger(flags))
{
task.NotifyDebuggerOfRuntimeAsyncState();
@@ -1053,21 +1070,32 @@ public static bool InstrumentCheckPoint
public static void ResumeRuntimeAsyncContext(Task task, ref AsyncDispatcherInfo info, AsyncInstrumentation.Flags flags)
{
info.CurrentTask = task;
+ AsyncProfiler.InitInfo(ref info.AsyncProfilerInfo);
if (AsyncInstrumentation.IsEnabled.ResumeAsyncContext(flags))
{
+ if (AsyncInstrumentation.IsEnabled.AsyncProfiler(flags))
+ {
+ AsyncProfiler.ResumeAsyncContext.Resume(ref info);
+ }
+
if (AsyncInstrumentation.IsEnabled.AsyncDebugger(flags))
{
- AsyncDebugger.ResumeAsyncContext(task.Id);
+ AsyncDebugger.ResumeAsyncContext(task);
}
}
}
[MethodImpl(MethodImplOptions.AggressiveInlining)]
- public static void SuspendRuntimeAsyncContext(ref AsyncDispatcherInfo info, AsyncInstrumentation.Flags flags, Continuation curContinuation)
+ public static void QueueSuspendedRuntimeAsyncContext(ref AsyncDispatcherInfo info, AsyncInstrumentation.Flags flags, Continuation curContinuation, Continuation nextContinuation)
{
if (AsyncInstrumentation.IsEnabled.SuspendAsyncContext(flags))
{
+ if (AsyncInstrumentation.IsEnabled.AsyncProfiler(flags))
+ {
+ AsyncProfiler.SuspendAsyncContext.Suspend(ref info, nextContinuation);
+ }
+
if (AsyncInstrumentation.IsEnabled.AsyncDebugger(flags))
{
AsyncDebugger.SuspendAsyncContext(ref info, curContinuation);
@@ -1076,10 +1104,15 @@ public static void SuspendRuntimeAsyncContext(ref AsyncDispatcherInfo info, Asyn
}
[MethodImpl(MethodImplOptions.AggressiveInlining)]
- public static void SuspendRuntimeAsyncContext(AsyncInstrumentation.Flags flags, Continuation curContinuation, Continuation newContinuation)
+ public static void AwaitSuspendedRuntimeAsyncContext(ref AsyncDispatcherInfo info, AsyncInstrumentation.Flags flags, Continuation curContinuation, Continuation newContinuation, Continuation? nextContinuation)
{
if (AsyncInstrumentation.IsEnabled.SuspendAsyncContext(flags))
{
+ if (AsyncInstrumentation.IsEnabled.AsyncProfiler(flags))
+ {
+ AsyncProfiler.SuspendAsyncContext.Suspend(ref info, nextContinuation ?? newContinuation);
+ }
+
if (AsyncInstrumentation.IsEnabled.AsyncDebugger(flags))
{
AsyncDebugger.SuspendAsyncContext(curContinuation, newContinuation);
@@ -1092,6 +1125,11 @@ public static void CompleteRuntimeAsyncContext(ref AsyncDispatcherInfo info, Asy
{
if (AsyncInstrumentation.IsEnabled.CompleteAsyncContext(flags))
{
+ if (AsyncInstrumentation.IsEnabled.AsyncProfiler(flags))
+ {
+ AsyncProfiler.CompleteAsyncContext.Complete(ref info.AsyncProfilerInfo);
+ }
+
if (AsyncInstrumentation.IsEnabled.AsyncDebugger(flags))
{
AsyncDebugger.CompleteAsyncContext(info.CurrentTask);
@@ -1099,10 +1137,20 @@ public static void CompleteRuntimeAsyncContext(ref AsyncDispatcherInfo info, Asy
}
}
- public static void UnwindRuntimeAsyncMethodUnhandledException(ref AsyncDispatcherInfo info, AsyncInstrumentation.Flags flags, Exception ex, Continuation curContinuation, uint _)
+ public static void UnwindRuntimeAsyncMethodUnhandledException(ref AsyncDispatcherInfo info, AsyncInstrumentation.Flags flags, Exception ex, Continuation curContinuation, uint unwindedFrames)
{
+ if (AsyncInstrumentation.IsEnabled.AsyncProfiler(flags))
+ {
+ AsyncProfiler.ContinuationWrapper.UnwindIndex(ref info.AsyncProfilerInfo, unwindedFrames);
+ }
+
if (AsyncInstrumentation.IsEnabled.UnwindAsyncException(flags))
{
+ if (AsyncInstrumentation.IsEnabled.AsyncProfiler(flags))
+ {
+ AsyncProfiler.AsyncMethodException.Unhandled(ref info.AsyncProfilerInfo, unwindedFrames);
+ }
+
if (AsyncInstrumentation.IsEnabled.AsyncDebugger(flags))
{
AsyncDebugger.AsyncMethodUnhandledException(info.CurrentTask, ex, curContinuation);
@@ -1110,10 +1158,20 @@ public static void UnwindRuntimeAsyncMethodUnhandledException(ref AsyncDispatche
}
}
- public static void UnwindRuntimeAsyncMethodHandledException(AsyncInstrumentation.Flags flags, Continuation curContinuation, uint unwindedFrames)
+ public static void UnwindRuntimeAsyncMethodHandledException(ref AsyncDispatcherInfo info, AsyncInstrumentation.Flags flags, Continuation curContinuation, uint unwindedFrames)
{
+ if (AsyncInstrumentation.IsEnabled.AsyncProfiler(flags))
+ {
+ AsyncProfiler.ContinuationWrapper.UnwindIndex(ref info.AsyncProfilerInfo, unwindedFrames);
+ }
+
if (AsyncInstrumentation.IsEnabled.UnwindAsyncException(flags))
{
+ if (AsyncInstrumentation.IsEnabled.AsyncProfiler(flags))
+ {
+ AsyncProfiler.AsyncMethodException.Handled(ref info.AsyncProfilerInfo, unwindedFrames);
+ }
+
if (AsyncInstrumentation.IsEnabled.AsyncDebugger(flags))
{
AsyncDebugger.AsyncMethodHandledException(curContinuation, unwindedFrames);
@@ -1126,6 +1184,11 @@ public static void ResumeRuntimeAsyncMethod(ref AsyncDispatcherInfo info, AsyncI
{
if (AsyncInstrumentation.IsEnabled.ResumeAsyncMethod(flags))
{
+ if (AsyncInstrumentation.IsEnabled.AsyncProfiler(flags))
+ {
+ AsyncProfiler.ResumeAsyncMethod.Resume(ref info.AsyncProfilerInfo);
+ }
+
if (AsyncInstrumentation.IsEnabled.AsyncDebugger(flags))
{
AsyncDebugger.ResumeAsyncMethod(ref info, curContinuation);
@@ -1134,16 +1197,40 @@ public static void ResumeRuntimeAsyncMethod(ref AsyncDispatcherInfo info, AsyncI
}
[MethodImpl(MethodImplOptions.AggressiveInlining)]
- public static void CompleteRuntimeAsyncMethod(AsyncInstrumentation.Flags flags, Continuation curContinuation)
+ public static void CompleteRuntimeAsyncMethod(ref AsyncDispatcherInfo info, AsyncInstrumentation.Flags flags, Continuation curContinuation)
{
+ if (AsyncInstrumentation.IsEnabled.AsyncProfiler(flags))
+ {
+ AsyncProfiler.ContinuationWrapper.IncrementIndex(ref info.AsyncProfilerInfo);
+ }
+
if (AsyncInstrumentation.IsEnabled.CompleteAsyncMethod(flags))
{
+ if (AsyncInstrumentation.IsEnabled.AsyncProfiler(flags))
+ {
+ AsyncProfiler.CompleteAsyncMethod.Complete(ref info.AsyncProfilerInfo);
+ }
+
if (AsyncInstrumentation.IsEnabled.AsyncDebugger(flags))
{
AsyncDebugger.CompleteAsyncMethod(curContinuation);
}
}
}
+
+ [MethodImpl(MethodImplOptions.AggressiveInlining)]
+ public static Continuation? ResumeContinuation(ref AsyncDispatcherInfo info, AsyncInstrumentation.Flags flags, Continuation curContinuation, ref byte resultLoc)
+ {
+ if (AsyncInstrumentation.IsEnabled.AsyncProfiler(flags))
+ {
+ return AsyncProfiler.ContinuationWrapper.Dispatch(ref info, curContinuation, ref resultLoc);
+ }
+
+ unsafe
+ {
+ return curContinuation.ResumeInfo->Resume(curContinuation, ref resultLoc);
+ }
+ }
}
internal static class AsyncDebugger
@@ -1154,9 +1241,9 @@ public static void CreateAsyncContext(Task task)
TplEventSource.Log.TraceOperationBegin(task.Id, "System.Runtime.CompilerServices.AsyncHelpers+RuntimeAsyncTask", 0);
}
- public static void ResumeAsyncContext(int id)
+ public static void ResumeAsyncContext(Task task)
{
- TplEventSource.Log.TraceSynchronousWorkBegin(id, CausalitySynchronousWork.Execution);
+ TplEventSource.Log.TraceSynchronousWorkBegin(task.Id, CausalitySynchronousWork.Execution);
}
public static void SuspendAsyncContext(ref AsyncDispatcherInfo info, Continuation curContinuation)
diff --git a/src/coreclr/System.Private.CoreLib/src/System/Runtime/CompilerServices/AsyncProfiler.CoreCLR.cs b/src/coreclr/System.Private.CoreLib/src/System/Runtime/CompilerServices/AsyncProfiler.CoreCLR.cs
new file mode 100644
index 00000000000000..9ce157dd648596
--- /dev/null
+++ b/src/coreclr/System.Private.CoreLib/src/System/Runtime/CompilerServices/AsyncProfiler.CoreCLR.cs
@@ -0,0 +1,627 @@
+// Licensed to the .NET Foundation under one or more agreements.
+// The .NET Foundation licenses this file to you under the MIT license.
+
+using System.Buffers;
+using System.Diagnostics;
+using System.Diagnostics.Tracing;
+using Serializer = System.Runtime.CompilerServices.AsyncProfiler.EventBuffer.Serializer;
+
+namespace System.Runtime.CompilerServices
+{
+ internal static partial class AsyncProfiler
+ {
+ internal static partial class CreateAsyncContext
+ {
+ public static void Create(ulong id, Continuation nextContinuation)
+ {
+ Info info = default;
+ AsyncThreadContext context = AsyncThreadContext.Acquire(ref info);
+
+ SyncPoint.Check(context);
+
+ EventKeywords eventKeywords = context.ActiveEventKeywords;
+ if (IsEnabled.AnyAsyncEvents(eventKeywords))
+ {
+ long currentTimestamp = Stopwatch.GetTimestamp();
+ if (IsEnabled.CreateAsyncContextEvent(eventKeywords))
+ {
+ EmitEvent(context, currentTimestamp, id);
+ }
+
+ if (IsEnabled.CreateAsyncCallstackEvent(eventKeywords))
+ {
+ AsyncCallstack.EmitEvent(context, currentTimestamp, AsyncEventID.CreateAsyncCallstack, id, nextContinuation);
+ }
+ }
+
+ AsyncThreadContext.Release(context);
+ }
+ }
+
+ internal static partial class ResumeAsyncContext
+ {
+ public static ulong GetId(ref AsyncDispatcherInfo info)
+ {
+ if (info.CurrentTask != null)
+ {
+ return (ulong)info.CurrentTask.Id;
+ }
+ return 0;
+ }
+
+ public static void Resume(ref AsyncDispatcherInfo info)
+ {
+ AsyncThreadContext context = AsyncThreadContext.Acquire(ref info.AsyncProfilerInfo);
+
+ Resume(ref info, context, GetId(ref info), context.ActiveEventKeywords);
+
+ AsyncThreadContext.Release(context);
+ }
+
+ public static void Resume(ref AsyncDispatcherInfo info, AsyncThreadContext context, ulong id, EventKeywords activeEventKeywords)
+ {
+ if (SyncPoint.Check(context))
+ {
+ return;
+ }
+
+ if (IsEnabled.AnyAsyncEvents(activeEventKeywords))
+ {
+ long currentTimestamp = Stopwatch.GetTimestamp();
+ if (IsEnabled.ResumeAsyncContextEvent(activeEventKeywords))
+ {
+ EmitEvent(context, currentTimestamp, id);
+ }
+
+ if (IsEnabled.ResumeAsyncCallstackEvent(activeEventKeywords))
+ {
+ AsyncCallstack.EmitEvent(context, currentTimestamp, id, info.NextContinuation);
+ }
+ }
+ }
+ }
+
+ internal static partial class SuspendAsyncContext
+ {
+ public static void Suspend(ref AsyncDispatcherInfo info, Continuation nextContinuation)
+ {
+ AsyncThreadContext context = AsyncThreadContext.Acquire(ref info.AsyncProfilerInfo);
+
+ SyncPoint.Check(context);
+
+ EventKeywords activeEventKeywords = context.ActiveEventKeywords;
+ if (IsEnabled.AnyAsyncEvents(activeEventKeywords))
+ {
+ long currentTimestamp = Stopwatch.GetTimestamp();
+ if (IsEnabled.SuspendAsyncContextEvent(activeEventKeywords))
+ {
+ EmitEvent(context, currentTimestamp);
+ }
+
+ if (IsEnabled.SuspendAsyncCallstackEvent(activeEventKeywords))
+ {
+ AsyncCallstack.EmitEvent(context, currentTimestamp, AsyncEventID.SuspendAsyncCallstack, GetId(ref info), nextContinuation);
+ }
+ }
+
+ AsyncThreadContext.Release(context);
+ }
+
+ private static ulong GetId(ref AsyncDispatcherInfo info)
+ {
+ if (info.CurrentTask != null)
+ {
+ return (ulong)info.CurrentTask.Id;
+ }
+ return 0;
+ }
+ }
+
+ ///
+ /// Provides a table of 32 functionally identical continuation wrapper methods, each with
+ /// a unique native IP address. When resuming an async continuation, the profiler dispatches
+ /// through the wrapper at index (ContinuationIndex & COUNT_MASK), then increments the index.
+ ///
+ /// This creates a rotating pattern of unique return addresses on the native callstack. An OS
+ /// CPU profiler (e.g., ETW, perf) captures these native IPs in its stack samples. The async
+ /// profiler emits the wrapper IP table in the metadata event, so a post-processing tool can
+ /// identify which wrapper IPs appear in a native callstack and correlate them with the
+ /// async resume callstack events emitted at the same logical point. This bridges the gap
+ /// between synchronous native stack samples and the asynchronous continuation chain.
+ ///
+ /// Every COUNT (32) continuations, a ResetAsyncContinuationWrapperIndex event is emitted
+ /// so the tool knows the index has wrapped around and can correctly map subsequent samples.
+ ///
+ /// Each wrapper is marked [NoInlining] to guarantee a distinct native IP, and
+ /// [AggressiveOptimization] to ensure stable JIT output (skip tiered compilation).
+ ///
+ [StackTraceHidden]
+ internal static partial class ContinuationWrapper
+ {
+ public static void InitInfo(ref Info info)
+ {
+ info.ContinuationTable = ref Unsafe.As(ref s_continuationWrappers);
+ info.ContinuationIndex = 0;
+ }
+
+ [MethodImpl(MethodImplOptions.AggressiveInlining)]
+ public static Continuation? Dispatch(ref AsyncDispatcherInfo info, Continuation curContinuation, ref byte resultLoc)
+ {
+ nint dispatcher = Unsafe.Add(ref info.AsyncProfilerInfo.ContinuationTable, info.AsyncProfilerInfo.ContinuationIndex & COUNT_MASK);
+ unsafe
+ {
+ return ((delegate*)(dispatcher))(curContinuation, ref resultLoc);
+ }
+ }
+
+ public static long[] GetContinuationWrapperIPs()
+ {
+ long[] ips = new long[COUNT];
+ for (int i = 0; i < COUNT; i++)
+ {
+ ips[i] = Unsafe.Add(ref Unsafe.As(ref s_continuationWrappers), i);
+ }
+ return ips;
+ }
+
+ [MethodImpl(MethodImplOptions.NoInlining | MethodImplOptions.AggressiveOptimization)]
+ private static unsafe Continuation? Continuation_Wrapper_0(Continuation continuation, ref byte resultLoc)
+ {
+ return continuation.ResumeInfo->Resume(continuation, ref resultLoc);
+ }
+
+ [MethodImpl(MethodImplOptions.NoInlining | MethodImplOptions.AggressiveOptimization)]
+ private static unsafe Continuation? Continuation_Wrapper_1(Continuation continuation, ref byte resultLoc)
+ {
+ return continuation.ResumeInfo->Resume(continuation, ref resultLoc);
+ }
+
+ [MethodImpl(MethodImplOptions.NoInlining | MethodImplOptions.AggressiveOptimization)]
+ private static unsafe Continuation? Continuation_Wrapper_2(Continuation continuation, ref byte resultLoc)
+ {
+ return continuation.ResumeInfo->Resume(continuation, ref resultLoc);
+ }
+
+ [MethodImpl(MethodImplOptions.NoInlining | MethodImplOptions.AggressiveOptimization)]
+ private static unsafe Continuation? Continuation_Wrapper_3(Continuation continuation, ref byte resultLoc)
+ {
+ return continuation.ResumeInfo->Resume(continuation, ref resultLoc);
+ }
+
+ [MethodImpl(MethodImplOptions.NoInlining | MethodImplOptions.AggressiveOptimization)]
+ private static unsafe Continuation? Continuation_Wrapper_4(Continuation continuation, ref byte resultLoc)
+ {
+ return continuation.ResumeInfo->Resume(continuation, ref resultLoc);
+ }
+
+ [MethodImpl(MethodImplOptions.NoInlining | MethodImplOptions.AggressiveOptimization)]
+ private static unsafe Continuation? Continuation_Wrapper_5(Continuation continuation, ref byte resultLoc)
+ {
+ return continuation.ResumeInfo->Resume(continuation, ref resultLoc);
+ }
+
+ [MethodImpl(MethodImplOptions.NoInlining | MethodImplOptions.AggressiveOptimization)]
+ private static unsafe Continuation? Continuation_Wrapper_6(Continuation continuation, ref byte resultLoc)
+ {
+ return continuation.ResumeInfo->Resume(continuation, ref resultLoc);
+ }
+
+ [MethodImpl(MethodImplOptions.NoInlining | MethodImplOptions.AggressiveOptimization)]
+ private static unsafe Continuation? Continuation_Wrapper_7(Continuation continuation, ref byte resultLoc)
+ {
+ return continuation.ResumeInfo->Resume(continuation, ref resultLoc);
+ }
+
+ [MethodImpl(MethodImplOptions.NoInlining | MethodImplOptions.AggressiveOptimization)]
+ private static unsafe Continuation? Continuation_Wrapper_8(Continuation continuation, ref byte resultLoc)
+ {
+ return continuation.ResumeInfo->Resume(continuation, ref resultLoc);
+ }
+
+ [MethodImpl(MethodImplOptions.NoInlining | MethodImplOptions.AggressiveOptimization)]
+ private static unsafe Continuation? Continuation_Wrapper_9(Continuation continuation, ref byte resultLoc)
+ {
+ return continuation.ResumeInfo->Resume(continuation, ref resultLoc);
+ }
+
+ [MethodImpl(MethodImplOptions.NoInlining | MethodImplOptions.AggressiveOptimization)]
+ private static unsafe Continuation? Continuation_Wrapper_10(Continuation continuation, ref byte resultLoc)
+ {
+ return continuation.ResumeInfo->Resume(continuation, ref resultLoc);
+ }
+
+ [MethodImpl(MethodImplOptions.NoInlining | MethodImplOptions.AggressiveOptimization)]
+ private static unsafe Continuation? Continuation_Wrapper_11(Continuation continuation, ref byte resultLoc)
+ {
+ return continuation.ResumeInfo->Resume(continuation, ref resultLoc);
+ }
+
+ [MethodImpl(MethodImplOptions.NoInlining | MethodImplOptions.AggressiveOptimization)]
+ private static unsafe Continuation? Continuation_Wrapper_12(Continuation continuation, ref byte resultLoc)
+ {
+ return continuation.ResumeInfo->Resume(continuation, ref resultLoc);
+ }
+
+ [MethodImpl(MethodImplOptions.NoInlining | MethodImplOptions.AggressiveOptimization)]
+ private static unsafe Continuation? Continuation_Wrapper_13(Continuation continuation, ref byte resultLoc)
+ {
+ return continuation.ResumeInfo->Resume(continuation, ref resultLoc);
+ }
+
+ [MethodImpl(MethodImplOptions.NoInlining | MethodImplOptions.AggressiveOptimization)]
+ private static unsafe Continuation? Continuation_Wrapper_14(Continuation continuation, ref byte resultLoc)
+ {
+ return continuation.ResumeInfo->Resume(continuation, ref resultLoc);
+ }
+
+ [MethodImpl(MethodImplOptions.NoInlining | MethodImplOptions.AggressiveOptimization)]
+ private static unsafe Continuation? Continuation_Wrapper_15(Continuation continuation, ref byte resultLoc)
+ {
+ return continuation.ResumeInfo->Resume(continuation, ref resultLoc);
+ }
+
+ [MethodImpl(MethodImplOptions.NoInlining | MethodImplOptions.AggressiveOptimization)]
+ private static unsafe Continuation? Continuation_Wrapper_16(Continuation continuation, ref byte resultLoc)
+ {
+ return continuation.ResumeInfo->Resume(continuation, ref resultLoc);
+ }
+
+ [MethodImpl(MethodImplOptions.NoInlining | MethodImplOptions.AggressiveOptimization)]
+ private static unsafe Continuation? Continuation_Wrapper_17(Continuation continuation, ref byte resultLoc)
+ {
+ return continuation.ResumeInfo->Resume(continuation, ref resultLoc);
+ }
+
+ [MethodImpl(MethodImplOptions.NoInlining | MethodImplOptions.AggressiveOptimization)]
+ private static unsafe Continuation? Continuation_Wrapper_18(Continuation continuation, ref byte resultLoc)
+ {
+ return continuation.ResumeInfo->Resume(continuation, ref resultLoc);
+ }
+
+ [MethodImpl(MethodImplOptions.NoInlining | MethodImplOptions.AggressiveOptimization)]
+ private static unsafe Continuation? Continuation_Wrapper_19(Continuation continuation, ref byte resultLoc)
+ {
+ return continuation.ResumeInfo->Resume(continuation, ref resultLoc);
+ }
+
+ [MethodImpl(MethodImplOptions.NoInlining | MethodImplOptions.AggressiveOptimization)]
+ private static unsafe Continuation? Continuation_Wrapper_20(Continuation continuation, ref byte resultLoc)
+ {
+ return continuation.ResumeInfo->Resume(continuation, ref resultLoc);
+ }
+
+ [MethodImpl(MethodImplOptions.NoInlining | MethodImplOptions.AggressiveOptimization)]
+ private static unsafe Continuation? Continuation_Wrapper_21(Continuation continuation, ref byte resultLoc)
+ {
+ return continuation.ResumeInfo->Resume(continuation, ref resultLoc);
+ }
+
+ [MethodImpl(MethodImplOptions.NoInlining | MethodImplOptions.AggressiveOptimization)]
+ private static unsafe Continuation? Continuation_Wrapper_22(Continuation continuation, ref byte resultLoc)
+ {
+ return continuation.ResumeInfo->Resume(continuation, ref resultLoc);
+ }
+
+ [MethodImpl(MethodImplOptions.NoInlining | MethodImplOptions.AggressiveOptimization)]
+ private static unsafe Continuation? Continuation_Wrapper_23(Continuation continuation, ref byte resultLoc)
+ {
+ return continuation.ResumeInfo->Resume(continuation, ref resultLoc);
+ }
+
+ [MethodImpl(MethodImplOptions.NoInlining | MethodImplOptions.AggressiveOptimization)]
+ private static unsafe Continuation? Continuation_Wrapper_24(Continuation continuation, ref byte resultLoc)
+ {
+ return continuation.ResumeInfo->Resume(continuation, ref resultLoc);
+ }
+
+ [MethodImpl(MethodImplOptions.NoInlining | MethodImplOptions.AggressiveOptimization)]
+ private static unsafe Continuation? Continuation_Wrapper_25(Continuation continuation, ref byte resultLoc)
+ {
+ return continuation.ResumeInfo->Resume(continuation, ref resultLoc);
+ }
+
+ [MethodImpl(MethodImplOptions.NoInlining | MethodImplOptions.AggressiveOptimization)]
+ private static unsafe Continuation? Continuation_Wrapper_26(Continuation continuation, ref byte resultLoc)
+ {
+ return continuation.ResumeInfo->Resume(continuation, ref resultLoc);
+ }
+
+ [MethodImpl(MethodImplOptions.NoInlining | MethodImplOptions.AggressiveOptimization)]
+ private static unsafe Continuation? Continuation_Wrapper_27(Continuation continuation, ref byte resultLoc)
+ {
+ return continuation.ResumeInfo->Resume(continuation, ref resultLoc);
+ }
+
+ [MethodImpl(MethodImplOptions.NoInlining | MethodImplOptions.AggressiveOptimization)]
+ private static unsafe Continuation? Continuation_Wrapper_28(Continuation continuation, ref byte resultLoc)
+ {
+ return continuation.ResumeInfo->Resume(continuation, ref resultLoc);
+ }
+
+ [MethodImpl(MethodImplOptions.NoInlining | MethodImplOptions.AggressiveOptimization)]
+ private static unsafe Continuation? Continuation_Wrapper_29(Continuation continuation, ref byte resultLoc)
+ {
+ return continuation.ResumeInfo->Resume(continuation, ref resultLoc);
+ }
+
+ [MethodImpl(MethodImplOptions.NoInlining | MethodImplOptions.AggressiveOptimization)]
+ private static unsafe Continuation? Continuation_Wrapper_30(Continuation continuation, ref byte resultLoc)
+ {
+ return continuation.ResumeInfo->Resume(continuation, ref resultLoc);
+ }
+
+ [MethodImpl(MethodImplOptions.NoInlining | MethodImplOptions.AggressiveOptimization)]
+ private static unsafe Continuation? Continuation_Wrapper_31(Continuation continuation, ref byte resultLoc)
+ {
+ return continuation.ResumeInfo->Resume(continuation, ref resultLoc);
+ }
+
+ private static unsafe ContinuationWrapperTable InitContinuationWrappers()
+ {
+ ContinuationWrapperTable wrappers = default;
+ wrappers[0] = (nint)(delegate*)&Continuation_Wrapper_0;
+ wrappers[1] = (nint)(delegate*)&Continuation_Wrapper_1;
+ wrappers[2] = (nint)(delegate*)&Continuation_Wrapper_2;
+ wrappers[3] = (nint)(delegate*)&Continuation_Wrapper_3;
+ wrappers[4] = (nint)(delegate*)&Continuation_Wrapper_4;
+ wrappers[5] = (nint)(delegate*)&Continuation_Wrapper_5;
+ wrappers[6] = (nint)(delegate*)&Continuation_Wrapper_6;
+ wrappers[7] = (nint)(delegate*)&Continuation_Wrapper_7;
+ wrappers[8] = (nint)(delegate*)&Continuation_Wrapper_8;
+ wrappers[9] = (nint)(delegate*)&Continuation_Wrapper_9;
+ wrappers[10] = (nint)(delegate*)&Continuation_Wrapper_10;
+ wrappers[11] = (nint)(delegate*)&Continuation_Wrapper_11;
+ wrappers[12] = (nint)(delegate*)&Continuation_Wrapper_12;
+ wrappers[13] = (nint)(delegate*)&Continuation_Wrapper_13;
+ wrappers[14] = (nint)(delegate*)&Continuation_Wrapper_14;
+ wrappers[15] = (nint)(delegate*)&Continuation_Wrapper_15;
+ wrappers[16] = (nint)(delegate*)&Continuation_Wrapper_16;
+ wrappers[17] = (nint)(delegate*)&Continuation_Wrapper_17;
+ wrappers[18] = (nint)(delegate*)&Continuation_Wrapper_18;
+ wrappers[19] = (nint)(delegate*)&Continuation_Wrapper_19;
+ wrappers[20] = (nint)(delegate*)&Continuation_Wrapper_20;
+ wrappers[21] = (nint)(delegate*)&Continuation_Wrapper_21;
+ wrappers[22] = (nint)(delegate*)&Continuation_Wrapper_22;
+ wrappers[23] = (nint)(delegate*)&Continuation_Wrapper_23;
+ wrappers[24] = (nint)(delegate*)&Continuation_Wrapper_24;
+ wrappers[25] = (nint)(delegate*)&Continuation_Wrapper_25;
+ wrappers[26] = (nint)(delegate*)&Continuation_Wrapper_26;
+ wrappers[27] = (nint)(delegate*)&Continuation_Wrapper_27;
+ wrappers[28] = (nint)(delegate*)&Continuation_Wrapper_28;
+ wrappers[29] = (nint)(delegate*)&Continuation_Wrapper_29;
+ wrappers[30] = (nint)(delegate*)&Continuation_Wrapper_30;
+ wrappers[31] = (nint)(delegate*)&Continuation_Wrapper_31;
+ return wrappers;
+ }
+
+ [InlineArray(COUNT)]
+ private struct ContinuationWrapperTable
+ {
+ private nint _element;
+ }
+
+ private static ContinuationWrapperTable s_continuationWrappers = InitContinuationWrappers();
+ }
+
+ private static partial class SyncPoint
+ {
+ private static unsafe void ResumeAsyncCallstacks(AsyncThreadContext context)
+ {
+ //Write recursively all the resume async callstack events.
+ AsyncDispatcherInfo* info = AsyncDispatcherInfo.t_current;
+ if (info != null)
+ {
+ ResumeRuntimeAsyncCallstacks(info, context);
+ }
+
+ }
+
+ private static unsafe void ResumeRuntimeAsyncCallstacks(AsyncDispatcherInfo* info, AsyncThreadContext context)
+ {
+ if (info != null)
+ {
+ ResumeRuntimeAsyncCallstacks(info->Next, context);
+ ResumeAsyncContext.Resume(ref *info, context, ResumeAsyncContext.GetId(ref *info), Config.ActiveEventKeywords);
+ }
+ }
+ }
+
+ private static partial class AsyncCallstack
+ {
+ private const int MaxAsyncMethodFrameSize = Serializer.MaxCompressedUInt64Size + Serializer.MaxCompressedUInt32Size;
+
+ public ref struct CaptureRuntimeAsyncCallstackState
+ {
+ public Continuation? Continuation;
+ public ulong LastNativeIP;
+ public byte Count;
+ }
+
+ public static bool CaptureRuntimeAsyncCallstack(byte[] buffer, ref int index, ref CaptureRuntimeAsyncCallstackState state)
+ {
+ if (index > buffer.Length || state.Continuation == null)
+ {
+ return false;
+ }
+
+ byte maxAsyncCallstackFrames = (byte)Math.Min(byte.MaxValue, (buffer.Length - index) / MaxAsyncMethodFrameSize);
+ if (maxAsyncCallstackFrames == 0)
+ {
+ return false;
+ }
+
+ ulong currentNativeIP = 0;
+ ulong previousNativeIP = state.LastNativeIP;
+
+ unsafe
+ {
+ currentNativeIP = (ulong)state.Continuation.ResumeInfo->DiagnosticIP;
+ }
+
+ Span callstackSpan = buffer.AsSpan(index);
+ int callstackSpanIndex = 0;
+
+ // First frame (Count == 0) is written as absolute; subsequent frames
+ // (including the first frame of a continuation call after overflow)
+ // are written as deltas from the previous frame.
+ if (state.Count == 0)
+ {
+ callstackSpanIndex += Serializer.WriteCompressedUInt64(callstackSpan.Slice(callstackSpanIndex, Serializer.MaxCompressedUInt64Size), currentNativeIP);
+ }
+ else
+ {
+ callstackSpanIndex += Serializer.WriteCompressedInt64(callstackSpan.Slice(callstackSpanIndex, Serializer.MaxCompressedInt64Size), (long)(currentNativeIP - previousNativeIP));
+ }
+
+ callstackSpanIndex += Serializer.WriteCompressedInt32(callstackSpan.Slice(callstackSpanIndex, Serializer.MaxCompressedInt32Size), state.Continuation.State);
+ state.Count++;
+
+ state.Continuation = state.Continuation.Next;
+ while (state.Count < maxAsyncCallstackFrames && state.Continuation != null)
+ {
+ previousNativeIP = currentNativeIP;
+
+ unsafe
+ {
+ currentNativeIP = (ulong)state.Continuation.ResumeInfo->DiagnosticIP;
+ }
+
+ callstackSpanIndex += Serializer.WriteCompressedInt64(callstackSpan.Slice(callstackSpanIndex, Serializer.MaxCompressedInt64Size), (long)(currentNativeIP - previousNativeIP));
+ callstackSpanIndex += Serializer.WriteCompressedInt32(callstackSpan.Slice(callstackSpanIndex, Serializer.MaxCompressedInt32Size), state.Continuation.State);
+
+ state.Count++;
+ state.Continuation = state.Continuation.Next;
+ }
+
+ state.LastNativeIP = currentNativeIP;
+ index += callstackSpanIndex;
+
+ return state.Continuation == null || state.Count == byte.MaxValue;
+ }
+
+ public static void EmitEvent(AsyncThreadContext context, long currentTimestamp, ulong id, Continuation? asyncCallstack)
+ {
+ EmitEvent(context, currentTimestamp, AsyncEventID.ResumeAsyncCallstack, id, AsyncCallstackType.Runtime, asyncCallstack);
+ }
+
+ public static void EmitEvent(AsyncThreadContext context, long currentTimestamp, AsyncEventID eventID, ulong id, Continuation? asyncCallstack)
+ {
+ EmitEvent(context, currentTimestamp, eventID, id, AsyncCallstackType.Runtime, asyncCallstack);
+ }
+
+ public static void EmitEvent(AsyncThreadContext context, long currentTimestamp, AsyncEventID eventID, ulong id, AsyncCallstackType type, Continuation? asyncCallstack)
+ {
+ EmitEvent(context, currentTimestamp, currentTimestamp - context.LastEventTimestamp, eventID, id, type, asyncCallstack);
+ }
+
+ public static void EmitEvent(AsyncThreadContext context, long currentTimestamp, long delta, AsyncEventID eventID, ulong id, AsyncCallstackType type, Continuation? asyncCallstack)
+ {
+ if (asyncCallstack != null)
+ {
+ ref EventBuffer eventBuffer = ref context.EventBuffer;
+
+ // Max callstack data that can fit in the buffer after flush.
+ int maxCallstackBytes = Math.Min(
+ byte.MaxValue * MaxAsyncMethodFrameSize,
+ eventBuffer.Data.Length);
+
+ CaptureRuntimeAsyncCallstackState state = default;
+ state.Continuation = asyncCallstack;
+
+ // Static callstack payload: type (1) + callstackId (1) + frameCount (1) + id (max 10 bytes compressed).
+ const int MaxStaticEventPayloadSize = sizeof(byte) + sizeof(byte) + sizeof(byte) + Serializer.MaxCompressedUInt64Size;
+
+ if (Serializer.AsyncEventHeader(context, ref eventBuffer, currentTimestamp, delta, eventID, MaxStaticEventPayloadSize, out Serializer.AsyncEventHeaderRollbackData rollbackData))
+ {
+ int frameCountOffset = CallstackHeader(ref eventBuffer, id, type, 0);
+
+ byte[] buffer = eventBuffer.Data;
+ int startIndex = eventBuffer.Index;
+ int currentIndex = startIndex;
+
+ if (!CaptureRuntimeAsyncCallstack(buffer, ref currentIndex, ref state))
+ {
+ byte[]? rentedArray = RentArray(maxCallstackBytes);
+ if (rentedArray != null)
+ {
+ int length = currentIndex - startIndex;
+ int index = length;
+
+ Buffer.BlockCopy(buffer, startIndex, rentedArray, 0, length);
+ CaptureRuntimeAsyncCallstack(rentedArray, ref index, ref state);
+
+ // Rollback async event header before flushing.
+ Serializer.RollbackAsyncEventHeader(context, in rollbackData);
+ context.Flush();
+
+ // Write the callstack again.
+ if (Serializer.AsyncEventHeader(context, ref eventBuffer, context.LastEventTimestamp, 0, eventID, MaxStaticEventPayloadSize + index))
+ {
+ CallstackHeader(ref eventBuffer, id, type, state.Count);
+ CallstackData(ref eventBuffer, rentedArray, index);
+ }
+
+ ArrayPool.Shared.Return(rentedArray);
+ }
+ else
+ {
+ // Rollback async event header since we can't write the callstack.
+ Serializer.RollbackAsyncEventHeader(context, in rollbackData);
+ }
+ }
+ else
+ {
+ // Patch frame count in the event buffer using the offset from CallstackHeader.
+ eventBuffer.Data[frameCountOffset] = state.Count;
+ eventBuffer.Index += currentIndex - startIndex;
+ }
+ }
+ }
+ }
+
+ private static int CallstackHeader(ref EventBuffer eventBuffer, ulong id, AsyncCallstackType type, byte callstackFrameCount)
+ {
+ // Callstack header layout: type (1 byte) + callstackId (1 byte, reserved for future use) + frameCount (1 byte) + id (max 10 bytes compressed).
+ const int MaxCallstackHeaderSize = sizeof(byte) + sizeof(byte) + sizeof(byte) + Serializer.MaxCompressedUInt64Size;
+
+ ref int index = ref eventBuffer.Index;
+
+ Span callstackHeaderSpan = eventBuffer.Data.AsSpan(index, MaxCallstackHeaderSize);
+ int spanIndex = 0;
+
+ callstackHeaderSpan[spanIndex++] = (byte)type;
+ callstackHeaderSpan[spanIndex++] = 0; // Reserved callstack ID for future callstack interning.
+
+ int frameCountOffset = index + spanIndex;
+ callstackHeaderSpan[spanIndex++] = callstackFrameCount;
+
+ spanIndex += Serializer.WriteCompressedUInt64(callstackHeaderSpan.Slice(spanIndex), id);
+ eventBuffer.Index += spanIndex;
+
+ return frameCountOffset;
+ }
+
+ private static void CallstackData(ref EventBuffer eventBuffer, byte[] callstackData, int callstackDataByteCount)
+ {
+ ref int index = ref eventBuffer.Index;
+ Buffer.BlockCopy(callstackData, 0, eventBuffer.Data, index, callstackDataByteCount);
+ index += callstackDataByteCount;
+ }
+
+ private static byte[]? RentArray(int minimumLength)
+ {
+ byte[]? rentedArray = null;
+ try
+ {
+ rentedArray = ArrayPool.Shared.Rent(minimumLength);
+ }
+ catch
+ {
+ //AsyncProfiler can't throw, return null if renting fails.
+ }
+
+ return rentedArray;
+ }
+ }
+ }
+}
diff --git a/src/coreclr/nativeaot/System.Private.CoreLib/src/System.Private.CoreLib.csproj b/src/coreclr/nativeaot/System.Private.CoreLib/src/System.Private.CoreLib.csproj
index d120f10da497ac..d8fa2461b4e64a 100644
--- a/src/coreclr/nativeaot/System.Private.CoreLib/src/System.Private.CoreLib.csproj
+++ b/src/coreclr/nativeaot/System.Private.CoreLib/src/System.Private.CoreLib.csproj
@@ -64,6 +64,7 @@
+
diff --git a/src/libraries/System.Diagnostics.Tracing/tests/BasicEventSourceTest/TestUtilities.cs b/src/libraries/System.Diagnostics.Tracing/tests/BasicEventSourceTest/TestUtilities.cs
index 9de8946d9275ce..14a8b96a6a3d25 100644
--- a/src/libraries/System.Diagnostics.Tracing/tests/BasicEventSourceTest/TestUtilities.cs
+++ b/src/libraries/System.Diagnostics.Tracing/tests/BasicEventSourceTest/TestUtilities.cs
@@ -33,6 +33,7 @@ public static void CheckNoEventSourcesRunning(string message = "")
eventSource.Name != "System.Runtime" &&
eventSource.Name != "System.Diagnostics.Metrics" &&
eventSource.Name != "Microsoft-Diagnostics-DiagnosticSource" &&
+ eventSource.Name != "System.Runtime.CompilerServices.AsyncProfilerEventSource" &&
// event source from xunit runner
eventSource.Name != "xUnit.TestEventSource" &&
diff --git a/src/libraries/System.Private.CoreLib/src/System.Private.CoreLib.Shared.projitems b/src/libraries/System.Private.CoreLib/src/System.Private.CoreLib.Shared.projitems
index 2c48c4cba7ffd1..1874075bdabba7 100644
--- a/src/libraries/System.Private.CoreLib/src/System.Private.CoreLib.Shared.projitems
+++ b/src/libraries/System.Private.CoreLib/src/System.Private.CoreLib.Shared.projitems
@@ -841,7 +841,7 @@
-
+
@@ -942,6 +942,8 @@
+
+
diff --git a/src/libraries/System.Private.CoreLib/src/System/Runtime/CompilerServices/AsyncInstrumentation.cs b/src/libraries/System.Private.CoreLib/src/System/Runtime/CompilerServices/AsyncInstrumentation.cs
index e04495632eb28b..c1d8a7f1c7deef 100644
--- a/src/libraries/System.Private.CoreLib/src/System/Runtime/CompilerServices/AsyncInstrumentation.cs
+++ b/src/libraries/System.Private.CoreLib/src/System/Runtime/CompilerServices/AsyncInstrumentation.cs
@@ -30,8 +30,8 @@ public enum Flags : uint
AsyncProfiler = 0x1000000,
AsyncDebugger = 0x2000000,
- // Bit 32 reserved for initialization state.
- Uninitialized = 0x80000000
+ // Bit 32 reserved for synchronization flag.
+ Synchronize = 0x80000000
}
public const Flags DefaultFlags =
@@ -49,7 +49,7 @@ public static class IsEnabled
public static bool ResumeAsyncMethod(Flags flags) => (Flags.ResumeAsyncMethod & flags) != 0;
public static bool CompleteAsyncMethod(Flags flags) => (Flags.CompleteAsyncMethod & flags) != 0;
public static bool AsyncProfiler(Flags flags) => (Flags.AsyncProfiler & flags) != 0;
- public static bool AsyncDebugger(Flags flags) => (Flags.AsyncDebugger & flags) != 0 && Task.s_asyncDebuggingEnabled;
+ public static bool AsyncDebugger(Flags flags) => (Flags.AsyncDebugger & flags) != 0;
}
public static Flags ActiveFlags => s_activeFlags;
@@ -58,9 +58,9 @@ public static class IsEnabled
public static Flags SyncActiveFlags()
{
Flags flags = s_activeFlags;
- if (IsUninitialized(flags))
+ if ((flags & Flags.Synchronize) != 0)
{
- return InitializeFlags();
+ return SynchronizeFlags();
}
return flags;
}
@@ -75,55 +75,32 @@ public static void UpdateAsyncProfilerFlags(Flags asyncProfilerFlags)
lock (s_lock)
{
s_asyncProfilerActiveFlags = asyncProfilerFlags;
- if (IsInitialized(s_activeFlags))
- {
- s_activeFlags = s_asyncProfilerActiveFlags | s_asyncDebuggerActiveFlags;
- }
- }
- }
-
- public static void UpdateAsyncDebuggerFlags(Flags asyncDebuggerFlags)
- {
- if (asyncDebuggerFlags != Flags.Disabled)
- {
- asyncDebuggerFlags |= Flags.AsyncDebugger;
- }
-
- lock (s_lock)
- {
- s_asyncDebuggerActiveFlags = asyncDebuggerFlags;
- if (IsInitialized(s_activeFlags))
- {
- s_activeFlags = s_asyncProfilerActiveFlags | s_asyncDebuggerActiveFlags;
- }
+ s_activeFlags |= Flags.Synchronize;
}
}
- private static Flags InitializeFlags()
+ private static Flags SynchronizeFlags()
{
_ = TplEventSource.Log; // Touch TplEventSource to trigger static constructor which will initialize TPL flags if EventSource is supported.
+ _ = AsyncProfilerEventSource.Log; // Touch AsyncProfilerEventSource to trigger static constructor which will initialize async profiler flags if EventSource is supported.
lock (s_lock)
{
- if (IsUninitialized(s_activeFlags))
+ Flags asyncDebuggerActiveFlags = Flags.Disabled;
+ if (Task.s_asyncDebuggingEnabled)
{
- s_activeFlags = s_asyncProfilerActiveFlags | s_asyncDebuggerActiveFlags;
+ asyncDebuggerActiveFlags = DefaultFlags | Flags.AsyncDebugger;
}
+ s_activeFlags = (s_asyncProfilerActiveFlags | asyncDebuggerActiveFlags) & ~Flags.Synchronize;
return s_activeFlags;
}
}
- private static bool IsInitialized(Flags flags) => !IsUninitialized(flags);
-
- private static bool IsUninitialized(Flags flags) => (flags & Flags.Uninitialized) != 0;
-
- private static Flags s_activeFlags = Flags.Uninitialized;
+ private static Flags s_activeFlags = Flags.Synchronize;
private static Flags s_asyncProfilerActiveFlags;
- private static Flags s_asyncDebuggerActiveFlags;
-
private static readonly Lock s_lock = new();
}
}
diff --git a/src/libraries/System.Private.CoreLib/src/System/Runtime/CompilerServices/AsyncProfiler.cs b/src/libraries/System.Private.CoreLib/src/System/Runtime/CompilerServices/AsyncProfiler.cs
new file mode 100644
index 00000000000000..65bcd8eb835515
--- /dev/null
+++ b/src/libraries/System.Private.CoreLib/src/System/Runtime/CompilerServices/AsyncProfiler.cs
@@ -0,0 +1,1144 @@
+// Licensed to the .NET Foundation under one or more agreements.
+// The .NET Foundation licenses this file to you under the MIT license.
+
+using System.Buffers.Binary;
+using System.Collections.Generic;
+using System.Diagnostics;
+using System.Diagnostics.Tracing;
+using System.Runtime.InteropServices;
+using System.Threading;
+using static System.Runtime.CompilerServices.AsyncProfilerEventSource;
+using Serializer = System.Runtime.CompilerServices.AsyncProfiler.EventBuffer.Serializer;
+
+namespace System.Runtime.CompilerServices
+{
+ internal static partial class AsyncProfiler
+ {
+ [Flags]
+ public enum AsyncCallstackType : byte
+ {
+ Compiler = 0x1,
+ Runtime = 0x2,
+ Cached = 0x80
+ }
+
+ internal enum AsyncEventID : byte
+ {
+ CreateAsyncContext = 1,
+ ResumeAsyncContext = 2,
+ SuspendAsyncContext = 3,
+ CompleteAsyncContext = 4,
+ UnwindAsyncException = 5,
+ CreateAsyncCallstack = 6,
+ ResumeAsyncCallstack = 7,
+ SuspendAsyncCallstack = 8,
+ ResumeAsyncMethod = 9,
+ CompleteAsyncMethod = 10,
+ ResetAsyncThreadContext = 11,
+ ResetAsyncContinuationWrapperIndex = 12,
+ AsyncProfilerMetadata = 13,
+ AsyncProfilerSyncClock = 14
+ }
+
+ internal ref struct Info
+ {
+ public object? Context;
+ public ref nint ContinuationTable;
+ public uint ContinuationIndex;
+ }
+
+ internal static void InitInfo(ref Info info)
+ {
+ info.Context = null;
+ info.ContinuationIndex = 0;
+ ContinuationWrapper.InitInfo(ref info);
+ }
+
+ internal static partial class Config
+ {
+ public static readonly Lock ConfigLock = new();
+
+ public static bool Changed(AsyncThreadContext context) => context.ConfigRevision != Revision;
+
+ public static void Update(EventLevel logLevel, EventKeywords eventKeywords)
+ {
+ lock (ConfigLock)
+ {
+ Revision++;
+
+ ActiveEventKeywords = 0;
+ if (logLevel == EventLevel.LogAlways || logLevel >= EventLevel.Informational)
+ {
+ ActiveEventKeywords = eventKeywords;
+ }
+
+ string? eventBufferSizeEnv = System.Environment.GetEnvironmentVariable("DOTNET_AsyncProfilerEventSource_EventBufferSize");
+ if (eventBufferSizeEnv != null && uint.TryParse(eventBufferSizeEnv, out uint eventBufferSize))
+ {
+ eventBufferSize = Math.Max(eventBufferSize, 1024);
+ EventBufferSize = Math.Min(eventBufferSize, 64 * 1024 - 256);
+ }
+
+ if (IsEnabled.AnyAsyncEvents(ActiveEventKeywords))
+ {
+ AsyncThreadContextCache.EnableFlushTimer();
+ AsyncThreadContextCache.DisableCleanupTimer();
+ }
+ else
+ {
+ AsyncThreadContextCache.DisableFlushTimer();
+ AsyncThreadContextCache.EnableCleanupTimer();
+ }
+
+ // Writer thread access both ActiveFlags and Revision without explicit acquire/release semantics,
+ // but Flags will be read before calling AcquireAsyncThreadContext that includes one volatile read
+ // acting as the load barrier for ActiveFlags and Revision.
+ Interlocked.MemoryBarrier();
+
+ UpdateFlags();
+ }
+ }
+
+ public static void EmitAsyncProfilerMetadataIfNeeded(AsyncThreadContext context)
+ {
+ if (s_metadataRevision != Revision)
+ {
+ lock (s_metadataRevisionLock)
+ {
+ if (s_metadataRevision != Revision)
+ {
+ long[] wrapperIPs = ContinuationWrapper.GetContinuationWrapperIPs();
+
+ // Metadata payload:
+ // [qpcFrequency (compressed uint64)]
+ // [qpcSync (compressed uint64)]
+ // [utcSync (compressed uint64)]
+ // [eventBufferSize (compressed uint32)]
+ // [wrapperCount byte]
+ // [wrapperIP0 (compressed uint64)] ... [wrapperIPn (compressed uint64)]
+ const int MaxStaticEventPayloadSize = Serializer.MaxCompressedUInt64Size + Serializer.MaxCompressedUInt64Size + Serializer.MaxCompressedUInt64Size + Serializer.MaxCompressedUInt32Size + 1;
+ int maxDynamicEventPayloadSize = wrapperIPs.Length * Serializer.MaxCompressedUInt64Size;
+
+ ref EventBuffer eventBuffer = ref context.EventBuffer;
+ if (Serializer.AsyncEventHeader(context, ref eventBuffer, AsyncEventID.AsyncProfilerMetadata, MaxStaticEventPayloadSize + maxDynamicEventPayloadSize))
+ {
+ SyncClock(out long utcTimeSync, out long qpcSync);
+
+ Span payloadSpan = eventBuffer.Data.AsSpan(eventBuffer.Index, MaxStaticEventPayloadSize + maxDynamicEventPayloadSize);
+ int payloadSpanIndex = 0;
+
+ payloadSpanIndex += Serializer.WriteCompressedUInt64(payloadSpan.Slice(payloadSpanIndex), (ulong)Stopwatch.Frequency);
+ payloadSpanIndex += Serializer.WriteCompressedUInt64(payloadSpan.Slice(payloadSpanIndex), (ulong)qpcSync);
+ payloadSpanIndex += Serializer.WriteCompressedUInt64(payloadSpan.Slice(payloadSpanIndex), (ulong)utcTimeSync);
+ payloadSpanIndex += Serializer.WriteCompressedUInt32(payloadSpan.Slice(payloadSpanIndex), EventBufferSize);
+
+ payloadSpan[payloadSpanIndex++] = (byte)wrapperIPs.Length;
+
+ for (int i = 0; i < wrapperIPs.Length; i++)
+ {
+ payloadSpanIndex += Serializer.WriteCompressedUInt64(payloadSpan.Slice(payloadSpanIndex), (ulong)wrapperIPs[i]);
+ }
+
+ eventBuffer.Index += payloadSpanIndex;
+
+ // Force flush to deliver event promptly.
+ context.Flush();
+
+ s_metadataRevision = Revision;
+ s_lastSyncClockEventTimestamp = Stopwatch.GetTimestamp();
+ }
+ }
+ }
+ }
+ }
+
+ public static void EmitSyncClockEventIfNeeded()
+ {
+ long currentTimestamp = Stopwatch.GetTimestamp();
+ if (s_lastSyncClockEventTimestamp == 0)
+ {
+ s_lastSyncClockEventTimestamp = currentTimestamp;
+ return;
+ }
+
+ if (currentTimestamp - s_lastSyncClockEventTimestamp < s_intervalBetweenSyncClockEvent)
+ {
+ return;
+ }
+
+ s_lastSyncClockEventTimestamp = currentTimestamp;
+
+ if (IsEnabled.AnyAsyncEvents(ActiveEventKeywords))
+ {
+ AsyncThreadContext transientContext = AsyncThreadContext.AcquireTransient();
+
+ // SyncClock payload:
+ // [qpcSync (compressed uint64)]
+ // [utcSync (compressed uint64)]
+ const int MaxEventPayloadSize = Serializer.MaxCompressedUInt64Size + Serializer.MaxCompressedUInt64Size;
+
+ ref EventBuffer eventBuffer = ref transientContext.EventBuffer;
+ if (Serializer.AsyncEventHeader(transientContext, ref eventBuffer, AsyncEventID.AsyncProfilerSyncClock, MaxEventPayloadSize))
+ {
+ SyncClock(out long utcTimeSync, out long qpcSync);
+
+ Span payloadSpan = eventBuffer.Data.AsSpan(eventBuffer.Index, MaxEventPayloadSize);
+ int payloadSpanIndex = 0;
+
+ payloadSpanIndex += Serializer.WriteCompressedUInt64(payloadSpan.Slice(payloadSpanIndex), (ulong)qpcSync);
+ payloadSpanIndex += Serializer.WriteCompressedUInt64(payloadSpan.Slice(payloadSpanIndex), (ulong)utcTimeSync);
+
+ eventBuffer.Index += payloadSpanIndex;
+
+ // Force flush to deliver event promptly.
+ transientContext.Flush();
+ }
+
+ AsyncThreadContext.Release(transientContext);
+ }
+ }
+
+ private static void SyncClock(out long utcTimeSync, out long qpcSync)
+ {
+ long qpcDiff = long.MaxValue;
+
+ utcTimeSync = 0;
+ qpcSync = 0;
+
+ // Run calibration loop to find the closest QPC timestamp to UTC timestamp.
+ // This is a best effort to minimize the max error between QPC and UTC timestamps.
+ for (int i = 0; i < 10; i++)
+ {
+ long qpc1 = Stopwatch.GetTimestamp();
+ long utcTime = DateTime.UtcNow.ToFileTimeUtc();
+ long qpc2 = Stopwatch.GetTimestamp();
+ long diff = qpc2 - qpc1;
+
+ if (diff < qpcDiff)
+ {
+ utcTimeSync = utcTime;
+ qpcSync = qpc1;
+ qpcDiff = diff;
+ }
+ }
+
+ // QPC and UTC clocks are not guaranteed to be perfectly linear, so this is a best effort to minimize the max error.
+ // Both QPC and DateTime.UtcNow should have a 100ns resolution (or better). If latency getting QPC and UTC time is small enough,
+ // the error introduced by non-linearity should be within 100ns.
+ qpcSync += qpcDiff / 2;
+ }
+
+ private static void UpdateFlags()
+ {
+ AsyncInstrumentation.Flags flags = AsyncInstrumentation.Flags.Disabled;
+ flags |= IsEnabled.CreateAsyncContextEvent(ActiveEventKeywords) || IsEnabled.CreateAsyncCallstackEvent(ActiveEventKeywords) ? AsyncInstrumentation.Flags.CreateAsyncContext : AsyncInstrumentation.Flags.Disabled;
+ flags |= IsEnabled.ResumeAsyncContextEvent(ActiveEventKeywords) || IsEnabled.ResumeAsyncCallstackEvent(ActiveEventKeywords) ? AsyncInstrumentation.Flags.ResumeAsyncContext : AsyncInstrumentation.Flags.Disabled;
+ flags |= IsEnabled.SuspendAsyncContextEvent(ActiveEventKeywords) || IsEnabled.SuspendAsyncCallstackEvent(ActiveEventKeywords) ? AsyncInstrumentation.Flags.SuspendAsyncContext : AsyncInstrumentation.Flags.Disabled;
+ flags |= IsEnabled.CompleteAsyncContextEvent(ActiveEventKeywords) ? AsyncInstrumentation.Flags.CompleteAsyncContext : AsyncInstrumentation.Flags.Disabled;
+ flags |= IsEnabled.UnwindAsyncExceptionEvent(ActiveEventKeywords) ? AsyncInstrumentation.Flags.UnwindAsyncException : AsyncInstrumentation.Flags.Disabled;
+ flags |= IsEnabled.ResumeAsyncMethodEvent(ActiveEventKeywords) ? AsyncInstrumentation.Flags.ResumeAsyncMethod : AsyncInstrumentation.Flags.Disabled;
+ flags |= IsEnabled.CompleteAsyncMethodEvent(ActiveEventKeywords) ? AsyncInstrumentation.Flags.CompleteAsyncMethod : AsyncInstrumentation.Flags.Disabled;
+
+ AsyncInstrumentation.UpdateAsyncProfilerFlags(flags);
+ }
+
+ public static void CaptureState()
+ {
+ AsyncThreadContextCache.Flush(true);
+ }
+
+ public static EventKeywords ActiveEventKeywords { get; private set; }
+
+ public static uint Revision { get; private set; }
+
+ // Use 16KB - 256 event buffer as default. 256 bytes reserved for event header.
+ // 16KB events pack cleanly into a 64KB ETW/EventPipe/UserEvents buffer.
+ public static uint EventBufferSize { get; private set; } = 16 * 1024 - 256;
+
+ private static readonly Lock s_metadataRevisionLock = new();
+
+ private static uint s_metadataRevision;
+
+ private static long s_lastSyncClockEventTimestamp;
+
+ private static readonly long s_intervalBetweenSyncClockEvent = Stopwatch.Frequency * 60; // 1 minute
+ }
+
+ internal struct EventBuffer
+ {
+ public byte[] Data;
+
+ public int Index;
+
+ public uint EventCount;
+
+ public static class Serializer
+ {
+ public const int MaxCompressedUInt32Size = 5;
+ public const int MaxCompressedInt32Size = 5;
+ public const int MaxCompressedUInt64Size = 10;
+ public const int MaxCompressedInt64Size = 10;
+ public const int MaxEventHeaderSize = 37;
+ public const int MaxAsyncEventHeaderSize = 11;
+
+ public ref struct AsyncEventHeaderRollbackData
+ {
+ public int Index;
+ public uint EventCount;
+ public long LastEventTimestamp;
+ }
+
+ [MethodImpl(MethodImplOptions.AggressiveInlining)]
+ public static int WriteCompressedInt32(Span buffer, int value)
+ {
+ return WriteCompressedUInt32(buffer, ZigzagEncodeInt32(value));
+ }
+
+ [MethodImpl(MethodImplOptions.AggressiveInlining)]
+ public static int WriteCompressedUInt32(Span buffer, uint value)
+ {
+ if (buffer.Length < MaxCompressedUInt32Size)
+ {
+ ThrowHelper.ThrowArgumentOutOfRangeException(ExceptionArgument.length);
+ }
+
+ ref byte dst = ref MemoryMarshal.GetReference(buffer);
+ int index = 0;
+
+ while (value > 0x7Fu)
+ {
+ Unsafe.Add(ref dst, index++) = (byte)((uint)value | ~0x7Fu);
+ value >>= 7;
+ }
+
+ Unsafe.Add(ref dst, index++) = (byte)value;
+ return index;
+ }
+
+ [MethodImpl(MethodImplOptions.AggressiveInlining)]
+ public static int WriteCompressedInt64(Span buffer, long value)
+ {
+ return WriteCompressedUInt64(buffer, ZigzagEncodeInt64(value));
+ }
+
+ [MethodImpl(MethodImplOptions.AggressiveInlining)]
+ public static int WriteCompressedUInt64(Span buffer, ulong value)
+ {
+ if (buffer.Length < MaxCompressedUInt64Size)
+ {
+ ThrowHelper.ThrowArgumentOutOfRangeException(ExceptionArgument.length);
+ }
+
+ ref byte dst = ref MemoryMarshal.GetReference(buffer);
+ int index = 0;
+
+ while (value > 0x7Fu)
+ {
+ Unsafe.Add(ref dst, index++) = (byte)((uint)value | ~0x7Fu);
+ value >>= 7;
+ }
+
+ Unsafe.Add(ref dst, index++) = (byte)value;
+ return index;
+ }
+
+ public static uint ZigzagEncodeInt32(int value) => (uint)((value << 1) ^ (value >> 31));
+
+ public static ulong ZigzagEncodeInt64(long value) => (ulong)((value << 1) ^ (value >> 63));
+
+ public static void Header(AsyncThreadContext context, ref EventBuffer eventBuffer)
+ {
+ long currentTimestamp = Stopwatch.GetTimestamp();
+
+ eventBuffer.Index = 0;
+ eventBuffer.EventCount = 0;
+ context.LastEventTimestamp = currentTimestamp;
+
+ Span headerSpan = eventBuffer.Data.AsSpan(0, MaxEventHeaderSize);
+ int headerSpanIndex = 0;
+
+ // Version
+ headerSpan[headerSpanIndex++] = 1;
+
+ // Total size in bytes, will be updated on flush.
+ BinaryPrimitives.WriteUInt32LittleEndian(headerSpan.Slice(headerSpanIndex), 0);
+ headerSpanIndex += sizeof(uint);
+
+ // Async Thread Context ID
+ BinaryPrimitives.WriteUInt32LittleEndian(headerSpan.Slice(headerSpanIndex), context.AsyncThreadContextId);
+ headerSpanIndex += sizeof(uint);
+
+ // OS Thread ID
+ BinaryPrimitives.WriteUInt64LittleEndian(headerSpan.Slice(headerSpanIndex), context.OsThreadId);
+ headerSpanIndex += sizeof(ulong);
+
+ // Total event count, will be updated on flush.
+ BinaryPrimitives.WriteUInt32LittleEndian(headerSpan.Slice(headerSpanIndex), 0);
+ headerSpanIndex += sizeof(uint);
+
+ // Start timestamp
+ BinaryPrimitives.WriteUInt64LittleEndian(headerSpan.Slice(headerSpanIndex), (ulong)currentTimestamp);
+ headerSpanIndex += sizeof(ulong);
+
+ // End timestamp, will be updated on flush.
+ BinaryPrimitives.WriteUInt64LittleEndian(headerSpan.Slice(headerSpanIndex), 0);
+ headerSpanIndex += sizeof(ulong);
+
+ eventBuffer.Index = headerSpanIndex;
+ }
+
+ public static bool AsyncEventHeader(AsyncThreadContext context, ref EventBuffer eventBuffer, AsyncEventID eventID, int maxEventPayloadSize)
+ {
+ long currentTimestamp = Stopwatch.GetTimestamp();
+ long delta = currentTimestamp - context.LastEventTimestamp;
+ return AsyncEventHeader(context, ref eventBuffer, currentTimestamp, delta, eventID, maxEventPayloadSize);
+ }
+
+ public static bool AsyncEventHeader(AsyncThreadContext context, ref EventBuffer eventBuffer, long currentTimestamp, AsyncEventID eventID, int maxEventPayloadSize)
+ {
+ long delta = currentTimestamp - context.LastEventTimestamp;
+ return AsyncEventHeader(context, ref eventBuffer, currentTimestamp, delta, eventID, maxEventPayloadSize);
+ }
+
+ public static bool AsyncEventHeader(AsyncThreadContext context, ref EventBuffer eventBuffer, long currentTimestamp, long delta, AsyncEventID eventID, int maxEventPayloadSize, out AsyncEventHeaderRollbackData rollbackData)
+ {
+ byte[] buffer = eventBuffer.Data;
+ int index = eventBuffer.Index;
+ long previousTimestamp = context.LastEventTimestamp;
+
+ if ((index + MaxAsyncEventHeaderSize + maxEventPayloadSize) <= buffer.Length && delta >= 0)
+ {
+ context.LastEventTimestamp = currentTimestamp;
+ }
+ else
+ {
+ // Event is too big for buffer, drop it.
+ if (MaxAsyncEventHeaderSize + maxEventPayloadSize > buffer.Length)
+ {
+ rollbackData = default;
+ return false;
+ }
+
+ context.Flush();
+
+ previousTimestamp = context.LastEventTimestamp;
+ delta = 0;
+ index = eventBuffer.Index;
+ }
+
+ // Capture state after potential flush but before writing the header.
+ rollbackData = new AsyncEventHeaderRollbackData
+ {
+ Index = index,
+ EventCount = eventBuffer.EventCount,
+ LastEventTimestamp = previousTimestamp,
+ };
+
+ Span headerSpan = buffer.AsSpan(index, MaxAsyncEventHeaderSize);
+ int headerSpanIndex = 0;
+
+ headerSpan[headerSpanIndex++] = (byte)eventID; // eventID
+ headerSpanIndex += WriteCompressedUInt64(headerSpan.Slice(headerSpanIndex), (ulong)delta); // Timestamp delta from last event
+
+ eventBuffer.Index += headerSpanIndex;
+ eventBuffer.EventCount++;
+
+ return true;
+ }
+
+ public static bool AsyncEventHeader(AsyncThreadContext context, ref EventBuffer eventBuffer, long currentTimestamp, long delta, AsyncEventID eventID, int maxEventPayloadSize)
+ {
+ byte[] buffer = eventBuffer.Data;
+ int index = eventBuffer.Index;
+
+ if ((index + MaxAsyncEventHeaderSize + maxEventPayloadSize) <= buffer.Length && delta >= 0)
+ {
+ context.LastEventTimestamp = currentTimestamp;
+ }
+ else
+ {
+ // Event is too big for buffer, drop it.
+ if (MaxAsyncEventHeaderSize + maxEventPayloadSize > buffer.Length)
+ {
+ return false;
+ }
+
+ context.Flush();
+
+ delta = 0;
+ index = eventBuffer.Index;
+ }
+
+ Span headerSpan = buffer.AsSpan(index, MaxAsyncEventHeaderSize);
+ int headerSpanIndex = 0;
+
+ headerSpan[headerSpanIndex++] = (byte)eventID; // eventID
+ headerSpanIndex += WriteCompressedUInt64(headerSpan.Slice(headerSpanIndex), (ulong)delta); // Timestamp delta from last event
+
+ eventBuffer.Index += headerSpanIndex;
+ eventBuffer.EventCount++;
+
+ return true;
+ }
+
+ public static void RollbackAsyncEventHeader(AsyncThreadContext context, in AsyncEventHeaderRollbackData rollbackData)
+ {
+ ref EventBuffer eventBuffer = ref context.EventBuffer;
+ eventBuffer.Index = rollbackData.Index;
+ eventBuffer.EventCount = rollbackData.EventCount;
+ context.LastEventTimestamp = rollbackData.LastEventTimestamp;
+ }
+ }
+ }
+
+ internal sealed class AsyncThreadContext
+ {
+ private static uint s_nextAsyncThreadContextId;
+
+ public AsyncThreadContext()
+ {
+ _eventBuffer.Data = Array.Empty();
+ AsyncThreadContextId = Interlocked.Increment(ref s_nextAsyncThreadContextId);
+ OsThreadId = Thread.CurrentOSThreadId;
+ }
+
+ private EventBuffer _eventBuffer;
+
+ public long LastEventTimestamp;
+
+ public EventKeywords ActiveEventKeywords;
+
+ public readonly ulong OsThreadId;
+
+ public readonly uint AsyncThreadContextId;
+
+ public uint ConfigRevision;
+
+ public volatile bool InUse;
+
+ public volatile bool BlockContext;
+
+ public ref EventBuffer EventBuffer
+ {
+ [MethodImpl(MethodImplOptions.AggressiveInlining)]
+ get
+ {
+ if (_eventBuffer.Data.Length == 0)
+ {
+ InitializeBuffer();
+ }
+
+ Debug.Assert(InUse || BlockContext);
+ return ref _eventBuffer;
+ }
+ }
+
+ [MethodImpl(MethodImplOptions.AggressiveInlining)]
+ public static AsyncThreadContext Acquire(ref Info info)
+ {
+ AsyncThreadContext context = Get(ref info);
+ Debug.Assert(!context.InUse);
+
+ context.InUse = true;
+ if (context.BlockContext)
+ {
+ WaitOnBlockedAsyncThreadContext(context);
+ }
+
+ return context;
+ }
+
+ [MethodImpl(MethodImplOptions.AggressiveInlining)]
+ public static void Release(AsyncThreadContext context)
+ {
+ Debug.Assert(context.InUse);
+ context.InUse = false;
+ }
+
+ [MethodImpl(MethodImplOptions.AggressiveInlining)]
+ public static AsyncThreadContext Get()
+ {
+ AsyncThreadContext? context = t_asyncThreadContext;
+ if (context != null)
+ {
+ return context;
+ }
+
+ return CreateAsyncThreadContext();
+ }
+
+ [MethodImpl(MethodImplOptions.AggressiveInlining)]
+ public static AsyncThreadContext Get(ref Info info)
+ {
+ Debug.Assert(info.Context == null || info.Context is AsyncThreadContext);
+
+ AsyncThreadContext? context = Unsafe.As(info.Context);
+ if (context != null)
+ {
+ return context;
+ }
+
+ return GetAsyncThreadContext(ref info);
+ }
+
+ public static AsyncThreadContext AcquireTransient()
+ {
+ AsyncThreadContext? context;
+ if (t_asyncThreadContext != null)
+ {
+ context = Get();
+ Debug.Assert(!context.InUse);
+ }
+ else
+ {
+ context = new AsyncThreadContext();
+ context.ConfigRevision = Config.Revision;
+ context.ActiveEventKeywords = Config.ActiveEventKeywords;
+ }
+
+ context.InUse = true;
+ if (context.BlockContext)
+ {
+ WaitOnBlockedAsyncThreadContext(context);
+ }
+
+ return context;
+ }
+
+ public void Reclaim()
+ {
+ Debug.Assert(InUse || BlockContext);
+
+ _eventBuffer.Data = Array.Empty();
+ _eventBuffer.Index = 0;
+ _eventBuffer.EventCount = 0;
+ }
+
+ public void Flush()
+ {
+ Debug.Assert(InUse || BlockContext);
+
+ if (_eventBuffer.EventCount == 0)
+ {
+ return;
+ }
+
+ ref EventBuffer eventBuffer = ref EventBuffer;
+
+ Span headerSpan = eventBuffer.Data.AsSpan(0, Serializer.MaxEventHeaderSize);
+
+ int spanIndex = 1; // Skip version
+
+ // Fill in total size in header before flushing.
+ BinaryPrimitives.WriteUInt32LittleEndian(headerSpan.Slice(spanIndex), (uint)eventBuffer.Index);
+ spanIndex += sizeof(uint);
+
+ spanIndex += sizeof(uint) + sizeof(ulong); // Skip AsyncThreadContextId and OSThreadId
+
+ // Fill in event count in header before flushing.
+ BinaryPrimitives.WriteUInt32LittleEndian(headerSpan.Slice(spanIndex), eventBuffer.EventCount);
+ spanIndex += sizeof(uint);
+
+ spanIndex += sizeof(ulong); // Skip start timestamp
+
+ // Fill in end timestamp in header before flushing.
+ BinaryPrimitives.WriteUInt64LittleEndian(headerSpan.Slice(spanIndex), (ulong)LastEventTimestamp);
+
+ try
+ {
+ Log.AsyncEvents(eventBuffer.Data.AsSpan(0, eventBuffer.Index));
+ }
+ catch
+ {
+ // AsyncProfiler can't throw, ignore exception and lose buffer.
+ }
+
+ Serializer.Header(this, ref eventBuffer);
+ }
+
+ [MethodImpl(MethodImplOptions.NoInlining)]
+ private void InitializeBuffer()
+ {
+ try
+ {
+ _eventBuffer.Data = new byte[Config.EventBufferSize];
+ Serializer.Header(this, ref _eventBuffer);
+ }
+ catch
+ {
+ // Async Profiler can't throw, ignore exception and use empty buffer.
+ // This will cause event to drop and attempt to reallocate buffer on next event.
+ _eventBuffer.Data = Array.Empty();
+ }
+ }
+
+ [MethodImpl(MethodImplOptions.NoInlining)]
+ private static void WaitOnBlockedAsyncThreadContext(AsyncThreadContext context)
+ {
+ context.InUse = false;
+ // Intentionally acquire and release CacheLock to wait for the flush thread
+ // to finish any work that is currently synchronized on this lock.
+ lock (AsyncThreadContextCache.CacheLock) { }
+ context.InUse = true;
+ }
+
+ [MethodImpl(MethodImplOptions.NoInlining)]
+ private static AsyncThreadContext GetAsyncThreadContext(ref Info info)
+ {
+ AsyncThreadContext context = Get();
+ info.Context = t_asyncThreadContext;
+ return context;
+ }
+
+ [MethodImpl(MethodImplOptions.NoInlining)]
+ private static AsyncThreadContext CreateAsyncThreadContext()
+ {
+ AsyncThreadContext context = new AsyncThreadContext();
+ AsyncThreadContextCache.Add(context);
+ t_asyncThreadContext = context;
+ return context;
+ }
+
+ [ThreadStatic]
+ private static AsyncThreadContext? t_asyncThreadContext;
+ }
+
+ internal static partial class CreateAsyncContext
+ {
+ public static void EmitEvent(AsyncThreadContext context, long currentTimestamp, ulong id)
+ {
+ const int MaxEventPayloadSize = Serializer.MaxCompressedUInt64Size;
+
+ ref EventBuffer eventBuffer = ref context.EventBuffer;
+ if (Serializer.AsyncEventHeader(context, ref eventBuffer, currentTimestamp, AsyncEventID.CreateAsyncContext, MaxEventPayloadSize))
+ {
+ eventBuffer.Index += Serializer.WriteCompressedUInt64(eventBuffer.Data.AsSpan(eventBuffer.Index, MaxEventPayloadSize), id);
+ }
+ }
+ }
+
+ internal static partial class ResumeAsyncContext
+ {
+ public static void EmitEvent(AsyncThreadContext context, long currentTimestamp, ulong id)
+ {
+ const int MaxEventPayloadSize = Serializer.MaxCompressedUInt64Size;
+
+ ref EventBuffer eventBuffer = ref context.EventBuffer;
+ if (Serializer.AsyncEventHeader(context, ref eventBuffer, currentTimestamp, AsyncEventID.ResumeAsyncContext, MaxEventPayloadSize))
+ {
+ eventBuffer.Index += Serializer.WriteCompressedUInt64(eventBuffer.Data.AsSpan(eventBuffer.Index, MaxEventPayloadSize), id);
+ }
+ }
+ }
+
+ internal static partial class SuspendAsyncContext
+ {
+ public static void EmitEvent(AsyncThreadContext context, long currentTimestamp)
+ {
+ Serializer.AsyncEventHeader(context, ref context.EventBuffer, currentTimestamp, AsyncEventID.SuspendAsyncContext, 0);
+ }
+ }
+
+ internal static partial class CompleteAsyncContext
+ {
+ public static void Complete(ref Info info)
+ {
+ AsyncThreadContext context = AsyncThreadContext.Acquire(ref info);
+
+ SyncPoint.Check(context);
+
+ if (IsEnabled.CompleteAsyncContextEvent(context.ActiveEventKeywords))
+ {
+ EmitEvent(context, Stopwatch.GetTimestamp());
+ }
+
+ AsyncThreadContext.Release(context);
+ }
+
+ public static void EmitEvent(AsyncThreadContext context, long currentTimestamp)
+ {
+ Serializer.AsyncEventHeader(context, ref context.EventBuffer, currentTimestamp, AsyncEventID.CompleteAsyncContext, 0);
+ }
+ }
+
+ internal static partial class AsyncMethodException
+ {
+ public static void Unhandled(ref Info info, uint unwindedFrames)
+ {
+ AsyncThreadContext context = AsyncThreadContext.Acquire(ref info);
+
+ SyncPoint.Check(context);
+
+ EventKeywords activeEventKeywords = context.ActiveEventKeywords;
+ if (IsEnabled.AnyAsyncEvents(activeEventKeywords))
+ {
+ long currentTimestamp = Stopwatch.GetTimestamp();
+ if (IsEnabled.UnwindAsyncExceptionEvent(activeEventKeywords))
+ {
+ EmitEvent(context, currentTimestamp, unwindedFrames);
+ }
+
+ if (IsEnabled.CompleteAsyncContextEvent(activeEventKeywords))
+ {
+ CompleteAsyncContext.EmitEvent(context, currentTimestamp);
+ }
+ }
+
+ AsyncThreadContext.Release(context);
+ }
+
+ public static void Handled(ref Info info, uint unwindedFrames)
+ {
+ AsyncThreadContext context = AsyncThreadContext.Acquire(ref info);
+
+ SyncPoint.Check(context);
+ if (IsEnabled.UnwindAsyncExceptionEvent(context.ActiveEventKeywords))
+ {
+ EmitEvent(context, Stopwatch.GetTimestamp(), unwindedFrames);
+ }
+
+ AsyncThreadContext.Release(context);
+ }
+
+ public static void EmitEvent(AsyncThreadContext context, long currentTimestamp, uint unwindedFrames)
+ {
+ // unwinded frames
+ const int MaxEventPayloadSize = Serializer.MaxCompressedUInt32Size;
+
+ ref EventBuffer eventBuffer = ref context.EventBuffer;
+ if (Serializer.AsyncEventHeader(context, ref eventBuffer, currentTimestamp, AsyncEventID.UnwindAsyncException, MaxEventPayloadSize))
+ {
+ eventBuffer.Index += Serializer.WriteCompressedUInt32(eventBuffer.Data.AsSpan(eventBuffer.Index, MaxEventPayloadSize), unwindedFrames);
+ }
+ }
+ }
+
+ internal static partial class ResumeAsyncMethod
+ {
+ public static void Resume(ref Info info)
+ {
+ AsyncThreadContext context = AsyncThreadContext.Acquire(ref info);
+
+ SyncPoint.Check(context);
+ if (IsEnabled.ResumeAsyncMethodEvent(context.ActiveEventKeywords))
+ {
+ EmitEvent(context);
+ }
+
+ AsyncThreadContext.Release(context);
+ }
+
+ public static void EmitEvent(AsyncThreadContext context)
+ {
+ Serializer.AsyncEventHeader(context, ref context.EventBuffer, AsyncEventID.ResumeAsyncMethod, 0);
+ }
+ }
+
+ internal static partial class CompleteAsyncMethod
+ {
+ public static void Complete(ref Info info)
+ {
+ AsyncThreadContext context = AsyncThreadContext.Acquire(ref info);
+
+ SyncPoint.Check(context);
+ if (IsEnabled.CompleteAsyncMethodEvent(context.ActiveEventKeywords))
+ {
+ EmitEvent(context);
+ }
+
+ AsyncThreadContext.Release(context);
+ }
+
+ public static void EmitEvent(AsyncThreadContext context)
+ {
+ Serializer.AsyncEventHeader(context, ref context.EventBuffer, AsyncEventID.CompleteAsyncMethod, 0);
+ }
+ }
+
+ internal static partial class ContinuationWrapper
+ {
+ public const byte COUNT = 32;
+ public const byte COUNT_MASK = COUNT - 1;
+
+ [MethodImpl(MethodImplOptions.AggressiveInlining)]
+ public static void IncrementIndex(ref Info info)
+ {
+ info.ContinuationIndex++;
+ if ((info.ContinuationIndex & COUNT_MASK) == 0)
+ {
+ ResetIndex(ref info);
+ }
+ }
+
+ public static void UnwindIndex(ref Info info, uint unwindedFrames)
+ {
+ uint oldIndex = info.ContinuationIndex;
+ info.ContinuationIndex += unwindedFrames;
+
+ if ((oldIndex & ~COUNT_MASK) != (info.ContinuationIndex & ~COUNT_MASK))
+ {
+ ResetIndex(ref info);
+ }
+ }
+
+ private static void ResetIndex(ref Info info)
+ {
+ AsyncThreadContext context = AsyncThreadContext.Acquire(ref info);
+
+ SyncPoint.Check(context);
+ if (IsEnabled.AnyAsyncEvents(context.ActiveEventKeywords))
+ {
+ EmitEvent(context);
+ }
+
+ AsyncThreadContext.Release(context);
+ }
+
+ private static void EmitEvent(AsyncThreadContext context)
+ {
+ Serializer.AsyncEventHeader(context, ref context.EventBuffer, AsyncEventID.ResetAsyncContinuationWrapperIndex, 0);
+ }
+ }
+
+ private static partial class SyncPoint
+ {
+ [MethodImpl(MethodImplOptions.AggressiveInlining)]
+ public static bool Check(AsyncThreadContext context)
+ {
+ if (Config.Changed(context))
+ {
+ ResetContext(context);
+ return true;
+ }
+ return false;
+ }
+
+ private static void ResetContext(AsyncThreadContext context)
+ {
+ context.Flush();
+
+ context.ConfigRevision = Config.Revision;
+ context.ActiveEventKeywords = Config.ActiveEventKeywords;
+
+ if (IsEnabled.AnyAsyncEvents(context.ActiveEventKeywords))
+ {
+ Config.EmitAsyncProfilerMetadataIfNeeded(context);
+ EmitEvent(context);
+ }
+
+ ResumeAsyncCallstacks(context);
+ }
+
+ private static void EmitEvent(AsyncThreadContext context)
+ {
+ Serializer.AsyncEventHeader(context, ref context.EventBuffer, AsyncEventID.ResetAsyncThreadContext, 0);
+ }
+ }
+
+ private static class IsEnabled
+ {
+ public static bool CreateAsyncContextEvent(EventKeywords eventKeywords) => (eventKeywords & Keywords.CreateAsyncContext) != 0;
+ public static bool ResumeAsyncContextEvent(EventKeywords eventKeywords) => (eventKeywords & Keywords.ResumeAsyncContext) != 0;
+ public static bool SuspendAsyncContextEvent(EventKeywords eventKeywords) => (eventKeywords & Keywords.SuspendAsyncContext) != 0;
+ public static bool CompleteAsyncContextEvent(EventKeywords eventKeywords) => (eventKeywords & Keywords.CompleteAsyncContext) != 0;
+ public static bool UnwindAsyncExceptionEvent(EventKeywords eventKeywords) => (eventKeywords & Keywords.UnwindAsyncException) != 0;
+ public static bool CreateAsyncCallstackEvent(EventKeywords eventKeywords) => (eventKeywords & Keywords.CreateAsyncCallstack) != 0;
+ public static bool ResumeAsyncCallstackEvent(EventKeywords eventKeywords) => (eventKeywords & Keywords.ResumeAsyncCallstack) != 0;
+ public static bool SuspendAsyncCallstackEvent(EventKeywords eventKeywords) => (eventKeywords & Keywords.SuspendAsyncCallstack) != 0;
+ public static bool ResumeAsyncMethodEvent(EventKeywords eventKeywords) => (eventKeywords & Keywords.ResumeAsyncMethod) != 0;
+ public static bool CompleteAsyncMethodEvent(EventKeywords eventKeywords) => (eventKeywords & Keywords.CompleteAsyncMethod) != 0;
+ public static bool AnyAsyncEvents(EventKeywords eventKeywords) => (eventKeywords & AsyncEventKeywords) != 0;
+ }
+
+ private static class AsyncThreadContextCache
+ {
+ public static Lock CacheLock { get; private set; } = new Lock();
+
+ public static void Add(AsyncThreadContext context)
+ {
+ AsyncThreadContextHolder contextHolder = new AsyncThreadContextHolder(context, Thread.CurrentThread);
+ lock (CacheLock)
+ {
+ s_cache.Add(contextHolder);
+ }
+ }
+
+ public static void Flush(bool force)
+ {
+ lock (CacheLock)
+ {
+ FlushCore(force);
+ }
+ }
+
+ public static void EnableFlushTimer()
+ {
+ lock (CacheLock)
+ {
+ s_flushTimer ??= new Timer(PeriodicFlush, null, Timeout.Infinite, Timeout.Infinite, false);
+ s_flushTimer.Change(AsyncThreadContextCacheFlushTimerIntervalMs, Timeout.Infinite);
+ }
+ }
+
+ public static void DisableFlushTimer()
+ {
+ lock (CacheLock)
+ {
+ s_flushTimer?.Change(Timeout.Infinite, Timeout.Infinite);
+ }
+ }
+
+ public static void EnableCleanupTimer()
+ {
+ lock (CacheLock)
+ {
+ s_cleanupTimer ??= new Timer(Cleanup, null, Timeout.Infinite, Timeout.Infinite, false);
+ s_cleanupTimer?.Change(AsyncThreadContextCacheCleanupTimerIntervalMs, Timeout.Infinite);
+ }
+ }
+
+ public static void DisableCleanupTimer()
+ {
+ lock (CacheLock)
+ {
+ s_cleanupTimer?.Change(Timeout.Infinite, Timeout.Infinite);
+ }
+ }
+
+ private static void Cleanup(object? state)
+ {
+ _ = state;
+
+ lock (CacheLock)
+ {
+ FlushCore(true);
+
+ if (s_cache.Count > 0)
+ {
+ // Restart cleanup timer.
+ s_cleanupTimer?.Change(AsyncThreadContextCacheCleanupTimerIntervalMs, Timeout.Infinite);
+ }
+ }
+ }
+
+ private static void PeriodicFlush(object? state)
+ {
+ _ = state;
+
+ lock (CacheLock)
+ {
+ FlushCore(false);
+
+ if (IsEnabled.AnyAsyncEvents(Config.ActiveEventKeywords))
+ {
+ // Restart flush timer.
+ s_flushTimer?.Change(AsyncThreadContextCacheFlushTimerIntervalMs, Timeout.Infinite);
+ }
+ else
+ {
+ // Start cleanup timer.
+ s_cleanupTimer?.Change(AsyncThreadContextCacheCleanupTimerIntervalMs, Timeout.Infinite);
+ }
+ }
+ }
+
+ private static void FlushCore(bool force)
+ {
+ // Make sure all dead threads are flushed and removed from the cache.
+ for (int i = s_cache.Count - 1; i >= 0; i--)
+ {
+ AsyncThreadContextHolder contextHolder = s_cache[i];
+ if (!contextHolder.OwnerThread.TryGetTarget(out Thread? target) || !target.IsAlive)
+ {
+ // Thread is dead, flush its buffer and remove from cache.
+ AsyncThreadContext context = contextHolder.Context;
+
+ Debug.Assert(!context.InUse);
+ context.InUse = true;
+
+ context.Flush();
+
+ context.Reclaim();
+ s_cache.RemoveAt(i);
+
+ context.InUse = false;
+ }
+ }
+
+ long frequency = Stopwatch.Frequency;
+
+ // Look at live threads, only flush if forced or contexts that have been idle for 250 milliseconds.
+ long idleWriteTimestamp = Stopwatch.GetTimestamp() - (frequency / 4);
+
+ // Additionally, reclaim buffers for contexts that have been idle for 30 seconds to avoid keeping
+ // large buffers around indefinitely for threads that are no longer running async code.
+ long idleReclaimBufferTimestamp = Stopwatch.GetTimestamp() - frequency * 30;
+
+ // Spin wait timeout, 100 milliseconds.
+ long spinWaitTimeout = frequency / 10;
+
+ foreach (AsyncThreadContextHolder contextHolder in s_cache)
+ {
+ AsyncThreadContext context = contextHolder.Context;
+
+ // Read LastEventTimestamp without atomics, could cause teared reads but not critical.
+ long lastEventWriteTimestamp = context.LastEventTimestamp;
+ if (force || lastEventWriteTimestamp < idleWriteTimestamp)
+ {
+ context.BlockContext = true;
+ SpinWait sw = default;
+ long timeout = Stopwatch.GetTimestamp() + spinWaitTimeout;
+ while (context.InUse)
+ {
+ sw.SpinOnce();
+ if (Stopwatch.GetTimestamp() > timeout)
+ {
+ // AsyncThreadContext has been busy for too long, skip flushing this time.
+ // NOTE, this should not happen under normal conditions, contexts are only
+ // held InUse for a very short time writing events. If this do happen then
+ // then write probably triggered a flush or thread have been preempted for
+ // a long time while holding the context. Either way, skipping flush this time
+ // should be ok, as the next flush will pick it up and flushing is best effort.
+ break;
+ }
+ }
+
+ if (!context.InUse)
+ {
+ context.Flush();
+
+ if (force || lastEventWriteTimestamp < idleReclaimBufferTimestamp)
+ {
+ context.Reclaim();
+ }
+ }
+
+ context.BlockContext = false;
+ }
+ }
+
+ Config.EmitSyncClockEventIfNeeded();
+ }
+
+ private sealed class AsyncThreadContextHolder
+ {
+ public AsyncThreadContextHolder(AsyncThreadContext context, Thread ownerThread)
+ {
+ Context = context;
+ OwnerThread = new WeakReference(ownerThread);
+ }
+
+ public readonly AsyncThreadContext Context;
+ public readonly WeakReference OwnerThread;
+ }
+
+ private const int AsyncThreadContextCacheFlushTimerIntervalMs = 1000;
+ private static Timer? s_flushTimer;
+
+ private const int AsyncThreadContextCacheCleanupTimerIntervalMs = 30000;
+ private static Timer? s_cleanupTimer;
+
+ private static List s_cache = new List();
+ }
+ }
+}
diff --git a/src/libraries/System.Private.CoreLib/src/System/Runtime/CompilerServices/AsyncProfilerEventSource.cs b/src/libraries/System.Private.CoreLib/src/System/Runtime/CompilerServices/AsyncProfilerEventSource.cs
new file mode 100644
index 00000000000000..7d61ce2f5cb75f
--- /dev/null
+++ b/src/libraries/System.Private.CoreLib/src/System/Runtime/CompilerServices/AsyncProfilerEventSource.cs
@@ -0,0 +1,99 @@
+// Licensed to the .NET Foundation under one or more agreements.
+// The .NET Foundation licenses this file to you under the MIT license.
+
+using System.Diagnostics.CodeAnalysis;
+using System.Diagnostics.Tracing;
+
+namespace System.Runtime.CompilerServices
+{
+ /// Provides an event source for tracing async execution.
+ [EventSource(Name = "System.Runtime.CompilerServices.AsyncProfilerEventSource")]
+ internal sealed partial class AsyncProfilerEventSource : EventSource
+ {
+ private const string EventSourceSuppressMessage = "Parameters to this method are primitive and are trimmer safe";
+
+ public static readonly AsyncProfilerEventSource Log = new AsyncProfilerEventSource();
+
+ public static class Keywords // this name is important for EventSource
+ {
+ public const EventKeywords CreateAsyncContext = (EventKeywords)0x1;
+ public const EventKeywords ResumeAsyncContext = (EventKeywords)0x2;
+ public const EventKeywords SuspendAsyncContext = (EventKeywords)0x4;
+ public const EventKeywords CompleteAsyncContext = (EventKeywords)0x8;
+ public const EventKeywords UnwindAsyncException = (EventKeywords)0x10;
+ public const EventKeywords CreateAsyncCallstack = (EventKeywords)0x20;
+ public const EventKeywords ResumeAsyncCallstack = (EventKeywords)0x40;
+ public const EventKeywords SuspendAsyncCallstack = (EventKeywords)0x80;
+ public const EventKeywords ResumeAsyncMethod = (EventKeywords)0x100;
+ public const EventKeywords CompleteAsyncMethod = (EventKeywords)0x200;
+ }
+
+ public const EventKeywords AsyncEventKeywords =
+ Keywords.CreateAsyncContext |
+ Keywords.ResumeAsyncContext |
+ Keywords.SuspendAsyncContext |
+ Keywords.CompleteAsyncContext |
+ Keywords.CreateAsyncCallstack |
+ Keywords.ResumeAsyncCallstack |
+ Keywords.SuspendAsyncCallstack |
+ Keywords.UnwindAsyncException |
+ Keywords.ResumeAsyncMethod |
+ Keywords.CompleteAsyncMethod;
+
+ public const int FlushCommand = 1;
+
+ //----------------------- Event IDs (must be unique) -----------------------
+ public const int ASYNC_EVENTS_ID = 1;
+
+ //-----------------------------------------------------------------------------------
+ //
+ // Events
+ //
+ [Event(
+ ASYNC_EVENTS_ID,
+ Version = 1,
+ Opcode = EventOpcode.Info,
+ Level = EventLevel.Informational,
+ Keywords = AsyncEventKeywords,
+ Message = "")]
+ public void AsyncEvents(byte[] buffer)
+ {
+ AsyncEvents(buffer.AsSpan());
+ }
+
+ [NonEvent]
+ [UnconditionalSuppressMessage("ReflectionAnalysis", "IL2026:UnrecognizedReflectionPattern", Justification = EventSourceSuppressMessage)]
+ public void AsyncEvents(ReadOnlySpan buffer)
+ {
+ unsafe
+ {
+ fixed (byte* pBuffer = buffer)
+ {
+ int length = buffer.Length;
+ EventData* eventPayload = stackalloc EventData[2];
+ eventPayload[0].Size = sizeof(int);
+ eventPayload[0].DataPointer = ((IntPtr)(&length));
+ eventPayload[0].Reserved = 0;
+ eventPayload[1].Size = sizeof(byte) * length;
+ eventPayload[1].DataPointer = length != 0 ? ((IntPtr)pBuffer) : ((IntPtr)(&length));
+ eventPayload[1].Reserved = 0;
+ WriteEventCore(ASYNC_EVENTS_ID, 2, eventPayload);
+ }
+ }
+ }
+
+ ///
+ /// Get callbacks when the ETW sends us commands
+ ///
+ protected override void OnEventCommand(EventCommandEventArgs command)
+ {
+ if (command.Command == (EventCommand)FlushCommand || command.Command == EventCommand.SendManifest)
+ {
+ AsyncProfiler.Config.CaptureState();
+ return;
+ }
+
+ AsyncProfiler.Config.Update(m_level, m_matchAnyKeyword);
+ }
+ }
+}
diff --git a/src/libraries/System.Private.CoreLib/src/System/Threading/Tasks/TplEventSource.cs b/src/libraries/System.Private.CoreLib/src/System/Threading/Tasks/TplEventSource.cs
index 89e537c74e92eb..90267e2eb8d031 100644
--- a/src/libraries/System.Private.CoreLib/src/System/Threading/Tasks/TplEventSource.cs
+++ b/src/libraries/System.Private.CoreLib/src/System/Threading/Tasks/TplEventSource.cs
@@ -34,26 +34,6 @@ protected override void OnEventCommand(EventCommandEventArgs command)
Debug = IsEnabled(EventLevel.Informational, Keywords.Debug);
DebugActivityId = IsEnabled(EventLevel.Informational, Keywords.DebugActivityId);
-
- // Until debugger explicitly set the AsyncInstrumentation keyword, we enable async instrumentation when
- // Tasks, AsyncCausalitySynchronousWork, AsyncCausalityOperation and TasksFlowActivityIds keywords are enabled.
- bool asyncInstrumentationEnabled = IsEnabled(EventLevel.Informational, Keywords.AsyncInstrumentation);
- if (!asyncInstrumentationEnabled)
- {
- asyncInstrumentationEnabled = IsEnabled(EventLevel.Informational, Keywords.Tasks);
- asyncInstrumentationEnabled &= IsEnabled(EventLevel.Informational, Keywords.AsyncCausalityOperation);
- asyncInstrumentationEnabled &= IsEnabled(EventLevel.Informational, Keywords.AsyncCausalitySynchronousWork);
- asyncInstrumentationEnabled &= IsEnabled(EventLevel.Informational, Keywords.TasksFlowActivityIds);
- }
-
- if (asyncInstrumentationEnabled)
- {
- AsyncInstrumentation.UpdateAsyncDebuggerFlags(AsyncInstrumentation.DefaultFlags);
- }
- else
- {
- AsyncInstrumentation.UpdateAsyncDebuggerFlags(AsyncInstrumentation.Flags.Disabled);
- }
}
///
diff --git a/src/libraries/System.Runtime/tests/System.Threading.Tasks.Tests/System.Runtime.CompilerServices/AsyncProfilerTests.cs b/src/libraries/System.Runtime/tests/System.Threading.Tasks.Tests/System.Runtime.CompilerServices/AsyncProfilerTests.cs
new file mode 100644
index 00000000000000..96c890c0475c74
--- /dev/null
+++ b/src/libraries/System.Runtime/tests/System.Threading.Tasks.Tests/System.Runtime.CompilerServices/AsyncProfilerTests.cs
@@ -0,0 +1,2306 @@
+// Licensed to the .NET Foundation under one or more agreements.
+// The .NET Foundation licenses this file to you under the MIT license.
+
+using System.Buffers.Binary;
+using System.Collections.Concurrent;
+using System.Collections.Generic;
+using System.Diagnostics;
+using System.Diagnostics.Tracing;
+using System.Reflection;
+using System.Linq;
+using Microsoft.DotNet.XUnitExtensions;
+using Xunit;
+
+namespace System.Threading.Tasks.Tests
+{
+ // Mirrors AsyncProfiler.AsyncEventID from the runtime (which is internal and inaccessible from tests).
+ public enum AsyncEventID : byte
+ {
+ CreateAsyncContext = 1,
+ ResumeAsyncContext = 2,
+ SuspendAsyncContext = 3,
+ CompleteAsyncContext = 4,
+ UnwindAsyncException = 5,
+ CreateAsyncCallstack = 6,
+ ResumeAsyncCallstack = 7,
+ SuspendAsyncCallstack = 8,
+ ResumeAsyncMethod = 9,
+ CompleteAsyncMethod = 10,
+ ResetAsyncThreadContext = 11,
+ ResetAsyncContinuationWrapperIndex = 12,
+ AsyncProfilerMetadata = 13,
+ AsyncProfilerSyncClock = 14,
+ }
+
+ public class AsyncProfilerTests
+ {
+ private const string AsyncProfilerEventSourceName = "System.Runtime.CompilerServices.AsyncProfilerEventSource";
+ private const int AsyncEventsId = 1;
+ private const int HeaderSize = 1 + sizeof(uint) + sizeof(uint) + sizeof(ulong) + sizeof(uint) + sizeof(ulong) + sizeof(ulong);
+
+ // AsyncProfilerEventSource Keywords matching the event source definition
+ private const EventKeywords CreateAsyncContextKeyword = (EventKeywords)0x1;
+ private const EventKeywords ResumeAsyncContextKeyword = (EventKeywords)0x2;
+ private const EventKeywords SuspendAsyncContextKeyword = (EventKeywords)0x4;
+ private const EventKeywords CompleteAsyncContextKeyword = (EventKeywords)0x8;
+ private const EventKeywords UnwindAsyncExceptionKeyword = (EventKeywords)0x10;
+ private const EventKeywords CreateAsyncCallstackKeyword = (EventKeywords)0x20;
+ private const EventKeywords ResumeAsyncCallstackKeyword = (EventKeywords)0x40;
+ private const EventKeywords SuspendAsyncCallstackKeyword = (EventKeywords)0x80;
+ private const EventKeywords ResumeAsyncMethodKeyword = (EventKeywords)0x100;
+ private const EventKeywords CompleteAsyncMethodKeyword = (EventKeywords)0x200;
+
+ private const EventKeywords AllKeywords =
+ CreateAsyncContextKeyword | ResumeAsyncContextKeyword | SuspendAsyncContextKeyword |
+ CompleteAsyncContextKeyword | UnwindAsyncExceptionKeyword |
+ CreateAsyncCallstackKeyword | ResumeAsyncCallstackKeyword | SuspendAsyncCallstackKeyword |
+ ResumeAsyncMethodKeyword | CompleteAsyncMethodKeyword;
+
+ private const EventKeywords CoreKeywords =
+ CreateAsyncContextKeyword | ResumeAsyncContextKeyword | SuspendAsyncContextKeyword | CompleteAsyncContextKeyword;
+
+ private const EventKeywords MethodKeywords =
+ ResumeAsyncMethodKeyword | CompleteAsyncMethodKeyword;
+
+ private const EventKeywords CallstackKeywords =
+ CreateAsyncContextKeyword | CreateAsyncCallstackKeyword |
+ ResumeAsyncContextKeyword | ResumeAsyncCallstackKeyword | CompleteAsyncContextKeyword |
+ CompleteAsyncMethodKeyword | UnwindAsyncExceptionKeyword;
+
+ private static readonly MethodInfo s_getMethodFromNativeIP =
+ typeof(StackFrame).GetMethod("GetMethodFromNativeIP", BindingFlags.Static | BindingFlags.NonPublic)!;
+
+ private static MethodBase? GetMethodFromNativeIP(ulong nativeIP)
+ => (MethodBase?)s_getMethodFromNativeIP.Invoke(null, new object[] { (IntPtr)nativeIP });
+
+ [System.Runtime.CompilerServices.RuntimeAsyncMethodGeneration(true)]
+ static async Task Func()
+ {
+ await Task.Yield();
+ }
+
+ [System.Runtime.CompilerServices.RuntimeAsyncMethodGeneration(true)]
+ static async Task FuncChained()
+ {
+ await FuncInner();
+ }
+
+ [System.Runtime.CompilerServices.RuntimeAsyncMethodGeneration(true)]
+ static async Task FuncInner()
+ {
+ await Task.Yield();
+ }
+
+ [System.Runtime.CompilerServices.RuntimeAsyncMethodGeneration(true)]
+ static async Task OuterCatches()
+ {
+ try
+ {
+ await InnerThrows();
+ }
+ catch (InvalidOperationException)
+ {
+ }
+ await Task.Yield();
+ }
+
+ [System.Runtime.CompilerServices.RuntimeAsyncMethodGeneration(true)]
+ static async Task InnerThrows()
+ {
+ await Task.Yield();
+ throw new InvalidOperationException("inner");
+ }
+
+ [System.Runtime.CompilerServices.RuntimeAsyncMethodGeneration(true)]
+ static async Task DeepOuterCatches()
+ {
+ try
+ {
+ await DeepMiddle();
+ }
+ catch (InvalidOperationException)
+ {
+ }
+ }
+
+ [System.Runtime.CompilerServices.RuntimeAsyncMethodGeneration(true)]
+ static async Task DeepMiddle()
+ {
+ await DeepInnerThrows();
+ }
+
+ [System.Runtime.CompilerServices.RuntimeAsyncMethodGeneration(true)]
+ static async Task DeepInnerThrows()
+ {
+ await Task.Yield();
+ throw new InvalidOperationException("deep inner");
+ }
+
+ [System.Runtime.CompilerServices.RuntimeAsyncMethodGeneration(true)]
+ static async Task DeepUnhandledOuter()
+ {
+ await DeepUnhandledMiddle();
+ }
+
+ [System.Runtime.CompilerServices.RuntimeAsyncMethodGeneration(true)]
+ static async Task DeepUnhandledMiddle()
+ {
+ await DeepUnhandledInnerThrows();
+ }
+
+ [System.Runtime.CompilerServices.RuntimeAsyncMethodGeneration(true)]
+ static async Task DeepUnhandledInnerThrows()
+ {
+ await Task.Yield();
+ throw new InvalidOperationException("deep unhandled");
+ }
+
+ [System.Runtime.CompilerServices.RuntimeAsyncMethodGeneration(true)]
+ static async Task RecursiveFunc(int depth)
+ {
+ if (depth <= 1)
+ {
+ await Task.Yield();
+ return;
+ }
+ await RecursiveFunc(depth - 1);
+ }
+
+ [System.Runtime.CompilerServices.RuntimeAsyncMethodGeneration(true)]
+ static async Task WrapperTestA(List<(string MethodName, int WrapperSlot)> captures)
+ {
+ await WrapperTestB(captures);
+ captures.Add((nameof(WrapperTestA), GetCurrentWrapperSlot(nameof(WrapperTestA))));
+ }
+
+ [System.Runtime.CompilerServices.RuntimeAsyncMethodGeneration(true)]
+ static async Task WrapperTestB(List<(string MethodName, int WrapperSlot)> captures)
+ {
+ await WrapperTestC(captures);
+ captures.Add((nameof(WrapperTestB), GetCurrentWrapperSlot(nameof(WrapperTestB))));
+ }
+
+ [System.Runtime.CompilerServices.RuntimeAsyncMethodGeneration(true)]
+ static async Task WrapperTestC(List<(string MethodName, int WrapperSlot)> captures)
+ {
+ await Task.Yield();
+ captures.Add((nameof(WrapperTestC), GetCurrentWrapperSlot(nameof(WrapperTestC))));
+ }
+
+ private static TestEventListener CreateListener(EventKeywords keywords)
+ {
+ var listener = new TestEventListener();
+ listener.AddSource(AsyncProfilerEventSourceName, EventLevel.Informational, keywords);
+ return listener;
+ }
+
+ private static void SendFlushCommand()
+ {
+ const int FlushCommand = 1;
+ foreach (EventSource source in EventSource.GetSources())
+ {
+ if (source.Name == AsyncProfilerEventSourceName)
+ {
+ EventSource.SendCommand(source, (EventCommand)FlushCommand, null);
+ return;
+ }
+ }
+ }
+
+ private static ulong GetCurrentOSThreadId()
+ {
+ return (ulong)typeof(Thread)
+ .GetProperty("CurrentOSThreadId", BindingFlags.Static | BindingFlags.NonPublic)!
+ .GetValue(null)!;
+ }
+
+ private static int GetCurrentWrapperSlot(string resumedMethodName)
+ {
+ var st = new StackTrace();
+ for (int i = 0; i < st.FrameCount - 1; i++)
+ {
+ string? name = st.GetFrame(i)?.GetMethod()?.Name;
+ if (name is not null && name.Contains(resumedMethodName))
+ {
+ // The next frame should be the Continuation_Wrapper_N that dispatched this method.
+ string? wrapperName = st.GetFrame(i + 1)?.GetMethod()?.Name;
+ if (wrapperName is not null && wrapperName.StartsWith("Continuation_Wrapper_", StringComparison.Ordinal))
+ {
+ return int.Parse(wrapperName.Substring("Continuation_Wrapper_".Length));
+ }
+ return -1;
+ }
+ }
+ return -1;
+ }
+
+ private delegate bool EventVisitor(AsyncEventID eventId, ReadOnlySpan buffer, ref int index);
+
+ private delegate bool EventVisitorWithTimestamp(AsyncEventID eventId, long timestamp, ReadOnlySpan buffer, ref int index);
+
+ private static void ParseEventBuffer(ReadOnlySpan buffer, EventVisitor visitor)
+ {
+ ParseEventBuffer(buffer, (AsyncEventID eventId, long _, ReadOnlySpan buf, ref int idx) =>
+ visitor(eventId, buf, ref idx));
+ }
+
+ private static void ParseEventBuffer(ReadOnlySpan buffer, EventVisitorWithTimestamp visitor)
+ {
+ EventBufferHeader? header = ParseEventBufferHeader(buffer);
+ if (header is null)
+ return;
+
+ int index = HeaderSize;
+ long baseTimestamp = (long)header.Value.StartTimestamp;
+
+ while (index < buffer.Length)
+ {
+ if (index + 2 > buffer.Length)
+ break;
+
+ AsyncEventID eventId = (AsyncEventID)buffer[index++];
+
+ long delta = (long)ReadCompressedUInt64(buffer, ref index);
+ baseTimestamp += delta;
+
+ if (!visitor(eventId, baseTimestamp, buffer, ref index))
+ break;
+ }
+ }
+
+ private static bool SkipEventPayload(AsyncEventID eventId, ReadOnlySpan buffer, ref int index)
+ {
+ switch (eventId)
+ {
+ case AsyncEventID.CreateAsyncContext:
+ case AsyncEventID.ResumeAsyncContext:
+ ReadCompressedUInt64(buffer, ref index);
+ return true;
+ case AsyncEventID.SuspendAsyncContext:
+ case AsyncEventID.CompleteAsyncContext:
+ case AsyncEventID.ResumeAsyncMethod:
+ case AsyncEventID.CompleteAsyncMethod:
+ case AsyncEventID.ResetAsyncThreadContext:
+ case AsyncEventID.ResetAsyncContinuationWrapperIndex:
+ return true;
+ case AsyncEventID.AsyncProfilerMetadata:
+ SkipMetadataPayload(buffer, ref index);
+ return true;
+ case AsyncEventID.AsyncProfilerSyncClock:
+ ReadCompressedUInt64(buffer, ref index); // qpcSync
+ ReadCompressedUInt64(buffer, ref index); // utcSync
+ return true;
+ case AsyncEventID.UnwindAsyncException:
+ ReadCompressedUInt32(buffer, ref index);
+ return true;
+ case AsyncEventID.CreateAsyncCallstack:
+ case AsyncEventID.ResumeAsyncCallstack:
+ case AsyncEventID.SuspendAsyncCallstack:
+ SkipCallstackPayload(buffer, ref index);
+ return true;
+ default:
+ return false;
+ }
+ }
+
+ private static uint ReadCompressedUInt32(ReadOnlySpan buffer, ref int index)
+ {
+ EventBuffer.Deserializer.ReadCompressedUInt32(buffer, ref index, out uint value);
+ return value;
+ }
+
+ private static ulong ReadCompressedUInt64(ReadOnlySpan buffer, ref int index)
+ {
+ EventBuffer.Deserializer.ReadCompressedUInt64(buffer, ref index, out ulong value);
+ return value;
+ }
+
+ private static void SkipCallstackPayload(ReadOnlySpan buffer, ref int index)
+ {
+ ReadCallstackPayload(buffer, ref index, out _, out _);
+ }
+
+ private static void ReadCallstackPayload(ReadOnlySpan buffer, ref int index,
+ out byte frameCount, out List<(ulong NativeIP, int State)> frames)
+ {
+ ReadCallstackPayload(buffer, ref index, out _, out frameCount, out frames);
+ }
+
+ private static void ReadCallstackPayload(ReadOnlySpan buffer, ref int index,
+ out ulong taskId, out byte frameCount, out List<(ulong NativeIP, int State)> frames)
+ {
+ index++; // type
+ index++; // callstack ID (reserved)
+ frameCount = buffer[index++];
+ taskId = ReadCompressedUInt64(buffer, ref index);
+ frames = new List<(ulong, int)>(frameCount);
+
+ if (frameCount == 0)
+ return;
+
+ ulong currentNativeIP = ReadCompressedUInt64(buffer, ref index);
+ int state = ReadCompressedInt32(buffer, ref index);
+ frames.Add((currentNativeIP, state));
+
+ for (int i = 1; i < frameCount; i++)
+ {
+ long delta = ReadCompressedInt64(buffer, ref index);
+ state = ReadCompressedInt32(buffer, ref index);
+ currentNativeIP = (ulong)((long)currentNativeIP + delta);
+ frames.Add((currentNativeIP, state));
+ }
+ }
+
+ private static int ReadCompressedInt32(ReadOnlySpan buffer, ref int index)
+ {
+ EventBuffer.Deserializer.ReadCompressedInt32(buffer, ref index, out int value);
+ return value;
+ }
+
+ private static long ReadCompressedInt64(ReadOnlySpan buffer, ref int index)
+ {
+ EventBuffer.Deserializer.ReadCompressedInt64(buffer, ref index, out long value);
+ return value;
+ }
+
+ private static void SkipMetadataPayload(ReadOnlySpan buffer, ref int index)
+ {
+ ReadMetadataPayload(buffer, ref index, out _, out _, out _, out _, out _);
+ }
+
+ private static void ReadMetadataPayload(ReadOnlySpan buffer, ref int index,
+ out ulong qpcFrequency, out ulong qpcSync, out ulong utcSync, out uint eventBufferSize, out long[] wrapperIPs)
+ {
+ qpcFrequency = ReadCompressedUInt64(buffer, ref index);
+ qpcSync = ReadCompressedUInt64(buffer, ref index);
+ utcSync = ReadCompressedUInt64(buffer, ref index);
+ eventBufferSize = ReadCompressedUInt32(buffer, ref index);
+ byte wrapperCount = buffer[index++];
+ wrapperIPs = new long[wrapperCount];
+ for (int i = 0; i < wrapperCount; i++)
+ {
+ wrapperIPs[i] = (long)ReadCompressedUInt64(buffer, ref index);
+ }
+ }
+
+ private record struct MetadataFromBuffer(ulong QpcFrequency, ulong QpcSync, ulong UtcSync, uint EventBufferSize, long[] WrapperIPs);
+
+ private static List CollectMetadataFromBuffer(ConcurrentQueue events)
+ {
+ var metadataList = new List();
+ ForEachEventBufferPayload(events, buffer =>
+ {
+ ParseEventBuffer(buffer, (AsyncEventID eventId, ReadOnlySpan buf, ref int idx) =>
+ {
+ if (eventId == AsyncEventID.AsyncProfilerMetadata)
+ {
+ ReadMetadataPayload(buf, ref idx, out ulong freq, out ulong qpcSync, out ulong utcSync, out uint bufSize, out long[] ips);
+ metadataList.Add(new MetadataFromBuffer(freq, qpcSync, utcSync, bufSize, ips));
+ return true;
+ }
+ return SkipEventPayload(eventId, buf, ref idx);
+ });
+ });
+ return metadataList;
+ }
+
+ private static ulong ParseOsThreadId(ReadOnlySpan buffer)
+ {
+ return ParseEventBufferHeader(buffer)?.OsThreadId ?? 0;
+ }
+
+ private readonly record struct EventBufferHeader(byte Version, uint TotalSize, uint AsyncThreadContextId, ulong OsThreadId, uint EventCount, ulong StartTimestamp, ulong EndTimestamp);
+
+ private static EventBufferHeader? ParseEventBufferHeader(ReadOnlySpan buffer)
+ {
+ if (buffer.Length < HeaderSize || buffer[0] != 1)
+ return null;
+
+ int index = 1;
+ EventBuffer.Deserializer.ReadUInt32(buffer, ref index, out uint totalSize);
+ EventBuffer.Deserializer.ReadUInt32(buffer, ref index, out uint contextId);
+ EventBuffer.Deserializer.ReadUInt64(buffer, ref index, out ulong threadId);
+ EventBuffer.Deserializer.ReadUInt32(buffer, ref index, out uint eventCount);
+ EventBuffer.Deserializer.ReadUInt64(buffer, ref index, out ulong startTs);
+ EventBuffer.Deserializer.ReadUInt64(buffer, ref index, out ulong endTs);
+
+ return new EventBufferHeader(buffer[0], totalSize, contextId, threadId, eventCount, startTs, endTs);
+ }
+
+ private static List CollectAsyncEventIds(ConcurrentQueue events)
+ {
+ var allEventIds = new List();
+ ForEachEventBufferPayload(events, buffer =>
+ {
+ ParseEventBuffer(buffer, (AsyncEventID eventId, ReadOnlySpan buf, ref int idx) =>
+ {
+ allEventIds.Add(eventId);
+ return SkipEventPayload(eventId, buf, ref idx);
+ });
+ });
+ return allEventIds;
+ }
+
+ private static List<(AsyncEventID EventId, long Timestamp)> CollectAsyncEventIdsWithTimestamps(ConcurrentQueue events)
+ {
+ var allEvents = new List<(AsyncEventID EventId, long Timestamp)>();
+ ForEachEventBufferPayload(events, buffer =>
+ {
+ ParseEventBuffer(buffer, (AsyncEventID eventId, long timestamp, ReadOnlySpan buf, ref int idx) =>
+ {
+ allEvents.Add((eventId, timestamp));
+ return SkipEventPayload(eventId, buf, ref idx);
+ });
+ });
+ allEvents.Sort((a, b) => a.Timestamp.CompareTo(b.Timestamp));
+ return allEvents;
+ }
+
+ private static HashSet CollectOsThreadIds(ConcurrentQueue events)
+ {
+ var threadIds = new HashSet();
+ ForEachEventBufferPayload(events, buffer =>
+ {
+ ulong tid = ParseOsThreadId(buffer);
+ if (tid != 0)
+ threadIds.Add(tid);
+ });
+ return threadIds;
+ }
+
+ private static List CollectUnwindFrameCounts(ConcurrentQueue events)
+ {
+ var frameCounts = new List();
+ ForEachEventBufferPayload(events, buffer =>
+ {
+ ParseEventBuffer(buffer, (AsyncEventID eventId, ReadOnlySpan buf, ref int idx) =>
+ {
+ if (eventId == AsyncEventID.UnwindAsyncException)
+ {
+ frameCounts.Add(ReadCompressedUInt32(buf, ref idx));
+ return true;
+ }
+ return SkipEventPayload(eventId, buf, ref idx);
+ });
+ });
+ return frameCounts;
+ }
+
+ private static List<(ulong TaskId, byte FrameCount, List<(ulong NativeIP, int State)> Frames)> CollectCallstacks(
+ ConcurrentQueue events)
+ {
+ return CollectCallstacks(events, AsyncEventID.ResumeAsyncCallstack, threadId: null);
+ }
+
+ private static List<(ulong TaskId, byte FrameCount, List<(ulong NativeIP, int State)> Frames)> CollectCallstacks(
+ ConcurrentQueue events, ulong? threadId)
+ {
+ return CollectCallstacks(events, AsyncEventID.ResumeAsyncCallstack, threadId);
+ }
+
+ private static List<(ulong TaskId, byte FrameCount, List<(ulong NativeIP, int State)> Frames)> CollectCallstacks(
+ ConcurrentQueue events, AsyncEventID callstackEventId)
+ {
+ return CollectCallstacks(events, callstackEventId, threadId: null);
+ }
+
+ private static List<(ulong TaskId, byte FrameCount, List<(ulong NativeIP, int State)> Frames)> CollectCallstacks(
+ ConcurrentQueue events, AsyncEventID callstackEventId, ulong? threadId)
+ {
+ var callstacks = new List<(ulong, byte, List<(ulong, int)>)>();
+ ForEachEventBufferPayload(events, buffer =>
+ {
+ if (threadId.HasValue)
+ {
+ ulong tid = ParseOsThreadId(buffer);
+ if (tid != threadId.Value)
+ return;
+ }
+
+ ParseEventBuffer(buffer, (AsyncEventID eventId, ReadOnlySpan buf, ref int idx) =>
+ {
+ if (eventId == callstackEventId)
+ {
+ ReadCallstackPayload(buf, ref idx, out ulong taskId, out byte frameCount, out var frames);
+ callstacks.Add((taskId, frameCount, frames));
+ return true;
+ }
+ return SkipEventPayload(eventId, buf, ref idx);
+ });
+ });
+ return callstacks;
+ }
+
+ private static (byte FrameCount, List<(ulong NativeIP, int State)> Frames)? FindCallstackAfterTimestamp(
+ ConcurrentQueue events, ulong threadId, long afterTimestamp)
+ {
+ (byte FrameCount, List<(ulong, int)> Frames)? best = null;
+ long bestTimestamp = long.MaxValue;
+
+ ForEachEventBufferPayload(events, buffer =>
+ {
+ ulong tid = ParseOsThreadId(buffer);
+ if (tid != threadId)
+ return;
+
+ ParseEventBuffer(buffer, (AsyncEventID eventId, long timestamp, ReadOnlySpan buf, ref int idx) =>
+ {
+ if (eventId == AsyncEventID.ResumeAsyncCallstack)
+ {
+ ReadCallstackPayload(buf, ref idx, out byte frameCount, out var frames);
+ if (timestamp >= afterTimestamp && timestamp < bestTimestamp)
+ {
+ bestTimestamp = timestamp;
+ best = (frameCount, frames);
+ }
+ return true;
+ }
+ return SkipEventPayload(eventId, buf, ref idx);
+ });
+ });
+
+ return best;
+ }
+
+ private delegate void EventBufferPayloadAction(ReadOnlySpan payload);
+
+ private static void ForEachEventBufferPayload(ConcurrentQueue events, EventBufferPayloadAction action)
+ {
+ foreach (var e in events)
+ {
+ if (e.EventId == AsyncEventsId && e.Payload is { Count: >= 1 } && e.Payload[0] is byte[] rawPayload)
+ {
+ action(rawPayload);
+ }
+ }
+ }
+
+ // Uncomment at callsite to dump all collected event buffers to console for diagnostics:
+ private static void DumpCollectedEvents(ConcurrentQueue events)
+ {
+ ForEachEventBufferPayload(events, buffer => EventBuffer.OutputEventBuffer(buffer));
+ }
+
+ private static void RunScenarioAndFlush(Func scenario)
+ {
+ Task.Run(scenario).GetAwaiter().GetResult();
+ SendFlushCommand();
+ }
+
+ private static void RunScenario(Func scenario)
+ {
+ Task.Run(scenario).GetAwaiter().GetResult();
+ }
+
+ private static ConcurrentQueue CollectEvents(EventKeywords keywords, Action callback)
+ {
+ return CollectEvents(keywords, (_, _) => callback());
+ }
+
+ private static ConcurrentQueue CollectEvents(EventKeywords keywords, Action, EventKeywords> callback)
+ {
+ var events = new ConcurrentQueue();
+ using (var listener = CreateListener(keywords))
+ {
+ listener.RunWithCallback(events.Enqueue, () =>
+ {
+ SendFlushCommand();
+ events.Clear();
+ callback(events, keywords);
+ });
+ }
+ return events;
+ }
+
+ private static void AssertCallstackSimulationReachesZero(ConcurrentQueue events)
+ {
+ var eventIds = CollectAsyncEventIds(events);
+ var frameCounts = CollectUnwindFrameCounts(events);
+ var callstacks = CollectCallstacks(events);
+
+ int stackDepth = 0;
+ int unwindIdx = 0;
+ int callstackIdx = 0;
+
+ foreach (AsyncEventID id in eventIds)
+ {
+ switch (id)
+ {
+ case AsyncEventID.ResumeAsyncCallstack:
+ if (callstackIdx < callstacks.Count)
+ stackDepth = callstacks[callstackIdx++].FrameCount;
+ break;
+ case AsyncEventID.CompleteAsyncMethod:
+ if (stackDepth > 0)
+ stackDepth--;
+ break;
+ case AsyncEventID.UnwindAsyncException:
+ if (unwindIdx < frameCounts.Count)
+ stackDepth = Math.Max(0, stackDepth - (int)frameCounts[unwindIdx++]);
+ break;
+ }
+ }
+
+ Assert.True(callstackIdx > 0, "Expected at least one ResumeAsyncCallstack event");
+ Assert.Equal(0, stackDepth);
+ }
+
+ [ConditionalFact(typeof(PlatformDetection), nameof(PlatformDetection.IsRuntimeAsyncSupported))]
+ public void RuntimeAsync_EventBufferHeaderFormat()
+ {
+ var events = CollectEvents(CoreKeywords, () =>
+ {
+ RunScenarioAndFlush(async () =>
+ {
+ await Func();
+ });
+ });
+
+ // DumpCollectedEvents(events);
+
+ int buffersChecked = 0;
+ ForEachEventBufferPayload(events, buffer =>
+ {
+ EventBufferHeader? parsed = ParseEventBufferHeader(buffer);
+ Assert.NotNull(parsed);
+ EventBufferHeader header = parsed.Value;
+
+ Assert.Equal(1, header.Version);
+ Assert.Equal((uint)buffer.Length, header.TotalSize);
+ Assert.True(header.AsyncThreadContextId > 0, "Async thread context ID should be positive");
+ Assert.True(header.OsThreadId != 0, "OS thread ID should be non-zero");
+ Assert.True(header.StartTimestamp > 0, "Start timestamp should be positive");
+ Assert.True(header.EndTimestamp >= header.StartTimestamp,
+ $"End timestamp ({header.EndTimestamp}) should be >= start timestamp ({header.StartTimestamp})");
+
+ int eventCount = 0;
+ ParseEventBuffer(buffer, (AsyncEventID eventId, ReadOnlySpan buf, ref int idx) =>
+ {
+ eventCount++;
+ return SkipEventPayload(eventId, buf, ref idx);
+ });
+
+ Assert.Equal(header.EventCount, (uint)eventCount);
+ Assert.True(header.EventCount > 0, "Expected at least one event in buffer");
+
+ buffersChecked++;
+ });
+
+ Assert.True(buffersChecked > 0, "Expected at least one buffer");
+ }
+
+ [ConditionalFact(typeof(PlatformDetection), nameof(PlatformDetection.IsRuntimeAsyncSupported))]
+ public void RuntimeAsync_EventsEmitted()
+ {
+ var events = CollectEvents(AllKeywords, () =>
+ {
+ RunScenarioAndFlush(async () =>
+ {
+ await Func();
+ });
+ });
+
+ // DumpCollectedEvents(events);
+
+ Assert.True(events.Count > 0, "Expected at least one AsyncEvents event to be emitted");
+ Assert.Contains(events, e => e.EventId == AsyncEventsId);
+ }
+
+ [ConditionalFact(typeof(PlatformDetection), nameof(PlatformDetection.IsRuntimeAsyncSupported))]
+ public void RuntimeAsync_SuspendResumeCompleteEvents()
+ {
+ var events = CollectEvents(CoreKeywords, () =>
+ {
+ RunScenarioAndFlush(async () =>
+ {
+ // If not Yield here there won't be a SuspendAsyncContext.
+ // First call is a regular sync invocation (no continuation chain).
+ // Yield in Func will create an RuntimeAsyncTask with continuation chain
+ // and schedule on thread pool. When chain is resumed there will be
+ // ResumeAsyncContext and CompleteAsyncContext since the chain won't suspend again.
+ // The first Yield fixes that creating and schedule the RuntimeAsyncTask and Func
+ // will be called from the dispatch loop triggering the expected sequence of events.
+ await Task.Yield();
+ await Func();
+ });
+ });
+
+ // DumpCollectedEvents(events);
+
+ var eventIds = CollectAsyncEventIds(events);
+
+ Assert.Contains(AsyncEventID.ResumeAsyncContext, eventIds);
+ Assert.Contains(AsyncEventID.SuspendAsyncContext, eventIds);
+ Assert.Contains(AsyncEventID.CompleteAsyncContext, eventIds);
+ }
+
+ [ConditionalFact(typeof(PlatformDetection), nameof(PlatformDetection.IsRuntimeAsyncSupported))]
+ public void RuntimeAsync_ContextEventIdLifecycle()
+ {
+ var events = CollectEvents(CoreKeywords, () =>
+ {
+ RunScenarioAndFlush(async () =>
+ {
+ await Task.Yield();
+ await Func();
+ });
+ });
+
+ // DumpCollectedEvents(events);
+
+ var createIds = new List();
+ var resumeIds = new List();
+
+ ForEachEventBufferPayload(events, buffer =>
+ {
+ ParseEventBuffer(buffer, (AsyncEventID eventId, ReadOnlySpan buf, ref int idx) =>
+ {
+ if (eventId == AsyncEventID.CreateAsyncContext)
+ {
+ createIds.Add(ReadCompressedUInt64(buf, ref idx));
+ return true;
+ }
+ if (eventId == AsyncEventID.ResumeAsyncContext)
+ {
+ resumeIds.Add(ReadCompressedUInt64(buf, ref idx));
+ return true;
+ }
+ return SkipEventPayload(eventId, buf, ref idx);
+ });
+ });
+
+ Assert.True(createIds.Count > 0, "Expected at least one CreateAsyncContext with id");
+ Assert.True(resumeIds.Count > 0, "Expected at least one ResumeAsyncContext with id");
+
+ Assert.All(createIds, id => Assert.True(id > 0, "CreateAsyncContext id should be non-zero"));
+ Assert.All(resumeIds, id => Assert.True(id > 0, "ResumeAsyncContext id should be non-zero"));
+
+ foreach (ulong resumeId in resumeIds)
+ {
+ Assert.Contains(resumeId, createIds);
+ }
+ }
+
+ [ConditionalFact(typeof(PlatformDetection), nameof(PlatformDetection.IsRuntimeAsyncSupported))]
+ public void RuntimeAsync_ResumeCompleteMethodEvents()
+ {
+ var events = CollectEvents(MethodKeywords, () =>
+ {
+ RunScenarioAndFlush(async () =>
+ {
+ await FuncChained();
+ });
+ });
+
+ // DumpCollectedEvents(events);
+
+ var eventIds = CollectAsyncEventIds(events);
+
+ Assert.Contains(AsyncEventID.ResumeAsyncMethod, eventIds);
+ Assert.Contains(AsyncEventID.CompleteAsyncMethod, eventIds);
+ }
+
+ [ConditionalFact(typeof(PlatformDetection), nameof(PlatformDetection.IsRuntimeAsyncSupported))]
+ public void RuntimeAsync_EventSequenceOrder()
+ {
+ var events = CollectEvents(CoreKeywords, () =>
+ {
+ // Same scenario as SuspendResumeCompleteEvents; here we verify ordering.
+ RunScenarioAndFlush(async () =>
+ {
+ await Task.Yield();
+ await Func();
+ });
+ });
+
+ // DumpCollectedEvents(events);
+
+ var sortedEvents = CollectAsyncEventIdsWithTimestamps(events);
+ var coreEvents = sortedEvents.FindAll(e => e.EventId == AsyncEventID.ResumeAsyncContext || e.EventId == AsyncEventID.SuspendAsyncContext || e.EventId == AsyncEventID.CompleteAsyncContext);
+
+ Assert.Equal(AsyncEventID.ResumeAsyncContext, coreEvents[0].EventId);
+ Assert.Equal(AsyncEventID.SuspendAsyncContext, coreEvents[1].EventId);
+ Assert.Equal(AsyncEventID.ResumeAsyncContext, coreEvents[2].EventId);
+ Assert.Equal(AsyncEventID.CompleteAsyncContext, coreEvents[3].EventId);
+ }
+
+ [ConditionalFact(typeof(PlatformDetection), nameof(PlatformDetection.IsRuntimeAsyncSupported))]
+ public void RuntimeAsync_CreateAsyncContextEmittedOnFirstAwait()
+ {
+ var events = CollectEvents(CreateAsyncContextKeyword | CompleteAsyncContextKeyword, () =>
+ {
+ RunScenarioAndFlush(async () =>
+ {
+ await Func();
+ });
+ });
+
+ // DumpCollectedEvents(events);
+
+ var eventIds = CollectAsyncEventIds(events);
+ Assert.Contains(AsyncEventID.CreateAsyncContext, eventIds);
+ }
+
+ [ConditionalFact(typeof(PlatformDetection), nameof(PlatformDetection.IsRuntimeAsyncSupported))]
+ public void RuntimeAsync_CreateAsyncCallstackEmittedOnFirstAwait()
+ {
+ var events = CollectEvents(CreateAsyncCallstackKeyword | CompleteAsyncContextKeyword, () =>
+ {
+ RunScenarioAndFlush(async () =>
+ {
+ await Func();
+ });
+ });
+
+ // DumpCollectedEvents(events);
+
+ var callstacks = CollectCallstacks(events, AsyncEventID.CreateAsyncCallstack);
+
+ Assert.NotEmpty(callstacks);
+ Assert.All(callstacks, cs =>
+ {
+ Assert.True(cs.FrameCount > 0, "Expected at least one frame in create callstack");
+ Assert.True(cs.TaskId != 0, "Expected non-zero task ID in create callstack");
+ Assert.True(cs.Frames[0].NativeIP != 0, "Expected non-zero NativeIP in first frame");
+ });
+ }
+
+ [ConditionalFact(typeof(PlatformDetection), nameof(PlatformDetection.IsRuntimeAsyncSupported))]
+ public void RuntimeAsync_CreateCallstackDepthMatchesChain()
+ {
+ var events = CollectEvents(CreateAsyncCallstackKeyword | CompleteAsyncContextKeyword, () =>
+ {
+ // FuncChained -> FuncInner -> lambda: create callstack at FuncInner's
+ // first await should reflect the 3-level chain.
+ RunScenarioAndFlush(async () =>
+ {
+ await FuncChained();
+ });
+ });
+
+ // DumpCollectedEvents(events);
+
+ var callstacks = CollectCallstacks(events, AsyncEventID.CreateAsyncCallstack);
+
+ Assert.NotEmpty(callstacks);
+ Assert.Contains(callstacks, cs => cs.FrameCount == 3);
+ }
+
+ [ConditionalFact(typeof(PlatformDetection), nameof(PlatformDetection.IsRuntimeAsyncSupported))]
+ public void RuntimeAsync_SuspendAsyncCallstackEmittedOnAwait()
+ {
+ var events = CollectEvents(SuspendAsyncCallstackKeyword | CompleteAsyncContextKeyword, () =>
+ {
+ RunScenarioAndFlush(async () =>
+ {
+ // First Yield pushes execution into the dispatch loop.
+ // Then Func()'s Yield triggers a suspend inside the loop
+ // where the SuspendAsyncCallstack event is emitted.
+ await Task.Yield();
+ await Func();
+ });
+ });
+
+ // DumpCollectedEvents(events);
+
+ var callstacks = CollectCallstacks(events, AsyncEventID.SuspendAsyncCallstack);
+
+ Assert.NotEmpty(callstacks);
+ Assert.All(callstacks, cs =>
+ {
+ Assert.True(cs.FrameCount > 0, "Expected at least one frame in suspend callstack");
+ Assert.True(cs.TaskId != 0, "Expected non-zero task ID in suspend callstack");
+ Assert.True(cs.Frames[0].NativeIP != 0, "Expected non-zero NativeIP in first frame");
+ });
+ }
+
+ [ConditionalFact(typeof(PlatformDetection), nameof(PlatformDetection.IsRuntimeAsyncSupported))]
+ public void RuntimeAsync_SuspendCallstackDepthMatchesChain()
+ {
+ var events = CollectEvents(SuspendAsyncCallstackKeyword | CompleteAsyncContextKeyword, () =>
+ {
+ // FuncChained -> FuncInner -> lambda: 3 levels deep when FuncInner suspends.
+ RunScenarioAndFlush(async () =>
+ {
+ await Task.Yield();
+ await FuncChained();
+ });
+ });
+
+ // DumpCollectedEvents(events);
+
+ var callstacks = CollectCallstacks(events, AsyncEventID.SuspendAsyncCallstack);
+
+ Assert.NotEmpty(callstacks);
+ Assert.Contains(callstacks, cs => cs.FrameCount == 3);
+ }
+
+ [ConditionalFact(typeof(PlatformDetection), nameof(PlatformDetection.IsRuntimeAsyncSupported))]
+ public void RuntimeAsync_SuspendCallstackPrecedesComplete()
+ {
+ // Use a single-level async method so all events belong to the same context.
+ // This avoids ordering ambiguity from nested async calls.
+ var events = CollectEvents(SuspendAsyncCallstackKeyword | CompleteAsyncContextKeyword, () =>
+ {
+ RunScenarioAndFlush(async () =>
+ {
+ // First Yield pushes into dispatch loop; second Yield triggers suspend.
+ await Task.Yield();
+ await Task.Yield();
+ });
+ });
+
+ // DumpCollectedEvents(events);
+
+ var eventIds = CollectAsyncEventIdsWithTimestamps(events);
+
+ int suspendIdx = eventIds.FindIndex(e => e.EventId == AsyncEventID.SuspendAsyncCallstack);
+ int completeIdx = eventIds.FindIndex(e => e.EventId == AsyncEventID.CompleteAsyncContext);
+
+ Assert.True(suspendIdx >= 0, "Expected SuspendAsyncCallstack event");
+ Assert.True(completeIdx >= 0, "Expected CompleteAsyncContext event");
+ Assert.True(suspendIdx < completeIdx,
+ $"SuspendAsyncCallstack (index {suspendIdx}) should precede CompleteAsyncContext (index {completeIdx})");
+ }
+
+ [ConditionalFact(typeof(PlatformDetection), nameof(PlatformDetection.IsRuntimeAsyncSupported))]
+ public void RuntimeAsync_SuspendCallstackDeeperThanInitialResume()
+ {
+ // After the initial Yield, the first resume is at the lambda level (depth 1).
+ // Then FuncChained -> FuncInner builds the full chain and suspends at depth 3.
+ // The suspend callstack should be deeper than the initial resume.
+ var events = CollectEvents(
+ ResumeAsyncCallstackKeyword | SuspendAsyncCallstackKeyword | CompleteAsyncContextKeyword, () =>
+ {
+ RunScenarioAndFlush(async () =>
+ {
+ await Task.Yield();
+ await FuncChained();
+ });
+ });
+
+ // DumpCollectedEvents(events);
+
+ var resumeStacks = CollectCallstacks(events, AsyncEventID.ResumeAsyncCallstack);
+ var suspendStacks = CollectCallstacks(events, AsyncEventID.SuspendAsyncCallstack);
+
+ Assert.NotEmpty(resumeStacks);
+ Assert.NotEmpty(suspendStacks);
+
+ // The shallowest resume is after the initial Yield (just the lambda).
+ // The deepest suspend captures the full chain (FuncInner -> FuncChained -> lambda).
+ // Use min/max to avoid cross-buffer ordering dependence.
+ byte minResumeDepth = resumeStacks.Min(cs => cs.FrameCount);
+ byte maxSuspendDepth = suspendStacks.Max(cs => cs.FrameCount);
+
+ Assert.True(maxSuspendDepth > minResumeDepth,
+ $"Suspend callstack depth ({maxSuspendDepth}) should be deeper than shallowest resume callstack depth ({minResumeDepth})");
+ }
+
+ [ConditionalFact(typeof(PlatformDetection), nameof(PlatformDetection.IsRuntimeAsyncSupported))]
+ public void RuntimeAsync_CreateCallstackPrecedesResumeCallstack()
+ {
+ var events = CollectEvents(CreateAsyncContextKeyword | CreateAsyncCallstackKeyword | ResumeAsyncContextKeyword | ResumeAsyncCallstackKeyword | CompleteAsyncContextKeyword, () =>
+ {
+ RunScenarioAndFlush(async () =>
+ {
+ await Func();
+ });
+ });
+
+ // DumpCollectedEvents(events);
+
+ // Collect all callstack events with their task IDs sorted by timestamp.
+ var callstackEvents = new List<(AsyncEventID EventId, ulong TaskId, long Timestamp)>();
+ ForEachEventBufferPayload(events, buffer =>
+ {
+ ParseEventBuffer(buffer, (AsyncEventID eventId, long timestamp, ReadOnlySpan buf, ref int idx) =>
+ {
+ if (eventId is AsyncEventID.CreateAsyncCallstack or AsyncEventID.ResumeAsyncCallstack)
+ {
+ ReadCallstackPayload(buf, ref idx, out ulong taskId, out byte _, out _);
+ callstackEvents.Add((eventId, taskId, timestamp));
+ return true;
+ }
+ return SkipEventPayload(eventId, buf, ref idx);
+ });
+ });
+ callstackEvents.Sort((a, b) => a.Timestamp.CompareTo(b.Timestamp));
+
+ // For each task that has both Create and Resume, verify Create comes first.
+ var taskIds = callstackEvents.Where(e => e.EventId == AsyncEventID.CreateAsyncCallstack).Select(e => e.TaskId).ToHashSet();
+ Assert.NotEmpty(taskIds);
+
+ foreach (ulong taskId in taskIds)
+ {
+ int createIdx = callstackEvents.FindIndex(e => e.EventId == AsyncEventID.CreateAsyncCallstack && e.TaskId == taskId);
+ int resumeIdx = callstackEvents.FindIndex(e => e.EventId == AsyncEventID.ResumeAsyncCallstack && e.TaskId == taskId);
+
+ Assert.True(createIdx >= 0, $"Expected CreateAsyncCallstack for task {taskId}");
+ Assert.True(resumeIdx >= 0, $"Expected ResumeAsyncCallstack for task {taskId}");
+ Assert.True(createIdx < resumeIdx,
+ $"For task {taskId}: CreateAsyncCallstack (index {createIdx}) should precede ResumeAsyncCallstack (index {resumeIdx})");
+ }
+ }
+
+ [ConditionalFact(typeof(PlatformDetection), nameof(PlatformDetection.IsRuntimeAsyncSupported))]
+ public void RuntimeAsync_CreateAndFirstResumeCallstacksMatch()
+ {
+ var events = CollectEvents(CreateAsyncCallstackKeyword | ResumeAsyncCallstackKeyword | CompleteAsyncContextKeyword, () =>
+ {
+ RunScenarioAndFlush(async () =>
+ {
+ await Func();
+ });
+ });
+
+ // DumpCollectedEvents(events);
+
+ var createStacks = CollectCallstacks(events, AsyncEventID.CreateAsyncCallstack);
+ var resumeStacks = CollectCallstacks(events, AsyncEventID.ResumeAsyncCallstack);
+
+ Assert.NotEmpty(createStacks);
+ Assert.NotEmpty(resumeStacks);
+
+ foreach (var (taskId, _, createFrames) in createStacks)
+ {
+ var matchingResume = resumeStacks.FirstOrDefault(r => r.TaskId == taskId);
+ Assert.True(matchingResume.Frames is not null,
+ $"Expected a ResumeAsyncCallstack for task {taskId}");
+
+ Assert.Equal(createFrames.Count, matchingResume.Frames!.Count);
+ for (int i = 0; i < createFrames.Count; i++)
+ {
+ Assert.Equal(createFrames[i].NativeIP, matchingResume.Frames[i].NativeIP);
+ }
+ }
+ }
+
+ [ConditionalFact(typeof(PlatformDetection), nameof(PlatformDetection.IsRuntimeAsyncSupported))]
+ public void RuntimeAsync_CallstackEmittedOnResume()
+ {
+ var events = CollectEvents(CallstackKeywords, () =>
+ {
+ RunScenarioAndFlush(async () =>
+ {
+ await Func();
+ });
+ });
+
+ // DumpCollectedEvents(events);
+
+ var callstacks = CollectCallstacks(events);
+
+ Assert.NotEmpty(callstacks);
+ Assert.All(callstacks, cs =>
+ {
+ Assert.True(cs.FrameCount > 0, "Expected at least one frame in callstack");
+ Assert.True(cs.TaskId != 0, "Expected non-zero task ID in resume callstack");
+ Assert.True(cs.Frames[0].NativeIP != 0, "Expected non-zero NativeIP in first frame");
+ });
+ }
+
+ [ConditionalFact(typeof(PlatformDetection), nameof(PlatformDetection.IsRuntimeAsyncSupported))]
+ public void RuntimeAsync_CallstackDepthMatchesChain()
+ {
+ var events = CollectEvents(CallstackKeywords, () =>
+ {
+ // FuncChained -> FuncInner -> lambda: 3 levels deep after FuncInner yields.
+ RunScenarioAndFlush(async () =>
+ {
+ await FuncChained();
+ });
+ });
+
+ // DumpCollectedEvents(events);
+
+ var callstacks = CollectCallstacks(events);
+
+ Assert.NotEmpty(callstacks);
+ Assert.Contains(callstacks, cs => cs.FrameCount == 3);
+ }
+
+ [ConditionalFact(typeof(PlatformDetection), nameof(PlatformDetection.IsRuntimeAsyncSupported))]
+ public void RuntimeAsync_CallstackSimulation_NormalCompletion()
+ {
+ var events = CollectEvents(CallstackKeywords, () =>
+ {
+ RunScenarioAndFlush(async () =>
+ {
+ await FuncChained();
+ });
+ });
+
+ // DumpCollectedEvents(events);
+
+ AssertCallstackSimulationReachesZero(events);
+ }
+
+ [ConditionalFact(typeof(PlatformDetection), nameof(PlatformDetection.IsRuntimeAsyncSupported))]
+ public void RuntimeAsync_CallstackSimulation_HandledException()
+ {
+ var events = CollectEvents(CallstackKeywords, () =>
+ {
+ // DeepOuterCatches -> DeepMiddle -> DeepInnerThrows: exception is caught
+ // within the chain. Unwind pops 2 frames, execution resumes in outer.
+ RunScenarioAndFlush(async () =>
+ {
+ await DeepOuterCatches();
+ });
+ });
+
+ // DumpCollectedEvents(events);
+
+ AssertCallstackSimulationReachesZero(events);
+ }
+
+ [ConditionalFact(typeof(PlatformDetection), nameof(PlatformDetection.IsRuntimeAsyncSupported))]
+ public void RuntimeAsync_CallstackSimulation_UnhandledException()
+ {
+ var events = CollectEvents(CallstackKeywords, () =>
+ {
+ // DeepUnhandledOuter -> DeepUnhandledMiddle -> DeepUnhandledInnerThrows:
+ // no catch in the chain. Unwind pops all 3 frames, task faults.
+ Task task = Task.Run(DeepUnhandledOuter);
+ try
+ {
+ task.GetAwaiter().GetResult();
+ }
+ catch (InvalidOperationException)
+ {
+ }
+ SendFlushCommand();
+ });
+
+ // DumpCollectedEvents(events);
+
+ AssertCallstackSimulationReachesZero(events);
+ }
+
+ [ConditionalFact(typeof(PlatformDetection), nameof(PlatformDetection.IsRuntimeAsyncSupported))]
+ public void RuntimeAsync_UnhandledExceptionUnwind()
+ {
+ var events = CollectEvents(UnwindAsyncExceptionKeyword | CoreKeywords, () =>
+ {
+ // lambda -> DeepUnhandledOuter -> DeepUnhandledMiddle -> DeepUnhandledInnerThrows (4 levels).
+ // No try/catch in the chain — UnwindToPossibleHandler returns null,
+ // triggering the unhandled exception path which faults the task.
+ // unwindedFrames starts at 1 (current) + walks 2 more continuations = 3.
+ try
+ {
+ RunScenario(async () =>
+ {
+ await DeepUnhandledOuter();
+ });
+ }
+ catch (InvalidOperationException)
+ {
+ }
+
+ SendFlushCommand();
+ });
+
+ // DumpCollectedEvents(events);
+
+ var eventIds = CollectAsyncEventIds(events);
+ var frameCounts = CollectUnwindFrameCounts(events);
+
+ Assert.Contains(AsyncEventID.ResumeAsyncContext, eventIds);
+ Assert.Contains(AsyncEventID.UnwindAsyncException, eventIds);
+ Assert.Contains(AsyncEventID.CompleteAsyncContext, eventIds);
+
+ Assert.NotEmpty(frameCounts);
+ Assert.All(frameCounts, count => Assert.Equal(4u, count));
+ }
+
+ [ConditionalFact(typeof(PlatformDetection), nameof(PlatformDetection.IsRuntimeAsyncSupported))]
+ public void RuntimeAsync_HandledExceptionUnwind()
+ {
+ var events = CollectEvents(UnwindAsyncExceptionKeyword | CoreKeywords, () =>
+ {
+ // DeepOuterCatches -> DeepMiddle -> DeepInnerThrows (3 levels).
+ // DeepOuterCatches has try/catch — UnwindToPossibleHandler finds the handler.
+ // unwindedFrames starts at 1 (current) + walks 1 to find handler = 2.
+ RunScenarioAndFlush(async () =>
+ {
+ await DeepOuterCatches();
+ });
+ });
+
+ // DumpCollectedEvents(events);
+
+ var eventIds = CollectAsyncEventIds(events);
+ var frameCounts = CollectUnwindFrameCounts(events);
+
+ Assert.Contains(AsyncEventID.ResumeAsyncContext, eventIds);
+ Assert.Contains(AsyncEventID.UnwindAsyncException, eventIds);
+ Assert.Contains(AsyncEventID.CompleteAsyncContext, eventIds);
+
+ Assert.NotEmpty(frameCounts);
+ Assert.All(frameCounts, count => Assert.Equal(2u, count));
+ }
+
+ [ConditionalFact(typeof(PlatformDetection), nameof(PlatformDetection.IsRuntimeAsyncSupported))]
+ public void RuntimeAsync_WrapperIndexMatchesCallstack()
+ {
+ var captures = new List<(string MethodName, int WrapperSlot)>();
+ ulong scenarioThreadId = 0;
+ long scenarioTimestamp = 0;
+
+ var events = CollectEvents(CallstackKeywords, () =>
+ {
+ // Capture a timestamp just before the scenario runs.
+ // The callstack event closest after this timestamp on the
+ // scenario thread is the one we want — simulating how a CPU
+ // sampler would correlate a sample with a callstack.
+ scenarioTimestamp = Stopwatch.GetTimestamp();
+
+ // WrapperTestA -> WrapperTestB -> WrapperTestC.
+ // Each method captures which Continuation_Wrapper_N dispatched it.
+ RunScenarioAndFlush(async () =>
+ {
+ await WrapperTestA(captures);
+ scenarioThreadId = GetCurrentOSThreadId();
+ });
+ });
+
+ // DumpCollectedEvents(events);
+
+ Assert.True(scenarioThreadId != 0, "Failed to capture scenario thread ID");
+ Assert.True(captures.Count == 3, $"Expected 3 wrapper captures, got {captures.Count}");
+ Assert.All(captures, c => Assert.True(c.WrapperSlot >= 0, $"{c.MethodName} did not find Continuation_Wrapper_N on stack (slot={c.WrapperSlot})"));
+
+ int slotC = captures.First(c => c.MethodName == nameof(WrapperTestC)).WrapperSlot;
+ int slotB = captures.First(c => c.MethodName == nameof(WrapperTestB)).WrapperSlot;
+ int slotA = captures.First(c => c.MethodName == nameof(WrapperTestA)).WrapperSlot;
+
+ Assert.Equal(slotC + 1, slotB);
+ Assert.Equal(slotB + 1, slotA);
+
+ var chainStack = FindCallstackAfterTimestamp(events, scenarioThreadId, scenarioTimestamp);
+
+ Assert.True(chainStack.HasValue, "No callstack found after scenario timestamp on scenario thread");
+ Assert.True(chainStack.Value.FrameCount == 4, $"Expected callstack with 4 frames, got {chainStack.Value.FrameCount}");
+
+ var resolvedNames = new List();
+ foreach (var (nativeIP, _) in chainStack.Value.Frames)
+ {
+ var method = GetMethodFromNativeIP(nativeIP);
+ resolvedNames.Add(method?.Name ?? "");
+ }
+
+ Assert.Equal(nameof(WrapperTestC), resolvedNames[slotC]);
+ Assert.Equal(nameof(WrapperTestB), resolvedNames[slotB]);
+ Assert.Equal(nameof(WrapperTestA), resolvedNames[slotA]);
+ }
+
+ [ConditionalFact(typeof(PlatformDetection), nameof(PlatformDetection.IsRuntimeAsyncSupported))]
+ public void RuntimeAsync_WrapperIndexResetEmitted()
+ {
+ var events = CollectEvents(AllKeywords, () =>
+ {
+ // Recursive chain 34 levels deep crosses the 32-slot boundary,
+ // triggering at least one ResetAsyncContinuationWrapperIndex event.
+ RunScenarioAndFlush(async () =>
+ {
+ await RecursiveFunc(34);
+ });
+ });
+
+ // DumpCollectedEvents(events);
+
+ var eventIds = CollectAsyncEventIds(events);
+
+ Assert.Contains(AsyncEventID.ResetAsyncContinuationWrapperIndex, eventIds);
+ }
+
+ [ConditionalFact(typeof(PlatformDetection), nameof(PlatformDetection.IsRuntimeAsyncSupported))]
+ public void RuntimeAsync_WrapperIndexNoResetUnder32()
+ {
+ var events = CollectEvents(AllKeywords, () =>
+ {
+ // A shallow chain stays within the first 32 slots —
+ // no reset event should be emitted.
+ RunScenarioAndFlush(async () =>
+ {
+ await RecursiveFunc(2);
+ });
+ });
+
+ // DumpCollectedEvents(events);
+
+ var eventIds = CollectAsyncEventIds(events);
+
+ Assert.DoesNotContain(AsyncEventID.ResetAsyncContinuationWrapperIndex, eventIds);
+ }
+
+ [ConditionalFact(typeof(PlatformDetection), nameof(PlatformDetection.IsRuntimeAsyncSupported))]
+ public void RuntimeAsync_PeriodicTimerFlush()
+ {
+ var events = CollectEvents(CoreKeywords, (collectedEvents, _) =>
+ {
+ // Run scenario — do NOT flush explicitly afterwards.
+ RunScenario(async () =>
+ {
+ await Func();
+ });
+
+ // Wait for the periodic flush timer (1s interval) to detect the idle buffer and flush it automatically.
+ Thread.Sleep(1000);
+
+ // Poll to make sure the expected buffer got flush.
+ SpinWait.SpinUntil(() =>
+ {
+ var ids = CollectAsyncEventIds(collectedEvents);
+ return ids.Exists(id => id == AsyncEventID.ResumeAsyncContext || id == AsyncEventID.SuspendAsyncContext || id == AsyncEventID.CompleteAsyncContext);
+ }, TimeSpan.FromSeconds(20));
+ });
+
+ // DumpCollectedEvents(events);
+
+ var eventIds = CollectAsyncEventIds(events);
+ int coreEventCount = eventIds.FindAll(id => id == AsyncEventID.ResumeAsyncContext || id == AsyncEventID.SuspendAsyncContext || id == AsyncEventID.CompleteAsyncContext).Count;
+
+ Assert.True(coreEventCount > 0, "Expected periodic timer to flush buffer with core lifecycle events");
+ }
+
+ [ConditionalFact(typeof(PlatformDetection), nameof(PlatformDetection.IsRuntimeAsyncSupported))]
+ public void RuntimeAsync_PeriodicTimerFlush_PreservesOwnerThreadId()
+ {
+ // This test verifies that when the background flush timer flushes a thread's buffer,
+ // the new header written afterwards preserves the owning thread's OS thread ID
+ // (not the timer thread's ID).
+ //
+ // Strategy: run async work on a dedicated thread so its profiler context gets events.
+ // Between two batches of work, wait for the flush timer to fire. Both buffer flushes
+ // from the dedicated thread should carry the same OsThreadId.
+
+ ulong workerOsThreadId = 0;
+ var workerIdReady = new ManualResetEventSlim(false);
+ var firstBatchDone = new ManualResetEventSlim(false);
+ var firstFlushSeen = new ManualResetEventSlim(false);
+ var workerEvents = new ConcurrentQueue();
+
+ using (var listener = CreateListener(CoreKeywords))
+ {
+ listener.RunWithCallback(e =>
+ {
+ if (!workerIdReady.IsSet)
+ return;
+ if (e.EventId != AsyncEventsId || e.Payload is null || e.Payload.Count == 0)
+ return;
+ if (e.Payload[0] is not byte[] payload)
+ return;
+ EventBufferHeader? header = ParseEventBufferHeader(payload);
+ if (header is not null && header.Value.OsThreadId == workerOsThreadId)
+ workerEvents.Enqueue(e);
+ }, () =>
+ {
+ SendFlushCommand();
+
+ var thread = new Thread(() =>
+ {
+ workerOsThreadId = GetCurrentOSThreadId();
+ workerIdReady.Set();
+
+ // First batch: generate events on this thread's profiler context.
+ Func().GetAwaiter().GetResult();
+ firstBatchDone.Set();
+
+ // Wait for the flush to deliver our first buffer before generating more events.
+ firstFlushSeen.Wait(TimeSpan.FromSeconds(20));
+
+ // Second batch: generate more events on the same thread's context.
+ Func().GetAwaiter().GetResult();
+ });
+
+ thread.IsBackground = true;
+ thread.Start();
+
+ // Wait for the worker to finish its first batch, then force flush.
+ firstBatchDone.Wait(TimeSpan.FromSeconds(20));
+ SendFlushCommand();
+
+ // Poll for first buffer from our worker thread.
+ SpinWait.SpinUntil(() => workerEvents.Count >= 1, TimeSpan.FromSeconds(20));
+ firstFlushSeen.Set();
+
+ // Wait for the worker to finish its second batch.
+ thread.Join(TimeSpan.FromSeconds(20));
+
+ // Force a flush to deliver the second batch.
+ SendFlushCommand();
+
+ // Poll for second buffer from our worker thread.
+ SpinWait.SpinUntil(() => workerEvents.Count >= 2, TimeSpan.FromSeconds(20));
+ });
+ }
+
+ // DumpCollectedEvents(workerEvents);
+
+ Assert.True(workerOsThreadId != 0, "Failed to capture worker OS thread ID");
+
+ // The key assertion: find buffers that contain CreateAsyncContext events (our work batches).
+ // There must be at least 2 such buffers (one per Func() call), and ALL of them must
+ // have the worker's OsThreadId — proving the timer flush didn't corrupt the header.
+ int workBufferCount = 0;
+ foreach (EventWrittenEventArgs e in workerEvents)
+ {
+ if (e.EventId != AsyncEventsId || e.Payload is null || e.Payload.Count == 0)
+ continue;
+ if (e.Payload[0] is not byte[] payload)
+ continue;
+
+ bool hasCreateEvent = false;
+ ParseEventBuffer(payload, (AsyncEventID eventId, ReadOnlySpan buf, ref int idx) =>
+ {
+ if (eventId == AsyncEventID.CreateAsyncContext)
+ hasCreateEvent = true;
+ return SkipEventPayload(eventId, buf, ref idx);
+ });
+
+ if (hasCreateEvent)
+ {
+ workBufferCount++;
+ EventBufferHeader? header = ParseEventBufferHeader(payload);
+ Assert.NotNull(header);
+ Assert.Equal(workerOsThreadId, header.Value.OsThreadId);
+ }
+ }
+
+ Assert.True(workBufferCount >= 2, $"Expected at least 2 buffers with CreateAsyncContext from the worker thread, got {workBufferCount}");
+ }
+
+
+ [ConditionalFact(typeof(PlatformDetection), nameof(PlatformDetection.IsRuntimeAsyncSupported))]
+ public void RuntimeAsync_DeadThreadFlush()
+ {
+ var events = CollectEvents(CoreKeywords, (collectedEvents, _) =>
+ {
+ // Spawn a dedicated thread that runs async work then exits.
+ // Its thread-local buffer becomes orphaned when the thread dies.
+ var thread = new Thread(() =>
+ {
+ RunScenario(async () =>
+ {
+ await Func();
+ });
+ });
+
+ thread.IsBackground = true;
+ thread.Start();
+ bool joined = thread.Join(TimeSpan.FromSeconds(20));
+
+ Assert.True(joined, "Expected worker thread to terminate within timeout before waiting for orphaned buffer flush");
+
+ // Do NOT send a flush command.
+ // Wait for the periodic flush timer to detect the dead thread and flush its orphaned buffer.
+ Thread.Sleep(1000);
+
+ // Poll to make sure the expected buffer got flush.
+ SpinWait.SpinUntil(() =>
+ {
+ var ids = CollectAsyncEventIds(collectedEvents);
+ return ids.Exists(id => id == AsyncEventID.ResumeAsyncContext || id == AsyncEventID.SuspendAsyncContext || id == AsyncEventID.CompleteAsyncContext);
+ }, TimeSpan.FromSeconds(20));
+ });
+
+ // DumpCollectedEvents(events);
+
+ var eventIds = CollectAsyncEventIds(events);
+ int coreEventCount = eventIds.FindAll(id => id == AsyncEventID.ResumeAsyncContext || id == AsyncEventID.SuspendAsyncContext || id == AsyncEventID.CompleteAsyncContext).Count;
+
+ Assert.True(coreEventCount > 0, "Expected periodic timer to flush dead thread's buffer");
+ }
+
+ [ConditionalFact(typeof(PlatformDetection), nameof(PlatformDetection.IsRuntimeAsyncSupported))]
+ public void RuntimeAsync_NoSyncClockEventBeforeInterval()
+ {
+ var events = CollectEvents(CoreKeywords, () =>
+ {
+ RunScenarioAndFlush(async () =>
+ {
+ await Func();
+ });
+ });
+
+ // DumpCollectedEvents(events);
+
+ var eventIds = CollectAsyncEventIds(events);
+
+ Assert.DoesNotContain(AsyncEventID.AsyncProfilerSyncClock, eventIds);
+ }
+
+ [ConditionalFact(typeof(PlatformDetection), nameof(PlatformDetection.IsRuntimeAsyncSupported))]
+ public void RuntimeAsync_NoEventsWhenDisabled()
+ {
+ // Run async work WITHOUT a listener attached
+ Task.Run(async () =>
+ {
+ for (int i = 0; i < 50; i++)
+ {
+ await Func();
+ }
+ }).GetAwaiter().GetResult();
+
+ // Now attach listener and verify no stale events are emitted
+ var events = CollectEvents(CoreKeywords, () =>
+ {
+ // Don't run any async work - just check nothing comes through from before
+ Thread.Sleep(100);
+ });
+
+ // DumpCollectedEvents(events);
+
+ // There may be a ResetAsyncThreadContext from the SyncPoint when keywords change,
+ // but there should be no suspend/resume/complete events from the earlier work.
+ var eventIds = CollectAsyncEventIds(events);
+ int contextEvents = eventIds.FindAll(id => id == AsyncEventID.ResumeAsyncContext || id == AsyncEventID.SuspendAsyncContext || id == AsyncEventID.CompleteAsyncContext).Count;
+
+ Assert.Equal(0, contextEvents);
+ }
+
+ public static IEnumerable