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 KeywordGatekeepingData() + { + yield return new object[] { (long)CreateAsyncContextKeyword, new AsyncEventID[] { AsyncEventID.ResetAsyncThreadContext, AsyncEventID.CreateAsyncContext, AsyncEventID.AsyncProfilerMetadata } }; + yield return new object[] { (long)ResumeAsyncContextKeyword, new AsyncEventID[] { AsyncEventID.ResetAsyncThreadContext, AsyncEventID.ResumeAsyncContext, AsyncEventID.AsyncProfilerMetadata } }; + yield return new object[] { (long)SuspendAsyncContextKeyword, new AsyncEventID[] { AsyncEventID.ResetAsyncThreadContext, AsyncEventID.SuspendAsyncContext, AsyncEventID.AsyncProfilerMetadata } }; + yield return new object[] { (long)CompleteAsyncContextKeyword, new AsyncEventID[] { AsyncEventID.ResetAsyncThreadContext, AsyncEventID.CompleteAsyncContext, AsyncEventID.AsyncProfilerMetadata } }; + yield return new object[] { (long)UnwindAsyncExceptionKeyword, new AsyncEventID[] { AsyncEventID.ResetAsyncThreadContext, AsyncEventID.UnwindAsyncException, AsyncEventID.AsyncProfilerMetadata } }; + yield return new object[] { (long)CreateAsyncCallstackKeyword, new AsyncEventID[] { AsyncEventID.ResetAsyncThreadContext, AsyncEventID.CreateAsyncCallstack, AsyncEventID.AsyncProfilerMetadata } }; + yield return new object[] { (long)ResumeAsyncCallstackKeyword, new AsyncEventID[] { AsyncEventID.ResetAsyncThreadContext, AsyncEventID.ResumeAsyncCallstack, AsyncEventID.AsyncProfilerMetadata } }; + yield return new object[] { (long)SuspendAsyncCallstackKeyword, new AsyncEventID[] { AsyncEventID.ResetAsyncThreadContext, AsyncEventID.SuspendAsyncCallstack, AsyncEventID.AsyncProfilerMetadata } }; + yield return new object[] { (long)ResumeAsyncMethodKeyword, new AsyncEventID[] { AsyncEventID.ResetAsyncThreadContext, AsyncEventID.ResumeAsyncMethod, AsyncEventID.AsyncProfilerMetadata } }; + yield return new object[] { (long)CompleteAsyncMethodKeyword, new AsyncEventID[] { AsyncEventID.ResetAsyncThreadContext, AsyncEventID.CompleteAsyncMethod, AsyncEventID.AsyncProfilerMetadata } }; + } + + [ConditionalTheory(typeof(PlatformDetection), nameof(PlatformDetection.IsRuntimeAsyncSupported))] + [MemberData(nameof(KeywordGatekeepingData))] + public void RuntimeAsync_KeywordGatekeeping(long keywordValue, AsyncEventID[] allowedEventIds) + { + EventKeywords kw = (EventKeywords)keywordValue; + var allowed = new HashSet(allowedEventIds); + + var events = CollectEvents(kw, () => + { + // Run a scenario that exercises all event types: resume, suspend, + // complete, method events, callstacks, and exception unwinds. + // Only the events matching the enabled keyword should be emitted. + RunScenarioAndFlush(async () => + { + await OuterCatches(); + await FuncChained(); + }); + }); + + // DumpCollectedEvents(events); + + var eventIds = CollectAsyncEventIds(events); + var unexpected = eventIds.FindAll(id => !allowed.Contains(id)); + + Assert.True(unexpected.Count == 0, + $"Keyword 0x{(long)kw:X}: unexpected event IDs [{string.Join(", ", unexpected)}], " + + $"allowed [{string.Join(", ", allowed)}]"); + } + + [ConditionalFact(typeof(PlatformDetection), nameof(PlatformDetection.IsRuntimeAsyncSupported))] + public void RuntimeAsync_ResetAsyncThreadContextEvent() + { + var events = CollectEvents(CoreKeywords, () => + { + RunScenarioAndFlush(async () => + { + await Func(); + }); + }); + + // DumpCollectedEvents(events); + + var eventIds = CollectAsyncEventIds(events); + + Assert.Contains(AsyncEventID.ResetAsyncThreadContext, eventIds); + } + + [ConditionalFact(typeof(PlatformDetection), nameof(PlatformDetection.IsRuntimeAsyncSupported))] + public void RuntimeAsync_MetadataEventEmittedOnEnable() + { + var events = CollectEvents(AllKeywords, () => + { + RunScenarioAndFlush(async () => + { + await Func(); + }); + }); + + // DumpCollectedEvents(events); + + var metadataList = CollectMetadataFromBuffer(events); + Assert.True(metadataList.Count >= 1, "Expected at least one metadata event in buffer"); + + MetadataFromBuffer meta = metadataList[0]; + Assert.True(meta.QpcFrequency > 0, $"QPC frequency should be positive, got {meta.QpcFrequency}"); + Assert.True(meta.QpcSync > 0, $"QPC sync timestamp should be positive, got {meta.QpcSync}"); + Assert.True(meta.UtcSync > 0, $"UTC sync timestamp should be positive, got {meta.UtcSync}"); + Assert.True(meta.EventBufferSize > 0, $"Event buffer size should be positive, got {meta.EventBufferSize}"); + Assert.True(meta.WrapperIPs.Length > 0, "Wrapper IPs array should not be empty"); + Assert.All(meta.WrapperIPs, ip => Assert.True(ip != 0, "Each wrapper IP should be non-zero")); + } + + [ConditionalFact(typeof(PlatformDetection), nameof(PlatformDetection.IsRuntimeAsyncSupported))] + public void RuntimeAsync_MetadataEventEmittedOnceAcrossThreads() + { + const int threadCount = 8; + + var events = CollectEvents(AllKeywords, () => + { + using var barrier = new Barrier(threadCount); + var tasks = new Task[threadCount]; + for (int i = 0; i < threadCount; i++) + { + tasks[i] = Task.Factory.StartNew(() => + { + barrier.SignalAndWait(); + Func().GetAwaiter().GetResult(); + }, TaskCreationOptions.LongRunning); + } + Task.WaitAll(tasks); + SendFlushCommand(); + }); + + // DumpCollectedEvents(events); + + var metadataList = CollectMetadataFromBuffer(events); + Assert.True(metadataList.Count == 1, $"Expected exactly 1 metadata event across {threadCount} threads, got {metadataList.Count}"); + } + + [ConditionalFact(typeof(PlatformDetection), nameof(PlatformDetection.IsRuntimeAsyncSupported))] + public void RuntimeAsync_CallstackNativeIPDeltaRoundtrip() + { + // Verify that delta-encoded NativeIPs in callstacks roundtrip correctly, + // including both positive and negative deltas. With multiple distinct async + // methods at different JIT-assigned addresses, the deltas between consecutive + // NativeIPs will naturally span both directions. This exercises the full + // zigzag + LEB128 encode/decode path through the production serializer. + var events = CollectEvents(CallstackKeywords, () => + { + RunScenarioAndFlush(async () => + { + // Run several different call chains to maximize address variation. + await FuncChained(); + await DeepOuterCatches(); + await RecursiveFunc(10); + }); + }); + + var callstacks = CollectCallstacks(events); + Assert.NotEmpty(callstacks); + + // Find callstacks with 3+ frames — enough depth for meaningful deltas. + var deepCallstacks = callstacks.Where(cs => cs.FrameCount >= 3).ToList(); + Assert.True(deepCallstacks.Count > 0, + "Expected at least one callstack with 3+ frames for delta verification"); + + bool hasPositiveDelta = false; + bool hasNegativeDelta = false; + + foreach (var cs in deepCallstacks) + { + for (int i = 0; i < cs.Frames.Count; i++) + { + var (nativeIP, _) = cs.Frames[i]; + Assert.True(nativeIP != 0, $"Frame {i} has zero NativeIP"); + var method = GetMethodFromNativeIP(nativeIP); + Assert.True(method is not null, + $"Frame {i}: NativeIP 0x{nativeIP:X} does not resolve to a managed method"); + + if (i > 0) + { + long delta = (long)(cs.Frames[i].NativeIP - cs.Frames[i - 1].NativeIP); + if (delta > 0) + hasPositiveDelta = true; + else if (delta < 0) + hasNegativeDelta = true; + } + } + } + + // With multiple distinct async methods at different addresses, we expect + // both positive and negative deltas. If the JIT happens to lay out all + // methods monotonically (extremely unlikely), at minimum we must see + // non-zero deltas proving the encoding works. + Assert.True(hasPositiveDelta || hasNegativeDelta, + "Expected at least one non-zero NativeIP delta across all callstack frames"); + } + + [ConditionalFact(typeof(PlatformDetection), nameof(PlatformDetection.IsRuntimeAsyncSupported))] + public void RuntimeAsync_CallstackStressWithVaryingDepths() + { + // Stress test: run many async calls with varying callstack depths. + // Varying sizes mean some callstacks will land at buffer boundaries, + // naturally exercising the overflow/rewind path in callstack emission. + // RecursiveFunc(d) produces exactly d frames on the chain. The lambda + // that calls it adds one more frame, so the total callstack depth is d + 1. + const int iterations = 200; + int[] depths = new int[iterations]; + var rng = new Random(42); + for (int i = 0; i < iterations; i++) + depths[i] = rng.Next(1, 120); + + var events = CollectEvents(CallstackKeywords, () => + { + RunScenarioAndFlush(async () => + { + for (int i = 0; i < iterations; i++) + await RecursiveFunc(depths[i]); + }); + }); + + // DumpCollectedEvents(events); + + // Collect all resume callstacks with timestamps, sorted by timestamp. + var callstacksWithTimestamp = new List<(long Timestamp, byte FrameCount, List<(ulong NativeIP, int State)> Frames)>(); + ForEachEventBufferPayload(events, buffer => + { + 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); + callstacksWithTimestamp.Add((timestamp, frameCount, frames)); + return true; + } + return SkipEventPayload(eventId, buf, ref idx); + }); + }); + + callstacksWithTimestamp.Sort((a, b) => a.Timestamp.CompareTo(b.Timestamp)); + + // Verify all callstacks have valid frame data that resolves to managed methods. + foreach (var cs in callstacksWithTimestamp) + { + Assert.True(cs.FrameCount > 0, "Callstack has 0 frames"); + Assert.Equal(cs.FrameCount, cs.Frames.Count); + for (int f = 0; f < cs.Frames.Count; f++) + { + var (nativeIP, _) = cs.Frames[f]; + Assert.True(nativeIP != 0, $"Frame {f} has zero NativeIP"); + var method = GetMethodFromNativeIP(nativeIP); + Assert.True(method is not null, + $"Frame {f}: NativeIP 0x{nativeIP:X} does not resolve to a managed method"); + } + } + + // One resume callstack per iteration; find our sequence at the end + // (earlier entries may be from metadata/warmup). + Assert.True(callstacksWithTimestamp.Count >= iterations, + $"Expected at least {iterations} callstacks, got {callstacksWithTimestamp.Count}"); + + int startOffset = callstacksWithTimestamp.Count - iterations; + for (int i = 0; i < iterations; i++) + { + int expected = depths[i] + 1; + int actual = callstacksWithTimestamp[startOffset + i].FrameCount; + Assert.True(actual == expected, + $"Iteration {i}: expected depth {expected} (RecursiveFunc({depths[i]}) + lambda), got {actual}"); + } + + // Verify multiple buffer flushes occurred. + int bufferCount = 0; + ForEachEventBufferPayload(events, _ => bufferCount++); + Assert.True(bufferCount >= 3, $"Expected at least 3 buffer flushes, got {bufferCount}"); + } + + [ConditionalFact(typeof(PlatformDetection), nameof(PlatformDetection.IsRuntimeAsyncSupported))] + public void RuntimeAsync_CallstackOverflowPathProducesValidFrames() + { + // Targeted test: run random-depth callstacks until we detect the overflow + // path was exercised, then validate the affected callstack. + // The overflow path fires when a large callstack doesn't fit inline in the + // remaining buffer space — the code rewinds, flushes, and re-writes the + // callstack as the first event in a fresh buffer. + bool overflowDetected = false; + var rng = new Random(42); + + for (int attempt = 0; attempt < 10 && !overflowDetected; attempt++) + { + int iterations = 500; + int[] depths = new int[iterations]; + for (int i = 0; i < iterations; i++) + depths[i] = rng.Next(50, 250); + + var events = CollectEvents(ResumeAsyncCallstackKeyword, () => + { + RunScenarioAndFlush(async () => + { + for (int i = 0; i < iterations; i++) + await RecursiveFunc(depths[i]); + }); + }); + + // Check each buffer: if the first event is a large ResumeAsyncCallstack, + // the overflow path flushed the previous buffer and re-wrote here. + ForEachEventBufferPayload(events, buffer => + { + if (overflowDetected) + return; + + int index = HeaderSize; + if (index + 2 > buffer.Length) + return; + + AsyncEventID firstEvent = (AsyncEventID)buffer[index++]; + ReadCompressedUInt64(buffer, ref index); + if (firstEvent != AsyncEventID.ResumeAsyncCallstack) + return; + + ReadCallstackPayload(buffer, ref index, out byte frameCount, out var frames); + if (frameCount <= 30) + return; + + overflowDetected = true; + + Assert.Equal(frameCount, frames.Count); + for (int f = 0; f < frames.Count; f++) + { + var (nativeIP, _) = frames[f]; + Assert.True(nativeIP != 0, $"Overflow callstack frame {f} has zero NativeIP"); + var method = GetMethodFromNativeIP(nativeIP); + Assert.True(method is not null, + $"Overflow callstack frame {f}: NativeIP 0x{nativeIP:X} does not resolve to a managed method"); + } + }); + } + + Assert.True(overflowDetected, + "Failed to trigger callstack buffer overflow after 10 attempts"); + } + + [ConditionalFact(typeof(PlatformDetection), nameof(PlatformDetection.IsRuntimeAsyncSupported))] + public void RuntimeAsync_CallstackDepthCappedAtMaxFrames() + { + // Verify that callstack depth is capped when the continuation chain + // exceeds the maximum frame count (255, limited by byte storage). + // RecursiveFunc(300) produces a 300-deep chain + 1 lambda = 301 frames. + const int requestedDepth = 300; + + var events = CollectEvents(ResumeAsyncCallstackKeyword, () => + { + RunScenarioAndFlush(async () => + { + await RecursiveFunc(requestedDepth); + }); + }); + + // DumpCollectedEvents(events); + + var callstacks = CollectCallstacks(events); + Assert.True(callstacks.Count >= 1, "Expected at least one callstack"); + + // Find the callstack from our deep RecursiveFunc call. + // The max frame count is capped at 255 (byte.MaxValue) since the + // CaptureRuntimeAsyncCallstackState.Count is a byte. + // RecursiveFunc(300) + 1 lambda = 301 frames, capped to 255. + var deepest = callstacks.MaxBy(cs => cs.FrameCount); + Assert.Equal(255, deepest.FrameCount); + Assert.Equal(deepest.FrameCount, deepest.Frames.Count); + + // Verify all frames are valid. + foreach (var (nativeIP, _) in deepest.Frames) + { + Assert.True(nativeIP != 0, "Frame has zero NativeIP"); + var method = GetMethodFromNativeIP(nativeIP); + Assert.True(method is not null, + $"NativeIP 0x{nativeIP:X} does not resolve to a managed method"); + } + } + + [ConditionalFact(typeof(PlatformDetection), nameof(PlatformDetection.IsRuntimeAsyncSupported))] + public void RuntimeAsync_MetadataWrapperIPsMatchMethods() + { + var events = CollectEvents(AllKeywords, () => + { + RunScenarioAndFlush(async () => + { + await Func(); + }); + }); + + // DumpCollectedEvents(events); + + var metadataList = CollectMetadataFromBuffer(events); + Assert.True(metadataList.Count >= 1, "Expected at least one metadata event in buffer"); + + long[] wrapperIPs = metadataList[0].WrapperIPs; + + Type cwType = typeof(object).Assembly.GetType("System.Runtime.CompilerServices.AsyncProfiler+ContinuationWrapper"); + Assert.NotNull(cwType); + + for (int i = 0; i < wrapperIPs.Length; i++) + { + string expectedName = $"Continuation_Wrapper_{i}"; + MethodInfo method = cwType.GetMethod(expectedName, BindingFlags.NonPublic | BindingFlags.Static); + Assert.True(method is not null, $"Expected method '{expectedName}' to exist on ContinuationWrapper type"); + + System.Runtime.CompilerServices.RuntimeHelpers.PrepareMethod(method.MethodHandle); + long expectedIP = method.MethodHandle.GetFunctionPointer().ToInt64(); + + Assert.True(wrapperIPs[i] == expectedIP, + $"Wrapper IP mismatch at index {i}: metadata has 0x{wrapperIPs[i]:X}, " + + $"method '{expectedName}' has 0x{expectedIP:X}"); + } + } + } + + internal static class EventBuffer + { + public static int OutputEventBuffer(ReadOnlySpan buffer) + { + Console.WriteLine("--- AsyncEvents ---"); + + int index = 0; + + if ((uint)buffer.Length < 1) + { + Console.WriteLine("Buffer too small."); + Console.WriteLine("----------------------------------"); + return index; + } + + byte version = buffer[index++]; + Console.WriteLine($"Version: {version}"); + + if (version != 1) + { + Console.WriteLine($"Unsupported version: {version}"); + Console.WriteLine("----------------------------------"); + return index; + } + + Deserializer.ReadUInt32(buffer, ref index, out uint totalSize); + Deserializer.ReadUInt32(buffer, ref index, out uint contextId); + Deserializer.ReadUInt64(buffer, ref index, out ulong osThreadId); + Deserializer.ReadUInt32(buffer, ref index, out uint totalEventCount); + Deserializer.ReadUInt64(buffer, ref index, out ulong startTimestamp); + Deserializer.ReadUInt64(buffer, ref index, out ulong endTimestamp); + + Console.WriteLine($"TotalSize (bytes): {totalSize}"); + Console.WriteLine($"AsyncThreadContextId: {contextId}"); + Console.WriteLine($"OSThreadId: {osThreadId}"); + Console.WriteLine($"TotalEventCount: {totalEventCount}"); + Console.WriteLine($"StartTimestamp: 0x{startTimestamp:X16}"); + Console.WriteLine($"EndTimestamp: 0x{endTimestamp:X16}"); + + int eventCount = 0; + ulong currentTimestamp = startTimestamp; + + while (index < buffer.Length) + { + if (index + 2 > buffer.Length) + { + Console.WriteLine($"Trailing bytes: {buffer.Length - index} (incomplete entry header)."); + break; + } + + AsyncEventID eventId = (AsyncEventID)buffer[index++]; + + Deserializer.ReadCompressedUInt64(buffer, ref index, out ulong delta); + currentTimestamp += delta; + + Console.WriteLine($"Entry[{eventCount}]: Timestamp=0x{currentTimestamp:X16}, EventId={eventId}"); + + int payloadStart = index; + try + { + index += eventId switch + { + AsyncEventID.CreateAsyncContext => OutputCreateAsyncContextEvent(buffer.Slice(index)), + AsyncEventID.ResumeAsyncContext => OutputResumeAsyncContextEvent(buffer.Slice(index)), + AsyncEventID.SuspendAsyncContext => OutputSuspendAsyncContextEvent(), + AsyncEventID.CompleteAsyncContext => OutputCompleteAsyncContextEvent(), + AsyncEventID.UnwindAsyncException => OutputUnwindAsyncExceptionEvent(buffer.Slice(index)), + AsyncEventID.CreateAsyncCallstack => OutputAsyncCallstackEvent("CreateAsyncCallstack", buffer.Slice(index)), + AsyncEventID.ResumeAsyncCallstack => OutputAsyncCallstackEvent("ResumeAsyncCallstack", buffer.Slice(index)), + AsyncEventID.SuspendAsyncCallstack => OutputAsyncCallstackEvent("SuspendAsyncCallstack", buffer.Slice(index)), + AsyncEventID.ResumeAsyncMethod => OutputResumeAsyncMethodEvent(), + AsyncEventID.CompleteAsyncMethod => OutputCompleteAsyncMethodEvent(), + AsyncEventID.ResetAsyncThreadContext => OutputResetAsyncThreadContextEvent(), + AsyncEventID.ResetAsyncContinuationWrapperIndex => OutputResetAsyncContinuationWrapperIndexEvent(), + AsyncEventID.AsyncProfilerMetadata => OutputAsyncProfilerMetadataEvent(buffer.Slice(index)), + _ => throw new InvalidOperationException($"Unknown eventId {eventId}."), + }; + } + catch (Exception ex) + { + Console.WriteLine($" Failed decoding entry payload at offset {payloadStart}: {ex.GetType().Name}: {ex.Message}"); + break; + } + + eventCount++; + } + + Console.WriteLine($"TotalEntriesDecoded: {eventCount}"); + Console.WriteLine("----------------------------------"); + + return index; + } + + private static int OutputCreateAsyncContextEvent(ReadOnlySpan buffer) + { + int index = 0; + Deserializer.ReadCompressedUInt64(buffer, ref index, out ulong id); + Console.WriteLine("--- CreateAsyncContext ---"); + Console.WriteLine($" ID: {id}"); + Console.WriteLine("----------------------------"); + return index; + } + + private static int OutputResumeAsyncContextEvent(ReadOnlySpan buffer) + { + int index = 0; + Deserializer.ReadCompressedUInt64(buffer, ref index, out ulong id); + Console.WriteLine("--- ResumeAsyncContext ---"); + Console.WriteLine($" ID: {id}"); + Console.WriteLine("----------------------------"); + return index; + } + + private static int OutputSuspendAsyncContextEvent() + { + Console.WriteLine("--- SuspendAsyncContext ---"); + Console.WriteLine("----------------------------"); + return 0; + } + + private static int OutputCompleteAsyncContextEvent() + { + Console.WriteLine("--- CompleteAsyncContext ---"); + Console.WriteLine("----------------------------"); + return 0; + } + + private static int OutputUnwindAsyncExceptionEvent(ReadOnlySpan buffer) + { + uint unwindedFrames; + int index = 0; + + Deserializer.ReadCompressedUInt32(buffer, ref index, out unwindedFrames); + index += OutputUnwindAsyncExceptionEvent(unwindedFrames); + + return index; + } + + private static int OutputUnwindAsyncExceptionEvent(uint unwindedFrames) + { + Console.WriteLine("--- UnwindAsyncException ---"); + Console.WriteLine($"Unwinded Frames: {unwindedFrames}"); + Console.WriteLine("----------------------------"); + return 0; + } + + private static int OutputResumeAsyncMethodEvent() + { + Console.WriteLine("--- ResumeAsyncMethod ---"); + Console.WriteLine("----------------------------"); + return 0; + } + + private static int OutputCompleteAsyncMethodEvent() + { + Console.WriteLine("--- CompleteAsyncMethod ---"); + Console.WriteLine("----------------------------"); + return 0; + } + + private static int OutputResetAsyncContinuationWrapperIndexEvent() + { + Console.WriteLine("--- ResetAsyncContinuationWrapperIndex ---"); + Console.WriteLine("----------------------------"); + return 0; + } + + private static int OutputResetAsyncThreadContextEvent() + { + Console.WriteLine("--- ResetAsyncThreadContext ---"); + Console.WriteLine("----------------------------"); + return 0; + } + + private static int OutputAsyncProfilerMetadataEvent(ReadOnlySpan buffer) + { + int index = 0; + Console.WriteLine("--- AsyncProfilerMetadata ---"); + + Deserializer.ReadCompressedUInt64(buffer, ref index, out ulong qpcFrequency); + Console.WriteLine($" QPCFrequency: {qpcFrequency}"); + + Deserializer.ReadCompressedUInt64(buffer, ref index, out ulong qpcSync); + Console.WriteLine($" QPCSync: {qpcSync}"); + + Deserializer.ReadCompressedUInt64(buffer, ref index, out ulong utcSync); + Console.WriteLine($" UTCSync: {utcSync}"); + + Deserializer.ReadCompressedUInt32(buffer, ref index, out uint eventBufferSize); + Console.WriteLine($" EventBufferSize: {eventBufferSize}"); + + byte wrapperCount = buffer[index++]; + Console.WriteLine($" WrapperCount: {wrapperCount}"); + + for (int i = 0; i < wrapperCount; i++) + { + Deserializer.ReadCompressedUInt64(buffer, ref index, out ulong ip); + Console.WriteLine($" Wrapper[{i}]: 0x{ip:X16}"); + } + + Console.WriteLine("----------------------------"); + return index; + } + + private static int OutputAsyncCallstackEvent(string eventName, ReadOnlySpan buffer) + { + ulong id; + byte type; + byte callstackId; + byte asyncCallstackLength; + int index = 0; + + type = buffer[index++]; + callstackId = buffer[index++]; + asyncCallstackLength = buffer[index++]; + Deserializer.ReadCompressedUInt64(buffer, ref index, out id); + + Console.WriteLine($"--- {eventName} ---"); + Console.WriteLine($"ID: {id}"); + Console.WriteLine($"Type: {type}"); + Console.WriteLine($"CallstackId: {callstackId}"); + Console.WriteLine($"Length: {asyncCallstackLength}"); + + if (asyncCallstackLength == 0) + { + return index; + } + + ulong previousNativeIP; + ulong currentNativeIP; + int state; + + Deserializer.ReadCompressedUInt64(buffer, ref index, out currentNativeIP); + Deserializer.ReadCompressedInt32(buffer, ref index, out state); + + OutputAsyncFrame(currentNativeIP, state, 0); + + for (int i = 1; i < asyncCallstackLength; i++) + { + previousNativeIP = currentNativeIP; + Deserializer.ReadCompressedInt64(buffer, ref index, out long nativeIPDelta); + Deserializer.ReadCompressedInt32(buffer, ref index, out state); + currentNativeIP = previousNativeIP + (ulong)nativeIPDelta; + OutputAsyncFrame(currentNativeIP, state, i); + } + + return index; + } + + private static readonly MethodInfo? s_getMethodFromNativeIP = + typeof(StackFrame).GetMethod("GetMethodFromNativeIP", BindingFlags.Static | BindingFlags.NonPublic | BindingFlags.Public); + + private static string ResolveAsyncMethodName(nint nativeIP) + { + if (s_getMethodFromNativeIP is not null) + { + try + { + MethodBase? method = s_getMethodFromNativeIP.Invoke(null, [nativeIP]) as MethodBase; + return method?.Name ?? string.Empty; + } + catch + { + } + } + + return string.Empty; + } + + private static void OutputAsyncFrame(ulong nativeIP, int state, int frameIndex) + { + string asyncMethodName = ResolveAsyncMethodName((nint)nativeIP); + asyncMethodName = !string.IsNullOrEmpty(asyncMethodName) ? asyncMethodName : $"??"; + string nativeIPString = $"0x{nativeIP:X}"; + Console.WriteLine($" Frame {frameIndex}: AsyncMethod = {asyncMethodName}, NativeIP = {nativeIPString}, State = {state}"); + } + + internal static class Deserializer + { + public static void ReadInt32(ReadOnlySpan buffer, ref int index, out int value) + { + uint uValue; + ReadUInt32(buffer, ref index, out uValue); + value = (int)uValue; + } + + public static void ReadCompressedInt32(ReadOnlySpan buffer, ref int index, out int value) + { + uint uValue; + ReadCompressedUInt32(buffer, ref index, out uValue); + value = ZigzagDecodeInt32(uValue); + } + + public static void ReadUInt32(ReadOnlySpan buffer, ref int index, out uint value) + { + value = BinaryPrimitives.ReadUInt32LittleEndian(buffer.Slice(index)); + index += 4; + } + + public static void ReadCompressedUInt32(ReadOnlySpan buffer, ref int index, out uint value) + { + int shift = 0; + byte b; + + value = 0; + do + { + b = buffer[index++]; + value |= (uint)(b & 0x7F) << shift; + shift += 7; + } while ((b & 0x80) != 0); + } + + public static void ReadInt64(ReadOnlySpan buffer, ref int index, out long value) + { + ulong uValue; + ReadUInt64(buffer, ref index, out uValue); + value = (long)uValue; + } + + public static void ReadCompressedInt64(ReadOnlySpan buffer, ref int index, out long value) + { + ulong uValue; + ReadCompressedUInt64(buffer, ref index, out uValue); + value = ZigzagDecodeInt64(uValue); + } + + public static void ReadUInt64(ReadOnlySpan buffer, ref int index, out ulong value) + { + value = BinaryPrimitives.ReadUInt64LittleEndian(buffer.Slice(index)); + index += 8; + } + + public static void ReadCompressedUInt64(ReadOnlySpan buffer, ref int index, out ulong value) + { + int shift = 0; + byte b; + + value = 0; + do + { + b = buffer[index++]; + value |= (ulong)(b & 0x7F) << shift; + shift += 7; + } while ((b & 0x80) != 0); + } + + private static int ZigzagDecodeInt32(uint value) => (int)((value >> 1) ^ (~(value & 1) + 1)); + + private static long ZigzagDecodeInt64(ulong value) => (long)((value >> 1) ^ (~(value & 1) + 1)); + } + } +} diff --git a/src/libraries/System.Runtime/tests/System.Threading.Tasks.Tests/System.Runtime.CompilerServices/RuntimeAsyncTests.cs b/src/libraries/System.Runtime/tests/System.Threading.Tasks.Tests/System.Runtime.CompilerServices/RuntimeAsyncTests.cs index 7e28cbbfd1e1d9..5501e1feca5fba 100644 --- a/src/libraries/System.Runtime/tests/System.Threading.Tasks.Tests/System.Runtime.CompilerServices/RuntimeAsyncTests.cs +++ b/src/libraries/System.Runtime/tests/System.Threading.Tasks.Tests/System.Runtime.CompilerServices/RuntimeAsyncTests.cs @@ -22,25 +22,25 @@ public class RuntimeAsyncTests private static readonly FieldInfo s_activeFlagsField = GetCorLibClassStaticField("System.Runtime.CompilerServices.AsyncInstrumentation", "s_activeFlags"); private static readonly object s_debuggerLock = new object(); - private static TestEventListener? s_debuggerTplInstance; // AsyncDebugger(0x2000000) | all event flags(0x7F) private const uint EnabledInstrumentationFlags = 0x200007F; private const uint DisabledInstrumentationFlags = 0x0; - private const uint UninitializedInstrumentationFlags = 0x80000000; + private const uint SynchronizeInstrumentationFlags = 0x80000000; private static void AttachDebugger() { - // Simulate a debugger attach to process, creating TPL event source session + setting s_asyncDebuggingEnabled. + // Simulate a debugger attach to process by setting s_asyncDebuggingEnabled + // and triggering a flag synchronization via the Synchronize bit. lock (s_debuggerLock) { uint flags = Convert.ToUInt32(s_activeFlagsField.GetValue(null)); - Assert.True(flags == UninitializedInstrumentationFlags || flags == DisabledInstrumentationFlags, $"ActiveFlags equals {flags}, expected {UninitializedInstrumentationFlags} || {DisabledInstrumentationFlags}"); + Assert.True(flags == SynchronizeInstrumentationFlags || flags == DisabledInstrumentationFlags, $"ActiveFlags equals {flags}, expected {SynchronizeInstrumentationFlags} || {DisabledInstrumentationFlags}"); - s_debuggerTplInstance = new TestEventListener("System.Threading.Tasks.TplEventSource", EventLevel.Verbose); s_asyncDebuggingEnabledField.SetValue(null, true); + s_activeFlagsField.SetValue(null, flags | SynchronizeInstrumentationFlags); - // Initialize flags and collections. + // Run an async method to trigger SyncActiveFlags which will pick up the Synchronize bit. Func().GetAwaiter().GetResult(); flags = Convert.ToUInt32(s_activeFlagsField.GetValue(null)); @@ -62,11 +62,15 @@ private static void DetachDebugger() // Simulate a debugger detach from process. lock (s_debuggerLock) { + uint flags = Convert.ToUInt32(s_activeFlagsField.GetValue(null)); s_asyncDebuggingEnabledField.SetValue(null, false); - s_debuggerTplInstance?.Dispose(); - s_debuggerTplInstance = null; + s_activeFlagsField.SetValue(null, flags | SynchronizeInstrumentationFlags); - uint flags = Convert.ToUInt32(s_activeFlagsField.GetValue(null)); + // Run an async method to trigger SyncActiveFlags which will detect + // s_asyncDebuggingEnabled is false and clear the debugger flags. + Func().GetAwaiter().GetResult(); + + flags = Convert.ToUInt32(s_activeFlagsField.GetValue(null)); Assert.True(flags == DisabledInstrumentationFlags, $"ActiveFlags equals {flags}, expected {DisabledInstrumentationFlags}"); } } @@ -181,7 +185,6 @@ static void ValidateTimestampsCleared() } [ConditionalFact(typeof(RuntimeAsyncTests), nameof(IsRemoteExecutorAndRuntimeAsyncSupported))] - [ActiveIssue("https://github.com/dotnet/runtime/issues/124072", typeof(PlatformDetection), nameof(PlatformDetection.IsInterpreter))] public void RuntimeAsync_TaskCompleted() { RemoteExecutor.Invoke(async () => @@ -201,7 +204,6 @@ public void RuntimeAsync_TaskCompleted() } [ConditionalFact(typeof(RuntimeAsyncTests), nameof(IsRemoteExecutorAndRuntimeAsyncSupported))] - [ActiveIssue("https://github.com/dotnet/runtime/issues/124072", typeof(PlatformDetection), nameof(PlatformDetection.IsInterpreter))] public void RuntimeAsync_ExceptionCleanup() { RemoteExecutor.Invoke(async () => @@ -227,7 +229,6 @@ public void RuntimeAsync_ExceptionCleanup() } [ConditionalFact(typeof(RuntimeAsyncTests), nameof(IsRemoteExecutorAndRuntimeAsyncSupported))] - [ActiveIssue("https://github.com/dotnet/runtime/issues/124072", typeof(PlatformDetection), nameof(PlatformDetection.IsInterpreter))] public void RuntimeAsync_DebuggerDetach() { RemoteExecutor.Invoke(async () => @@ -288,7 +289,6 @@ public void RuntimeAsync_DebuggerDetach() } [ConditionalFact(typeof(RuntimeAsyncTests), nameof(IsRemoteExecutorAndRuntimeAsyncSupported))] - [ActiveIssue("https://github.com/dotnet/runtime/issues/124072", typeof(PlatformDetection), nameof(PlatformDetection.IsInterpreter))] public void RuntimeAsync_ValueTypeResult() { RemoteExecutor.Invoke(async () => @@ -309,7 +309,6 @@ public void RuntimeAsync_ValueTypeResult() } [ConditionalFact(typeof(RuntimeAsyncTests), nameof(IsRemoteExecutorAndRuntimeAsyncSupported))] - [ActiveIssue("https://github.com/dotnet/runtime/issues/124072", typeof(PlatformDetection), nameof(PlatformDetection.IsInterpreter))] public void RuntimeAsync_HandledExceptionPartialUnwind() { RemoteExecutor.Invoke(async () => @@ -329,7 +328,6 @@ public void RuntimeAsync_HandledExceptionPartialUnwind() } [ConditionalFact(typeof(RuntimeAsyncTests), nameof(IsRemoteExecutorAndRuntimeAsyncSupported))] - [ActiveIssue("https://github.com/dotnet/runtime/issues/124072", typeof(PlatformDetection), nameof(PlatformDetection.IsInterpreter))] public void RuntimeAsync_CancellationCleanup() { RemoteExecutor.Invoke(async () => @@ -358,7 +356,6 @@ public void RuntimeAsync_CancellationCleanup() } [ConditionalFact(typeof(RuntimeAsyncTests), nameof(IsRemoteExecutorAndRuntimeAsyncSupported))] - [ActiveIssue("https://github.com/dotnet/runtime/issues/124072", typeof(PlatformDetection), nameof(PlatformDetection.IsInterpreter))] public void RuntimeAsync_TimestampsTrackedWhileInFlight() { RemoteExecutor.Invoke(async () => @@ -421,7 +418,6 @@ public void RuntimeAsync_TimestampsTrackedWhileInFlight() } [ConditionalFact(typeof(RuntimeAsyncTests), nameof(IsRemoteExecutorAndRuntimeAsyncSupported))] - [ActiveIssue("https://github.com/dotnet/runtime/issues/124072", typeof(PlatformDetection), nameof(PlatformDetection.IsInterpreter))] public void RuntimeAsync_ContinuationTimestampObservedDuringResume() { RemoteExecutor.Invoke(async () => @@ -454,7 +450,6 @@ public void RuntimeAsync_ContinuationTimestampObservedDuringResume() } [ConditionalFact(typeof(RuntimeAsyncTests), nameof(IsRemoteExecutorAndRuntimeAsyncSupported))] - [ActiveIssue("https://github.com/dotnet/runtime/issues/124072", typeof(PlatformDetection), nameof(PlatformDetection.IsInterpreter))] public void RuntimeAsync_InFlightInstrumentationUpgrade() { RemoteExecutor.Invoke(async () => @@ -504,7 +499,6 @@ public void RuntimeAsync_InFlightInstrumentationUpgrade() } [ConditionalFact(typeof(RuntimeAsyncTests), nameof(IsRemoteExecutorAndRuntimeAsyncSupported))] - [ActiveIssue("https://github.com/dotnet/runtime/issues/124072", typeof(PlatformDetection), nameof(PlatformDetection.IsInterpreter))] public void RuntimeAsync_TplEvents() { RemoteExecutor.Invoke(() => @@ -539,7 +533,6 @@ public void RuntimeAsync_TplEvents() } [ConditionalFact(typeof(RuntimeAsyncTests), nameof(IsRemoteExecutorAndRuntimeAsyncSupported))] - [ActiveIssue("https://github.com/dotnet/runtime/issues/124072", typeof(PlatformDetection), nameof(PlatformDetection.IsInterpreter))] public void RuntimeAsync_NoTplEventsWithoutDebugger() { RemoteExecutor.Invoke(() => diff --git a/src/libraries/System.Runtime/tests/System.Threading.Tasks.Tests/System.Threading.Tasks.Tests.csproj b/src/libraries/System.Runtime/tests/System.Threading.Tasks.Tests/System.Threading.Tasks.Tests.csproj index a2298f56f14bd5..a8c936a9e0093c 100644 --- a/src/libraries/System.Runtime/tests/System.Threading.Tasks.Tests/System.Threading.Tasks.Tests.csproj +++ b/src/libraries/System.Runtime/tests/System.Threading.Tasks.Tests/System.Threading.Tasks.Tests.csproj @@ -59,6 +59,7 @@ +