From 8b0a6dec31185a0f11ab52e44955c6d7efb9b188 Mon Sep 17 00:00:00 2001 From: lateralusX Date: Thu, 2 Apr 2026 15:47:16 +0200 Subject: [PATCH 01/22] High-performance async method profiling over EventSource. Commit adds AsyncProfilerBufferedEventSource - a high-performance EventSource for async method profiling that uses per-thread buffered event emission with centralized flush coordination. Key design: - Per-thread event buffers with lock-free acquire/release for zero-contention writes on the hot path. - Delta timestamp encoding using compressed variable-length integers, reducing per-event timestamp overhead from 8 bytes to typically 1-2 bytes under load. - Delta IP encoding using compressed variable length integers, reducing bytes used per frame IP. - Variable-length compressed integers using LEB128 and zigzag encoding. - Centralized AsyncThreadContextCache with background flush timer for idle and dead thread buffer reclamation. - Continuation wrapper table for compact async callstack representation, mapping runtime IPs to table indices. Makes it possible to match sync callstacks captured by OS CPU profiler with resume async callstack event. Event types cover the full async lifecycle: - async context: create/resume/suspend/complete. - async method: resume/complete. - exception unwind: unhandled/handled. - async callstacks: create/resume/suspend. Buffer management: - Configurable buffer size via DOTNET_AsyncProfilerBufferedEventSource_EventBufferSize (default 16KB - 256 bytes). - Optimized buffer serialization methods, low overhead serializing events. - SyncPoint mechanism for coordinated config changes across writer threads. - BlockContext flag for safe flush-thread access to live thread buffers with 100ms spin timeout to prevent flush thread stalls. Integration: - Async profiler wired into AsyncInstrumentation alongside the async debugger. - EventSource + AsyncProfiler, cross runtime support. Runtime specific parts implemented in a CoreCLR specific source file. - Mono stub for potential future platform support (AsyncV1). Includes comprehensive test coverage (AsyncProfilerTests) validating event correctness, buffer serialization, delta encoding, callstack capture, config changes, and multi-threaded stress scenarios. --- .../System.Private.CoreLib.csproj | 1 + .../CompilerServices/AsyncHelpers.CoreCLR.cs | 132 +- .../CompilerServices/AsyncProfiler.CoreCLR.cs | 575 +++++ .../src/System.Private.CoreLib.csproj | 1 + .../System.Private.CoreLib.Shared.projitems | 4 +- .../CompilerServices/AsyncInstrumentation.cs | 3 +- .../Runtime/CompilerServices/AsyncProfiler.cs | 1067 ++++++++ .../AsyncProfilerBufferedEventSource.cs | 102 + .../AsyncProfilerTests.cs | 2140 +++++++++++++++++ .../System.Threading.Tasks.Tests.csproj | 1 + .../System.Private.CoreLib.csproj | 1 + .../CompilerServices/AsyncProfiler.Mono.cs | 26 + 12 files changed, 4031 insertions(+), 22 deletions(-) create mode 100644 src/coreclr/System.Private.CoreLib/src/System/Runtime/CompilerServices/AsyncProfiler.CoreCLR.cs create mode 100644 src/libraries/System.Private.CoreLib/src/System/Runtime/CompilerServices/AsyncProfiler.cs create mode 100644 src/libraries/System.Private.CoreLib/src/System/Runtime/CompilerServices/AsyncProfilerBufferedEventSource.cs create mode 100644 src/libraries/System.Runtime/tests/System.Threading.Tasks.Tests/System.Runtime.CompilerServices/AsyncProfilerTests.cs create mode 100644 src/mono/System.Private.CoreLib/src/System/Runtime/CompilerServices/AsyncProfiler.Mono.cs 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..a3c80e5d8e3a8e 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] @@ -496,7 +503,7 @@ internal unsafe bool HandleSuspended(ref RuntimeAsyncAwaitState state) internal void InstrumentedHandleSuspended(AsyncInstrumentation.Flags flags, ref RuntimeAsyncAwaitState state, Continuation? newContinuation = null) { - if (AsyncInstrumentation.IsEnabled.AsyncDebugger(flags)) + if (AsyncDebugger.IsEnabled(flags)) { Continuation? nextContinuation = t_runtimeAsyncAwaitState.SentinelContinuation!.Next; @@ -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); 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,7 +845,16 @@ private static void InstrumentedFinalizeRuntimeAsyncTask(RuntimeAsyncTask { if (AsyncInstrumentation.IsEnabled.CreateAsyncContext(flags)) { - if (AsyncInstrumentation.IsEnabled.AsyncDebugger(flags)) + if (AsyncInstrumentation.IsEnabled.AsyncProfiler(flags)) + { + Continuation? nc = t_runtimeAsyncAwaitState.SentinelContinuation!.Next; + if (nc != null) + { + AsyncProfiler.CreateAsyncContext.Create((ulong)task.Id, nc); + } + } + + if (AsyncDebugger.IsEnabled(flags)) { task.NotifyDebuggerOfRuntimeAsyncState(); AsyncDebugger.CreateAsyncContext(task); @@ -1053,10 +1070,16 @@ 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.AsyncDebugger(flags)) + if (AsyncInstrumentation.IsEnabled.AsyncProfiler(flags)) + { + AsyncProfiler.ResumeAsyncContext.Resume(ref info); + } + + if (AsyncDebugger.IsEnabled(flags)) { AsyncDebugger.ResumeAsyncContext(task.Id); } @@ -1064,11 +1087,16 @@ public static void ResumeRuntimeAsyncContext(Task task, ref AsyncDispatcherInfo } [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.AsyncDebugger(flags)) + if (AsyncInstrumentation.IsEnabled.AsyncProfiler(flags)) + { + AsyncProfiler.SuspendAsyncContext.Suspend(ref info, nextContinuation); + } + + if (AsyncDebugger.IsEnabled(flags)) { AsyncDebugger.SuspendAsyncContext(ref info, curContinuation); } @@ -1076,11 +1104,17 @@ 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) { if (AsyncInstrumentation.IsEnabled.SuspendAsyncContext(flags)) { - if (AsyncInstrumentation.IsEnabled.AsyncDebugger(flags)) + if (AsyncInstrumentation.IsEnabled.AsyncProfiler(flags)) + { + Continuation? nextContinuation = t_runtimeAsyncAwaitState.SentinelContinuation!.Next; + AsyncProfiler.SuspendAsyncContext.Suspend(ref info, nextContinuation != null ? nextContinuation : newContinuation); + } + + if (AsyncDebugger.IsEnabled(flags)) { AsyncDebugger.SuspendAsyncContext(curContinuation, newContinuation); } @@ -1092,29 +1126,54 @@ public static void CompleteRuntimeAsyncContext(ref AsyncDispatcherInfo info, Asy { if (AsyncInstrumentation.IsEnabled.CompleteAsyncContext(flags)) { - if (AsyncInstrumentation.IsEnabled.AsyncDebugger(flags)) + if (AsyncInstrumentation.IsEnabled.AsyncProfiler(flags)) + { + AsyncProfiler.CompleteAsyncContext.Complete(ref info.AsyncProfilerInfo); + } + + if (AsyncDebugger.IsEnabled(flags)) { AsyncDebugger.CompleteAsyncContext(info.CurrentTask); } } } - 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.AsyncDebugger(flags)) + if (AsyncInstrumentation.IsEnabled.AsyncProfiler(flags)) + { + AsyncProfiler.AsyncMethodException.Unhandled(ref info.AsyncProfilerInfo, unwindedFrames); + } + + if (AsyncDebugger.IsEnabled(flags)) { AsyncDebugger.AsyncMethodUnhandledException(info.CurrentTask, ex, curContinuation); } } } - 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.AsyncDebugger(flags)) + if (AsyncInstrumentation.IsEnabled.AsyncProfiler(flags)) + { + AsyncProfiler.AsyncMethodException.Handled(ref info.AsyncProfilerInfo, unwindedFrames); + } + + if (AsyncDebugger.IsEnabled(flags)) { AsyncDebugger.AsyncMethodHandledException(curContinuation, unwindedFrames); } @@ -1126,7 +1185,12 @@ public static void ResumeRuntimeAsyncMethod(ref AsyncDispatcherInfo info, AsyncI { if (AsyncInstrumentation.IsEnabled.ResumeAsyncMethod(flags)) { - if (AsyncInstrumentation.IsEnabled.AsyncDebugger(flags)) + if (AsyncInstrumentation.IsEnabled.AsyncProfiler(flags)) + { + AsyncProfiler.ResumeAsyncMethod.Resume(ref info.AsyncProfilerInfo); + } + + if (AsyncDebugger.IsEnabled(flags)) { AsyncDebugger.ResumeAsyncMethod(ref info, curContinuation); } @@ -1134,20 +1198,46 @@ 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.AsyncDebugger(flags)) + if (AsyncInstrumentation.IsEnabled.AsyncProfiler(flags)) + { + AsyncProfiler.CompleteAsyncMethod.Complete(ref info.AsyncProfilerInfo); + } + + if (AsyncDebugger.IsEnabled(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 { + public static bool IsEnabled(AsyncInstrumentation.Flags flags) => AsyncInstrumentation.IsEnabled.AsyncDebugger(flags) && IsAsyncDebuggingEnabled(); + public static void CreateAsyncContext(Task task) { Task.AddToActiveTasks(task); @@ -1239,6 +1329,8 @@ public static void HandleSuspendedFailed(Task task, Continuation? nextContinuati Task.RemoveRuntimeAsyncTask(task); } } + + private static bool IsAsyncDebuggingEnabled() => Task.s_asyncDebuggingEnabled; } } } 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..017700db4506e9 --- /dev/null +++ b/src/coreclr/System.Private.CoreLib/src/System/Runtime/CompilerServices/AsyncProfiler.CoreCLR.cs @@ -0,0 +1,575 @@ +// 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 static System.Runtime.CompilerServices.AsyncProfilerBufferedEventSource; + +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 + { + [MethodImpl(MethodImplOptions.AggressiveInlining)] + 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); + } + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + 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); + } + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + private static ulong GetId(ref AsyncDispatcherInfo info) + { + if (info.CurrentTask != null) + { + return (ulong)info.CurrentTask.Id; + } + return 0; + } + } + + [StackTraceHidden] + internal static partial class ContinuationWrapper + { + [MethodImpl(MethodImplOptions.AggressiveInlining)] + 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); + } + } + + internal 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 + { + [MethodImpl(MethodImplOptions.AggressiveInlining)] + private static unsafe void ResumeAsyncCallstacks(AsyncThreadContext context) + { + //Write recursivly all the resume async callstack events. + AsyncDispatcherInfo* info = AsyncDispatcherInfo.t_current; + if (info != null) + { + ResumeRuntimeAsyncCallstacks(info, context); + } + + } + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + 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 + { + public struct CaptureRuntimeAsyncCallstackState + { + public Continuation Continuation; + public ulong LastNativeIP; + public byte Count; + } + + public static bool CaptureRuntimeAsyncCallstack(Span buffer, ref int index, ref CaptureRuntimeAsyncCallstackState state) + { + if (index > buffer.Length) + { + return false; + } + + ulong currentNativeIP = 0; + ulong previousNativeIP = state.LastNativeIP; + byte maxAsyncCallstackLength = (byte)Math.Min(byte.MaxValue, (buffer.Length - index) / ASYNC_METHOD_INFO_SIZE); + + unsafe + { + currentNativeIP = (ulong)state.Continuation.ResumeInfo->DiagnosticIP; + } + + // 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) + { + EventBuffer.Serializer.WriteCompressedUInt64(buffer, ref index, currentNativeIP); + } + else + { + EventBuffer.Serializer.WriteCompressedInt64(buffer, ref index, (long)(currentNativeIP - previousNativeIP)); + } + + EventBuffer.Serializer.WriteCompressedInt32(buffer, ref index, state.Continuation.State); + state.Count++; + + state.Continuation = state.Continuation.Next!; + while (state.Count < maxAsyncCallstackLength && state.Continuation != null) + { + previousNativeIP = currentNativeIP; + + unsafe + { + currentNativeIP = (ulong)state.Continuation.ResumeInfo->DiagnosticIP; + } + + EventBuffer.Serializer.WriteCompressedInt64(buffer, ref index, (long)(currentNativeIP - previousNativeIP)); + EventBuffer.Serializer.WriteCompressedInt32(buffer, ref index, state.Continuation.State); + + state.Count++; + state.Continuation = state.Continuation.Next!; + } + + state.LastNativeIP = currentNativeIP; + + return state.Continuation == null || state.Count == byte.MaxValue; + } + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static void EmitEvent(AsyncThreadContext context, long currentTimestamp, ulong id, Continuation? asyncCallstack) + { + EmitEvent(context, currentTimestamp, AsyncEventID.ResumeAsyncCallstack, id, AsyncCallstackType.Runtime, asyncCallstack); + } + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static void EmitEvent(AsyncThreadContext context, long currentTimestamp, AsyncEventID eventID, ulong id, Continuation? asyncCallstack) + { + EmitEvent(context, currentTimestamp, eventID, id, AsyncCallstackType.Runtime, asyncCallstack); + } + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + 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 * ASYNC_METHOD_INFO_SIZE, + eventBuffer.Data.Length); + + CaptureRuntimeAsyncCallstackState state = default; + state.Continuation = asyncCallstack; + + // Callstack envelope: id (max 10 bytes compressed) + type (1) + callstackId (1) + frameCount (1) + const int maxEnvelopeSize = EventBuffer.Serializer.MaxCompressedUInt64Size + sizeof(byte) + sizeof(byte) + sizeof(byte); + + int savedAsyncEventHeaderIndex = eventBuffer.Index; + if (EventBuffer.Serializer.AsyncEventHeader(context, ref eventBuffer, currentTimestamp, delta, eventID, maxEnvelopeSize)) + { + EventBuffer.Serializer.CallstackHeader(ref eventBuffer, id, type, 0); + + Span inlineEventBuffer = eventBuffer.Data.AsSpan(eventBuffer.Index); + int index = 0; + + if (!CaptureRuntimeAsyncCallstack(inlineEventBuffer, ref index, ref state)) + { + byte[]? rentedArray = RentArray(maxCallstackBytes); + if (rentedArray != null) + { + inlineEventBuffer.Slice(0, index).CopyTo(rentedArray); + CaptureRuntimeAsyncCallstack(rentedArray.AsSpan(0, maxCallstackBytes), ref index, ref state); + + // Remove async event header from the event buffer before flushing. + EventBuffer.Serializer.RemoveAsyncEventHeader(context, savedAsyncEventHeaderIndex); + context.Flush(); + + // Write the callstack again. + if (EventBuffer.Serializer.AsyncEventHeader(context, ref eventBuffer, context.LastEventTimestamp, 0, eventID, maxEnvelopeSize + index)) + { + EventBuffer.Serializer.CallstackHeader(ref eventBuffer, id, type, state.Count); + EventBuffer.Serializer.CallstackData(ref eventBuffer, rentedArray, index); + } + + ArrayPool.Shared.Return(rentedArray); + } + else + { + // Remove async event header from the event buffer since we can't write the callstack. + EventBuffer.Serializer.RemoveAsyncEventHeader(context, savedAsyncEventHeaderIndex); + } + } + else + { + // Patch frame count in the event buffer. + eventBuffer.Data[eventBuffer.Index - 1] = state.Count; + eventBuffer.Index += index; + } + } + } + } + + 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.Private.CoreLib/src/System.Private.CoreLib.Shared.projitems b/src/libraries/System.Private.CoreLib/src/System.Private.CoreLib.Shared.projitems index 2c48c4cba7ffd1..8138c459a5e6c2 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 @@ -942,6 +942,8 @@ + + @@ -2988,4 +2990,4 @@ - + \ No newline at end of file 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..ef60b3f747fd06 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 @@ -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; @@ -102,6 +102,7 @@ public static void UpdateAsyncDebuggerFlags(Flags asyncDebuggerFlags) private static Flags InitializeFlags() { _ = TplEventSource.Log; // Touch TplEventSource to trigger static constructor which will initialize TPL flags if EventSource is supported. + _ = AsyncProfilerBufferedEventSource.Log; // Touch AsyncProfilerBufferedEventSource to trigger static constructor which will initialize async profiler flags if EventSource is supported. lock (s_lock) { 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..b6898c59e96466 --- /dev/null +++ b/src/libraries/System.Private.CoreLib/src/System/Runtime/CompilerServices/AsyncProfiler.cs @@ -0,0 +1,1067 @@ +// 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.Reflection; +using System.Runtime.InteropServices; +using System.Threading; +using static System.Runtime.CompilerServices.AsyncProfilerBufferedEventSource; + +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 + } + + internal ref struct Info + { + public object? Context; + public ref nint ContinuationTable; + public uint ContinuationIndex; + } + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + 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(); + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + 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.Informational) + { + ActiveEventKeywords = eventKeywords; + } + + string? eventBufferSizeEnv = System.Environment.GetEnvironmentVariable("DOTNET_AsyncProfilerBufferedEventSource_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 (8 bytes)] [eventBufferSize (4 bytes)] [wrapperCount byte] [wrapperIP0 (8 bytes)] ... [wrapperIPn (8 bytes)] + int maxEventPayloadSize = sizeof(long) + sizeof(uint) + 1 + (wrapperIPs.Length * sizeof(long)); + + ref EventBuffer eventBuffer = ref context.EventBuffer; + if (EventBuffer.Serializer.AsyncEventHeader(context, ref eventBuffer, AsyncEventID.AsyncProfilerMetadata, maxEventPayloadSize)) + { + byte[] buffer = eventBuffer.Data; + ref int index = ref eventBuffer.Index; + + EventBuffer.Serializer.WriteUInt64(buffer, ref index, (ulong)Stopwatch.Frequency); + EventBuffer.Serializer.WriteUInt32(buffer, ref index, EventBufferSize); + Unsafe.Add(ref MemoryMarshal.GetArrayDataReference(buffer), index++) = (byte)wrapperIPs.Length; + + for (int i = 0; i < wrapperIPs.Length; i++) + { + EventBuffer.Serializer.WriteUInt64(buffer, ref index, (ulong)wrapperIPs[i]); + } + } + + // Force flush to deliver metadata event promptly. + context.Flush(); + + s_metadataRevision = Revision; + } + } + } + } + + 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; + } + + 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; + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static void WriteInt32(Span buffer, ref int index, int value) + { + WriteUInt32(buffer, ref index, (uint)value); + } + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static void WriteInt32(byte[] buffer, ref int index, int value) + { + WriteUInt32(buffer, ref index, (uint)value); + } + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static void WriteCompressedInt32(Span buffer, ref int index, int value) + { + WriteCompressedUInt32(buffer, ref index, ZigzagEncodeInt32(value)); + } + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static void WriteCompressedInt32(byte[] buffer, ref int index, int value) + { + WriteCompressedUInt32(buffer, ref index, ZigzagEncodeInt32(value)); + } + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static void WriteUInt32(Span buffer, ref int index, uint value) + { + Debug.Assert((uint)index <= (uint)(buffer.Length - sizeof(uint))); + WriteUInt32(ref MemoryMarshal.GetReference(buffer), ref index, value); + } + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static void WriteUInt32(byte[] buffer, ref int index, uint value) + { + Debug.Assert((uint)index <= (uint)(buffer.Length - sizeof(uint))); + WriteUInt32(ref MemoryMarshal.GetArrayDataReference(buffer), ref index, value); + } + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + private static void WriteUInt32(ref byte buffer, ref int index, uint value) + { + if (!BitConverter.IsLittleEndian) + value = BinaryPrimitives.ReverseEndianness(value); + + Unsafe.WriteUnaligned(ref Unsafe.Add(ref buffer, index), value); + index += 4; + } + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static void WriteCompressedUInt32(Span buffer, ref int index, uint value) + { + Debug.Assert((uint)index <= (uint)(buffer.Length - MaxCompressedUInt32Size)); + WriteCompressedUInt32(ref MemoryMarshal.GetReference(buffer), ref index, value); + } + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static void WriteCompressedUInt32(byte[] buffer, ref int index, uint value) + { + Debug.Assert((uint)index <= (uint)(buffer.Length - MaxCompressedUInt32Size)); + WriteCompressedUInt32(ref MemoryMarshal.GetArrayDataReference(buffer), ref index, value); + } + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + private static void WriteCompressedUInt32(ref byte buffer, ref int index, uint value) + { + while (value > 0x7Fu) + { + Unsafe.Add(ref buffer, index++) = (byte)((uint)value | ~0x7Fu); + value >>= 7; + } + Unsafe.Add(ref buffer, index++) = (byte)value; + } + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static void WriteInt64(Span buffer, ref int index, long value) + { + WriteUInt64(buffer, ref index, (ulong)value); + } + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static void WriteInt64(byte[] buffer, ref int index, long value) + { + WriteUInt64(buffer, ref index, (ulong)value); + } + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static void WriteCompressedInt64(Span buffer, ref int index, long value) + { + WriteCompressedUInt64(buffer, ref index, ZigzagEncodeInt64(value)); + } + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static void WriteCompressedInt64(byte[] buffer, ref int index, long value) + { + WriteCompressedUInt64(buffer, ref index, ZigzagEncodeInt64(value)); + } + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static void WriteUInt64(Span buffer, ref int index, ulong value) + { + Debug.Assert((uint)index <= (uint)(buffer.Length - sizeof(ulong))); + WriteUInt64(ref MemoryMarshal.GetReference(buffer), ref index, value); + } + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static void WriteUInt64(byte[] buffer, ref int index, ulong value) + { + Debug.Assert((uint)index <= (uint)(buffer.Length -sizeof(ulong))); + WriteUInt64(ref MemoryMarshal.GetArrayDataReference(buffer), ref index, value); + } + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + private static void WriteUInt64(ref byte buffer, ref int index, ulong value) + { + if (!BitConverter.IsLittleEndian) + value = BinaryPrimitives.ReverseEndianness(value); + + Unsafe.WriteUnaligned(ref Unsafe.Add(ref buffer, index), value); + index += sizeof(ulong); + } + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static void WriteCompressedUInt64(Span buffer, ref int index, ulong value) + { + Debug.Assert((uint)index <= (uint)(buffer.Length - MaxCompressedUInt64Size)); + WriteCompressedUInt64(ref MemoryMarshal.GetReference(buffer), ref index, value); + } + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static void WriteCompressedUInt64(byte[] buffer, ref int index, ulong value) + { + Debug.Assert((uint)index <= (uint)(buffer.Length - MaxCompressedUInt64Size)); + WriteCompressedUInt64(ref MemoryMarshal.GetArrayDataReference(buffer), ref index, value); + } + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + private static void WriteCompressedUInt64(ref byte buffer, ref int index, ulong value) + { + while (value > 0x7Fu) + { + Unsafe.Add(ref buffer, index++) = (byte)((uint)value | ~0x7Fu); + value >>= 7; + } + + Unsafe.Add(ref buffer, index++) = (byte)value; + } + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static uint ZigzagEncodeInt32(int value) => (uint)((value << 1) ^ (value >> 31)); + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static ulong ZigzagEncodeInt64(long value) => (ulong)((value << 1) ^ (value >> 63)); + + public static void Header(AsyncThreadContext context, ref EventBuffer eventBuffer) + { + byte[] buffer = eventBuffer.Data; + ref int index = ref eventBuffer.Index; + long currentTimestamp = Stopwatch.GetTimestamp(); + + index = 0; + eventBuffer.EventCount = 0; + context.LastEventTimestamp = currentTimestamp; + + //Write header to buffer + Unsafe.Add(ref MemoryMarshal.GetArrayDataReference(buffer), index++) = 1; // Version + WriteUInt32(buffer, ref index, 0); // Total size in bytes, will be updated on flush. + WriteUInt32(buffer, ref index, context.AsyncThreadContextId); // Async Thread Context ID + WriteUInt64(buffer, ref index, Thread.CurrentOSThreadId); // OS Thread ID + WriteUInt32(buffer, ref index, 0); // Total event count, will be updated on flush. + WriteUInt64(buffer, ref index, (ulong)currentTimestamp); // Start timestamp + WriteUInt64(buffer, ref index, 0); // End timestamp, will be updated on flush. + } + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + 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); + } + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + 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) + { + const int maxEventHeaderSize = MaxCompressedUInt64Size + sizeof(byte); + + byte[] buffer = eventBuffer.Data; + ref int index = ref eventBuffer.Index; + + if ((index + maxEventHeaderSize + maxEventPayloadSize) <= buffer.Length && delta >= 0) + { + context.LastEventTimestamp = currentTimestamp; + } + else + { + // Event is too big for buffer, drop it. + if (maxEventHeaderSize + maxEventPayloadSize > buffer.Length) + { + return false; + } + + context.Flush(); + delta = 0; + } + + WriteCompressedUInt64(buffer, ref index, (ulong)delta); //Timestamp delta from last event + Unsafe.Add(ref MemoryMarshal.GetArrayDataReference(buffer), index++) = (byte)eventID; // eventID + + eventBuffer.EventCount++; + return true; + } + + public static void RemoveAsyncEventHeader(AsyncThreadContext context, int savedAsyncEventHeaderIndex) + { + if (context.EventBuffer.EventCount > 0) + { + context.EventBuffer.Index = savedAsyncEventHeaderIndex; + context.EventBuffer.EventCount--; + } + } + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static void CallstackHeader(ref EventBuffer eventBuffer, ulong id, AsyncCallstackType type, byte callstackFrameCount) + { + byte[] buffer = eventBuffer.Data; + ref int index = ref eventBuffer.Index; + + WriteCompressedUInt64(buffer, ref index, id); + + ref byte dst = ref MemoryMarshal.GetArrayDataReference(buffer); + + Unsafe.Add(ref dst, index++) = (byte)type; + Unsafe.Add(ref dst, index++) = 0; // Reserved callstack ID for future callstack interning. + Unsafe.Add(ref dst, index++) = callstackFrameCount; + } + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static void CallstackData(ref EventBuffer eventBuffer, ReadOnlySpan callstackData, int callstackDataByteCount) + { + ref int index = ref eventBuffer.Index; + Unsafe.CopyBlockUnaligned(ref Unsafe.Add(ref MemoryMarshal.GetArrayDataReference(eventBuffer.Data), index), ref MemoryMarshal.GetReference(callstackData), (uint)callstackDataByteCount); + index += callstackDataByteCount; + } + } + } + + internal sealed class AsyncThreadContext + { + private static uint s_nextAsyncThreadContextId; + + public AsyncThreadContext() + { + _eventBuffer.Data = Array.Empty(); + AsyncThreadContextId = Interlocked.Increment(ref s_nextAsyncThreadContextId); + } + + private EventBuffer _eventBuffer; + + public long LastEventTimestamp; + + public EventKeywords ActiveEventKeywords; + + 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) + { + _eventBuffer.Data = new byte[Config.EventBufferSize]; + EventBuffer.Serializer.Header(this, ref _eventBuffer); + } + + 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) + { + context.InUse = false; + lock (AsyncThreadContextCache.CacheLock) { ; } + context.InUse = true; + } + + 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 Create(); + } + + [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; + } + + context = Get(); + info.Context = t_asyncThreadContext; + + 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); + + ref EventBuffer eventBuffer = ref EventBuffer; + + if (eventBuffer.EventCount == 0) + { + return; + } + + int index = 1; // Skip version + + // Fill in total size in header before flushing. + EventBuffer.Serializer.WriteUInt32(eventBuffer.Data, ref index, (uint)eventBuffer.Index); + + index += sizeof(uint) + sizeof(ulong); // Skip AsyncThreadContextId and OSThreadId + + // Fill in event count in header before flushing. + EventBuffer.Serializer.WriteUInt32(eventBuffer.Data, ref index, eventBuffer.EventCount); + + index += sizeof(ulong); // Skip start timestamp + + // Fill in end timestamp in header before flushing. + EventBuffer.Serializer.WriteUInt64(eventBuffer.Data, ref index, (ulong)LastEventTimestamp); + + try + { + EmitEvent(eventBuffer.Data.AsSpan().Slice(0, eventBuffer.Index)); + } + catch + { + // AsyncProfiler can't throw, ignore exception and loose buffer. + } + + EventBuffer.Serializer.Header(this, ref eventBuffer); + } + + private static void EmitEvent(Span buffer) + { + Log.AsyncEvents(buffer); + } + + private static AsyncThreadContext Create() + { + AsyncThreadContext context = new AsyncThreadContext(); + AsyncThreadContextCache.Add(context); + t_asyncThreadContext = context; + return context; + } + + [ThreadStatic] + private static AsyncThreadContext? t_asyncThreadContext; + } + + internal static partial class CreateAsyncContext + { + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static void EmitEvent(AsyncThreadContext context, ulong id) + { + const int maxEventPayloadSize = EventBuffer.Serializer.MaxCompressedUInt64Size; + + if (EventBuffer.Serializer.AsyncEventHeader(context, ref context.EventBuffer, AsyncEventID.CreateAsyncContext, maxEventPayloadSize)) + { + EventBuffer.Serializer.WriteCompressedUInt64(context.EventBuffer.Data, ref context.EventBuffer.Index, id); + } + } + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static void EmitEvent(AsyncThreadContext context, long currentTimestamp, ulong id) + { + const int maxEventPayloadSize = EventBuffer.Serializer.MaxCompressedUInt64Size; + + if (EventBuffer.Serializer.AsyncEventHeader(context, ref context.EventBuffer, currentTimestamp, AsyncEventID.CreateAsyncContext, maxEventPayloadSize)) + { + EventBuffer.Serializer.WriteCompressedUInt64(context.EventBuffer.Data, ref context.EventBuffer.Index, id); + } + } + } + + internal static partial class ResumeAsyncContext + { + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static void EmitEvent(AsyncThreadContext context, ulong id) + { + const int maxEventPayloadSize = EventBuffer.Serializer.MaxCompressedUInt64Size; + + if (EventBuffer.Serializer.AsyncEventHeader(context, ref context.EventBuffer, AsyncEventID.ResumeAsyncContext, maxEventPayloadSize)) + { + EventBuffer.Serializer.WriteCompressedUInt64(context.EventBuffer.Data, ref context.EventBuffer.Index, id); + } + } + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static void EmitEvent(AsyncThreadContext context, long currentTimestamp, ulong id) + { + const int maxEventPayloadSize = EventBuffer.Serializer.MaxCompressedUInt64Size; + + if (EventBuffer.Serializer.AsyncEventHeader(context, ref context.EventBuffer, currentTimestamp, AsyncEventID.ResumeAsyncContext, maxEventPayloadSize)) + { + EventBuffer.Serializer.WriteCompressedUInt64(context.EventBuffer.Data, ref context.EventBuffer.Index, id); + } + } + } + + internal static partial class SuspendAsyncContext + { + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static void EmitEvent(AsyncThreadContext context, long currentTimestamp) + { + EventBuffer.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); + } + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static void EmitEvent(AsyncThreadContext context, long currentTimestamp) + { + EventBuffer.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); + } + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static void EmitEvent(AsyncThreadContext context, long currentTimestamp, uint unwindedFrames) + { + // unwinded frames + const int maxEventPayloadSize = EventBuffer.Serializer.MaxCompressedUInt32Size; + + ref EventBuffer eventBuffer = ref context.EventBuffer; + + if (EventBuffer.Serializer.AsyncEventHeader(context, ref eventBuffer, currentTimestamp, AsyncEventID.UnwindAsyncException, maxEventPayloadSize)) + { + EventBuffer.Serializer.WriteCompressedUInt32(eventBuffer.Data, ref eventBuffer.Index, 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); + } + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static void EmitEvent(AsyncThreadContext context) + { + EventBuffer.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); + } + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static void EmitEvent(AsyncThreadContext context) + { + EventBuffer.Serializer.AsyncEventHeader(context, ref context.EventBuffer, AsyncEventID.CompleteAsyncMethod, 0); + } + } + + internal static partial class ContinuationWrapper + { +#pragma warning disable CA1823 + public const byte COUNT = 32; + public const byte COUNT_MASK = COUNT - 1; +#pragma warning restore CA1823 + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static void IncrementIndex(ref Info info) + { + info.ContinuationIndex++; + if ((info.ContinuationIndex & COUNT_MASK) == 0) + { + ResetIndex(ref info); + } + } + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + 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); + } + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + private static void EmitEvent(AsyncThreadContext context) + { + EventBuffer.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); + } + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + private static void EmitEvent(AsyncThreadContext context) + { + EventBuffer.Serializer.AsyncEventHeader(context, ref context.EventBuffer, AsyncEventID.ResetAsyncThreadContext, 0); + } + } + + private static partial class AsyncCallstack + { +#pragma warning disable CA1823 + private const int ASYNC_METHOD_INFO_SIZE = EventBuffer.Serializer.MaxCompressedUInt64Size + EventBuffer.Serializer.MaxCompressedUInt32Size; +#pragma warning restore CA1823 + } + + 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) + { + // Make sure all dead threads are flushed and removed from the cache. + for (int i = s_cache.Count - 1; i >= 0; i--) + { + var 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; + } + } + + // Look at live threads, only flush if forced or contexts that have been idle for 250 milliseconds. + long idleWriteTimestamp = Stopwatch.GetTimestamp() - (Stopwatch.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() - Stopwatch.Frequency * 30; + + // Spin wait timeout, 100 milliseconds. + long spinWaitTimeout = Stopwatch.Frequency / 10; + + foreach (var 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; + } + } + } + } + + public static void EnableFlushTimer() + { + lock (CacheLock) + { + s_flushTimer ??= new Timer(PeriodicFlush, null, Timeout.Infinite, Timeout.Infinite); + s_flushTimer.Change(ASYNC_THREAD_CONTEXT_CACHE_FLUSH_TIMER_INTERVAL_MS, 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); + s_cleanupTimer?.Change(ASYNC_THREAD_CONTEXT_CACHE_CLEANUP_TIMER_INTERVAL_MS, Timeout.Infinite); + } + } + + public static void DisableCleanupTimer() + { + lock (CacheLock) + { + s_cleanupTimer?.Change(Timeout.Infinite, Timeout.Infinite); + } + } + + private static void Cleanup(object? state) + { + _ = state; + + lock (CacheLock) + { + Flush(true); + + if (s_cache.Count > 0) + { + // Restart cleanup timer. + s_cleanupTimer?.Change(ASYNC_THREAD_CONTEXT_CACHE_CLEANUP_TIMER_INTERVAL_MS, Timeout.Infinite); + } + } + } + + private static void PeriodicFlush(object? state) + { + _ = state; + + lock (CacheLock) + { + Flush(false); + + if (IsEnabled.AnyAsyncEvents(Config.ActiveEventKeywords)) + { + // Restart flush timer. + s_flushTimer?.Change(ASYNC_THREAD_CONTEXT_CACHE_FLUSH_TIMER_INTERVAL_MS, Timeout.Infinite); + } + else + { + // Start cleanup timer. + s_cleanupTimer?.Change(ASYNC_THREAD_CONTEXT_CACHE_CLEANUP_TIMER_INTERVAL_MS, Timeout.Infinite); + } + } + } + + private sealed class AsyncThreadContextHolder + { + public AsyncThreadContextHolder(AsyncThreadContext context, Thread ownerThread) + { + Context = context; + OwnerThread = new WeakReference(ownerThread); + } + + public AsyncThreadContext Context; + public WeakReference OwnerThread; + } + + private const int ASYNC_THREAD_CONTEXT_CACHE_FLUSH_TIMER_INTERVAL_MS = 1000; + private static Timer? s_flushTimer; + + private const int ASYNC_THREAD_CONTEXT_CACHE_CLEANUP_TIMER_INTERVAL_MS = 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/AsyncProfilerBufferedEventSource.cs b/src/libraries/System.Private.CoreLib/src/System/Runtime/CompilerServices/AsyncProfilerBufferedEventSource.cs new file mode 100644 index 00000000000000..6db30fa99dbb02 --- /dev/null +++ b/src/libraries/System.Private.CoreLib/src/System/Runtime/CompilerServices/AsyncProfilerBufferedEventSource.cs @@ -0,0 +1,102 @@ +// 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.AsyncProfilerBufferedEventSource", + Guid = "742BD0FE-9200-4DE2-9059-576F35EBEB62" + )] + internal sealed partial class AsyncProfilerBufferedEventSource : EventSource + { + private const string EventSourceSuppressMessage = "Parameters to this method are primitive and are trimmer safe"; + + public static readonly AsyncProfilerBufferedEventSource Log = new AsyncProfilerBufferedEventSource(); + + 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 = ((IntPtr)pBuffer); + 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.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..8b6183a98ebbea --- /dev/null +++ b/src/libraries/System.Runtime/tests/System.Threading.Tasks.Tests/System.Runtime.CompilerServices/AsyncProfilerTests.cs @@ -0,0 +1,2140 @@ +// 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, + } + + public class AsyncProfilerTests + { + private static bool IsRuntimeAsyncSupported => PlatformDetection.IsRuntimeAsyncSupported; + + private const string AsyncProfilerEventSourceName = "System.Runtime.CompilerServices.AsyncProfilerBufferedEventSource"; + private const int AsyncEventsId = 1; + private const int HeaderSize = 1 + sizeof(uint) + sizeof(uint) + sizeof(ulong) + sizeof(uint) + sizeof(ulong) + sizeof(ulong); + + // AsyncProfilerBufferedEventSource 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; + + [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; + + long delta = (long)ReadCompressedUInt64(buffer, ref index); + baseTimestamp += delta; + + AsyncEventID eventId = (AsyncEventID)buffer[index++]; + + 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.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) + { + taskId = ReadCompressedUInt64(buffer, ref index); + index++; // type + index++; // callstack ID (reserved) + frameCount = buffer[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 _); + } + + private static void ReadMetadataPayload(ReadOnlySpan buffer, ref int index, + out ulong qpcFrequency, out uint eventBufferSize, out long[] wrapperIPs) + { + EventBuffer.Deserializer.ReadUInt64(buffer, ref index, out qpcFrequency); + EventBuffer.Deserializer.ReadUInt32(buffer, ref index, out eventBufferSize); + byte wrapperCount = buffer[index++]; + wrapperIPs = new long[wrapperCount]; + for (int i = 0; i < wrapperCount; i++) + { + EventBuffer.Deserializer.ReadInt64(buffer, ref index, out wrapperIPs[i]); + } + } + + private record struct MetadataFromBuffer(ulong QpcFrequency, 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 uint bufSize, out long[] ips); + metadataList.Add(new MetadataFromBuffer(freq, 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 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) + { + var events = new ConcurrentQueue(); + using (var listener = CreateListener(keywords)) + { + listener.RunWithCallback(events.Enqueue, () => + { + SendFlushCommand(); + events.Clear(); + callback(); + }); + } + 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(AsyncProfilerTests), nameof(IsRuntimeAsyncSupported))] + [ActiveIssue("https://github.com/dotnet/runtime/issues/124072", typeof(PlatformDetection), nameof(PlatformDetection.IsInterpreter))] + 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(AsyncProfilerTests), nameof(IsRuntimeAsyncSupported))] + [ActiveIssue("https://github.com/dotnet/runtime/issues/124072", typeof(PlatformDetection), nameof(PlatformDetection.IsInterpreter))] + 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(AsyncProfilerTests), nameof(IsRuntimeAsyncSupported))] + [ActiveIssue("https://github.com/dotnet/runtime/issues/124072", typeof(PlatformDetection), nameof(PlatformDetection.IsInterpreter))] + 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(AsyncProfilerTests), nameof(IsRuntimeAsyncSupported))] + [ActiveIssue("https://github.com/dotnet/runtime/issues/124072", typeof(PlatformDetection), nameof(PlatformDetection.IsInterpreter))] + 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(AsyncProfilerTests), nameof(IsRuntimeAsyncSupported))] + [ActiveIssue("https://github.com/dotnet/runtime/issues/124072", typeof(PlatformDetection), nameof(PlatformDetection.IsInterpreter))] + 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(AsyncProfilerTests), nameof(IsRuntimeAsyncSupported))] + [ActiveIssue("https://github.com/dotnet/runtime/issues/124072", typeof(PlatformDetection), nameof(PlatformDetection.IsInterpreter))] + 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 eventIds = CollectAsyncEventIds(events); + var coreEvents = eventIds.FindAll(id => id == AsyncEventID.ResumeAsyncContext || id == AsyncEventID.SuspendAsyncContext || id == AsyncEventID.CompleteAsyncContext); + + Assert.Equal(AsyncEventID.ResumeAsyncContext, coreEvents[0]); + Assert.Equal(AsyncEventID.SuspendAsyncContext, coreEvents[1]); + Assert.Equal(AsyncEventID.ResumeAsyncContext, coreEvents[2]); + Assert.Equal(AsyncEventID.CompleteAsyncContext, coreEvents[3]); + } + + [ConditionalFact(typeof(AsyncProfilerTests), nameof(IsRuntimeAsyncSupported))] + [ActiveIssue("https://github.com/dotnet/runtime/issues/124072", typeof(PlatformDetection), nameof(PlatformDetection.IsInterpreter))] + 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(AsyncProfilerTests), nameof(IsRuntimeAsyncSupported))] + [ActiveIssue("https://github.com/dotnet/runtime/issues/124072", typeof(PlatformDetection), nameof(PlatformDetection.IsInterpreter))] + 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(AsyncProfilerTests), nameof(IsRuntimeAsyncSupported))] + [ActiveIssue("https://github.com/dotnet/runtime/issues/124072", typeof(PlatformDetection), nameof(PlatformDetection.IsInterpreter))] + 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(AsyncProfilerTests), nameof(IsRuntimeAsyncSupported))] + [ActiveIssue("https://github.com/dotnet/runtime/issues/124072", typeof(PlatformDetection), nameof(PlatformDetection.IsInterpreter))] + 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(AsyncProfilerTests), nameof(IsRuntimeAsyncSupported))] + [ActiveIssue("https://github.com/dotnet/runtime/issues/124072", typeof(PlatformDetection), nameof(PlatformDetection.IsInterpreter))] + 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(AsyncProfilerTests), nameof(IsRuntimeAsyncSupported))] + [ActiveIssue("https://github.com/dotnet/runtime/issues/124072", typeof(PlatformDetection), nameof(PlatformDetection.IsInterpreter))] + 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(); + }); + }); + + var eventIds = CollectAsyncEventIds(events); + + int suspendIdx = eventIds.IndexOf(AsyncEventID.SuspendAsyncCallstack); + int completeIdx = eventIds.IndexOf(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(AsyncProfilerTests), nameof(IsRuntimeAsyncSupported))] + [ActiveIssue("https://github.com/dotnet/runtime/issues/124072", typeof(PlatformDetection), nameof(PlatformDetection.IsInterpreter))] + 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(); + }); + }); + + var resumeStacks = CollectCallstacks(events, AsyncEventID.ResumeAsyncCallstack); + var suspendStacks = CollectCallstacks(events, AsyncEventID.SuspendAsyncCallstack); + + Assert.NotEmpty(resumeStacks); + Assert.NotEmpty(suspendStacks); + + // The first resume is the lambda resuming after Yield (depth 1). + byte firstResumeDepth = resumeStacks[0].FrameCount; + byte maxSuspendDepth = suspendStacks.Max(cs => cs.FrameCount); + + Assert.True(maxSuspendDepth > firstResumeDepth, + $"Suspend callstack depth ({maxSuspendDepth}) should be deeper than initial resume callstack depth ({firstResumeDepth})"); + } + + [ConditionalFact(typeof(AsyncProfilerTests), nameof(IsRuntimeAsyncSupported))] + [ActiveIssue("https://github.com/dotnet/runtime/issues/124072", typeof(PlatformDetection), nameof(PlatformDetection.IsInterpreter))] + 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 in order. + var callstackEvents = new List<(AsyncEventID EventId, ulong TaskId)>(); + ForEachEventBufferPayload(events, buffer => + { + ParseEventBuffer(buffer, (AsyncEventID eventId, 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)); + return true; + } + return SkipEventPayload(eventId, buf, ref idx); + }); + }); + + // 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(AsyncProfilerTests), nameof(IsRuntimeAsyncSupported))] + [ActiveIssue("https://github.com/dotnet/runtime/issues/124072", typeof(PlatformDetection), nameof(PlatformDetection.IsInterpreter))] + 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(AsyncProfilerTests), nameof(IsRuntimeAsyncSupported))] + [ActiveIssue("https://github.com/dotnet/runtime/issues/124072", typeof(PlatformDetection), nameof(PlatformDetection.IsInterpreter))] + 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(AsyncProfilerTests), nameof(IsRuntimeAsyncSupported))] + [ActiveIssue("https://github.com/dotnet/runtime/issues/124072", typeof(PlatformDetection), nameof(PlatformDetection.IsInterpreter))] + 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(AsyncProfilerTests), nameof(IsRuntimeAsyncSupported))] + [ActiveIssue("https://github.com/dotnet/runtime/issues/124072", typeof(PlatformDetection), nameof(PlatformDetection.IsInterpreter))] + public void RuntimeAsync_CallstackSimulation_NormalCompletion() + { + var events = CollectEvents(CallstackKeywords, () => + { + RunScenarioAndFlush(async () => + { + await FuncChained(); + }); + }); + + // DumpCollectedEvents(events); + + AssertCallstackSimulationReachesZero(events); + } + + [ConditionalFact(typeof(AsyncProfilerTests), nameof(IsRuntimeAsyncSupported))] + [ActiveIssue("https://github.com/dotnet/runtime/issues/124072", typeof(PlatformDetection), nameof(PlatformDetection.IsInterpreter))] + 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(AsyncProfilerTests), nameof(IsRuntimeAsyncSupported))] + [ActiveIssue("https://github.com/dotnet/runtime/issues/124072", typeof(PlatformDetection), nameof(PlatformDetection.IsInterpreter))] + 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(AsyncProfilerTests), nameof(IsRuntimeAsyncSupported))] + [ActiveIssue("https://github.com/dotnet/runtime/issues/124072", typeof(PlatformDetection), nameof(PlatformDetection.IsInterpreter))] + 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(AsyncProfilerTests), nameof(IsRuntimeAsyncSupported))] + [ActiveIssue("https://github.com/dotnet/runtime/issues/124072", typeof(PlatformDetection), nameof(PlatformDetection.IsInterpreter))] + 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(AsyncProfilerTests), nameof(IsRuntimeAsyncSupported))] + [ActiveIssue("https://github.com/dotnet/runtime/issues/124072", typeof(PlatformDetection), nameof(PlatformDetection.IsInterpreter))] + 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}"); + + MethodInfo getMethodFromIP = typeof(System.Diagnostics.StackFrame).GetMethod("GetMethodFromNativeIP", BindingFlags.Static | BindingFlags.NonPublic)!; + Assert.NotNull(getMethodFromIP); + + var resolvedNames = new List(); + foreach (var (nativeIP, _) in chainStack.Value.Frames) + { + var method = (MethodBase?)getMethodFromIP.Invoke(null, new object[] { (IntPtr)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(AsyncProfilerTests), nameof(IsRuntimeAsyncSupported))] + [ActiveIssue("https://github.com/dotnet/runtime/issues/124072", typeof(PlatformDetection), nameof(PlatformDetection.IsInterpreter))] + 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(AsyncProfilerTests), nameof(IsRuntimeAsyncSupported))] + [ActiveIssue("https://github.com/dotnet/runtime/issues/124072", typeof(PlatformDetection), nameof(PlatformDetection.IsInterpreter))] + 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(AsyncProfilerTests), nameof(IsRuntimeAsyncSupported))] + [ActiveIssue("https://github.com/dotnet/runtime/issues/124072", typeof(PlatformDetection), nameof(PlatformDetection.IsInterpreter))] + public void RuntimeAsync_PeriodicTimerFlush() + { + var events = CollectEvents(CoreKeywords, () => + { + // 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(2000); + }); + + // 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(AsyncProfilerTests), nameof(IsRuntimeAsyncSupported))] + [ActiveIssue("https://github.com/dotnet/runtime/issues/124072", typeof(PlatformDetection), nameof(PlatformDetection.IsInterpreter))] + public void RuntimeAsync_MultiThreadFlush() + { + const int threadCount = 4; + var events = CollectEvents(CoreKeywords, () => + { + // Ensure enough thread pool threads are available for concurrent execution. + ThreadPool.GetMinThreads(out int prevWorker, out int prevIO); + ThreadPool.SetMinThreads(threadCount, prevIO); + + var tasks = new Task[threadCount]; + for (int i = 0; i < threadCount; i++) + { + tasks[i] = Task.Run(async () => + { + await Func(); + }); + } + + Task.WhenAll(tasks).GetAwaiter().GetResult(); + ThreadPool.SetMinThreads(prevWorker, prevIO); + SendFlushCommand(); + }); + + // DumpCollectedEvents(events); + + var threadIds = CollectOsThreadIds(events); + + Assert.True(threadIds.Count > 1, $"Expected events from multiple threads, got {threadIds.Count} distinct OS thread ID(s)"); + } + + [ConditionalFact(typeof(AsyncProfilerTests), nameof(IsRuntimeAsyncSupported))] + [ActiveIssue("https://github.com/dotnet/runtime/issues/124072", typeof(PlatformDetection), nameof(PlatformDetection.IsInterpreter))] + public void RuntimeAsync_DeadThreadFlush() + { + var events = CollectEvents(CoreKeywords, () => + { + // 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(); + thread.Join(TimeSpan.FromSeconds(10)); + + // Do NOT send a flush command. + // Wait for the periodic flush timer to detect the dead thread + // and flush its orphaned buffer. + Thread.Sleep(2000); + }); + + // 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(AsyncProfilerTests), nameof(IsRuntimeAsyncSupported))] + [ActiveIssue("https://github.com/dotnet/runtime/issues/124072", typeof(PlatformDetection), nameof(PlatformDetection.IsInterpreter))] + 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)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(AsyncProfilerTests), nameof(IsRuntimeAsyncSupported))] + [ActiveIssue("https://github.com/dotnet/runtime/issues/124072", typeof(PlatformDetection), nameof(PlatformDetection.IsInterpreter))] + [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(AsyncProfilerTests), nameof(IsRuntimeAsyncSupported))] + [ActiveIssue("https://github.com/dotnet/runtime/issues/124072", typeof(PlatformDetection), nameof(PlatformDetection.IsInterpreter))] + 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(AsyncProfilerTests), nameof(IsRuntimeAsyncSupported))] + [ActiveIssue("https://github.com/dotnet/runtime/issues/124072", typeof(PlatformDetection), nameof(PlatformDetection.IsInterpreter))] + 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.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(AsyncProfilerTests), nameof(IsRuntimeAsyncSupported))] + [ActiveIssue("https://github.com/dotnet/runtime/issues/124072", typeof(PlatformDetection), nameof(PlatformDetection.IsInterpreter))] + 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(AsyncProfilerTests), nameof(IsRuntimeAsyncSupported))] + [ActiveIssue("https://github.com/dotnet/runtime/issues/124072", typeof(PlatformDetection), nameof(PlatformDetection.IsInterpreter))] + 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. + MethodInfo getMethodFromIP = typeof(System.Diagnostics.StackFrame).GetMethod("GetMethodFromNativeIP", BindingFlags.Static | BindingFlags.NonPublic)!; + Assert.NotNull(getMethodFromIP); + + 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 = (MethodBase?)getMethodFromIP.Invoke(null, new object[] { (IntPtr)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(AsyncProfilerTests), nameof(IsRuntimeAsyncSupported))] + [ActiveIssue("https://github.com/dotnet/runtime/issues/124072", typeof(PlatformDetection), nameof(PlatformDetection.IsInterpreter))] + 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. + MethodInfo getMethodFromIP = typeof(System.Diagnostics.StackFrame).GetMethod("GetMethodFromNativeIP", BindingFlags.Static | BindingFlags.NonPublic)!; + Assert.NotNull(getMethodFromIP); + + 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; + + ReadCompressedUInt64(buffer, ref index); + AsyncEventID firstEvent = (AsyncEventID)buffer[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 = (MethodBase?)getMethodFromIP.Invoke(null, new object[] { (IntPtr)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(AsyncProfilerTests), nameof(IsRuntimeAsyncSupported))] + [ActiveIssue("https://github.com/dotnet/runtime/issues/124072", typeof(PlatformDetection), nameof(PlatformDetection.IsInterpreter))] + 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. + MethodInfo getMethodFromIP = typeof(System.Diagnostics.StackFrame).GetMethod("GetMethodFromNativeIP", BindingFlags.Static | BindingFlags.NonPublic)!; + Assert.NotNull(getMethodFromIP); + + foreach (var (nativeIP, _) in deepest.Frames) + { + Assert.True(nativeIP != 0, "Frame has zero NativeIP"); + var method = (MethodBase?)getMethodFromIP.Invoke(null, new object[] { (IntPtr)nativeIP }); + Assert.True(method is not null, + $"NativeIP 0x{nativeIP:X} does not resolve to a managed method"); + } + } + + [ConditionalFact(typeof(AsyncProfilerTests), nameof(IsRuntimeAsyncSupported))] + [ActiveIssue("https://github.com/dotnet/runtime/issues/124072", typeof(PlatformDetection), nameof(PlatformDetection.IsInterpreter))] + 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; + } + + Deserializer.ReadCompressedUInt64(buffer, ref index, out ulong delta); + currentTimestamp += delta; + + AsyncEventID eventId = (AsyncEventID)buffer[index++]; + + 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.ReadUInt64(buffer, ref index, out ulong qpcFrequency); + Console.WriteLine($" QPCFrequency: {qpcFrequency}"); + + Deserializer.ReadUInt32(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.ReadUInt64(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; + + Deserializer.ReadCompressedUInt64(buffer, ref index, out id); + type = buffer[index++]; + callstackId = buffer[index++]; + asyncCallstackLength = buffer[index++]; + + 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.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 @@ + diff --git a/src/mono/System.Private.CoreLib/System.Private.CoreLib.csproj b/src/mono/System.Private.CoreLib/System.Private.CoreLib.csproj index b1d2370713ea23..6f6cd7d98c3540 100644 --- a/src/mono/System.Private.CoreLib/System.Private.CoreLib.csproj +++ b/src/mono/System.Private.CoreLib/System.Private.CoreLib.csproj @@ -237,6 +237,7 @@ + diff --git a/src/mono/System.Private.CoreLib/src/System/Runtime/CompilerServices/AsyncProfiler.Mono.cs b/src/mono/System.Private.CoreLib/src/System/Runtime/CompilerServices/AsyncProfiler.Mono.cs new file mode 100644 index 00000000000000..df4294b3e39daf --- /dev/null +++ b/src/mono/System.Private.CoreLib/src/System/Runtime/CompilerServices/AsyncProfiler.Mono.cs @@ -0,0 +1,26 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +namespace System.Runtime.CompilerServices +{ + internal static partial class AsyncProfiler + { + internal static partial class ContinuationWrapper + { + public static void InitInfo(ref Info info) + { + info.ContinuationIndex = 0; + info.ContinuationTable = ref s_dummyContinuationTable; + } + + private static nint s_dummyContinuationTable; + } + + private static partial class SyncPoint + { + private static unsafe void ResumeAsyncCallstacks(AsyncThreadContext _) + { + } + } + } +} From c78aebf6520b5eb23de1911c88690765665c761f Mon Sep 17 00:00:00 2001 From: lateralusX Date: Tue, 21 Apr 2026 18:26:15 +0200 Subject: [PATCH 02/22] Additional adjustments: * Fix 0 length room for callstack. * Have AsyncEventHeader return header start index. * Add qpc and utc time to metadata to make time conversion possible. * Harden buffer allocation failure. * AI review feedback. * Adjust tests. --- .../CompilerServices/AsyncProfiler.CoreCLR.cs | 14 ++- .../Runtime/CompilerServices/AsyncProfiler.cs | 90 +++++++++++++------ .../AsyncProfilerTests.cs | 21 +++-- 3 files changed, 87 insertions(+), 38 deletions(-) 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 index 017700db4506e9..b5d69f44386900 100644 --- 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 @@ -430,9 +430,14 @@ public static bool CaptureRuntimeAsyncCallstack(Span buffer, ref int index return false; } + byte maxAsyncCallstackLength = (byte)Math.Min(byte.MaxValue, (buffer.Length - index) / ASYNC_METHOD_INFO_SIZE); + if (maxAsyncCallstackLength == 0) + { + return false; + } + ulong currentNativeIP = 0; ulong previousNativeIP = state.LastNativeIP; - byte maxAsyncCallstackLength = (byte)Math.Min(byte.MaxValue, (buffer.Length - index) / ASYNC_METHOD_INFO_SIZE); unsafe { @@ -511,8 +516,8 @@ public static void EmitEvent(AsyncThreadContext context, long currentTimestamp, // Callstack envelope: id (max 10 bytes compressed) + type (1) + callstackId (1) + frameCount (1) const int maxEnvelopeSize = EventBuffer.Serializer.MaxCompressedUInt64Size + sizeof(byte) + sizeof(byte) + sizeof(byte); - int savedAsyncEventHeaderIndex = eventBuffer.Index; - if (EventBuffer.Serializer.AsyncEventHeader(context, ref eventBuffer, currentTimestamp, delta, eventID, maxEnvelopeSize)) + int savedAsyncEventHeaderIndex = EventBuffer.Serializer.AsyncEventHeader(context, ref eventBuffer, currentTimestamp, delta, eventID, maxEnvelopeSize); + if (savedAsyncEventHeaderIndex >= 0) { EventBuffer.Serializer.CallstackHeader(ref eventBuffer, id, type, 0); @@ -532,7 +537,8 @@ public static void EmitEvent(AsyncThreadContext context, long currentTimestamp, context.Flush(); // Write the callstack again. - if (EventBuffer.Serializer.AsyncEventHeader(context, ref eventBuffer, context.LastEventTimestamp, 0, eventID, maxEnvelopeSize + index)) + savedAsyncEventHeaderIndex = EventBuffer.Serializer.AsyncEventHeader(context, ref eventBuffer, context.LastEventTimestamp, 0, eventID, maxEnvelopeSize + index); + if (savedAsyncEventHeaderIndex >= 0) { EventBuffer.Serializer.CallstackHeader(ref eventBuffer, id, type, state.Count); EventBuffer.Serializer.CallstackData(ref eventBuffer, rentedArray, index); 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 index b6898c59e96466..03d34bfde83558 100644 --- a/src/libraries/System.Private.CoreLib/src/System/Runtime/CompilerServices/AsyncProfiler.cs +++ b/src/libraries/System.Private.CoreLib/src/System/Runtime/CompilerServices/AsyncProfiler.cs @@ -110,16 +110,28 @@ public static void EmitAsyncProfilerMetadataIfNeeded(AsyncThreadContext context) { long[] wrapperIPs = ContinuationWrapper.GetContinuationWrapperIPs(); - // Metadata payload: [qpcFrequency (8 bytes)] [eventBufferSize (4 bytes)] [wrapperCount byte] [wrapperIP0 (8 bytes)] ... [wrapperIPn (8 bytes)] - int maxEventPayloadSize = sizeof(long) + sizeof(uint) + 1 + (wrapperIPs.Length * sizeof(long)); + // Metadata payload: + // [qpcFrequency (8 bytes)] + // [qpcSync (8 bytes)] + // [utcSync (8 bytes)] + // [eventBufferSize (4 bytes)] + // [wrapperCount byte] + // [wrapperIP0 (8 bytes)] ... [wrapperIPn (8 bytes)] + int maxEventPayloadSize = sizeof(long) + sizeof(long) + sizeof(long) + sizeof(uint) + 1 + (wrapperIPs.Length * sizeof(long)); ref EventBuffer eventBuffer = ref context.EventBuffer; - if (EventBuffer.Serializer.AsyncEventHeader(context, ref eventBuffer, AsyncEventID.AsyncProfilerMetadata, maxEventPayloadSize)) + if (EventBuffer.Serializer.AsyncEventHeader(context, ref eventBuffer, AsyncEventID.AsyncProfilerMetadata, maxEventPayloadSize) >= 0) { byte[] buffer = eventBuffer.Data; ref int index = ref eventBuffer.Index; - EventBuffer.Serializer.WriteUInt64(buffer, ref index, (ulong)Stopwatch.Frequency); + long qpcFrequency = Stopwatch.Frequency; + long qpcSync = Stopwatch.GetTimestamp(); + long utcSync = DateTime.UtcNow.ToFileTimeUtc(); + + EventBuffer.Serializer.WriteUInt64(buffer, ref index, (ulong)qpcFrequency); + EventBuffer.Serializer.WriteUInt64(buffer, ref index, (ulong)qpcSync); + EventBuffer.Serializer.WriteUInt64(buffer, ref index, (ulong)utcSync); EventBuffer.Serializer.WriteUInt32(buffer, ref index, EventBufferSize); Unsafe.Add(ref MemoryMarshal.GetArrayDataReference(buffer), index++) = (byte)wrapperIPs.Length; @@ -184,6 +196,7 @@ public static class Serializer public const int MaxCompressedInt32Size = 5; public const int MaxCompressedUInt64Size = 10; public const int MaxCompressedInt64Size = 10; + public const int HeaderSize = 37; [MethodImpl(MethodImplOptions.AggressiveInlining)] public static void WriteInt32(Span buffer, ref int index, int value) @@ -349,17 +362,20 @@ public static void Header(AsyncThreadContext context, ref EventBuffer eventBuffe context.LastEventTimestamp = currentTimestamp; //Write header to buffer - Unsafe.Add(ref MemoryMarshal.GetArrayDataReference(buffer), index++) = 1; // Version - WriteUInt32(buffer, ref index, 0); // Total size in bytes, will be updated on flush. - WriteUInt32(buffer, ref index, context.AsyncThreadContextId); // Async Thread Context ID - WriteUInt64(buffer, ref index, Thread.CurrentOSThreadId); // OS Thread ID - WriteUInt32(buffer, ref index, 0); // Total event count, will be updated on flush. - WriteUInt64(buffer, ref index, (ulong)currentTimestamp); // Start timestamp - WriteUInt64(buffer, ref index, 0); // End timestamp, will be updated on flush. + if (buffer.Length >= HeaderSize) + { + Unsafe.Add(ref MemoryMarshal.GetArrayDataReference(buffer), index++) = 1; // Version + WriteUInt32(buffer, ref index, 0); // Total size in bytes, will be updated on flush. + WriteUInt32(buffer, ref index, context.AsyncThreadContextId); // Async Thread Context ID + WriteUInt64(buffer, ref index, Thread.CurrentOSThreadId); // OS Thread ID + WriteUInt32(buffer, ref index, 0); // Total event count, will be updated on flush. + WriteUInt64(buffer, ref index, (ulong)currentTimestamp); // Start timestamp + WriteUInt64(buffer, ref index, 0); // End timestamp, will be updated on flush. + } } [MethodImpl(MethodImplOptions.AggressiveInlining)] - public static bool AsyncEventHeader(AsyncThreadContext context, ref EventBuffer eventBuffer, AsyncEventID eventID, int maxEventPayloadSize) + public static int AsyncEventHeader(AsyncThreadContext context, ref EventBuffer eventBuffer, AsyncEventID eventID, int maxEventPayloadSize) { long currentTimestamp = Stopwatch.GetTimestamp(); long delta = currentTimestamp - context.LastEventTimestamp; @@ -367,19 +383,21 @@ public static bool AsyncEventHeader(AsyncThreadContext context, ref EventBuffer } [MethodImpl(MethodImplOptions.AggressiveInlining)] - public static bool AsyncEventHeader(AsyncThreadContext context, ref EventBuffer eventBuffer, long currentTimestamp, AsyncEventID eventID, int maxEventPayloadSize) + public static int 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) + public static int AsyncEventHeader(AsyncThreadContext context, ref EventBuffer eventBuffer, long currentTimestamp, long delta, AsyncEventID eventID, int maxEventPayloadSize) { const int maxEventHeaderSize = MaxCompressedUInt64Size + sizeof(byte); byte[] buffer = eventBuffer.Data; ref int index = ref eventBuffer.Index; + int asyncHeaderIndex = index; + if ((index + maxEventHeaderSize + maxEventPayloadSize) <= buffer.Length && delta >= 0) { context.LastEventTimestamp = currentTimestamp; @@ -389,18 +407,20 @@ public static bool AsyncEventHeader(AsyncThreadContext context, ref EventBuffer // Event is too big for buffer, drop it. if (maxEventHeaderSize + maxEventPayloadSize > buffer.Length) { - return false; + return -1; } context.Flush(); + delta = 0; + asyncHeaderIndex = 0; } WriteCompressedUInt64(buffer, ref index, (ulong)delta); //Timestamp delta from last event Unsafe.Add(ref MemoryMarshal.GetArrayDataReference(buffer), index++) = (byte)eventID; // eventID eventBuffer.EventCount++; - return true; + return asyncHeaderIndex; } public static void RemoveAsyncEventHeader(AsyncThreadContext context, int savedAsyncEventHeaderIndex) @@ -468,7 +488,7 @@ public ref EventBuffer EventBuffer { if (_eventBuffer.Data.Length == 0) { - _eventBuffer.Data = new byte[Config.EventBufferSize]; + _eventBuffer.Data = AllocBuffer(); EventBuffer.Serializer.Header(this, ref _eventBuffer); } @@ -571,7 +591,7 @@ public void Flush() } catch { - // AsyncProfiler can't throw, ignore exception and loose buffer. + // AsyncProfiler can't throw, ignore exception and lose buffer. } EventBuffer.Serializer.Header(this, ref eventBuffer); @@ -590,6 +610,20 @@ private static AsyncThreadContext Create() return context; } + private static byte[] AllocBuffer() + { + try + { + return new byte[Config.EventBufferSize]; + } + 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. + return Array.Empty(); + } + } + [ThreadStatic] private static AsyncThreadContext? t_asyncThreadContext; } @@ -601,7 +635,7 @@ public static void EmitEvent(AsyncThreadContext context, ulong id) { const int maxEventPayloadSize = EventBuffer.Serializer.MaxCompressedUInt64Size; - if (EventBuffer.Serializer.AsyncEventHeader(context, ref context.EventBuffer, AsyncEventID.CreateAsyncContext, maxEventPayloadSize)) + if (EventBuffer.Serializer.AsyncEventHeader(context, ref context.EventBuffer, AsyncEventID.CreateAsyncContext, maxEventPayloadSize) >= 0) { EventBuffer.Serializer.WriteCompressedUInt64(context.EventBuffer.Data, ref context.EventBuffer.Index, id); } @@ -612,7 +646,7 @@ public static void EmitEvent(AsyncThreadContext context, long currentTimestamp, { const int maxEventPayloadSize = EventBuffer.Serializer.MaxCompressedUInt64Size; - if (EventBuffer.Serializer.AsyncEventHeader(context, ref context.EventBuffer, currentTimestamp, AsyncEventID.CreateAsyncContext, maxEventPayloadSize)) + if (EventBuffer.Serializer.AsyncEventHeader(context, ref context.EventBuffer, currentTimestamp, AsyncEventID.CreateAsyncContext, maxEventPayloadSize) >= 0) { EventBuffer.Serializer.WriteCompressedUInt64(context.EventBuffer.Data, ref context.EventBuffer.Index, id); } @@ -626,7 +660,7 @@ public static void EmitEvent(AsyncThreadContext context, ulong id) { const int maxEventPayloadSize = EventBuffer.Serializer.MaxCompressedUInt64Size; - if (EventBuffer.Serializer.AsyncEventHeader(context, ref context.EventBuffer, AsyncEventID.ResumeAsyncContext, maxEventPayloadSize)) + if (EventBuffer.Serializer.AsyncEventHeader(context, ref context.EventBuffer, AsyncEventID.ResumeAsyncContext, maxEventPayloadSize) >= 0) { EventBuffer.Serializer.WriteCompressedUInt64(context.EventBuffer.Data, ref context.EventBuffer.Index, id); } @@ -637,7 +671,7 @@ public static void EmitEvent(AsyncThreadContext context, long currentTimestamp, { const int maxEventPayloadSize = EventBuffer.Serializer.MaxCompressedUInt64Size; - if (EventBuffer.Serializer.AsyncEventHeader(context, ref context.EventBuffer, currentTimestamp, AsyncEventID.ResumeAsyncContext, maxEventPayloadSize)) + if (EventBuffer.Serializer.AsyncEventHeader(context, ref context.EventBuffer, currentTimestamp, AsyncEventID.ResumeAsyncContext, maxEventPayloadSize) >= 0) { EventBuffer.Serializer.WriteCompressedUInt64(context.EventBuffer.Data, ref context.EventBuffer.Index, id); } @@ -723,7 +757,7 @@ public static void EmitEvent(AsyncThreadContext context, long currentTimestamp, ref EventBuffer eventBuffer = ref context.EventBuffer; - if (EventBuffer.Serializer.AsyncEventHeader(context, ref eventBuffer, currentTimestamp, AsyncEventID.UnwindAsyncException, maxEventPayloadSize)) + if (EventBuffer.Serializer.AsyncEventHeader(context, ref eventBuffer, currentTimestamp, AsyncEventID.UnwindAsyncException, maxEventPayloadSize) >= 0) { EventBuffer.Serializer.WriteCompressedUInt32(eventBuffer.Data, ref eventBuffer.Index, unwindedFrames); } @@ -776,10 +810,8 @@ public static void EmitEvent(AsyncThreadContext context) internal static partial class ContinuationWrapper { -#pragma warning disable CA1823 public const byte COUNT = 32; public const byte COUNT_MASK = COUNT - 1; -#pragma warning restore CA1823 [MethodImpl(MethodImplOptions.AggressiveInlining)] public static void IncrementIndex(ref Info info) @@ -975,7 +1007,7 @@ public static void EnableFlushTimer() { lock (CacheLock) { - s_flushTimer ??= new Timer(PeriodicFlush, null, Timeout.Infinite, Timeout.Infinite); + s_flushTimer ??= new Timer(PeriodicFlush, null, Timeout.Infinite, Timeout.Infinite, false); s_flushTimer.Change(ASYNC_THREAD_CONTEXT_CACHE_FLUSH_TIMER_INTERVAL_MS, Timeout.Infinite); } } @@ -992,7 +1024,7 @@ public static void EnableCleanupTimer() { lock (CacheLock) { - s_cleanupTimer ??= new Timer(Cleanup, null, Timeout.Infinite, Timeout.Infinite); + s_cleanupTimer ??= new Timer(Cleanup, null, Timeout.Infinite, Timeout.Infinite, false); s_cleanupTimer?.Change(ASYNC_THREAD_CONTEXT_CACHE_CLEANUP_TIMER_INTERVAL_MS, Timeout.Infinite); } } @@ -1050,8 +1082,8 @@ public AsyncThreadContextHolder(AsyncThreadContext context, Thread ownerThread) OwnerThread = new WeakReference(ownerThread); } - public AsyncThreadContext Context; - public WeakReference OwnerThread; + public readonly AsyncThreadContext Context; + public readonly WeakReference OwnerThread; } private const int ASYNC_THREAD_CONTEXT_CACHE_FLUSH_TIMER_INTERVAL_MS = 1000; 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 index 8b6183a98ebbea..042ff2af7c53d2 100644 --- 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 @@ -356,13 +356,15 @@ private static long ReadCompressedInt64(ReadOnlySpan buffer, ref int index private static void SkipMetadataPayload(ReadOnlySpan buffer, ref int index) { - ReadMetadataPayload(buffer, ref index, out _, out _, out _); + ReadMetadataPayload(buffer, ref index, out _, out _, out _, out _, out _); } private static void ReadMetadataPayload(ReadOnlySpan buffer, ref int index, - out ulong qpcFrequency, out uint eventBufferSize, out long[] wrapperIPs) + out ulong qpcFrequency, out ulong qpcSync, out ulong utcSync, out uint eventBufferSize, out long[] wrapperIPs) { EventBuffer.Deserializer.ReadUInt64(buffer, ref index, out qpcFrequency); + EventBuffer.Deserializer.ReadUInt64(buffer, ref index, out qpcSync); + EventBuffer.Deserializer.ReadUInt64(buffer, ref index, out utcSync); EventBuffer.Deserializer.ReadUInt32(buffer, ref index, out eventBufferSize); byte wrapperCount = buffer[index++]; wrapperIPs = new long[wrapperCount]; @@ -372,7 +374,7 @@ private static void ReadMetadataPayload(ReadOnlySpan buffer, ref int index } } - private record struct MetadataFromBuffer(ulong QpcFrequency, uint EventBufferSize, long[] WrapperIPs); + private record struct MetadataFromBuffer(ulong QpcFrequency, ulong QpcSync, ulong UtcSync, uint EventBufferSize, long[] WrapperIPs); private static List CollectMetadataFromBuffer(ConcurrentQueue events) { @@ -383,8 +385,8 @@ private static List CollectMetadataFromBuffer(ConcurrentQueu { if (eventId == AsyncEventID.AsyncProfilerMetadata) { - ReadMetadataPayload(buf, ref idx, out ulong freq, out uint bufSize, out long[] ips); - metadataList.Add(new MetadataFromBuffer(freq, bufSize, ips)); + 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); @@ -1454,6 +1456,7 @@ public static IEnumerable KeywordGatekeepingData() 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 } }; } @@ -1526,6 +1529,8 @@ public void RuntimeAsync_MetadataEventEmittedOnEnable() 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")); @@ -1973,6 +1978,12 @@ private static int OutputAsyncProfilerMetadataEvent(ReadOnlySpan buffer) Deserializer.ReadUInt64(buffer, ref index, out ulong qpcFrequency); Console.WriteLine($" QPCFrequency: {qpcFrequency}"); + Deserializer.ReadUInt64(buffer, ref index, out ulong qpcSync); + Console.WriteLine($" QPCSync: {qpcSync}"); + + Deserializer.ReadUInt64(buffer, ref index, out ulong utcSync); + Console.WriteLine($" UTCSync: {utcSync}"); + Deserializer.ReadUInt32(buffer, ref index, out uint eventBufferSize); Console.WriteLine($" EventBufferSize: {eventBufferSize}"); From e06efd3bff73a490da3205339aebd187e74dc340 Mon Sep 17 00:00:00 2001 From: lateralusX Date: Tue, 21 Apr 2026 18:57:57 +0200 Subject: [PATCH 03/22] Fix line ending. --- .../src/System.Private.CoreLib.Shared.projitems | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) 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 8138c459a5e6c2..d06bf1e088ee5c 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 @@ -2990,4 +2990,4 @@ - \ No newline at end of file + From 69a54fd559018708006f64c2cb08b29792682c90 Mon Sep 17 00:00:00 2001 From: lateralusX Date: Tue, 21 Apr 2026 19:30:25 +0200 Subject: [PATCH 04/22] Fix Mono build. --- .../System/Runtime/CompilerServices/AsyncProfiler.CoreCLR.cs | 2 +- .../System/Runtime/CompilerServices/AsyncProfiler.Mono.cs | 5 +++++ 2 files changed, 6 insertions(+), 1 deletion(-) 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 index b5d69f44386900..4b19396cf820f5 100644 --- 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 @@ -140,7 +140,7 @@ public static void InitInfo(ref Info info) } } - internal static long[] GetContinuationWrapperIPs() + public static long[] GetContinuationWrapperIPs() { long[] ips = new long[COUNT]; for (int i = 0; i < COUNT; i++) diff --git a/src/mono/System.Private.CoreLib/src/System/Runtime/CompilerServices/AsyncProfiler.Mono.cs b/src/mono/System.Private.CoreLib/src/System/Runtime/CompilerServices/AsyncProfiler.Mono.cs index df4294b3e39daf..1d1cacec051398 100644 --- a/src/mono/System.Private.CoreLib/src/System/Runtime/CompilerServices/AsyncProfiler.Mono.cs +++ b/src/mono/System.Private.CoreLib/src/System/Runtime/CompilerServices/AsyncProfiler.Mono.cs @@ -13,6 +13,11 @@ public static void InitInfo(ref Info info) info.ContinuationTable = ref s_dummyContinuationTable; } + public static long[] GetContinuationWrapperIPs() + { + return new long[COUNT]; + } + private static nint s_dummyContinuationTable; } From ebe8c1c84835d0a54ec86e31b7803ef17a2e5ce7 Mon Sep 17 00:00:00 2001 From: lateralusX Date: Wed, 22 Apr 2026 15:35:16 +0200 Subject: [PATCH 05/22] Review feedback. --- .../CompilerServices/AsyncProfiler.CoreCLR.cs | 2 +- .../Runtime/CompilerServices/AsyncProfiler.cs | 150 +++++++++--------- .../CompilerServices/AsyncProfiler.Mono.cs | 2 +- 3 files changed, 79 insertions(+), 75 deletions(-) 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 index 4b19396cf820f5..67dba17fdfb95f 100644 --- 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 @@ -394,7 +394,7 @@ private static partial class SyncPoint [MethodImpl(MethodImplOptions.AggressiveInlining)] private static unsafe void ResumeAsyncCallstacks(AsyncThreadContext context) { - //Write recursivly all the resume async callstack events. + //Write recursively all the resume async callstack events. AsyncDispatcherInfo* info = AsyncDispatcherInfo.t_current; if (info != null) { 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 index 03d34bfde83558..935c05133d2378 100644 --- a/src/libraries/System.Private.CoreLib/src/System/Runtime/CompilerServices/AsyncProfiler.cs +++ b/src/libraries/System.Private.CoreLib/src/System/Runtime/CompilerServices/AsyncProfiler.cs @@ -5,7 +5,6 @@ using System.Collections.Generic; using System.Diagnostics; using System.Diagnostics.Tracing; -using System.Reflection; using System.Runtime.InteropServices; using System.Threading; using static System.Runtime.CompilerServices.AsyncProfilerBufferedEventSource; @@ -930,76 +929,7 @@ public static void Flush(bool force) { lock (CacheLock) { - // Make sure all dead threads are flushed and removed from the cache. - for (int i = s_cache.Count - 1; i >= 0; i--) - { - var 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; - } - } - - // Look at live threads, only flush if forced or contexts that have been idle for 250 milliseconds. - long idleWriteTimestamp = Stopwatch.GetTimestamp() - (Stopwatch.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() - Stopwatch.Frequency * 30; - - // Spin wait timeout, 100 milliseconds. - long spinWaitTimeout = Stopwatch.Frequency / 10; - - foreach (var 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; - } - } + FlushCore(force); } } @@ -1043,7 +973,7 @@ private static void Cleanup(object? state) lock (CacheLock) { - Flush(true); + FlushCore(true); if (s_cache.Count > 0) { @@ -1059,7 +989,7 @@ private static void PeriodicFlush(object? state) lock (CacheLock) { - Flush(false); + FlushCore(false); if (IsEnabled.AnyAsyncEvents(Config.ActiveEventKeywords)) { @@ -1074,6 +1004,80 @@ private static void PeriodicFlush(object? state) } } + 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--) + { + var 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; + } + } + + // Look at live threads, only flush if forced or contexts that have been idle for 250 milliseconds. + long idleWriteTimestamp = Stopwatch.GetTimestamp() - (Stopwatch.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() - Stopwatch.Frequency * 30; + + // Spin wait timeout, 100 milliseconds. + long spinWaitTimeout = Stopwatch.Frequency / 10; + + foreach (var 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; + } + } + } + private sealed class AsyncThreadContextHolder { public AsyncThreadContextHolder(AsyncThreadContext context, Thread ownerThread) diff --git a/src/mono/System.Private.CoreLib/src/System/Runtime/CompilerServices/AsyncProfiler.Mono.cs b/src/mono/System.Private.CoreLib/src/System/Runtime/CompilerServices/AsyncProfiler.Mono.cs index 1d1cacec051398..7948ffc5c4bdbf 100644 --- a/src/mono/System.Private.CoreLib/src/System/Runtime/CompilerServices/AsyncProfiler.Mono.cs +++ b/src/mono/System.Private.CoreLib/src/System/Runtime/CompilerServices/AsyncProfiler.Mono.cs @@ -15,7 +15,7 @@ public static void InitInfo(ref Info info) public static long[] GetContinuationWrapperIPs() { - return new long[COUNT]; + return Array.Empty(); } private static nint s_dummyContinuationTable; From da09376a887d2111f7d8ee15f810cd2c9565b61e Mon Sep 17 00:00:00 2001 From: lateralusX Date: Thu, 23 Apr 2026 10:00:18 +0200 Subject: [PATCH 06/22] Refactor debugger flags and way to trigger it. --- .../CompilerServices/AsyncHelpers.CoreCLR.cs | 4 +- .../CompilerServices/AsyncInstrumentation.cs | 55 +++++++++++-------- .../System/Threading/Tasks/TplEventSource.cs | 20 ------- .../RuntimeAsyncTests.cs | 19 ++++--- 4 files changed, 45 insertions(+), 53 deletions(-) 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 a3c80e5d8e3a8e..d080526f8a8117 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 @@ -1236,7 +1236,7 @@ public static void CompleteRuntimeAsyncMethod(ref AsyncDispatcherInfo info, Asyn internal static class AsyncDebugger { - public static bool IsEnabled(AsyncInstrumentation.Flags flags) => AsyncInstrumentation.IsEnabled.AsyncDebugger(flags) && IsAsyncDebuggingEnabled(); + public static bool IsEnabled(AsyncInstrumentation.Flags flags) => AsyncInstrumentation.IsEnabled.AsyncDebugger(flags); public static void CreateAsyncContext(Task task) { @@ -1329,8 +1329,6 @@ public static void HandleSuspendedFailed(Task task, Continuation? nextContinuati Task.RemoveRuntimeAsyncTask(task); } } - - private static bool IsAsyncDebuggingEnabled() => Task.s_asyncDebuggingEnabled; } } } 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 ef60b3f747fd06..df0e271c873389 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 = @@ -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,10 +75,7 @@ public static void UpdateAsyncProfilerFlags(Flags asyncProfilerFlags) lock (s_lock) { s_asyncProfilerActiveFlags = asyncProfilerFlags; - if (IsInitialized(s_activeFlags)) - { - s_activeFlags = s_asyncProfilerActiveFlags | s_asyncDebuggerActiveFlags; - } + UpdateFlags(true); } } @@ -92,39 +89,53 @@ public static void UpdateAsyncDebuggerFlags(Flags asyncDebuggerFlags) lock (s_lock) { s_asyncDebuggerActiveFlags = asyncDebuggerFlags; - if (IsInitialized(s_activeFlags)) - { - s_activeFlags = s_asyncProfilerActiveFlags | s_asyncDebuggerActiveFlags; - } + UpdateFlags(true); } } - 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. _ = AsyncProfilerBufferedEventSource.Log; // Touch AsyncProfilerBufferedEventSource to trigger static constructor which will initialize async profiler flags if EventSource is supported. lock (s_lock) { - if (IsUninitialized(s_activeFlags)) - { - s_activeFlags = s_asyncProfilerActiveFlags | s_asyncDebuggerActiveFlags; - } - - return s_activeFlags; + return UpdateFlags(false); } } - private static bool IsInitialized(Flags flags) => !IsUninitialized(flags); + private static Flags UpdateFlags(bool setSynchronizeFlag) + { + if (!IsEnabled.AsyncDebugger(s_internalAsyncDebuggerActiveFlags) && Task.s_asyncDebuggingEnabled) + { + s_internalAsyncDebuggerActiveFlags = DefaultFlags | Flags.AsyncDebugger; + } + else if (IsEnabled.AsyncDebugger(s_internalAsyncDebuggerActiveFlags) && !Task.s_asyncDebuggingEnabled) + { + s_internalAsyncDebuggerActiveFlags = Flags.Disabled; + } - private static bool IsUninitialized(Flags flags) => (flags & Flags.Uninitialized) != 0; + s_activeFlags = s_asyncProfilerActiveFlags | s_asyncDebuggerActiveFlags | s_internalAsyncDebuggerActiveFlags; + if (setSynchronizeFlag) + { + s_activeFlags |= Flags.Synchronize; + } + else + { + s_activeFlags &= ~Flags.Synchronize; + } - private static Flags s_activeFlags = Flags.Uninitialized; + return s_activeFlags; + } + + private static Flags s_activeFlags = Flags.Synchronize; private static Flags s_asyncProfilerActiveFlags; private static Flags s_asyncDebuggerActiveFlags; + private static Flags s_internalAsyncDebuggerActiveFlags; + private static readonly Lock s_lock = new(); } } 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/RuntimeAsyncTests.cs b/src/libraries/System.Runtime/tests/System.Threading.Tasks.Tests/System.Runtime.CompilerServices/RuntimeAsyncTests.cs index 7e28cbbfd1e1d9..e48bdea01dfb09 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, (uint)s_activeFlagsField.GetValue(null) | 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)); @@ -63,8 +63,11 @@ private static void DetachDebugger() lock (s_debuggerLock) { s_asyncDebuggingEnabledField.SetValue(null, false); - s_debuggerTplInstance?.Dispose(); - s_debuggerTplInstance = null; + s_activeFlagsField.SetValue(null, (uint)s_activeFlagsField.GetValue(null) | SynchronizeInstrumentationFlags); + + // Run an async method to trigger SyncActiveFlags which will detect + // s_asyncDebuggingEnabled is false and clear the debugger flags. + Func().GetAwaiter().GetResult(); uint flags = Convert.ToUInt32(s_activeFlagsField.GetValue(null)); Assert.True(flags == DisabledInstrumentationFlags, $"ActiveFlags equals {flags}, expected {DisabledInstrumentationFlags}"); From 31dcc36483c98627e1ad25f8a7b48a293818e9e2 Mon Sep 17 00:00:00 2001 From: lateralusX Date: Thu, 23 Apr 2026 11:12:19 +0200 Subject: [PATCH 07/22] Fix failing tests on CI. --- .../BasicEventSourceTest/TestUtilities.cs | 1 + .../AsyncProfilerTests.cs | 62 +++++++------------ 2 files changed, 25 insertions(+), 38 deletions(-) diff --git a/src/libraries/System.Diagnostics.Tracing/tests/BasicEventSourceTest/TestUtilities.cs b/src/libraries/System.Diagnostics.Tracing/tests/BasicEventSourceTest/TestUtilities.cs index 9de8946d9275ce..f07a9ab5b8fd04 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.AsyncProfilerBufferedEventSource" && // event source from xunit runner eventSource.Name != "xUnit.TestEventSource" && 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 index 042ff2af7c53d2..03c1435c46f4ac 100644 --- 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 @@ -569,6 +569,11 @@ private static void RunScenario(Func scenario) } 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)) @@ -577,7 +582,7 @@ private static ConcurrentQueue CollectEvents(EventKeyword { SendFlushCommand(); events.Clear(); - callback(); + callback(events, keywords); }); } return events; @@ -1330,7 +1335,7 @@ public void RuntimeAsync_WrapperIndexNoResetUnder32() [ActiveIssue("https://github.com/dotnet/runtime/issues/124072", typeof(PlatformDetection), nameof(PlatformDetection.IsInterpreter))] public void RuntimeAsync_PeriodicTimerFlush() { - var events = CollectEvents(CoreKeywords, () => + var events = CollectEvents(CoreKeywords, (collectedEvents, _) => { // Run scenario — do NOT flush explicitly afterwards. RunScenario(async () => @@ -1338,9 +1343,15 @@ public void RuntimeAsync_PeriodicTimerFlush() await Func(); }); - // Wait for the periodic flush timer (1s interval) to detect the idle - // buffer and flush it automatically. + // Wait for the periodic flush timer (1s interval) to detect the idle buffer and flush it automatically. Thread.Sleep(2000); + + // Use polling instead of a fixed sleep to wait for the flush, so the test is robust on slow/loaded CI machines. + SpinWait.SpinUntil(() => + { + var ids = CollectAsyncEventIds(collectedEvents); + return ids.Exists(id => id == AsyncEventID.ResumeAsyncContext || id == AsyncEventID.SuspendAsyncContext || id == AsyncEventID.CompleteAsyncContext); + }, TimeSpan.FromSeconds(15)); }); // DumpCollectedEvents(events); @@ -1351,43 +1362,12 @@ public void RuntimeAsync_PeriodicTimerFlush() Assert.True(coreEventCount > 0, "Expected periodic timer to flush buffer with core lifecycle events"); } - [ConditionalFact(typeof(AsyncProfilerTests), nameof(IsRuntimeAsyncSupported))] - [ActiveIssue("https://github.com/dotnet/runtime/issues/124072", typeof(PlatformDetection), nameof(PlatformDetection.IsInterpreter))] - public void RuntimeAsync_MultiThreadFlush() - { - const int threadCount = 4; - var events = CollectEvents(CoreKeywords, () => - { - // Ensure enough thread pool threads are available for concurrent execution. - ThreadPool.GetMinThreads(out int prevWorker, out int prevIO); - ThreadPool.SetMinThreads(threadCount, prevIO); - - var tasks = new Task[threadCount]; - for (int i = 0; i < threadCount; i++) - { - tasks[i] = Task.Run(async () => - { - await Func(); - }); - } - - Task.WhenAll(tasks).GetAwaiter().GetResult(); - ThreadPool.SetMinThreads(prevWorker, prevIO); - SendFlushCommand(); - }); - - // DumpCollectedEvents(events); - - var threadIds = CollectOsThreadIds(events); - - Assert.True(threadIds.Count > 1, $"Expected events from multiple threads, got {threadIds.Count} distinct OS thread ID(s)"); - } [ConditionalFact(typeof(AsyncProfilerTests), nameof(IsRuntimeAsyncSupported))] [ActiveIssue("https://github.com/dotnet/runtime/issues/124072", typeof(PlatformDetection), nameof(PlatformDetection.IsInterpreter))] public void RuntimeAsync_DeadThreadFlush() { - var events = CollectEvents(CoreKeywords, () => + 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. @@ -1404,9 +1384,15 @@ public void RuntimeAsync_DeadThreadFlush() thread.Join(TimeSpan.FromSeconds(10)); // Do NOT send a flush command. - // Wait for the periodic flush timer to detect the dead thread - // and flush its orphaned buffer. + // Wait for the periodic flush timer to detect the dead thread and flush its orphaned buffer. Thread.Sleep(2000); + + // Use polling instead of a fixed sleep so the test is robust on slow/loaded CI machines. + SpinWait.SpinUntil(() => + { + var ids = CollectAsyncEventIds(collectedEvents); + return ids.Exists(id => id == AsyncEventID.ResumeAsyncContext || id == AsyncEventID.SuspendAsyncContext || id == AsyncEventID.CompleteAsyncContext); + }, TimeSpan.FromSeconds(15)); }); // DumpCollectedEvents(events); From e707a2ba8103295114fbe52c78aae4ceddb1c281 Mon Sep 17 00:00:00 2001 From: lateralusX Date: Thu, 23 Apr 2026 11:12:55 +0200 Subject: [PATCH 08/22] Make async profiler CoreCLR specific for now. --- .../System.Private.CoreLib.Shared.projitems | 6 ++-- .../System.Private.CoreLib.csproj | 1 - .../CompilerServices/AsyncProfiler.Mono.cs | 31 ------------------- 3 files changed, 3 insertions(+), 35 deletions(-) delete mode 100644 src/mono/System.Private.CoreLib/src/System/Runtime/CompilerServices/AsyncProfiler.Mono.cs 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 d06bf1e088ee5c..41bed9448017b0 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,8 +942,8 @@ - - + + diff --git a/src/mono/System.Private.CoreLib/System.Private.CoreLib.csproj b/src/mono/System.Private.CoreLib/System.Private.CoreLib.csproj index 6f6cd7d98c3540..b1d2370713ea23 100644 --- a/src/mono/System.Private.CoreLib/System.Private.CoreLib.csproj +++ b/src/mono/System.Private.CoreLib/System.Private.CoreLib.csproj @@ -237,7 +237,6 @@ - diff --git a/src/mono/System.Private.CoreLib/src/System/Runtime/CompilerServices/AsyncProfiler.Mono.cs b/src/mono/System.Private.CoreLib/src/System/Runtime/CompilerServices/AsyncProfiler.Mono.cs deleted file mode 100644 index 7948ffc5c4bdbf..00000000000000 --- a/src/mono/System.Private.CoreLib/src/System/Runtime/CompilerServices/AsyncProfiler.Mono.cs +++ /dev/null @@ -1,31 +0,0 @@ -// Licensed to the .NET Foundation under one or more agreements. -// The .NET Foundation licenses this file to you under the MIT license. - -namespace System.Runtime.CompilerServices -{ - internal static partial class AsyncProfiler - { - internal static partial class ContinuationWrapper - { - public static void InitInfo(ref Info info) - { - info.ContinuationIndex = 0; - info.ContinuationTable = ref s_dummyContinuationTable; - } - - public static long[] GetContinuationWrapperIPs() - { - return Array.Empty(); - } - - private static nint s_dummyContinuationTable; - } - - private static partial class SyncPoint - { - private static unsafe void ResumeAsyncCallstacks(AsyncThreadContext _) - { - } - } - } -} From 4d72842876db4097805340a7ca01cc1e13f6440b Mon Sep 17 00:00:00 2001 From: lateralusX Date: Thu, 23 Apr 2026 13:33:38 +0200 Subject: [PATCH 09/22] Add periodic sync clock event + clock sync logic. --- .../Runtime/CompilerServices/AsyncProfiler.cs | 140 +++++++++++++++--- .../AsyncProfilerTests.cs | 44 ++++-- 2 files changed, 153 insertions(+), 31 deletions(-) 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 index 935c05133d2378..5f06d772e4f347 100644 --- a/src/libraries/System.Private.CoreLib/src/System/Runtime/CompilerServices/AsyncProfiler.cs +++ b/src/libraries/System.Private.CoreLib/src/System/Runtime/CompilerServices/AsyncProfiler.cs @@ -35,7 +35,8 @@ internal enum AsyncEventID : byte CompleteAsyncMethod = 10, ResetAsyncThreadContext = 11, ResetAsyncContinuationWrapperIndex = 12, - AsyncProfilerMetadata = 13 + AsyncProfilerMetadata = 13, + AsyncProfilerSyncClock = 14 } internal ref struct Info @@ -110,13 +111,13 @@ public static void EmitAsyncProfilerMetadataIfNeeded(AsyncThreadContext context) long[] wrapperIPs = ContinuationWrapper.GetContinuationWrapperIPs(); // Metadata payload: - // [qpcFrequency (8 bytes)] - // [qpcSync (8 bytes)] - // [utcSync (8 bytes)] - // [eventBufferSize (4 bytes)] + // [qpcFrequency (compressed uint64)] + // [qpcSync (compressed uint64)] + // [utcSync (compressed uint64)] + // [eventBufferSize (compressed uint32)] // [wrapperCount byte] - // [wrapperIP0 (8 bytes)] ... [wrapperIPn (8 bytes)] - int maxEventPayloadSize = sizeof(long) + sizeof(long) + sizeof(long) + sizeof(uint) + 1 + (wrapperIPs.Length * sizeof(long)); + // [wrapperIP0 (compressed uint64)] ... [wrapperIPn (compressed uint64)] + int maxEventPayloadSize = EventBuffer.Serializer.MaxCompressedUInt64Size + EventBuffer.Serializer.MaxCompressedUInt64Size + EventBuffer.Serializer.MaxCompressedUInt64Size + EventBuffer.Serializer.MaxCompressedUInt32Size + 1 + (wrapperIPs.Length * EventBuffer.Serializer.MaxCompressedUInt64Size); ref EventBuffer eventBuffer = ref context.EventBuffer; if (EventBuffer.Serializer.AsyncEventHeader(context, ref eventBuffer, AsyncEventID.AsyncProfilerMetadata, maxEventPayloadSize) >= 0) @@ -124,24 +125,22 @@ public static void EmitAsyncProfilerMetadataIfNeeded(AsyncThreadContext context) byte[] buffer = eventBuffer.Data; ref int index = ref eventBuffer.Index; - long qpcFrequency = Stopwatch.Frequency; - long qpcSync = Stopwatch.GetTimestamp(); - long utcSync = DateTime.UtcNow.ToFileTimeUtc(); + SyncClock(out long utcTimeSync, out long qpcSync); - EventBuffer.Serializer.WriteUInt64(buffer, ref index, (ulong)qpcFrequency); - EventBuffer.Serializer.WriteUInt64(buffer, ref index, (ulong)qpcSync); - EventBuffer.Serializer.WriteUInt64(buffer, ref index, (ulong)utcSync); - EventBuffer.Serializer.WriteUInt32(buffer, ref index, EventBufferSize); + EventBuffer.Serializer.WriteCompressedUInt64(buffer, ref index, (ulong)Stopwatch.Frequency); + EventBuffer.Serializer.WriteCompressedUInt64(buffer, ref index, (ulong)qpcSync); + EventBuffer.Serializer.WriteCompressedUInt64(buffer, ref index, (ulong)utcTimeSync); + EventBuffer.Serializer.WriteCompressedUInt32(buffer, ref index, EventBufferSize); Unsafe.Add(ref MemoryMarshal.GetArrayDataReference(buffer), index++) = (byte)wrapperIPs.Length; for (int i = 0; i < wrapperIPs.Length; i++) { - EventBuffer.Serializer.WriteUInt64(buffer, ref index, (ulong)wrapperIPs[i]); + EventBuffer.Serializer.WriteCompressedUInt64(buffer, ref index, (ulong)wrapperIPs[i]); } - } - // Force flush to deliver metadata event promptly. - context.Flush(); + // Force flush to deliver event promptly. + context.Flush(); + } s_metadataRevision = Revision; } @@ -149,6 +148,73 @@ public static void EmitAsyncProfilerMetadataIfNeeded(AsyncThreadContext context) } } + 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; + + // SyncClock payload: + // [qpcSync (compressed uint64)] + // [utcSync (compressed uint64)] + int maxEventPayloadSize = EventBuffer.Serializer.MaxCompressedUInt64Size + EventBuffer.Serializer.MaxCompressedUInt64Size; + + AsyncThreadContext transientContext = AsyncThreadContext.AcquireTransient(); + + ref EventBuffer eventBuffer = ref transientContext.EventBuffer; + if (EventBuffer.Serializer.AsyncEventHeader(transientContext, ref eventBuffer, AsyncEventID.AsyncProfilerSyncClock, maxEventPayloadSize) >= 0) + { + SyncClock(out long utcTimeSync, out long qpcSync); + EventBuffer.Serializer.WriteCompressedUInt64(eventBuffer.Data, ref eventBuffer.Index, (ulong)qpcSync); + EventBuffer.Serializer.WriteCompressedUInt64(eventBuffer.Data, ref eventBuffer.Index, (ulong)utcTimeSync); + + // 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; @@ -179,6 +245,10 @@ public static void CaptureState() 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 @@ -513,6 +583,30 @@ public static AsyncThreadContext Acquire(ref Info info) return context; } + public static AsyncThreadContext AcquireTransient() + { + AsyncThreadContext? context; + if (t_asyncThreadContext != null) + { + context = Get(); + Debug.Assert(!context.InUse); + } + else + { + context = new AsyncThreadContext(); + } + + context.InUse = true; + if (context.BlockContext) + { + context.InUse = false; + lock (AsyncThreadContextCache.CacheLock) {; } + context.InUse = true; + } + + return context; + } + [MethodImpl(MethodImplOptions.AggressiveInlining)] public static void Release(AsyncThreadContext context) { @@ -1027,15 +1121,17 @@ private static void FlushCore(bool force) } } + 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() - (Stopwatch.Frequency / 4); + 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() - Stopwatch.Frequency * 30; + long idleReclaimBufferTimestamp = Stopwatch.GetTimestamp() - frequency * 30; // Spin wait timeout, 100 milliseconds. - long spinWaitTimeout = Stopwatch.Frequency / 10; + long spinWaitTimeout = frequency / 10; foreach (var contextHolder in s_cache) { @@ -1076,6 +1172,8 @@ private static void FlushCore(bool force) context.BlockContext = false; } } + + Config.EmitSyncClockEventIfNeeded(); } private sealed class AsyncThreadContextHolder 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 index 03c1435c46f4ac..8c6891ae9957ed 100644 --- 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 @@ -29,6 +29,7 @@ public enum AsyncEventID : byte ResetAsyncThreadContext = 11, ResetAsyncContinuationWrapperIndex = 12, AsyncProfilerMetadata = 13, + AsyncProfilerSyncClock = 14, } public class AsyncProfilerTests @@ -281,6 +282,10 @@ private static bool SkipEventPayload(AsyncEventID eventId, ReadOnlySpan bu 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; @@ -362,15 +367,15 @@ private static void SkipMetadataPayload(ReadOnlySpan buffer, ref int index private static void ReadMetadataPayload(ReadOnlySpan buffer, ref int index, out ulong qpcFrequency, out ulong qpcSync, out ulong utcSync, out uint eventBufferSize, out long[] wrapperIPs) { - EventBuffer.Deserializer.ReadUInt64(buffer, ref index, out qpcFrequency); - EventBuffer.Deserializer.ReadUInt64(buffer, ref index, out qpcSync); - EventBuffer.Deserializer.ReadUInt64(buffer, ref index, out utcSync); - EventBuffer.Deserializer.ReadUInt32(buffer, ref index, out eventBufferSize); + 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++) { - EventBuffer.Deserializer.ReadInt64(buffer, ref index, out wrapperIPs[i]); + wrapperIPs[i] = (long)ReadCompressedUInt64(buffer, ref index); } } @@ -1403,6 +1408,25 @@ public void RuntimeAsync_DeadThreadFlush() Assert.True(coreEventCount > 0, "Expected periodic timer to flush dead thread's buffer"); } + [ConditionalFact(typeof(AsyncProfilerTests), nameof(IsRuntimeAsyncSupported))] + [ActiveIssue("https://github.com/dotnet/runtime/issues/124072", typeof(PlatformDetection), nameof(PlatformDetection.IsInterpreter))] + 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(AsyncProfilerTests), nameof(IsRuntimeAsyncSupported))] [ActiveIssue("https://github.com/dotnet/runtime/issues/124072", typeof(PlatformDetection), nameof(PlatformDetection.IsInterpreter))] public void RuntimeAsync_NoEventsWhenDisabled() @@ -1961,16 +1985,16 @@ private static int OutputAsyncProfilerMetadataEvent(ReadOnlySpan buffer) int index = 0; Console.WriteLine("--- AsyncProfilerMetadata ---"); - Deserializer.ReadUInt64(buffer, ref index, out ulong qpcFrequency); + Deserializer.ReadCompressedUInt64(buffer, ref index, out ulong qpcFrequency); Console.WriteLine($" QPCFrequency: {qpcFrequency}"); - Deserializer.ReadUInt64(buffer, ref index, out ulong qpcSync); + Deserializer.ReadCompressedUInt64(buffer, ref index, out ulong qpcSync); Console.WriteLine($" QPCSync: {qpcSync}"); - Deserializer.ReadUInt64(buffer, ref index, out ulong utcSync); + Deserializer.ReadCompressedUInt64(buffer, ref index, out ulong utcSync); Console.WriteLine($" UTCSync: {utcSync}"); - Deserializer.ReadUInt32(buffer, ref index, out uint eventBufferSize); + Deserializer.ReadCompressedUInt32(buffer, ref index, out uint eventBufferSize); Console.WriteLine($" EventBufferSize: {eventBufferSize}"); byte wrapperCount = buffer[index++]; @@ -1978,7 +2002,7 @@ private static int OutputAsyncProfilerMetadataEvent(ReadOnlySpan buffer) for (int i = 0; i < wrapperCount; i++) { - Deserializer.ReadUInt64(buffer, ref index, out ulong ip); + Deserializer.ReadCompressedUInt64(buffer, ref index, out ulong ip); Console.WriteLine($" Wrapper[{i}]: 0x{ip:X16}"); } From 654d38b3ace6af82c9496bf389cee31cf0fe3a99 Mon Sep 17 00:00:00 2001 From: lateralusX Date: Thu, 23 Apr 2026 14:13:29 +0200 Subject: [PATCH 10/22] Simplify the update of instrumentation flags + drop use of AsyncDebugger.IsEnable. --- .../CompilerServices/AsyncHelpers.CoreCLR.cs | 22 +++++----- .../CompilerServices/AsyncInstrumentation.cs | 40 ++++++------------- 2 files changed, 23 insertions(+), 39 deletions(-) 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 d080526f8a8117..42e466dabe004a 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 @@ -503,7 +503,7 @@ internal unsafe bool HandleSuspended(ref RuntimeAsyncAwaitState state) internal void InstrumentedHandleSuspended(AsyncInstrumentation.Flags flags, ref RuntimeAsyncAwaitState state, Continuation? newContinuation = null) { - if (AsyncDebugger.IsEnabled(flags)) + if (AsyncInstrumentation.IsEnabled.AsyncDebugger(flags)) { Continuation? nextContinuation = t_runtimeAsyncAwaitState.SentinelContinuation!.Next; @@ -854,7 +854,7 @@ private static void InstrumentedFinalizeRuntimeAsyncTask(RuntimeAsyncTask } } - if (AsyncDebugger.IsEnabled(flags)) + if (AsyncInstrumentation.IsEnabled.AsyncDebugger(flags)) { task.NotifyDebuggerOfRuntimeAsyncState(); AsyncDebugger.CreateAsyncContext(task); @@ -1079,7 +1079,7 @@ public static void ResumeRuntimeAsyncContext(Task task, ref AsyncDispatcherInfo AsyncProfiler.ResumeAsyncContext.Resume(ref info); } - if (AsyncDebugger.IsEnabled(flags)) + if (AsyncInstrumentation.IsEnabled.AsyncDebugger(flags)) { AsyncDebugger.ResumeAsyncContext(task.Id); } @@ -1096,7 +1096,7 @@ public static void QueueSuspendedRuntimeAsyncContext(ref AsyncDispatcherInfo inf AsyncProfiler.SuspendAsyncContext.Suspend(ref info, nextContinuation); } - if (AsyncDebugger.IsEnabled(flags)) + if (AsyncInstrumentation.IsEnabled.AsyncDebugger(flags)) { AsyncDebugger.SuspendAsyncContext(ref info, curContinuation); } @@ -1114,7 +1114,7 @@ public static void AwaitSuspendedRuntimeAsyncContext(ref AsyncDispatcherInfo inf AsyncProfiler.SuspendAsyncContext.Suspend(ref info, nextContinuation != null ? nextContinuation : newContinuation); } - if (AsyncDebugger.IsEnabled(flags)) + if (AsyncInstrumentation.IsEnabled.AsyncDebugger(flags)) { AsyncDebugger.SuspendAsyncContext(curContinuation, newContinuation); } @@ -1131,7 +1131,7 @@ public static void CompleteRuntimeAsyncContext(ref AsyncDispatcherInfo info, Asy AsyncProfiler.CompleteAsyncContext.Complete(ref info.AsyncProfilerInfo); } - if (AsyncDebugger.IsEnabled(flags)) + if (AsyncInstrumentation.IsEnabled.AsyncDebugger(flags)) { AsyncDebugger.CompleteAsyncContext(info.CurrentTask); } @@ -1152,7 +1152,7 @@ public static void UnwindRuntimeAsyncMethodUnhandledException(ref AsyncDispatche AsyncProfiler.AsyncMethodException.Unhandled(ref info.AsyncProfilerInfo, unwindedFrames); } - if (AsyncDebugger.IsEnabled(flags)) + if (AsyncInstrumentation.IsEnabled.AsyncDebugger(flags)) { AsyncDebugger.AsyncMethodUnhandledException(info.CurrentTask, ex, curContinuation); } @@ -1173,7 +1173,7 @@ public static void UnwindRuntimeAsyncMethodHandledException(ref AsyncDispatcherI AsyncProfiler.AsyncMethodException.Handled(ref info.AsyncProfilerInfo, unwindedFrames); } - if (AsyncDebugger.IsEnabled(flags)) + if (AsyncInstrumentation.IsEnabled.AsyncDebugger(flags)) { AsyncDebugger.AsyncMethodHandledException(curContinuation, unwindedFrames); } @@ -1190,7 +1190,7 @@ public static void ResumeRuntimeAsyncMethod(ref AsyncDispatcherInfo info, AsyncI AsyncProfiler.ResumeAsyncMethod.Resume(ref info.AsyncProfilerInfo); } - if (AsyncDebugger.IsEnabled(flags)) + if (AsyncInstrumentation.IsEnabled.AsyncDebugger(flags)) { AsyncDebugger.ResumeAsyncMethod(ref info, curContinuation); } @@ -1212,7 +1212,7 @@ public static void CompleteRuntimeAsyncMethod(ref AsyncDispatcherInfo info, Asyn AsyncProfiler.CompleteAsyncMethod.Complete(ref info.AsyncProfilerInfo); } - if (AsyncDebugger.IsEnabled(flags)) + if (AsyncInstrumentation.IsEnabled.AsyncDebugger(flags)) { AsyncDebugger.CompleteAsyncMethod(curContinuation); } @@ -1236,8 +1236,6 @@ public static void CompleteRuntimeAsyncMethod(ref AsyncDispatcherInfo info, Asyn internal static class AsyncDebugger { - public static bool IsEnabled(AsyncInstrumentation.Flags flags) => AsyncInstrumentation.IsEnabled.AsyncDebugger(flags); - public static void CreateAsyncContext(Task task) { Task.AddToActiveTasks(task); 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 df0e271c873389..ce1cb548c18a75 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 @@ -75,7 +75,7 @@ public static void UpdateAsyncProfilerFlags(Flags asyncProfilerFlags) lock (s_lock) { s_asyncProfilerActiveFlags = asyncProfilerFlags; - UpdateFlags(true); + s_activeFlags |= Flags.Synchronize; } } @@ -89,7 +89,7 @@ public static void UpdateAsyncDebuggerFlags(Flags asyncDebuggerFlags) lock (s_lock) { s_asyncDebuggerActiveFlags = asyncDebuggerFlags; - UpdateFlags(true); + s_activeFlags |= Flags.Synchronize; } } @@ -100,32 +100,18 @@ private static Flags SynchronizeFlags() lock (s_lock) { - return UpdateFlags(false); - } - } - - private static Flags UpdateFlags(bool setSynchronizeFlag) - { - if (!IsEnabled.AsyncDebugger(s_internalAsyncDebuggerActiveFlags) && Task.s_asyncDebuggingEnabled) - { - s_internalAsyncDebuggerActiveFlags = DefaultFlags | Flags.AsyncDebugger; - } - else if (IsEnabled.AsyncDebugger(s_internalAsyncDebuggerActiveFlags) && !Task.s_asyncDebuggingEnabled) - { - s_internalAsyncDebuggerActiveFlags = Flags.Disabled; - } - - s_activeFlags = s_asyncProfilerActiveFlags | s_asyncDebuggerActiveFlags | s_internalAsyncDebuggerActiveFlags; - if (setSynchronizeFlag) - { - s_activeFlags |= Flags.Synchronize; - } - else - { - s_activeFlags &= ~Flags.Synchronize; + if (!IsEnabled.AsyncDebugger(s_internalAsyncDebuggerActiveFlags) && Task.s_asyncDebuggingEnabled) + { + s_internalAsyncDebuggerActiveFlags = DefaultFlags | Flags.AsyncDebugger; + } + else if (IsEnabled.AsyncDebugger(s_internalAsyncDebuggerActiveFlags) && !Task.s_asyncDebuggingEnabled) + { + s_internalAsyncDebuggerActiveFlags = Flags.Disabled; + } + + s_activeFlags = (s_asyncProfilerActiveFlags | s_asyncDebuggerActiveFlags | s_internalAsyncDebuggerActiveFlags) & ~Flags.Synchronize; + return s_activeFlags; } - - return s_activeFlags; } private static Flags s_activeFlags = Flags.Synchronize; From 00f01a83c489ddf8dbae75bf00d8edc3be691959 Mon Sep 17 00:00:00 2001 From: lateralusX Date: Thu, 23 Apr 2026 16:03:29 +0200 Subject: [PATCH 11/22] Review feedback. --- .../Runtime/CompilerServices/AsyncProfiler.cs | 42 +++++++++++-------- .../AsyncProfilerBufferedEventSource.cs | 4 +- 2 files changed, 26 insertions(+), 20 deletions(-) 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 index 5f06d772e4f347..c3df3690c6e39f 100644 --- a/src/libraries/System.Private.CoreLib/src/System/Runtime/CompilerServices/AsyncProfiler.cs +++ b/src/libraries/System.Private.CoreLib/src/System/Runtime/CompilerServices/AsyncProfiler.cs @@ -143,6 +143,7 @@ public static void EmitAsyncProfilerMetadataIfNeeded(AsyncThreadContext context) } s_metadataRevision = Revision; + s_lastSyncClockEventTimestamp = Stopwatch.GetTimestamp(); } } } @@ -164,25 +165,28 @@ public static void EmitSyncClockEventIfNeeded() s_lastSyncClockEventTimestamp = currentTimestamp; - // SyncClock payload: - // [qpcSync (compressed uint64)] - // [utcSync (compressed uint64)] - int maxEventPayloadSize = EventBuffer.Serializer.MaxCompressedUInt64Size + EventBuffer.Serializer.MaxCompressedUInt64Size; + if (IsEnabled.AnyAsyncEvents(ActiveEventKeywords)) + { + AsyncThreadContext transientContext = AsyncThreadContext.AcquireTransient(); - AsyncThreadContext transientContext = AsyncThreadContext.AcquireTransient(); + // SyncClock payload: + // [qpcSync (compressed uint64)] + // [utcSync (compressed uint64)] + int maxEventPayloadSize = EventBuffer.Serializer.MaxCompressedUInt64Size + EventBuffer.Serializer.MaxCompressedUInt64Size; - ref EventBuffer eventBuffer = ref transientContext.EventBuffer; - if (EventBuffer.Serializer.AsyncEventHeader(transientContext, ref eventBuffer, AsyncEventID.AsyncProfilerSyncClock, maxEventPayloadSize) >= 0) - { - SyncClock(out long utcTimeSync, out long qpcSync); - EventBuffer.Serializer.WriteCompressedUInt64(eventBuffer.Data, ref eventBuffer.Index, (ulong)qpcSync); - EventBuffer.Serializer.WriteCompressedUInt64(eventBuffer.Data, ref eventBuffer.Index, (ulong)utcTimeSync); + ref EventBuffer eventBuffer = ref transientContext.EventBuffer; + if (EventBuffer.Serializer.AsyncEventHeader(transientContext, ref eventBuffer, AsyncEventID.AsyncProfilerSyncClock, maxEventPayloadSize) >= 0) + { + SyncClock(out long utcTimeSync, out long qpcSync); + EventBuffer.Serializer.WriteCompressedUInt64(eventBuffer.Data, ref eventBuffer.Index, (ulong)qpcSync); + EventBuffer.Serializer.WriteCompressedUInt64(eventBuffer.Data, ref eventBuffer.Index, (ulong)utcTimeSync); - // Force flush to deliver event promptly. - transientContext.Flush(); - } + // Force flush to deliver event promptly. + transientContext.Flush(); + } - AsyncThreadContext.Release(transientContext); + AsyncThreadContext.Release(transientContext); + } } private static void SyncClock(out long utcTimeSync, out long qpcSync) @@ -594,6 +598,8 @@ public static AsyncThreadContext AcquireTransient() else { context = new AsyncThreadContext(); + context.ConfigRevision = Config.Revision; + context.ActiveEventKeywords = Config.ActiveEventKeywords; } context.InUse = true; @@ -656,13 +662,13 @@ public void Flush() { Debug.Assert(InUse || BlockContext); - ref EventBuffer eventBuffer = ref EventBuffer; - - if (eventBuffer.EventCount == 0) + if (_eventBuffer.EventCount == 0) { return; } + ref EventBuffer eventBuffer = ref EventBuffer; + int index = 1; // Skip version // Fill in total size in header before flushing. diff --git a/src/libraries/System.Private.CoreLib/src/System/Runtime/CompilerServices/AsyncProfilerBufferedEventSource.cs b/src/libraries/System.Private.CoreLib/src/System/Runtime/CompilerServices/AsyncProfilerBufferedEventSource.cs index 6db30fa99dbb02..b45f31df12ab62 100644 --- a/src/libraries/System.Private.CoreLib/src/System/Runtime/CompilerServices/AsyncProfilerBufferedEventSource.cs +++ b/src/libraries/System.Private.CoreLib/src/System/Runtime/CompilerServices/AsyncProfilerBufferedEventSource.cs @@ -78,7 +78,7 @@ public void AsyncEvents(ReadOnlySpan buffer) eventPayload[0].DataPointer = ((IntPtr)(&length)); eventPayload[0].Reserved = 0; eventPayload[1].Size = sizeof(byte) * length; - eventPayload[1].DataPointer = ((IntPtr)pBuffer); + eventPayload[1].DataPointer = length != 0 ? ((IntPtr)pBuffer) : ((IntPtr)(&length)); eventPayload[1].Reserved = 0; WriteEventCore(ASYNC_EVENTS_ID, 2, eventPayload); } @@ -86,7 +86,7 @@ public void AsyncEvents(ReadOnlySpan buffer) } /// - /// Get callbacks when the ETW sends us commands` + /// Get callbacks when the ETW sends us commands /// protected override void OnEventCommand(EventCommandEventArgs command) { From ebf4173e2884f3c52b16e66bf8f319cc696011bb Mon Sep 17 00:00:00 2001 From: lateralusX Date: Thu, 23 Apr 2026 16:30:07 +0200 Subject: [PATCH 12/22] Harden order sensetive tests. --- .../AsyncProfilerTests.cs | 36 +++++++++++++------ 1 file changed, 26 insertions(+), 10 deletions(-) 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 index 8c6891ae9957ed..c90cf125265ecb 100644 --- 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 @@ -437,6 +437,21 @@ private static List CollectAsyncEventIds(ConcurrentQueue 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(); @@ -802,13 +817,13 @@ public void RuntimeAsync_EventSequenceOrder() // DumpCollectedEvents(events); - var eventIds = CollectAsyncEventIds(events); - var coreEvents = eventIds.FindAll(id => id == AsyncEventID.ResumeAsyncContext || id == AsyncEventID.SuspendAsyncContext || id == AsyncEventID.CompleteAsyncContext); + 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]); - Assert.Equal(AsyncEventID.SuspendAsyncContext, coreEvents[1]); - Assert.Equal(AsyncEventID.ResumeAsyncContext, coreEvents[2]); - Assert.Equal(AsyncEventID.CompleteAsyncContext, coreEvents[3]); + 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(AsyncProfilerTests), nameof(IsRuntimeAsyncSupported))] @@ -999,21 +1014,22 @@ public void RuntimeAsync_CreateCallstackPrecedesResumeCallstack() // DumpCollectedEvents(events); - // Collect all callstack events with their task IDs in order. - var callstackEvents = new List<(AsyncEventID EventId, ulong TaskId)>(); + // 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, ReadOnlySpan buf, ref int idx) => + 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)); + 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(); From 512771f5c8839d15b8f95af405978067c952848b Mon Sep 17 00:00:00 2001 From: lateralusX Date: Fri, 24 Apr 2026 13:55:18 +0200 Subject: [PATCH 13/22] Review feedback. --- .../CompilerServices/AsyncProfiler.CoreCLR.cs | 20 ++++--- .../BasicEventSourceTest/TestUtilities.cs | 2 +- .../System.Private.CoreLib.Shared.projitems | 2 +- .../CompilerServices/AsyncInstrumentation.cs | 31 ++-------- .../Runtime/CompilerServices/AsyncProfiler.cs | 59 ++----------------- ...tSource.cs => AsyncProfilerEventSource.cs} | 6 +- .../AsyncProfilerTests.cs | 4 +- 7 files changed, 28 insertions(+), 96 deletions(-) rename src/libraries/System.Private.CoreLib/src/System/Runtime/CompilerServices/{AsyncProfilerBufferedEventSource.cs => AsyncProfilerEventSource.cs} (93%) 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 index 67dba17fdfb95f..8c5e370b2720f1 100644 --- 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 @@ -4,7 +4,7 @@ using System.Buffers; using System.Diagnostics; using System.Diagnostics.Tracing; -using static System.Runtime.CompilerServices.AsyncProfilerBufferedEventSource; +using static System.Runtime.CompilerServices.AsyncProfilerEventSource; namespace System.Runtime.CompilerServices { @@ -423,7 +423,7 @@ public struct CaptureRuntimeAsyncCallstackState public byte Count; } - public static bool CaptureRuntimeAsyncCallstack(Span buffer, ref int index, ref CaptureRuntimeAsyncCallstackState state) + public static bool CaptureRuntimeAsyncCallstack(byte[] buffer, ref int index, ref CaptureRuntimeAsyncCallstackState state) { if (index > buffer.Length) { @@ -521,16 +521,20 @@ public static void EmitEvent(AsyncThreadContext context, long currentTimestamp, { EventBuffer.Serializer.CallstackHeader(ref eventBuffer, id, type, 0); - Span inlineEventBuffer = eventBuffer.Data.AsSpan(eventBuffer.Index); - int index = 0; + byte[] buffer = eventBuffer.Data; + int startIndex = eventBuffer.Index; + int currentIndex = startIndex; - if (!CaptureRuntimeAsyncCallstack(inlineEventBuffer, ref index, ref state)) + if (!CaptureRuntimeAsyncCallstack(buffer, ref currentIndex, ref state)) { byte[]? rentedArray = RentArray(maxCallstackBytes); if (rentedArray != null) { - inlineEventBuffer.Slice(0, index).CopyTo(rentedArray); - CaptureRuntimeAsyncCallstack(rentedArray.AsSpan(0, maxCallstackBytes), ref index, ref state); + int length = currentIndex - startIndex; + int index = length; + + Buffer.BlockCopy(buffer, startIndex, rentedArray, 0, length); + CaptureRuntimeAsyncCallstack(rentedArray, ref index, ref state); // Remove async event header from the event buffer before flushing. EventBuffer.Serializer.RemoveAsyncEventHeader(context, savedAsyncEventHeaderIndex); @@ -556,7 +560,7 @@ public static void EmitEvent(AsyncThreadContext context, long currentTimestamp, { // Patch frame count in the event buffer. eventBuffer.Data[eventBuffer.Index - 1] = state.Count; - eventBuffer.Index += index; + eventBuffer.Index += currentIndex - startIndex; } } } diff --git a/src/libraries/System.Diagnostics.Tracing/tests/BasicEventSourceTest/TestUtilities.cs b/src/libraries/System.Diagnostics.Tracing/tests/BasicEventSourceTest/TestUtilities.cs index f07a9ab5b8fd04..14a8b96a6a3d25 100644 --- a/src/libraries/System.Diagnostics.Tracing/tests/BasicEventSourceTest/TestUtilities.cs +++ b/src/libraries/System.Diagnostics.Tracing/tests/BasicEventSourceTest/TestUtilities.cs @@ -33,7 +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.AsyncProfilerBufferedEventSource" && + 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 41bed9448017b0..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 @@ -943,7 +943,7 @@ - + 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 ce1cb548c18a75..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 @@ -79,37 +79,20 @@ public static void UpdateAsyncProfilerFlags(Flags asyncProfilerFlags) } } - public static void UpdateAsyncDebuggerFlags(Flags asyncDebuggerFlags) - { - if (asyncDebuggerFlags != Flags.Disabled) - { - asyncDebuggerFlags |= Flags.AsyncDebugger; - } - - lock (s_lock) - { - s_asyncDebuggerActiveFlags = asyncDebuggerFlags; - s_activeFlags |= Flags.Synchronize; - } - } - private static Flags SynchronizeFlags() { _ = TplEventSource.Log; // Touch TplEventSource to trigger static constructor which will initialize TPL flags if EventSource is supported. - _ = AsyncProfilerBufferedEventSource.Log; // Touch AsyncProfilerBufferedEventSource to trigger static constructor which will initialize async profiler 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 (!IsEnabled.AsyncDebugger(s_internalAsyncDebuggerActiveFlags) && Task.s_asyncDebuggingEnabled) + Flags asyncDebuggerActiveFlags = Flags.Disabled; + if (Task.s_asyncDebuggingEnabled) { - s_internalAsyncDebuggerActiveFlags = DefaultFlags | Flags.AsyncDebugger; - } - else if (IsEnabled.AsyncDebugger(s_internalAsyncDebuggerActiveFlags) && !Task.s_asyncDebuggingEnabled) - { - s_internalAsyncDebuggerActiveFlags = Flags.Disabled; + asyncDebuggerActiveFlags = DefaultFlags | Flags.AsyncDebugger; } - s_activeFlags = (s_asyncProfilerActiveFlags | s_asyncDebuggerActiveFlags | s_internalAsyncDebuggerActiveFlags) & ~Flags.Synchronize; + s_activeFlags = (s_asyncProfilerActiveFlags | asyncDebuggerActiveFlags) & ~Flags.Synchronize; return s_activeFlags; } } @@ -118,10 +101,6 @@ private static Flags SynchronizeFlags() private static Flags s_asyncProfilerActiveFlags; - private static Flags s_asyncDebuggerActiveFlags; - - private static Flags s_internalAsyncDebuggerActiveFlags; - 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 index c3df3690c6e39f..69e127a25619ad 100644 --- a/src/libraries/System.Private.CoreLib/src/System/Runtime/CompilerServices/AsyncProfiler.cs +++ b/src/libraries/System.Private.CoreLib/src/System/Runtime/CompilerServices/AsyncProfiler.cs @@ -7,7 +7,7 @@ using System.Diagnostics.Tracing; using System.Runtime.InteropServices; using System.Threading; -using static System.Runtime.CompilerServices.AsyncProfilerBufferedEventSource; +using static System.Runtime.CompilerServices.AsyncProfilerEventSource; namespace System.Runtime.CompilerServices { @@ -73,7 +73,7 @@ public static void Update(EventLevel logLevel, EventKeywords eventKeywords) ActiveEventKeywords = eventKeywords; } - string? eventBufferSizeEnv = System.Environment.GetEnvironmentVariable("DOTNET_AsyncProfilerBufferedEventSource_EventBufferSize"); + string? eventBufferSizeEnv = System.Environment.GetEnvironmentVariable("DOTNET_AsyncProfilerEventSource_EventBufferSize"); if (eventBufferSizeEnv != null && uint.TryParse(eventBufferSizeEnv, out uint eventBufferSize)) { eventBufferSize = Math.Max(eventBufferSize, 1024); @@ -271,11 +271,6 @@ public static class Serializer public const int MaxCompressedInt64Size = 10; public const int HeaderSize = 37; - [MethodImpl(MethodImplOptions.AggressiveInlining)] - public static void WriteInt32(Span buffer, ref int index, int value) - { - WriteUInt32(buffer, ref index, (uint)value); - } [MethodImpl(MethodImplOptions.AggressiveInlining)] public static void WriteInt32(byte[] buffer, ref int index, int value) @@ -283,25 +278,12 @@ public static void WriteInt32(byte[] buffer, ref int index, int value) WriteUInt32(buffer, ref index, (uint)value); } - [MethodImpl(MethodImplOptions.AggressiveInlining)] - public static void WriteCompressedInt32(Span buffer, ref int index, int value) - { - WriteCompressedUInt32(buffer, ref index, ZigzagEncodeInt32(value)); - } - [MethodImpl(MethodImplOptions.AggressiveInlining)] public static void WriteCompressedInt32(byte[] buffer, ref int index, int value) { WriteCompressedUInt32(buffer, ref index, ZigzagEncodeInt32(value)); } - [MethodImpl(MethodImplOptions.AggressiveInlining)] - public static void WriteUInt32(Span buffer, ref int index, uint value) - { - Debug.Assert((uint)index <= (uint)(buffer.Length - sizeof(uint))); - WriteUInt32(ref MemoryMarshal.GetReference(buffer), ref index, value); - } - [MethodImpl(MethodImplOptions.AggressiveInlining)] public static void WriteUInt32(byte[] buffer, ref int index, uint value) { @@ -319,13 +301,6 @@ private static void WriteUInt32(ref byte buffer, ref int index, uint value) index += 4; } - [MethodImpl(MethodImplOptions.AggressiveInlining)] - public static void WriteCompressedUInt32(Span buffer, ref int index, uint value) - { - Debug.Assert((uint)index <= (uint)(buffer.Length - MaxCompressedUInt32Size)); - WriteCompressedUInt32(ref MemoryMarshal.GetReference(buffer), ref index, value); - } - [MethodImpl(MethodImplOptions.AggressiveInlining)] public static void WriteCompressedUInt32(byte[] buffer, ref int index, uint value) { @@ -344,37 +319,18 @@ private static void WriteCompressedUInt32(ref byte buffer, ref int index, uint v Unsafe.Add(ref buffer, index++) = (byte)value; } - [MethodImpl(MethodImplOptions.AggressiveInlining)] - public static void WriteInt64(Span buffer, ref int index, long value) - { - WriteUInt64(buffer, ref index, (ulong)value); - } - [MethodImpl(MethodImplOptions.AggressiveInlining)] public static void WriteInt64(byte[] buffer, ref int index, long value) { WriteUInt64(buffer, ref index, (ulong)value); } - [MethodImpl(MethodImplOptions.AggressiveInlining)] - public static void WriteCompressedInt64(Span buffer, ref int index, long value) - { - WriteCompressedUInt64(buffer, ref index, ZigzagEncodeInt64(value)); - } - [MethodImpl(MethodImplOptions.AggressiveInlining)] public static void WriteCompressedInt64(byte[] buffer, ref int index, long value) { WriteCompressedUInt64(buffer, ref index, ZigzagEncodeInt64(value)); } - [MethodImpl(MethodImplOptions.AggressiveInlining)] - public static void WriteUInt64(Span buffer, ref int index, ulong value) - { - Debug.Assert((uint)index <= (uint)(buffer.Length - sizeof(ulong))); - WriteUInt64(ref MemoryMarshal.GetReference(buffer), ref index, value); - } - [MethodImpl(MethodImplOptions.AggressiveInlining)] public static void WriteUInt64(byte[] buffer, ref int index, ulong value) { @@ -392,13 +348,6 @@ private static void WriteUInt64(ref byte buffer, ref int index, ulong value) index += sizeof(ulong); } - [MethodImpl(MethodImplOptions.AggressiveInlining)] - public static void WriteCompressedUInt64(Span buffer, ref int index, ulong value) - { - Debug.Assert((uint)index <= (uint)(buffer.Length - MaxCompressedUInt64Size)); - WriteCompressedUInt64(ref MemoryMarshal.GetReference(buffer), ref index, value); - } - [MethodImpl(MethodImplOptions.AggressiveInlining)] public static void WriteCompressedUInt64(byte[] buffer, ref int index, ulong value) { @@ -521,10 +470,10 @@ public static void CallstackHeader(ref EventBuffer eventBuffer, ulong id, AsyncC } [MethodImpl(MethodImplOptions.AggressiveInlining)] - public static void CallstackData(ref EventBuffer eventBuffer, ReadOnlySpan callstackData, int callstackDataByteCount) + public static void CallstackData(ref EventBuffer eventBuffer, byte[] callstackData, int callstackDataByteCount) { ref int index = ref eventBuffer.Index; - Unsafe.CopyBlockUnaligned(ref Unsafe.Add(ref MemoryMarshal.GetArrayDataReference(eventBuffer.Data), index), ref MemoryMarshal.GetReference(callstackData), (uint)callstackDataByteCount); + Buffer.BlockCopy(callstackData, 0, eventBuffer.Data, index, callstackDataByteCount); index += callstackDataByteCount; } } diff --git a/src/libraries/System.Private.CoreLib/src/System/Runtime/CompilerServices/AsyncProfilerBufferedEventSource.cs b/src/libraries/System.Private.CoreLib/src/System/Runtime/CompilerServices/AsyncProfilerEventSource.cs similarity index 93% rename from src/libraries/System.Private.CoreLib/src/System/Runtime/CompilerServices/AsyncProfilerBufferedEventSource.cs rename to src/libraries/System.Private.CoreLib/src/System/Runtime/CompilerServices/AsyncProfilerEventSource.cs index b45f31df12ab62..a30add55a5a4d6 100644 --- a/src/libraries/System.Private.CoreLib/src/System/Runtime/CompilerServices/AsyncProfilerBufferedEventSource.cs +++ b/src/libraries/System.Private.CoreLib/src/System/Runtime/CompilerServices/AsyncProfilerEventSource.cs @@ -8,14 +8,14 @@ namespace System.Runtime.CompilerServices { /// Provides an event source for tracing async execution. [EventSource( - Name = "System.Runtime.CompilerServices.AsyncProfilerBufferedEventSource", + Name = "System.Runtime.CompilerServices.AsyncProfilerEventSource", Guid = "742BD0FE-9200-4DE2-9059-576F35EBEB62" )] - internal sealed partial class AsyncProfilerBufferedEventSource : EventSource + internal sealed partial class AsyncProfilerEventSource : EventSource { private const string EventSourceSuppressMessage = "Parameters to this method are primitive and are trimmer safe"; - public static readonly AsyncProfilerBufferedEventSource Log = new AsyncProfilerBufferedEventSource(); + public static readonly AsyncProfilerEventSource Log = new AsyncProfilerEventSource(); public static class Keywords // this name is important for EventSource { 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 index c90cf125265ecb..3ca0463f6329a4 100644 --- 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 @@ -36,11 +36,11 @@ public class AsyncProfilerTests { private static bool IsRuntimeAsyncSupported => PlatformDetection.IsRuntimeAsyncSupported; - private const string AsyncProfilerEventSourceName = "System.Runtime.CompilerServices.AsyncProfilerBufferedEventSource"; + 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); - // AsyncProfilerBufferedEventSource Keywords matching the event source definition + // 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; From dc898b7d25dc7646258a2887009c58999c77ea0c Mon Sep 17 00:00:00 2001 From: lateralusX Date: Fri, 24 Apr 2026 14:30:18 +0200 Subject: [PATCH 14/22] Review feedback. --- .../Runtime/CompilerServices/AsyncProfiler.CoreCLR.cs | 8 ++++---- .../src/System/Runtime/CompilerServices/AsyncProfiler.cs | 6 +++--- 2 files changed, 7 insertions(+), 7 deletions(-) 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 index 8c5e370b2720f1..eccda83b395dfd 100644 --- 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 @@ -418,14 +418,14 @@ private static partial class AsyncCallstack { public struct CaptureRuntimeAsyncCallstackState { - public Continuation Continuation; + 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) + if (index > buffer.Length || state.Continuation == null) { return false; } @@ -459,7 +459,7 @@ public static bool CaptureRuntimeAsyncCallstack(byte[] buffer, ref int index, re EventBuffer.Serializer.WriteCompressedInt32(buffer, ref index, state.Continuation.State); state.Count++; - state.Continuation = state.Continuation.Next!; + state.Continuation = state.Continuation.Next; while (state.Count < maxAsyncCallstackLength && state.Continuation != null) { previousNativeIP = currentNativeIP; @@ -473,7 +473,7 @@ public static bool CaptureRuntimeAsyncCallstack(byte[] buffer, ref int index, re EventBuffer.Serializer.WriteCompressedInt32(buffer, ref index, state.Continuation.State); state.Count++; - state.Continuation = state.Continuation.Next!; + state.Continuation = state.Continuation.Next; } state.LastNativeIP = currentNativeIP; 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 index 69e127a25619ad..b171be29db04c1 100644 --- a/src/libraries/System.Private.CoreLib/src/System/Runtime/CompilerServices/AsyncProfiler.cs +++ b/src/libraries/System.Private.CoreLib/src/System/Runtime/CompilerServices/AsyncProfiler.cs @@ -140,10 +140,10 @@ public static void EmitAsyncProfilerMetadataIfNeeded(AsyncThreadContext context) // Force flush to deliver event promptly. context.Flush(); - } - s_metadataRevision = Revision; - s_lastSyncClockEventTimestamp = Stopwatch.GetTimestamp(); + s_metadataRevision = Revision; + s_lastSyncClockEventTimestamp = Stopwatch.GetTimestamp(); + } } } } From b16595d88e3296c6c79bc0091eb72a5948bf4501 Mon Sep 17 00:00:00 2001 From: lateralusX Date: Mon, 27 Apr 2026 19:56:04 +0200 Subject: [PATCH 15/22] Reduce Unsafe code in Serializer. --- .../CompilerServices/AsyncProfiler.CoreCLR.cs | 89 +++-- .../Runtime/CompilerServices/AsyncProfiler.cs | 306 ++++++++++-------- .../AsyncProfilerTests.cs | 14 +- 3 files changed, 238 insertions(+), 171 deletions(-) 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 index eccda83b395dfd..15def3ea5eed74 100644 --- 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 @@ -4,7 +4,7 @@ using System.Buffers; using System.Diagnostics; using System.Diagnostics.Tracing; -using static System.Runtime.CompilerServices.AsyncProfilerEventSource; +using Serializer = System.Runtime.CompilerServices.AsyncProfiler.EventBuffer.Serializer; namespace System.Runtime.CompilerServices { @@ -416,6 +416,8 @@ private static unsafe void ResumeRuntimeAsyncCallstacks(AsyncDispatcherInfo* inf private static partial class AsyncCallstack { + private const int MaxAsyncMethodFrameSize = Serializer.MaxCompressedUInt64Size + Serializer.MaxCompressedUInt32Size; + public struct CaptureRuntimeAsyncCallstackState { public Continuation? Continuation; @@ -430,8 +432,8 @@ public static bool CaptureRuntimeAsyncCallstack(byte[] buffer, ref int index, re return false; } - byte maxAsyncCallstackLength = (byte)Math.Min(byte.MaxValue, (buffer.Length - index) / ASYNC_METHOD_INFO_SIZE); - if (maxAsyncCallstackLength == 0) + byte maxAsyncCallstackFrames = (byte)Math.Min(byte.MaxValue, (buffer.Length - index) / MaxAsyncMethodFrameSize); + if (maxAsyncCallstackFrames == 0) { return false; } @@ -444,23 +446,31 @@ public static bool CaptureRuntimeAsyncCallstack(byte[] buffer, ref int index, re currentNativeIP = (ulong)state.Continuation.ResumeInfo->DiagnosticIP; } + Span callstackSpan = buffer.AsSpan(index, maxAsyncCallstackFrames * MaxAsyncMethodFrameSize); + int callstackSpanIndex = 0; + + Span frameSpan = callstackSpan.Slice(callstackSpanIndex, MaxAsyncMethodFrameSize); + int frameSpanIndex = 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) { - EventBuffer.Serializer.WriteCompressedUInt64(buffer, ref index, currentNativeIP); + frameSpanIndex += Serializer.WriteCompressedUInt64(frameSpan, frameSpanIndex, currentNativeIP); } else { - EventBuffer.Serializer.WriteCompressedInt64(buffer, ref index, (long)(currentNativeIP - previousNativeIP)); + frameSpanIndex += Serializer.WriteCompressedInt64(frameSpan, frameSpanIndex, (long)(currentNativeIP - previousNativeIP)); } - EventBuffer.Serializer.WriteCompressedInt32(buffer, ref index, state.Continuation.State); + frameSpanIndex += Serializer.WriteCompressedInt32(frameSpan, frameSpanIndex, state.Continuation.State); + + callstackSpanIndex += frameSpanIndex; state.Count++; state.Continuation = state.Continuation.Next; - while (state.Count < maxAsyncCallstackLength && state.Continuation != null) + while (state.Count < maxAsyncCallstackFrames && state.Continuation != null) { previousNativeIP = currentNativeIP; @@ -469,14 +479,20 @@ public static bool CaptureRuntimeAsyncCallstack(byte[] buffer, ref int index, re currentNativeIP = (ulong)state.Continuation.ResumeInfo->DiagnosticIP; } - EventBuffer.Serializer.WriteCompressedInt64(buffer, ref index, (long)(currentNativeIP - previousNativeIP)); - EventBuffer.Serializer.WriteCompressedInt32(buffer, ref index, state.Continuation.State); + frameSpan = callstackSpan.Slice(callstackSpanIndex, MaxAsyncMethodFrameSize); + frameSpanIndex = 0; + + frameSpanIndex += Serializer.WriteCompressedInt64(frameSpan, frameSpanIndex, (long)(currentNativeIP - previousNativeIP)); + frameSpanIndex += Serializer.WriteCompressedInt32(frameSpan, frameSpanIndex, state.Continuation.State); + + callstackSpanIndex += frameSpanIndex; state.Count++; state.Continuation = state.Continuation.Next; } state.LastNativeIP = currentNativeIP; + index += callstackSpanIndex; return state.Continuation == null || state.Count == byte.MaxValue; } @@ -507,19 +523,19 @@ public static void EmitEvent(AsyncThreadContext context, long currentTimestamp, // Max callstack data that can fit in the buffer after flush. int maxCallstackBytes = Math.Min( - byte.MaxValue * ASYNC_METHOD_INFO_SIZE, + byte.MaxValue * MaxAsyncMethodFrameSize, eventBuffer.Data.Length); CaptureRuntimeAsyncCallstackState state = default; state.Continuation = asyncCallstack; - // Callstack envelope: id (max 10 bytes compressed) + type (1) + callstackId (1) + frameCount (1) - const int maxEnvelopeSize = EventBuffer.Serializer.MaxCompressedUInt64Size + sizeof(byte) + sizeof(byte) + sizeof(byte); + // 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; - int savedAsyncEventHeaderIndex = EventBuffer.Serializer.AsyncEventHeader(context, ref eventBuffer, currentTimestamp, delta, eventID, maxEnvelopeSize); + int savedAsyncEventHeaderIndex = Serializer.AsyncEventHeader(context, ref eventBuffer, currentTimestamp, delta, eventID, MaxStaticEventPayloadSize); if (savedAsyncEventHeaderIndex >= 0) { - EventBuffer.Serializer.CallstackHeader(ref eventBuffer, id, type, 0); + int frameCountOffset = CallstackHeader(ref eventBuffer, id, type, 0); byte[] buffer = eventBuffer.Data; int startIndex = eventBuffer.Index; @@ -537,15 +553,15 @@ public static void EmitEvent(AsyncThreadContext context, long currentTimestamp, CaptureRuntimeAsyncCallstack(rentedArray, ref index, ref state); // Remove async event header from the event buffer before flushing. - EventBuffer.Serializer.RemoveAsyncEventHeader(context, savedAsyncEventHeaderIndex); + Serializer.RemoveAsyncEventHeader(context, savedAsyncEventHeaderIndex); context.Flush(); // Write the callstack again. - savedAsyncEventHeaderIndex = EventBuffer.Serializer.AsyncEventHeader(context, ref eventBuffer, context.LastEventTimestamp, 0, eventID, maxEnvelopeSize + index); + savedAsyncEventHeaderIndex = Serializer.AsyncEventHeader(context, ref eventBuffer, context.LastEventTimestamp, 0, eventID, MaxStaticEventPayloadSize + index); if (savedAsyncEventHeaderIndex >= 0) { - EventBuffer.Serializer.CallstackHeader(ref eventBuffer, id, type, state.Count); - EventBuffer.Serializer.CallstackData(ref eventBuffer, rentedArray, index); + CallstackHeader(ref eventBuffer, id, type, state.Count); + CallstackData(ref eventBuffer, rentedArray, index); } ArrayPool.Shared.Return(rentedArray); @@ -553,19 +569,50 @@ public static void EmitEvent(AsyncThreadContext context, long currentTimestamp, else { // Remove async event header from the event buffer since we can't write the callstack. - EventBuffer.Serializer.RemoveAsyncEventHeader(context, savedAsyncEventHeaderIndex); + Serializer.RemoveAsyncEventHeader(context, savedAsyncEventHeaderIndex); } } else { - // Patch frame count in the event buffer. - eventBuffer.Data[eventBuffer.Index - 1] = state.Count; + // Patch frame count in the event buffer using the offset from CallstackHeader. + eventBuffer.Data[frameCountOffset] = state.Count; eventBuffer.Index += currentIndex - startIndex; } } } } + [MethodImpl(MethodImplOptions.AggressiveInlining)] + 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, spanIndex, id); + eventBuffer.Index += spanIndex; + + return frameCountOffset; + } + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + 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; 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 index b171be29db04c1..31263b936af862 100644 --- a/src/libraries/System.Private.CoreLib/src/System/Runtime/CompilerServices/AsyncProfiler.cs +++ b/src/libraries/System.Private.CoreLib/src/System/Runtime/CompilerServices/AsyncProfiler.cs @@ -8,6 +8,7 @@ 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 { @@ -117,27 +118,39 @@ public static void EmitAsyncProfilerMetadataIfNeeded(AsyncThreadContext context) // [eventBufferSize (compressed uint32)] // [wrapperCount byte] // [wrapperIP0 (compressed uint64)] ... [wrapperIPn (compressed uint64)] - int maxEventPayloadSize = EventBuffer.Serializer.MaxCompressedUInt64Size + EventBuffer.Serializer.MaxCompressedUInt64Size + EventBuffer.Serializer.MaxCompressedUInt64Size + EventBuffer.Serializer.MaxCompressedUInt32Size + 1 + (wrapperIPs.Length * EventBuffer.Serializer.MaxCompressedUInt64Size); + 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 (EventBuffer.Serializer.AsyncEventHeader(context, ref eventBuffer, AsyncEventID.AsyncProfilerMetadata, maxEventPayloadSize) >= 0) + if (Serializer.AsyncEventHeader(context, ref eventBuffer, AsyncEventID.AsyncProfilerMetadata, MaxStaticEventPayloadSize + maxDynamicEventPayloadSize) >= 0) { byte[] buffer = eventBuffer.Data; - ref int index = ref eventBuffer.Index; + int index = eventBuffer.Index; SyncClock(out long utcTimeSync, out long qpcSync); - EventBuffer.Serializer.WriteCompressedUInt64(buffer, ref index, (ulong)Stopwatch.Frequency); - EventBuffer.Serializer.WriteCompressedUInt64(buffer, ref index, (ulong)qpcSync); - EventBuffer.Serializer.WriteCompressedUInt64(buffer, ref index, (ulong)utcTimeSync); - EventBuffer.Serializer.WriteCompressedUInt32(buffer, ref index, EventBufferSize); - Unsafe.Add(ref MemoryMarshal.GetArrayDataReference(buffer), index++) = (byte)wrapperIPs.Length; + Span payloadSpan = buffer.AsSpan(index, MaxStaticEventPayloadSize); + int payloadSpanIndex = 0; + + payloadSpanIndex += Serializer.WriteCompressedUInt64(payloadSpan, payloadSpanIndex, (ulong)Stopwatch.Frequency); + payloadSpanIndex += Serializer.WriteCompressedUInt64(payloadSpan, payloadSpanIndex, (ulong)qpcSync); + payloadSpanIndex += Serializer.WriteCompressedUInt64(payloadSpan, payloadSpanIndex, (ulong)utcTimeSync); + payloadSpanIndex += Serializer.WriteCompressedUInt32(payloadSpan, payloadSpanIndex, EventBufferSize); + + payloadSpan[payloadSpanIndex++] = (byte)wrapperIPs.Length; + + index += payloadSpanIndex; + + payloadSpan = buffer.AsSpan(index, maxDynamicEventPayloadSize); + payloadSpanIndex = 0; for (int i = 0; i < wrapperIPs.Length; i++) { - EventBuffer.Serializer.WriteCompressedUInt64(buffer, ref index, (ulong)wrapperIPs[i]); + payloadSpanIndex += Serializer.WriteCompressedUInt64(payloadSpan, payloadSpanIndex, (ulong)wrapperIPs[i]); } + eventBuffer.Index = index + payloadSpanIndex; + // Force flush to deliver event promptly. context.Flush(); @@ -172,14 +185,20 @@ public static void EmitSyncClockEventIfNeeded() // SyncClock payload: // [qpcSync (compressed uint64)] // [utcSync (compressed uint64)] - int maxEventPayloadSize = EventBuffer.Serializer.MaxCompressedUInt64Size + EventBuffer.Serializer.MaxCompressedUInt64Size; + const int MaxEventPayloadSize = Serializer.MaxCompressedUInt64Size + Serializer.MaxCompressedUInt64Size; ref EventBuffer eventBuffer = ref transientContext.EventBuffer; - if (EventBuffer.Serializer.AsyncEventHeader(transientContext, ref eventBuffer, AsyncEventID.AsyncProfilerSyncClock, maxEventPayloadSize) >= 0) + if (Serializer.AsyncEventHeader(transientContext, ref eventBuffer, AsyncEventID.AsyncProfilerSyncClock, MaxEventPayloadSize) >= 0) { SyncClock(out long utcTimeSync, out long qpcSync); - EventBuffer.Serializer.WriteCompressedUInt64(eventBuffer.Data, ref eventBuffer.Index, (ulong)qpcSync); - EventBuffer.Serializer.WriteCompressedUInt64(eventBuffer.Data, ref eventBuffer.Index, (ulong)utcTimeSync); + + Span payloadSpan = eventBuffer.Data.AsSpan(eventBuffer.Index, MaxEventPayloadSize); + int payloadSpanIndex = 0; + + payloadSpanIndex += Serializer.WriteCompressedUInt64(payloadSpan, payloadSpanIndex, (ulong)qpcSync); + payloadSpanIndex += Serializer.WriteCompressedUInt64(payloadSpan, payloadSpanIndex, (ulong)utcTimeSync); + + eventBuffer.Index += payloadSpanIndex; // Force flush to deliver event promptly. transientContext.Flush(); @@ -269,102 +288,109 @@ public static class Serializer public const int MaxCompressedInt32Size = 5; public const int MaxCompressedUInt64Size = 10; public const int MaxCompressedInt64Size = 10; - public const int HeaderSize = 37; - + public const int MaxEventHeaderSize = 37; + public const int MaxAsyncEventHeaderSize = 11; [MethodImpl(MethodImplOptions.AggressiveInlining)] - public static void WriteInt32(byte[] buffer, ref int index, int value) + public static int WriteCompressedInt32(Span buffer, int index, int value) { - WriteUInt32(buffer, ref index, (uint)value); + return WriteCompressedUInt32(buffer.Slice(index, MaxCompressedUInt32Size), ZigzagEncodeInt32(value)); } [MethodImpl(MethodImplOptions.AggressiveInlining)] - public static void WriteCompressedInt32(byte[] buffer, ref int index, int value) + public static int WriteCompressedInt32(byte[] buffer, int index, int value) { - WriteCompressedUInt32(buffer, ref index, ZigzagEncodeInt32(value)); + return WriteCompressedUInt32(buffer.AsSpan(index, MaxCompressedUInt32Size), ZigzagEncodeInt32(value)); } [MethodImpl(MethodImplOptions.AggressiveInlining)] - public static void WriteUInt32(byte[] buffer, ref int index, uint value) + public static int WriteCompressedInt32(Span buffer, int value) { - Debug.Assert((uint)index <= (uint)(buffer.Length - sizeof(uint))); - WriteUInt32(ref MemoryMarshal.GetArrayDataReference(buffer), ref index, value); + return WriteCompressedUInt32(buffer, ZigzagEncodeInt32(value)); } [MethodImpl(MethodImplOptions.AggressiveInlining)] - private static void WriteUInt32(ref byte buffer, ref int index, uint value) + public static int WriteCompressedUInt32(Span buffer, int index, uint value) { - if (!BitConverter.IsLittleEndian) - value = BinaryPrimitives.ReverseEndianness(value); - - Unsafe.WriteUnaligned(ref Unsafe.Add(ref buffer, index), value); - index += 4; + return WriteCompressedUInt32(buffer.Slice(index, MaxCompressedUInt32Size), value); } [MethodImpl(MethodImplOptions.AggressiveInlining)] - public static void WriteCompressedUInt32(byte[] buffer, ref int index, uint value) + public static int WriteCompressedUInt32(byte[] buffer, int index, uint value) { - Debug.Assert((uint)index <= (uint)(buffer.Length - MaxCompressedUInt32Size)); - WriteCompressedUInt32(ref MemoryMarshal.GetArrayDataReference(buffer), ref index, value); + return WriteCompressedUInt32(buffer.AsSpan(index, MaxCompressedUInt32Size), value); } [MethodImpl(MethodImplOptions.AggressiveInlining)] - private static void WriteCompressedUInt32(ref byte buffer, ref int index, uint value) + 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 buffer, index++) = (byte)((uint)value | ~0x7Fu); + Unsafe.Add(ref dst, index++) = (byte)((uint)value | ~0x7Fu); value >>= 7; } - Unsafe.Add(ref buffer, index++) = (byte)value; + + Unsafe.Add(ref dst, index++) = (byte)value; + return index; } [MethodImpl(MethodImplOptions.AggressiveInlining)] - public static void WriteInt64(byte[] buffer, ref int index, long value) + public static int WriteCompressedInt64(Span buffer, int index, long value) { - WriteUInt64(buffer, ref index, (ulong)value); + return WriteCompressedUInt64(buffer.Slice(index, MaxCompressedUInt64Size), ZigzagEncodeInt64(value)); } [MethodImpl(MethodImplOptions.AggressiveInlining)] - public static void WriteCompressedInt64(byte[] buffer, ref int index, long value) + public static int WriteCompressedInt64(byte[] buffer, int index, long value) { - WriteCompressedUInt64(buffer, ref index, ZigzagEncodeInt64(value)); + return WriteCompressedUInt64(buffer.AsSpan(index, MaxCompressedUInt64Size), ZigzagEncodeInt64(value)); } [MethodImpl(MethodImplOptions.AggressiveInlining)] - public static void WriteUInt64(byte[] buffer, ref int index, ulong value) + public static int WriteCompressedInt64(Span buffer, long value) { - Debug.Assert((uint)index <= (uint)(buffer.Length -sizeof(ulong))); - WriteUInt64(ref MemoryMarshal.GetArrayDataReference(buffer), ref index, value); + return WriteCompressedUInt64(buffer, ZigzagEncodeInt64(value)); } [MethodImpl(MethodImplOptions.AggressiveInlining)] - private static void WriteUInt64(ref byte buffer, ref int index, ulong value) + public static int WriteCompressedUInt64(Span buffer, int index, ulong value) { - if (!BitConverter.IsLittleEndian) - value = BinaryPrimitives.ReverseEndianness(value); - - Unsafe.WriteUnaligned(ref Unsafe.Add(ref buffer, index), value); - index += sizeof(ulong); + return WriteCompressedUInt64(buffer.Slice(index, MaxCompressedUInt64Size), value); } [MethodImpl(MethodImplOptions.AggressiveInlining)] - public static void WriteCompressedUInt64(byte[] buffer, ref int index, ulong value) + public static int WriteCompressedUInt64(byte[] buffer, int index, ulong value) { - Debug.Assert((uint)index <= (uint)(buffer.Length - MaxCompressedUInt64Size)); - WriteCompressedUInt64(ref MemoryMarshal.GetArrayDataReference(buffer), ref index, value); + return WriteCompressedUInt64(buffer.AsSpan(index, MaxCompressedUInt64Size), value); } [MethodImpl(MethodImplOptions.AggressiveInlining)] - private static void WriteCompressedUInt64(ref byte buffer, ref int index, ulong value) + public static int WriteCompressedUInt64(Span destination, ulong value) { + if (destination.Length < MaxCompressedUInt64Size) + { + ThrowHelper.ThrowArgumentOutOfRangeException(ExceptionArgument.length); + } + + ref byte dst = ref MemoryMarshal.GetReference(destination); + int index = 0; + while (value > 0x7Fu) { - Unsafe.Add(ref buffer, index++) = (byte)((uint)value | ~0x7Fu); + Unsafe.Add(ref dst, index++) = (byte)((uint)value | ~0x7Fu); value >>= 7; } - Unsafe.Add(ref buffer, index++) = (byte)value; + Unsafe.Add(ref dst, index++) = (byte)value; + return index; } [MethodImpl(MethodImplOptions.AggressiveInlining)] @@ -375,25 +401,43 @@ private static void WriteCompressedUInt64(ref byte buffer, ref int index, ulong public static void Header(AsyncThreadContext context, ref EventBuffer eventBuffer) { - byte[] buffer = eventBuffer.Data; - ref int index = ref eventBuffer.Index; long currentTimestamp = Stopwatch.GetTimestamp(); - index = 0; + eventBuffer.Index = 0; eventBuffer.EventCount = 0; context.LastEventTimestamp = currentTimestamp; - //Write header to buffer - if (buffer.Length >= HeaderSize) - { - Unsafe.Add(ref MemoryMarshal.GetArrayDataReference(buffer), index++) = 1; // Version - WriteUInt32(buffer, ref index, 0); // Total size in bytes, will be updated on flush. - WriteUInt32(buffer, ref index, context.AsyncThreadContextId); // Async Thread Context ID - WriteUInt64(buffer, ref index, Thread.CurrentOSThreadId); // OS Thread ID - WriteUInt32(buffer, ref index, 0); // Total event count, will be updated on flush. - WriteUInt64(buffer, ref index, (ulong)currentTimestamp); // Start timestamp - WriteUInt64(buffer, ref index, 0); // End timestamp, will be updated on flush. - } + var 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), Thread.CurrentOSThreadId); + 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; } [MethodImpl(MethodImplOptions.AggressiveInlining)] @@ -413,21 +457,19 @@ public static int AsyncEventHeader(AsyncThreadContext context, ref EventBuffer e public static int AsyncEventHeader(AsyncThreadContext context, ref EventBuffer eventBuffer, long currentTimestamp, long delta, AsyncEventID eventID, int maxEventPayloadSize) { - const int maxEventHeaderSize = MaxCompressedUInt64Size + sizeof(byte); - byte[] buffer = eventBuffer.Data; - ref int index = ref eventBuffer.Index; + int index = eventBuffer.Index; int asyncHeaderIndex = index; - if ((index + maxEventHeaderSize + maxEventPayloadSize) <= buffer.Length && delta >= 0) + if ((index + MaxAsyncEventHeaderSize + maxEventPayloadSize) <= buffer.Length && delta >= 0) { context.LastEventTimestamp = currentTimestamp; } else { // Event is too big for buffer, drop it. - if (maxEventHeaderSize + maxEventPayloadSize > buffer.Length) + if (MaxAsyncEventHeaderSize + maxEventPayloadSize > buffer.Length) { return -1; } @@ -435,13 +477,19 @@ public static int AsyncEventHeader(AsyncThreadContext context, ref EventBuffer e context.Flush(); delta = 0; - asyncHeaderIndex = 0; + index = eventBuffer.Index; + asyncHeaderIndex = index; } - WriteCompressedUInt64(buffer, ref index, (ulong)delta); //Timestamp delta from last event - Unsafe.Add(ref MemoryMarshal.GetArrayDataReference(buffer), index++) = (byte)eventID; // eventID + Span headerSpan = buffer.AsSpan(index, MaxAsyncEventHeaderSize); + int headerSpanIndex = 0; + + headerSpan[headerSpanIndex++] = (byte)eventID; // eventID + headerSpanIndex += WriteCompressedUInt64(headerSpan, headerSpanIndex, (ulong)delta); // Timestamp delta from last event + eventBuffer.Index += headerSpanIndex; eventBuffer.EventCount++; + return asyncHeaderIndex; } @@ -453,29 +501,6 @@ public static void RemoveAsyncEventHeader(AsyncThreadContext context, int savedA context.EventBuffer.EventCount--; } } - - [MethodImpl(MethodImplOptions.AggressiveInlining)] - public static void CallstackHeader(ref EventBuffer eventBuffer, ulong id, AsyncCallstackType type, byte callstackFrameCount) - { - byte[] buffer = eventBuffer.Data; - ref int index = ref eventBuffer.Index; - - WriteCompressedUInt64(buffer, ref index, id); - - ref byte dst = ref MemoryMarshal.GetArrayDataReference(buffer); - - Unsafe.Add(ref dst, index++) = (byte)type; - Unsafe.Add(ref dst, index++) = 0; // Reserved callstack ID for future callstack interning. - Unsafe.Add(ref dst, index++) = callstackFrameCount; - } - - [MethodImpl(MethodImplOptions.AggressiveInlining)] - public 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; - } } } @@ -511,7 +536,7 @@ public ref EventBuffer EventBuffer if (_eventBuffer.Data.Length == 0) { _eventBuffer.Data = AllocBuffer(); - EventBuffer.Serializer.Header(this, ref _eventBuffer); + Serializer.Header(this, ref _eventBuffer); } Debug.Assert(InUse || BlockContext); @@ -618,20 +643,24 @@ public void Flush() ref EventBuffer eventBuffer = ref EventBuffer; - int index = 1; // Skip version + Span headerSpan = eventBuffer.Data.AsSpan(0, Serializer.MaxEventHeaderSize); + + int spanIndex = 1; // Skip version // Fill in total size in header before flushing. - EventBuffer.Serializer.WriteUInt32(eventBuffer.Data, ref index, (uint)eventBuffer.Index); + BinaryPrimitives.WriteUInt32LittleEndian(headerSpan.Slice(spanIndex), (uint)eventBuffer.Index); + spanIndex += sizeof(uint); - index += sizeof(uint) + sizeof(ulong); // Skip AsyncThreadContextId and OSThreadId + spanIndex += sizeof(uint) + sizeof(ulong); // Skip AsyncThreadContextId and OSThreadId // Fill in event count in header before flushing. - EventBuffer.Serializer.WriteUInt32(eventBuffer.Data, ref index, eventBuffer.EventCount); + BinaryPrimitives.WriteUInt32LittleEndian(headerSpan.Slice(spanIndex), eventBuffer.EventCount); + spanIndex += sizeof(uint); - index += sizeof(ulong); // Skip start timestamp + spanIndex += sizeof(ulong); // Skip start timestamp // Fill in end timestamp in header before flushing. - EventBuffer.Serializer.WriteUInt64(eventBuffer.Data, ref index, (ulong)LastEventTimestamp); + BinaryPrimitives.WriteUInt64LittleEndian(headerSpan.Slice(spanIndex), (ulong)LastEventTimestamp); try { @@ -642,7 +671,7 @@ public void Flush() // AsyncProfiler can't throw, ignore exception and lose buffer. } - EventBuffer.Serializer.Header(this, ref eventBuffer); + Serializer.Header(this, ref eventBuffer); } private static void EmitEvent(Span buffer) @@ -681,22 +710,22 @@ internal static partial class CreateAsyncContext [MethodImpl(MethodImplOptions.AggressiveInlining)] public static void EmitEvent(AsyncThreadContext context, ulong id) { - const int maxEventPayloadSize = EventBuffer.Serializer.MaxCompressedUInt64Size; + const int MaxEventPayloadSize = Serializer.MaxCompressedUInt64Size; - if (EventBuffer.Serializer.AsyncEventHeader(context, ref context.EventBuffer, AsyncEventID.CreateAsyncContext, maxEventPayloadSize) >= 0) + if (Serializer.AsyncEventHeader(context, ref context.EventBuffer, AsyncEventID.CreateAsyncContext, MaxEventPayloadSize) >= 0) { - EventBuffer.Serializer.WriteCompressedUInt64(context.EventBuffer.Data, ref context.EventBuffer.Index, id); + context.EventBuffer.Index += Serializer.WriteCompressedUInt64(context.EventBuffer.Data, context.EventBuffer.Index, id); } } [MethodImpl(MethodImplOptions.AggressiveInlining)] public static void EmitEvent(AsyncThreadContext context, long currentTimestamp, ulong id) { - const int maxEventPayloadSize = EventBuffer.Serializer.MaxCompressedUInt64Size; + const int MaxEventPayloadSize = Serializer.MaxCompressedUInt64Size; - if (EventBuffer.Serializer.AsyncEventHeader(context, ref context.EventBuffer, currentTimestamp, AsyncEventID.CreateAsyncContext, maxEventPayloadSize) >= 0) + if (Serializer.AsyncEventHeader(context, ref context.EventBuffer, currentTimestamp, AsyncEventID.CreateAsyncContext, MaxEventPayloadSize) >= 0) { - EventBuffer.Serializer.WriteCompressedUInt64(context.EventBuffer.Data, ref context.EventBuffer.Index, id); + context.EventBuffer.Index += Serializer.WriteCompressedUInt64(context.EventBuffer.Data, context.EventBuffer.Index, id); } } } @@ -706,22 +735,22 @@ internal static partial class ResumeAsyncContext [MethodImpl(MethodImplOptions.AggressiveInlining)] public static void EmitEvent(AsyncThreadContext context, ulong id) { - const int maxEventPayloadSize = EventBuffer.Serializer.MaxCompressedUInt64Size; + const int MaxEventPayloadSize = Serializer.MaxCompressedUInt64Size; - if (EventBuffer.Serializer.AsyncEventHeader(context, ref context.EventBuffer, AsyncEventID.ResumeAsyncContext, maxEventPayloadSize) >= 0) + if (Serializer.AsyncEventHeader(context, ref context.EventBuffer, AsyncEventID.ResumeAsyncContext, MaxEventPayloadSize) >= 0) { - EventBuffer.Serializer.WriteCompressedUInt64(context.EventBuffer.Data, ref context.EventBuffer.Index, id); + context.EventBuffer.Index += Serializer.WriteCompressedUInt64(context.EventBuffer.Data, context.EventBuffer.Index, id); } } [MethodImpl(MethodImplOptions.AggressiveInlining)] public static void EmitEvent(AsyncThreadContext context, long currentTimestamp, ulong id) { - const int maxEventPayloadSize = EventBuffer.Serializer.MaxCompressedUInt64Size; + const int MaxEventPayloadSize = Serializer.MaxCompressedUInt64Size; - if (EventBuffer.Serializer.AsyncEventHeader(context, ref context.EventBuffer, currentTimestamp, AsyncEventID.ResumeAsyncContext, maxEventPayloadSize) >= 0) + if (Serializer.AsyncEventHeader(context, ref context.EventBuffer, currentTimestamp, AsyncEventID.ResumeAsyncContext, MaxEventPayloadSize) >= 0) { - EventBuffer.Serializer.WriteCompressedUInt64(context.EventBuffer.Data, ref context.EventBuffer.Index, id); + context.EventBuffer.Index += Serializer.WriteCompressedUInt64(context.EventBuffer.Data, context.EventBuffer.Index, id); } } } @@ -731,7 +760,7 @@ internal static partial class SuspendAsyncContext [MethodImpl(MethodImplOptions.AggressiveInlining)] public static void EmitEvent(AsyncThreadContext context, long currentTimestamp) { - EventBuffer.Serializer.AsyncEventHeader(context, ref context.EventBuffer, currentTimestamp, AsyncEventID.SuspendAsyncContext, 0); + Serializer.AsyncEventHeader(context, ref context.EventBuffer, currentTimestamp, AsyncEventID.SuspendAsyncContext, 0); } } @@ -754,7 +783,7 @@ public static void Complete(ref Info info) [MethodImpl(MethodImplOptions.AggressiveInlining)] public static void EmitEvent(AsyncThreadContext context, long currentTimestamp) { - EventBuffer.Serializer.AsyncEventHeader(context, ref context.EventBuffer, currentTimestamp, AsyncEventID.CompleteAsyncContext, 0); + Serializer.AsyncEventHeader(context, ref context.EventBuffer, currentTimestamp, AsyncEventID.CompleteAsyncContext, 0); } } @@ -801,13 +830,11 @@ public static void Handled(ref Info info, uint unwindedFrames) public static void EmitEvent(AsyncThreadContext context, long currentTimestamp, uint unwindedFrames) { // unwinded frames - const int maxEventPayloadSize = EventBuffer.Serializer.MaxCompressedUInt32Size; - - ref EventBuffer eventBuffer = ref context.EventBuffer; + const int MaxEventPayloadSize = Serializer.MaxCompressedUInt32Size; - if (EventBuffer.Serializer.AsyncEventHeader(context, ref eventBuffer, currentTimestamp, AsyncEventID.UnwindAsyncException, maxEventPayloadSize) >= 0) + if (Serializer.AsyncEventHeader(context, ref context.EventBuffer, currentTimestamp, AsyncEventID.UnwindAsyncException, MaxEventPayloadSize) >= 0) { - EventBuffer.Serializer.WriteCompressedUInt32(eventBuffer.Data, ref eventBuffer.Index, unwindedFrames); + context.EventBuffer.Index += Serializer.WriteCompressedUInt32(context.EventBuffer.Data, context.EventBuffer.Index, unwindedFrames); } } } @@ -830,7 +857,7 @@ public static void Resume(ref Info info) [MethodImpl(MethodImplOptions.AggressiveInlining)] public static void EmitEvent(AsyncThreadContext context) { - EventBuffer.Serializer.AsyncEventHeader(context, ref context.EventBuffer, AsyncEventID.ResumeAsyncMethod, 0); + Serializer.AsyncEventHeader(context, ref context.EventBuffer, AsyncEventID.ResumeAsyncMethod, 0); } } @@ -852,7 +879,7 @@ public static void Complete(ref Info info) [MethodImpl(MethodImplOptions.AggressiveInlining)] public static void EmitEvent(AsyncThreadContext context) { - EventBuffer.Serializer.AsyncEventHeader(context, ref context.EventBuffer, AsyncEventID.CompleteAsyncMethod, 0); + Serializer.AsyncEventHeader(context, ref context.EventBuffer, AsyncEventID.CompleteAsyncMethod, 0); } } @@ -899,7 +926,7 @@ private static void ResetIndex(ref Info info) [MethodImpl(MethodImplOptions.AggressiveInlining)] private static void EmitEvent(AsyncThreadContext context) { - EventBuffer.Serializer.AsyncEventHeader(context, ref context.EventBuffer, AsyncEventID.ResetAsyncContinuationWrapperIndex, 0); + Serializer.AsyncEventHeader(context, ref context.EventBuffer, AsyncEventID.ResetAsyncContinuationWrapperIndex, 0); } } @@ -935,17 +962,10 @@ private static void ResetContext(AsyncThreadContext context) [MethodImpl(MethodImplOptions.AggressiveInlining)] private static void EmitEvent(AsyncThreadContext context) { - EventBuffer.Serializer.AsyncEventHeader(context, ref context.EventBuffer, AsyncEventID.ResetAsyncThreadContext, 0); + Serializer.AsyncEventHeader(context, ref context.EventBuffer, AsyncEventID.ResetAsyncThreadContext, 0); } } - private static partial class AsyncCallstack - { -#pragma warning disable CA1823 - private const int ASYNC_METHOD_INFO_SIZE = EventBuffer.Serializer.MaxCompressedUInt64Size + EventBuffer.Serializer.MaxCompressedUInt32Size; -#pragma warning restore CA1823 - } - private static class IsEnabled { public static bool CreateAsyncContextEvent(EventKeywords eventKeywords) => (eventKeywords & Keywords.CreateAsyncContext) != 0; @@ -987,7 +1007,7 @@ public static void EnableFlushTimer() lock (CacheLock) { s_flushTimer ??= new Timer(PeriodicFlush, null, Timeout.Infinite, Timeout.Infinite, false); - s_flushTimer.Change(ASYNC_THREAD_CONTEXT_CACHE_FLUSH_TIMER_INTERVAL_MS, Timeout.Infinite); + s_flushTimer.Change(AsyncThreadContextCacheFlushTimerIntervalMs, Timeout.Infinite); } } @@ -1004,7 +1024,7 @@ public static void EnableCleanupTimer() lock (CacheLock) { s_cleanupTimer ??= new Timer(Cleanup, null, Timeout.Infinite, Timeout.Infinite, false); - s_cleanupTimer?.Change(ASYNC_THREAD_CONTEXT_CACHE_CLEANUP_TIMER_INTERVAL_MS, Timeout.Infinite); + s_cleanupTimer?.Change(AsyncThreadContextCacheCleanupTimerIntervalMs, Timeout.Infinite); } } @@ -1027,7 +1047,7 @@ private static void Cleanup(object? state) if (s_cache.Count > 0) { // Restart cleanup timer. - s_cleanupTimer?.Change(ASYNC_THREAD_CONTEXT_CACHE_CLEANUP_TIMER_INTERVAL_MS, Timeout.Infinite); + s_cleanupTimer?.Change(AsyncThreadContextCacheCleanupTimerIntervalMs, Timeout.Infinite); } } } @@ -1043,12 +1063,12 @@ private static void PeriodicFlush(object? state) if (IsEnabled.AnyAsyncEvents(Config.ActiveEventKeywords)) { // Restart flush timer. - s_flushTimer?.Change(ASYNC_THREAD_CONTEXT_CACHE_FLUSH_TIMER_INTERVAL_MS, Timeout.Infinite); + s_flushTimer?.Change(AsyncThreadContextCacheFlushTimerIntervalMs, Timeout.Infinite); } else { // Start cleanup timer. - s_cleanupTimer?.Change(ASYNC_THREAD_CONTEXT_CACHE_CLEANUP_TIMER_INTERVAL_MS, Timeout.Infinite); + s_cleanupTimer?.Change(AsyncThreadContextCacheCleanupTimerIntervalMs, Timeout.Infinite); } } } @@ -1143,10 +1163,10 @@ public AsyncThreadContextHolder(AsyncThreadContext context, Thread ownerThread) public readonly WeakReference OwnerThread; } - private const int ASYNC_THREAD_CONTEXT_CACHE_FLUSH_TIMER_INTERVAL_MS = 1000; + private const int AsyncThreadContextCacheFlushTimerIntervalMs = 1000; private static Timer? s_flushTimer; - private const int ASYNC_THREAD_CONTEXT_CACHE_CLEANUP_TIMER_INTERVAL_MS = 30000; + private const int AsyncThreadContextCacheCleanupTimerIntervalMs = 30000; private static Timer? s_cleanupTimer; private static List s_cache = new List(); 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 index 3ca0463f6329a4..e9a594ac20aa91 100644 --- 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 @@ -254,11 +254,11 @@ private static void ParseEventBuffer(ReadOnlySpan buffer, EventVisitorWith if (index + 2 > buffer.Length) break; + AsyncEventID eventId = (AsyncEventID)buffer[index++]; + long delta = (long)ReadCompressedUInt64(buffer, ref index); baseTimestamp += delta; - AsyncEventID eventId = (AsyncEventID)buffer[index++]; - if (!visitor(eventId, baseTimestamp, buffer, ref index)) break; } @@ -325,10 +325,10 @@ private static void ReadCallstackPayload(ReadOnlySpan buffer, ref int inde private static void ReadCallstackPayload(ReadOnlySpan buffer, ref int index, out ulong taskId, out byte frameCount, out List<(ulong NativeIP, int State)> frames) { - taskId = ReadCompressedUInt64(buffer, ref index); index++; // type index++; // callstack ID (reserved) frameCount = buffer[index++]; + taskId = ReadCompressedUInt64(buffer, ref index); frames = new List<(ulong, int)>(frameCount); if (frameCount == 0) @@ -1714,8 +1714,8 @@ public void RuntimeAsync_CallstackOverflowPathProducesValidFrames() if (index + 2 > buffer.Length) return; - ReadCompressedUInt64(buffer, ref index); AsyncEventID firstEvent = (AsyncEventID)buffer[index++]; + ReadCompressedUInt64(buffer, ref index); if (firstEvent != AsyncEventID.ResumeAsyncCallstack) return; @@ -1872,11 +1872,11 @@ public static int OutputEventBuffer(ReadOnlySpan buffer) break; } + AsyncEventID eventId = (AsyncEventID)buffer[index++]; + Deserializer.ReadCompressedUInt64(buffer, ref index, out ulong delta); currentTimestamp += delta; - AsyncEventID eventId = (AsyncEventID)buffer[index++]; - Console.WriteLine($"Entry[{eventCount}]: Timestamp=0x{currentTimestamp:X16}, EventId={eventId}"); int payloadStart = index; @@ -2034,10 +2034,10 @@ private static int OutputAsyncCallstackEvent(string eventName, ReadOnlySpan Date: Mon, 27 Apr 2026 20:03:57 +0200 Subject: [PATCH 16/22] Let source generator create GUID. --- .../Runtime/CompilerServices/AsyncProfilerEventSource.cs | 5 +---- 1 file changed, 1 insertion(+), 4 deletions(-) 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 index a30add55a5a4d6..7d61ce2f5cb75f 100644 --- a/src/libraries/System.Private.CoreLib/src/System/Runtime/CompilerServices/AsyncProfilerEventSource.cs +++ b/src/libraries/System.Private.CoreLib/src/System/Runtime/CompilerServices/AsyncProfilerEventSource.cs @@ -7,10 +7,7 @@ namespace System.Runtime.CompilerServices { /// Provides an event source for tracing async execution. - [EventSource( - Name = "System.Runtime.CompilerServices.AsyncProfilerEventSource", - Guid = "742BD0FE-9200-4DE2-9059-576F35EBEB62" - )] + [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"; From 934fb2ece44e238e8a90a8c61c75f8fe48d58f35 Mon Sep 17 00:00:00 2001 From: lateralusX Date: Mon, 27 Apr 2026 20:57:03 +0200 Subject: [PATCH 17/22] Added better rollback support for async header. --- .../CompilerServices/AsyncProfiler.CoreCLR.cs | 14 ++-- .../Runtime/CompilerServices/AsyncProfiler.cs | 84 ++++++++++++++----- 2 files changed, 71 insertions(+), 27 deletions(-) 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 index 15def3ea5eed74..d51dca091852ef 100644 --- 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 @@ -532,8 +532,7 @@ public static void EmitEvent(AsyncThreadContext context, long currentTimestamp, // 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; - int savedAsyncEventHeaderIndex = Serializer.AsyncEventHeader(context, ref eventBuffer, currentTimestamp, delta, eventID, MaxStaticEventPayloadSize); - if (savedAsyncEventHeaderIndex >= 0) + if (Serializer.AsyncEventHeader(context, ref eventBuffer, currentTimestamp, delta, eventID, MaxStaticEventPayloadSize, out var rollbackData)) { int frameCountOffset = CallstackHeader(ref eventBuffer, id, type, 0); @@ -552,13 +551,12 @@ public static void EmitEvent(AsyncThreadContext context, long currentTimestamp, Buffer.BlockCopy(buffer, startIndex, rentedArray, 0, length); CaptureRuntimeAsyncCallstack(rentedArray, ref index, ref state); - // Remove async event header from the event buffer before flushing. - Serializer.RemoveAsyncEventHeader(context, savedAsyncEventHeaderIndex); + // Rollback async event header before flushing. + Serializer.RollbackAsyncEventHeader(context, in rollbackData); context.Flush(); // Write the callstack again. - savedAsyncEventHeaderIndex = Serializer.AsyncEventHeader(context, ref eventBuffer, context.LastEventTimestamp, 0, eventID, MaxStaticEventPayloadSize + index); - if (savedAsyncEventHeaderIndex >= 0) + if (Serializer.AsyncEventHeader(context, ref eventBuffer, context.LastEventTimestamp, 0, eventID, MaxStaticEventPayloadSize + index)) { CallstackHeader(ref eventBuffer, id, type, state.Count); CallstackData(ref eventBuffer, rentedArray, index); @@ -568,8 +566,8 @@ public static void EmitEvent(AsyncThreadContext context, long currentTimestamp, } else { - // Remove async event header from the event buffer since we can't write the callstack. - Serializer.RemoveAsyncEventHeader(context, savedAsyncEventHeaderIndex); + // Rollback async event header since we can't write the callstack. + Serializer.RollbackAsyncEventHeader(context, in rollbackData); } } else 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 index 31263b936af862..12fdd77a60199a 100644 --- a/src/libraries/System.Private.CoreLib/src/System/Runtime/CompilerServices/AsyncProfiler.cs +++ b/src/libraries/System.Private.CoreLib/src/System/Runtime/CompilerServices/AsyncProfiler.cs @@ -122,7 +122,7 @@ public static void EmitAsyncProfilerMetadataIfNeeded(AsyncThreadContext context) int maxDynamicEventPayloadSize = wrapperIPs.Length * Serializer.MaxCompressedUInt64Size; ref EventBuffer eventBuffer = ref context.EventBuffer; - if (Serializer.AsyncEventHeader(context, ref eventBuffer, AsyncEventID.AsyncProfilerMetadata, MaxStaticEventPayloadSize + maxDynamicEventPayloadSize) >= 0) + if (Serializer.AsyncEventHeader(context, ref eventBuffer, AsyncEventID.AsyncProfilerMetadata, MaxStaticEventPayloadSize + maxDynamicEventPayloadSize)) { byte[] buffer = eventBuffer.Data; int index = eventBuffer.Index; @@ -188,7 +188,7 @@ public static void EmitSyncClockEventIfNeeded() const int MaxEventPayloadSize = Serializer.MaxCompressedUInt64Size + Serializer.MaxCompressedUInt64Size; ref EventBuffer eventBuffer = ref transientContext.EventBuffer; - if (Serializer.AsyncEventHeader(transientContext, ref eventBuffer, AsyncEventID.AsyncProfilerSyncClock, MaxEventPayloadSize) >= 0) + if (Serializer.AsyncEventHeader(transientContext, ref eventBuffer, AsyncEventID.AsyncProfilerSyncClock, MaxEventPayloadSize)) { SyncClock(out long utcTimeSync, out long qpcSync); @@ -291,6 +291,13 @@ public static class Serializer public const int MaxEventHeaderSize = 37; public const int MaxAsyncEventHeaderSize = 11; + public struct AsyncEventHeaderRollbackData + { + public int Index; + public uint EventCount; + public long LastEventTimestamp; + } + [MethodImpl(MethodImplOptions.AggressiveInlining)] public static int WriteCompressedInt32(Span buffer, int index, int value) { @@ -441,7 +448,7 @@ public static void Header(AsyncThreadContext context, ref EventBuffer eventBuffe } [MethodImpl(MethodImplOptions.AggressiveInlining)] - public static int AsyncEventHeader(AsyncThreadContext context, ref EventBuffer eventBuffer, AsyncEventID eventID, int maxEventPayloadSize) + public static bool AsyncEventHeader(AsyncThreadContext context, ref EventBuffer eventBuffer, AsyncEventID eventID, int maxEventPayloadSize) { long currentTimestamp = Stopwatch.GetTimestamp(); long delta = currentTimestamp - context.LastEventTimestamp; @@ -449,19 +456,17 @@ public static int AsyncEventHeader(AsyncThreadContext context, ref EventBuffer e } [MethodImpl(MethodImplOptions.AggressiveInlining)] - public static int AsyncEventHeader(AsyncThreadContext context, ref EventBuffer eventBuffer, long currentTimestamp, AsyncEventID eventID, int 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 int AsyncEventHeader(AsyncThreadContext context, ref EventBuffer eventBuffer, long currentTimestamp, long delta, AsyncEventID eventID, int 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; - int asyncHeaderIndex = index; - if ((index + MaxAsyncEventHeaderSize + maxEventPayloadSize) <= buffer.Length && delta >= 0) { context.LastEventTimestamp = currentTimestamp; @@ -471,16 +476,24 @@ public static int AsyncEventHeader(AsyncThreadContext context, ref EventBuffer e // Event is too big for buffer, drop it. if (MaxAsyncEventHeaderSize + maxEventPayloadSize > buffer.Length) { - return -1; + rollbackData = default; + return false; } context.Flush(); delta = 0; index = eventBuffer.Index; - asyncHeaderIndex = index; } + // Capture state after potential flush but before writing the header. + rollbackData = new AsyncEventHeaderRollbackData + { + Index = index, + EventCount = eventBuffer.EventCount, + LastEventTimestamp = context.LastEventTimestamp, + }; + Span headerSpan = buffer.AsSpan(index, MaxAsyncEventHeaderSize); int headerSpanIndex = 0; @@ -490,16 +503,49 @@ public static int AsyncEventHeader(AsyncThreadContext context, ref EventBuffer e eventBuffer.Index += headerSpanIndex; eventBuffer.EventCount++; - return asyncHeaderIndex; + return true; } - public static void RemoveAsyncEventHeader(AsyncThreadContext context, int savedAsyncEventHeaderIndex) + public static bool AsyncEventHeader(AsyncThreadContext context, ref EventBuffer eventBuffer, long currentTimestamp, long delta, AsyncEventID eventID, int maxEventPayloadSize) { - if (context.EventBuffer.EventCount > 0) + byte[] buffer = eventBuffer.Data; + int index = eventBuffer.Index; + + if ((index + MaxAsyncEventHeaderSize + maxEventPayloadSize) <= buffer.Length && delta >= 0) + { + context.LastEventTimestamp = currentTimestamp; + } + else { - context.EventBuffer.Index = savedAsyncEventHeaderIndex; - context.EventBuffer.EventCount--; + // 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, headerSpanIndex, (ulong)delta); // Timestamp delta from last event + + eventBuffer.Index += headerSpanIndex; + eventBuffer.EventCount++; + + return true; + } + + public static void RollbackAsyncEventHeader(AsyncThreadContext context, in AsyncEventHeaderRollbackData rollbackData) + { + context.EventBuffer.Index = rollbackData.Index; + context.EventBuffer.EventCount = rollbackData.EventCount; + context.LastEventTimestamp = rollbackData.LastEventTimestamp; } } } @@ -712,7 +758,7 @@ public static void EmitEvent(AsyncThreadContext context, ulong id) { const int MaxEventPayloadSize = Serializer.MaxCompressedUInt64Size; - if (Serializer.AsyncEventHeader(context, ref context.EventBuffer, AsyncEventID.CreateAsyncContext, MaxEventPayloadSize) >= 0) + if (Serializer.AsyncEventHeader(context, ref context.EventBuffer, AsyncEventID.CreateAsyncContext, MaxEventPayloadSize)) { context.EventBuffer.Index += Serializer.WriteCompressedUInt64(context.EventBuffer.Data, context.EventBuffer.Index, id); } @@ -723,7 +769,7 @@ public static void EmitEvent(AsyncThreadContext context, long currentTimestamp, { const int MaxEventPayloadSize = Serializer.MaxCompressedUInt64Size; - if (Serializer.AsyncEventHeader(context, ref context.EventBuffer, currentTimestamp, AsyncEventID.CreateAsyncContext, MaxEventPayloadSize) >= 0) + if (Serializer.AsyncEventHeader(context, ref context.EventBuffer, currentTimestamp, AsyncEventID.CreateAsyncContext, MaxEventPayloadSize)) { context.EventBuffer.Index += Serializer.WriteCompressedUInt64(context.EventBuffer.Data, context.EventBuffer.Index, id); } @@ -737,7 +783,7 @@ public static void EmitEvent(AsyncThreadContext context, ulong id) { const int MaxEventPayloadSize = Serializer.MaxCompressedUInt64Size; - if (Serializer.AsyncEventHeader(context, ref context.EventBuffer, AsyncEventID.ResumeAsyncContext, MaxEventPayloadSize) >= 0) + if (Serializer.AsyncEventHeader(context, ref context.EventBuffer, AsyncEventID.ResumeAsyncContext, MaxEventPayloadSize)) { context.EventBuffer.Index += Serializer.WriteCompressedUInt64(context.EventBuffer.Data, context.EventBuffer.Index, id); } @@ -748,7 +794,7 @@ public static void EmitEvent(AsyncThreadContext context, long currentTimestamp, { const int MaxEventPayloadSize = Serializer.MaxCompressedUInt64Size; - if (Serializer.AsyncEventHeader(context, ref context.EventBuffer, currentTimestamp, AsyncEventID.ResumeAsyncContext, MaxEventPayloadSize) >= 0) + if (Serializer.AsyncEventHeader(context, ref context.EventBuffer, currentTimestamp, AsyncEventID.ResumeAsyncContext, MaxEventPayloadSize)) { context.EventBuffer.Index += Serializer.WriteCompressedUInt64(context.EventBuffer.Data, context.EventBuffer.Index, id); } @@ -832,7 +878,7 @@ public static void EmitEvent(AsyncThreadContext context, long currentTimestamp, // unwinded frames const int MaxEventPayloadSize = Serializer.MaxCompressedUInt32Size; - if (Serializer.AsyncEventHeader(context, ref context.EventBuffer, currentTimestamp, AsyncEventID.UnwindAsyncException, MaxEventPayloadSize) >= 0) + if (Serializer.AsyncEventHeader(context, ref context.EventBuffer, currentTimestamp, AsyncEventID.UnwindAsyncException, MaxEventPayloadSize)) { context.EventBuffer.Index += Serializer.WriteCompressedUInt32(context.EventBuffer.Data, context.EventBuffer.Index, unwindedFrames); } From dc65f74bfc687a261c1418249903417275bdbd5f Mon Sep 17 00:00:00 2001 From: lateralusX Date: Tue, 28 Apr 2026 11:25:01 +0200 Subject: [PATCH 18/22] Changes after rebase. --- .../CompilerServices/AsyncHelpers.CoreCLR.cs | 15 +++++++-------- 1 file changed, 7 insertions(+), 8 deletions(-) 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 42e466dabe004a..9f00b0cc6404ac 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 @@ -505,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); @@ -672,7 +672,7 @@ private unsafe void InstrumentedDispatchContinuations(AsyncInstrumentation.Flags { newContinuation.Next = nextContinuation; - RuntimeAsyncInstrumentationHelpers.AwaitSuspendedRuntimeAsyncContext(ref asyncDispatcherInfo, flags, curContinuation, newContinuation); + RuntimeAsyncInstrumentationHelpers.AwaitSuspendedRuntimeAsyncContext(ref asyncDispatcherInfo, flags, curContinuation, newContinuation, awaitState.SentinelContinuation!.Next); InstrumentedHandleSuspended(flags, ref awaitState, newContinuation); awaitState.Pop(); @@ -847,10 +847,10 @@ private static void InstrumentedFinalizeRuntimeAsyncTask(RuntimeAsyncTask { if (AsyncInstrumentation.IsEnabled.AsyncProfiler(flags)) { - Continuation? nc = t_runtimeAsyncAwaitState.SentinelContinuation!.Next; - if (nc != null) + Continuation? nextContinuation = state.SentinelContinuation!.Next; + if (nextContinuation != null) { - AsyncProfiler.CreateAsyncContext.Create((ulong)task.Id, nc); + AsyncProfiler.CreateAsyncContext.Create((ulong)task.Id, nextContinuation); } } @@ -1104,14 +1104,13 @@ public static void QueueSuspendedRuntimeAsyncContext(ref AsyncDispatcherInfo inf } [MethodImpl(MethodImplOptions.AggressiveInlining)] - public static void AwaitSuspendedRuntimeAsyncContext(ref AsyncDispatcherInfo info, 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)) { - Continuation? nextContinuation = t_runtimeAsyncAwaitState.SentinelContinuation!.Next; - AsyncProfiler.SuspendAsyncContext.Suspend(ref info, nextContinuation != null ? nextContinuation : newContinuation); + AsyncProfiler.SuspendAsyncContext.Suspend(ref info, nextContinuation ?? newContinuation); } if (AsyncInstrumentation.IsEnabled.AsyncDebugger(flags)) From e06b524110a205586dd7f0084b97289c21870daf Mon Sep 17 00:00:00 2001 From: lateralusX Date: Tue, 28 Apr 2026 11:47:31 +0200 Subject: [PATCH 19/22] Span some more. --- .../CompilerServices/AsyncProfiler.CoreCLR.cs | 12 +-- .../Runtime/CompilerServices/AsyncProfiler.cs | 94 ++++--------------- 2 files changed, 25 insertions(+), 81 deletions(-) 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 index d51dca091852ef..dddfee17b6488b 100644 --- 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 @@ -457,14 +457,14 @@ public static bool CaptureRuntimeAsyncCallstack(byte[] buffer, ref int index, re // are written as deltas from the previous frame. if (state.Count == 0) { - frameSpanIndex += Serializer.WriteCompressedUInt64(frameSpan, frameSpanIndex, currentNativeIP); + frameSpanIndex += Serializer.WriteCompressedUInt64(frameSpan.Slice(frameSpanIndex, Serializer.MaxCompressedUInt64Size), currentNativeIP); } else { - frameSpanIndex += Serializer.WriteCompressedInt64(frameSpan, frameSpanIndex, (long)(currentNativeIP - previousNativeIP)); + frameSpanIndex += Serializer.WriteCompressedInt64(frameSpan.Slice(frameSpanIndex, Serializer.MaxCompressedInt64Size), (long)(currentNativeIP - previousNativeIP)); } - frameSpanIndex += Serializer.WriteCompressedInt32(frameSpan, frameSpanIndex, state.Continuation.State); + frameSpanIndex += Serializer.WriteCompressedInt32(frameSpan.Slice(frameSpanIndex, Serializer.MaxCompressedInt32Size), state.Continuation.State); callstackSpanIndex += frameSpanIndex; state.Count++; @@ -482,8 +482,8 @@ public static bool CaptureRuntimeAsyncCallstack(byte[] buffer, ref int index, re frameSpan = callstackSpan.Slice(callstackSpanIndex, MaxAsyncMethodFrameSize); frameSpanIndex = 0; - frameSpanIndex += Serializer.WriteCompressedInt64(frameSpan, frameSpanIndex, (long)(currentNativeIP - previousNativeIP)); - frameSpanIndex += Serializer.WriteCompressedInt32(frameSpan, frameSpanIndex, state.Continuation.State); + frameSpanIndex += Serializer.WriteCompressedInt64(frameSpan.Slice(frameSpanIndex, Serializer.MaxCompressedInt64Size), (long)(currentNativeIP - previousNativeIP)); + frameSpanIndex += Serializer.WriteCompressedInt32(frameSpan.Slice(frameSpanIndex, Serializer.MaxCompressedInt32Size), state.Continuation.State); callstackSpanIndex += frameSpanIndex; @@ -597,7 +597,7 @@ private static int CallstackHeader(ref EventBuffer eventBuffer, ulong id, AsyncC int frameCountOffset = index + spanIndex; callstackHeaderSpan[spanIndex++] = callstackFrameCount; - spanIndex += Serializer.WriteCompressedUInt64(callstackHeaderSpan, spanIndex, id); + spanIndex += Serializer.WriteCompressedUInt64(callstackHeaderSpan.Slice(spanIndex), id); eventBuffer.Index += spanIndex; return frameCountOffset; 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 index 12fdd77a60199a..27c1bf58768c62 100644 --- a/src/libraries/System.Private.CoreLib/src/System/Runtime/CompilerServices/AsyncProfiler.cs +++ b/src/libraries/System.Private.CoreLib/src/System/Runtime/CompilerServices/AsyncProfiler.cs @@ -124,32 +124,24 @@ public static void EmitAsyncProfilerMetadataIfNeeded(AsyncThreadContext context) ref EventBuffer eventBuffer = ref context.EventBuffer; if (Serializer.AsyncEventHeader(context, ref eventBuffer, AsyncEventID.AsyncProfilerMetadata, MaxStaticEventPayloadSize + maxDynamicEventPayloadSize)) { - byte[] buffer = eventBuffer.Data; - int index = eventBuffer.Index; - SyncClock(out long utcTimeSync, out long qpcSync); - Span payloadSpan = buffer.AsSpan(index, MaxStaticEventPayloadSize); + Span payloadSpan = eventBuffer.Data.AsSpan(eventBuffer.Index, MaxStaticEventPayloadSize + maxDynamicEventPayloadSize); int payloadSpanIndex = 0; - payloadSpanIndex += Serializer.WriteCompressedUInt64(payloadSpan, payloadSpanIndex, (ulong)Stopwatch.Frequency); - payloadSpanIndex += Serializer.WriteCompressedUInt64(payloadSpan, payloadSpanIndex, (ulong)qpcSync); - payloadSpanIndex += Serializer.WriteCompressedUInt64(payloadSpan, payloadSpanIndex, (ulong)utcTimeSync); - payloadSpanIndex += Serializer.WriteCompressedUInt32(payloadSpan, payloadSpanIndex, EventBufferSize); + 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; - index += payloadSpanIndex; - - payloadSpan = buffer.AsSpan(index, maxDynamicEventPayloadSize); - payloadSpanIndex = 0; - for (int i = 0; i < wrapperIPs.Length; i++) { - payloadSpanIndex += Serializer.WriteCompressedUInt64(payloadSpan, payloadSpanIndex, (ulong)wrapperIPs[i]); + payloadSpanIndex += Serializer.WriteCompressedUInt64(payloadSpan.Slice(payloadSpanIndex), (ulong)wrapperIPs[i]); } - eventBuffer.Index = index + payloadSpanIndex; + eventBuffer.Index += payloadSpanIndex; // Force flush to deliver event promptly. context.Flush(); @@ -195,8 +187,8 @@ public static void EmitSyncClockEventIfNeeded() Span payloadSpan = eventBuffer.Data.AsSpan(eventBuffer.Index, MaxEventPayloadSize); int payloadSpanIndex = 0; - payloadSpanIndex += Serializer.WriteCompressedUInt64(payloadSpan, payloadSpanIndex, (ulong)qpcSync); - payloadSpanIndex += Serializer.WriteCompressedUInt64(payloadSpan, payloadSpanIndex, (ulong)utcTimeSync); + payloadSpanIndex += Serializer.WriteCompressedUInt64(payloadSpan.Slice(payloadSpanIndex), (ulong)qpcSync); + payloadSpanIndex += Serializer.WriteCompressedUInt64(payloadSpan.Slice(payloadSpanIndex), (ulong)utcTimeSync); eventBuffer.Index += payloadSpanIndex; @@ -298,36 +290,12 @@ public struct AsyncEventHeaderRollbackData public long LastEventTimestamp; } - [MethodImpl(MethodImplOptions.AggressiveInlining)] - public static int WriteCompressedInt32(Span buffer, int index, int value) - { - return WriteCompressedUInt32(buffer.Slice(index, MaxCompressedUInt32Size), ZigzagEncodeInt32(value)); - } - - [MethodImpl(MethodImplOptions.AggressiveInlining)] - public static int WriteCompressedInt32(byte[] buffer, int index, int value) - { - return WriteCompressedUInt32(buffer.AsSpan(index, MaxCompressedUInt32Size), ZigzagEncodeInt32(value)); - } - [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, int index, uint value) - { - return WriteCompressedUInt32(buffer.Slice(index, MaxCompressedUInt32Size), value); - } - - [MethodImpl(MethodImplOptions.AggressiveInlining)] - public static int WriteCompressedUInt32(byte[] buffer, int index, uint value) - { - return WriteCompressedUInt32(buffer.AsSpan(index, MaxCompressedUInt32Size), value); - } - [MethodImpl(MethodImplOptions.AggressiveInlining)] public static int WriteCompressedUInt32(Span buffer, uint value) { @@ -349,18 +317,6 @@ public static int WriteCompressedUInt32(Span buffer, uint value) return index; } - [MethodImpl(MethodImplOptions.AggressiveInlining)] - public static int WriteCompressedInt64(Span buffer, int index, long value) - { - return WriteCompressedUInt64(buffer.Slice(index, MaxCompressedUInt64Size), ZigzagEncodeInt64(value)); - } - - [MethodImpl(MethodImplOptions.AggressiveInlining)] - public static int WriteCompressedInt64(byte[] buffer, int index, long value) - { - return WriteCompressedUInt64(buffer.AsSpan(index, MaxCompressedUInt64Size), ZigzagEncodeInt64(value)); - } - [MethodImpl(MethodImplOptions.AggressiveInlining)] public static int WriteCompressedInt64(Span buffer, long value) { @@ -368,26 +324,14 @@ public static int WriteCompressedInt64(Span buffer, long value) } [MethodImpl(MethodImplOptions.AggressiveInlining)] - public static int WriteCompressedUInt64(Span buffer, int index, ulong value) + public static int WriteCompressedUInt64(Span buffer, ulong value) { - return WriteCompressedUInt64(buffer.Slice(index, MaxCompressedUInt64Size), value); - } - - [MethodImpl(MethodImplOptions.AggressiveInlining)] - public static int WriteCompressedUInt64(byte[] buffer, int index, ulong value) - { - return WriteCompressedUInt64(buffer.AsSpan(index, MaxCompressedUInt64Size), value); - } - - [MethodImpl(MethodImplOptions.AggressiveInlining)] - public static int WriteCompressedUInt64(Span destination, ulong value) - { - if (destination.Length < MaxCompressedUInt64Size) + if (buffer.Length < MaxCompressedUInt64Size) { ThrowHelper.ThrowArgumentOutOfRangeException(ExceptionArgument.length); } - ref byte dst = ref MemoryMarshal.GetReference(destination); + ref byte dst = ref MemoryMarshal.GetReference(buffer); int index = 0; while (value > 0x7Fu) @@ -498,7 +442,7 @@ public static bool AsyncEventHeader(AsyncThreadContext context, ref EventBuffer int headerSpanIndex = 0; headerSpan[headerSpanIndex++] = (byte)eventID; // eventID - headerSpanIndex += WriteCompressedUInt64(headerSpan, headerSpanIndex, (ulong)delta); // Timestamp delta from last event + headerSpanIndex += WriteCompressedUInt64(headerSpan.Slice(headerSpanIndex), (ulong)delta); // Timestamp delta from last event eventBuffer.Index += headerSpanIndex; eventBuffer.EventCount++; @@ -533,7 +477,7 @@ public static bool AsyncEventHeader(AsyncThreadContext context, ref EventBuffer int headerSpanIndex = 0; headerSpan[headerSpanIndex++] = (byte)eventID; // eventID - headerSpanIndex += WriteCompressedUInt64(headerSpan, headerSpanIndex, (ulong)delta); // Timestamp delta from last event + headerSpanIndex += WriteCompressedUInt64(headerSpan.Slice(headerSpanIndex), (ulong)delta); // Timestamp delta from last event eventBuffer.Index += headerSpanIndex; eventBuffer.EventCount++; @@ -760,7 +704,7 @@ public static void EmitEvent(AsyncThreadContext context, ulong id) if (Serializer.AsyncEventHeader(context, ref context.EventBuffer, AsyncEventID.CreateAsyncContext, MaxEventPayloadSize)) { - context.EventBuffer.Index += Serializer.WriteCompressedUInt64(context.EventBuffer.Data, context.EventBuffer.Index, id); + context.EventBuffer.Index += Serializer.WriteCompressedUInt64(context.EventBuffer.Data.AsSpan(context.EventBuffer.Index, MaxEventPayloadSize), id); } } @@ -771,7 +715,7 @@ public static void EmitEvent(AsyncThreadContext context, long currentTimestamp, if (Serializer.AsyncEventHeader(context, ref context.EventBuffer, currentTimestamp, AsyncEventID.CreateAsyncContext, MaxEventPayloadSize)) { - context.EventBuffer.Index += Serializer.WriteCompressedUInt64(context.EventBuffer.Data, context.EventBuffer.Index, id); + context.EventBuffer.Index += Serializer.WriteCompressedUInt64(context.EventBuffer.Data.AsSpan(context.EventBuffer.Index, MaxEventPayloadSize), id); } } } @@ -785,7 +729,7 @@ public static void EmitEvent(AsyncThreadContext context, ulong id) if (Serializer.AsyncEventHeader(context, ref context.EventBuffer, AsyncEventID.ResumeAsyncContext, MaxEventPayloadSize)) { - context.EventBuffer.Index += Serializer.WriteCompressedUInt64(context.EventBuffer.Data, context.EventBuffer.Index, id); + context.EventBuffer.Index += Serializer.WriteCompressedUInt64(context.EventBuffer.Data.AsSpan(context.EventBuffer.Index, MaxEventPayloadSize), id); } } @@ -796,7 +740,7 @@ public static void EmitEvent(AsyncThreadContext context, long currentTimestamp, if (Serializer.AsyncEventHeader(context, ref context.EventBuffer, currentTimestamp, AsyncEventID.ResumeAsyncContext, MaxEventPayloadSize)) { - context.EventBuffer.Index += Serializer.WriteCompressedUInt64(context.EventBuffer.Data, context.EventBuffer.Index, id); + context.EventBuffer.Index += Serializer.WriteCompressedUInt64(context.EventBuffer.Data.AsSpan(context.EventBuffer.Index, MaxEventPayloadSize), id); } } } @@ -880,7 +824,7 @@ public static void EmitEvent(AsyncThreadContext context, long currentTimestamp, if (Serializer.AsyncEventHeader(context, ref context.EventBuffer, currentTimestamp, AsyncEventID.UnwindAsyncException, MaxEventPayloadSize)) { - context.EventBuffer.Index += Serializer.WriteCompressedUInt32(context.EventBuffer.Data, context.EventBuffer.Index, unwindedFrames); + context.EventBuffer.Index += Serializer.WriteCompressedUInt32(context.EventBuffer.Data.AsSpan(context.EventBuffer.Index, MaxEventPayloadSize), unwindedFrames); } } } From 8353942f277738bbf37aeeec2c5ea6d1f8c04529 Mon Sep 17 00:00:00 2001 From: lateralusX Date: Tue, 28 Apr 2026 11:58:44 +0200 Subject: [PATCH 20/22] Add description to continuation wrapper. --- .../CompilerServices/AsyncProfiler.CoreCLR.cs | 18 ++++++++++++++++++ 1 file changed, 18 insertions(+) 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 index dddfee17b6488b..6199b61c46e910 100644 --- 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 @@ -120,6 +120,24 @@ private static ulong GetId(ref AsyncDispatcherInfo info) } } + /// + /// 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 { From c50a69a2f1996274472d4b12a35c066ee13d1098 Mon Sep 17 00:00:00 2001 From: lateralusX Date: Tue, 28 Apr 2026 17:47:22 +0200 Subject: [PATCH 21/22] Dial back Agressive Inlining a little. --- .../CompilerServices/AsyncHelpers.CoreCLR.cs | 6 +- .../CompilerServices/AsyncProfiler.CoreCLR.cs | 11 -- .../Runtime/CompilerServices/AsyncProfiler.cs | 167 ++++++++---------- 3 files changed, 72 insertions(+), 112 deletions(-) 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 9f00b0cc6404ac..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 @@ -1081,7 +1081,7 @@ public static void ResumeRuntimeAsyncContext(Task task, ref AsyncDispatcherInfo if (AsyncInstrumentation.IsEnabled.AsyncDebugger(flags)) { - AsyncDebugger.ResumeAsyncContext(task.Id); + AsyncDebugger.ResumeAsyncContext(task); } } } @@ -1241,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 index 6199b61c46e910..5ddc029e2549d3 100644 --- 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 @@ -40,7 +40,6 @@ public static void Create(ulong id, Continuation nextContinuation) internal static partial class ResumeAsyncContext { - [MethodImpl(MethodImplOptions.AggressiveInlining)] public static ulong GetId(ref AsyncDispatcherInfo info) { if (info.CurrentTask != null) @@ -59,7 +58,6 @@ public static void Resume(ref AsyncDispatcherInfo info) AsyncThreadContext.Release(context); } - [MethodImpl(MethodImplOptions.AggressiveInlining)] public static void Resume(ref AsyncDispatcherInfo info, AsyncThreadContext context, ulong id, EventKeywords activeEventKeywords) { if (SyncPoint.Check(context)) @@ -109,7 +107,6 @@ public static void Suspend(ref AsyncDispatcherInfo info, Continuation nextContin AsyncThreadContext.Release(context); } - [MethodImpl(MethodImplOptions.AggressiveInlining)] private static ulong GetId(ref AsyncDispatcherInfo info) { if (info.CurrentTask != null) @@ -141,7 +138,6 @@ private static ulong GetId(ref AsyncDispatcherInfo info) [StackTraceHidden] internal static partial class ContinuationWrapper { - [MethodImpl(MethodImplOptions.AggressiveInlining)] public static void InitInfo(ref Info info) { info.ContinuationTable = ref Unsafe.As(ref s_continuationWrappers); @@ -409,7 +405,6 @@ private struct ContinuationWrapperTable private static partial class SyncPoint { - [MethodImpl(MethodImplOptions.AggressiveInlining)] private static unsafe void ResumeAsyncCallstacks(AsyncThreadContext context) { //Write recursively all the resume async callstack events. @@ -421,7 +416,6 @@ private static unsafe void ResumeAsyncCallstacks(AsyncThreadContext context) } - [MethodImpl(MethodImplOptions.AggressiveInlining)] private static unsafe void ResumeRuntimeAsyncCallstacks(AsyncDispatcherInfo* info, AsyncThreadContext context) { if (info != null) @@ -515,19 +509,16 @@ public static bool CaptureRuntimeAsyncCallstack(byte[] buffer, ref int index, re return state.Continuation == null || state.Count == byte.MaxValue; } - [MethodImpl(MethodImplOptions.AggressiveInlining)] public static void EmitEvent(AsyncThreadContext context, long currentTimestamp, ulong id, Continuation? asyncCallstack) { EmitEvent(context, currentTimestamp, AsyncEventID.ResumeAsyncCallstack, id, AsyncCallstackType.Runtime, asyncCallstack); } - [MethodImpl(MethodImplOptions.AggressiveInlining)] public static void EmitEvent(AsyncThreadContext context, long currentTimestamp, AsyncEventID eventID, ulong id, Continuation? asyncCallstack) { EmitEvent(context, currentTimestamp, eventID, id, AsyncCallstackType.Runtime, asyncCallstack); } - [MethodImpl(MethodImplOptions.AggressiveInlining)] 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); @@ -598,7 +589,6 @@ public static void EmitEvent(AsyncThreadContext context, long currentTimestamp, } } - [MethodImpl(MethodImplOptions.AggressiveInlining)] 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). @@ -621,7 +611,6 @@ private static int CallstackHeader(ref EventBuffer eventBuffer, ulong id, AsyncC return frameCountOffset; } - [MethodImpl(MethodImplOptions.AggressiveInlining)] private static void CallstackData(ref EventBuffer eventBuffer, byte[] callstackData, int callstackDataByteCount) { ref int index = ref eventBuffer.Index; 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 index 27c1bf58768c62..d22f094f6826c0 100644 --- a/src/libraries/System.Private.CoreLib/src/System/Runtime/CompilerServices/AsyncProfiler.cs +++ b/src/libraries/System.Private.CoreLib/src/System/Runtime/CompilerServices/AsyncProfiler.cs @@ -47,7 +47,6 @@ internal ref struct Info public uint ContinuationIndex; } - [MethodImpl(MethodImplOptions.AggressiveInlining)] internal static void InitInfo(ref Info info) { info.Context = null; @@ -59,7 +58,6 @@ internal static partial class Config { public static readonly Lock ConfigLock = new(); - [MethodImpl(MethodImplOptions.AggressiveInlining)] public static bool Changed(AsyncThreadContext context) => context.ConfigRevision != Revision; public static void Update(EventLevel logLevel, EventKeywords eventKeywords) @@ -344,10 +342,8 @@ public static int WriteCompressedUInt64(Span buffer, ulong value) return index; } - [MethodImpl(MethodImplOptions.AggressiveInlining)] public static uint ZigzagEncodeInt32(int value) => (uint)((value << 1) ^ (value >> 31)); - [MethodImpl(MethodImplOptions.AggressiveInlining)] public static ulong ZigzagEncodeInt64(long value) => (ulong)((value << 1) ^ (value >> 63)); public static void Header(AsyncThreadContext context, ref EventBuffer eventBuffer) @@ -391,7 +387,6 @@ public static void Header(AsyncThreadContext context, ref EventBuffer eventBuffe eventBuffer.Index = headerSpanIndex; } - [MethodImpl(MethodImplOptions.AggressiveInlining)] public static bool AsyncEventHeader(AsyncThreadContext context, ref EventBuffer eventBuffer, AsyncEventID eventID, int maxEventPayloadSize) { long currentTimestamp = Stopwatch.GetTimestamp(); @@ -399,7 +394,6 @@ public static bool AsyncEventHeader(AsyncThreadContext context, ref EventBuffer return AsyncEventHeader(context, ref eventBuffer, currentTimestamp, delta, eventID, maxEventPayloadSize); } - [MethodImpl(MethodImplOptions.AggressiveInlining)] public static bool AsyncEventHeader(AsyncThreadContext context, ref EventBuffer eventBuffer, long currentTimestamp, AsyncEventID eventID, int maxEventPayloadSize) { long delta = currentTimestamp - context.LastEventTimestamp; @@ -487,8 +481,9 @@ public static bool AsyncEventHeader(AsyncThreadContext context, ref EventBuffer public static void RollbackAsyncEventHeader(AsyncThreadContext context, in AsyncEventHeaderRollbackData rollbackData) { - context.EventBuffer.Index = rollbackData.Index; - context.EventBuffer.EventCount = rollbackData.EventCount; + ref EventBuffer eventBuffer = ref context.EventBuffer; + eventBuffer.Index = rollbackData.Index; + eventBuffer.EventCount = rollbackData.EventCount; context.LastEventTimestamp = rollbackData.LastEventTimestamp; } } @@ -525,8 +520,7 @@ public ref EventBuffer EventBuffer { if (_eventBuffer.Data.Length == 0) { - _eventBuffer.Data = AllocBuffer(); - Serializer.Header(this, ref _eventBuffer); + InitializeBuffer(); } Debug.Assert(InUse || BlockContext); @@ -543,35 +537,7 @@ public static AsyncThreadContext Acquire(ref Info info) context.InUse = true; if (context.BlockContext) { - context.InUse = false; - lock (AsyncThreadContextCache.CacheLock) { ; } - context.InUse = true; - } - - return context; - } - - 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) - { - context.InUse = false; - lock (AsyncThreadContextCache.CacheLock) {; } - context.InUse = true; + WaitOnBlockedAsyncThreadContext(context); } return context; @@ -593,7 +559,7 @@ public static AsyncThreadContext Get() return context; } - return Create(); + return CreateAsyncThreadContext(); } [MethodImpl(MethodImplOptions.AggressiveInlining)] @@ -607,8 +573,29 @@ public static AsyncThreadContext Get(ref Info info) return context; } - context = Get(); - info.Context = t_asyncThreadContext; + 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; } @@ -654,7 +641,7 @@ public void Flush() try { - EmitEvent(eventBuffer.Data.AsSpan().Slice(0, eventBuffer.Index)); + Log.AsyncEvents(eventBuffer.Data.AsSpan().Slice(0, eventBuffer.Index)); } catch { @@ -664,90 +651,81 @@ public void Flush() Serializer.Header(this, ref eventBuffer); } - private static void EmitEvent(Span buffer) - { - Log.AsyncEvents(buffer); - } - - private static AsyncThreadContext Create() - { - AsyncThreadContext context = new AsyncThreadContext(); - AsyncThreadContextCache.Add(context); - t_asyncThreadContext = context; - return context; - } - - private static byte[] AllocBuffer() + [MethodImpl(MethodImplOptions.NoInlining)] + private void InitializeBuffer() { try { - return new byte[Config.EventBufferSize]; + _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. - return Array.Empty(); + _eventBuffer.Data = Array.Empty(); } } + [MethodImpl(MethodImplOptions.NoInlining)] + private static void WaitOnBlockedAsyncThreadContext(AsyncThreadContext context) + { + context.InUse = false; + 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 { - [MethodImpl(MethodImplOptions.AggressiveInlining)] - public static void EmitEvent(AsyncThreadContext context, ulong id) - { - const int MaxEventPayloadSize = Serializer.MaxCompressedUInt64Size; - - if (Serializer.AsyncEventHeader(context, ref context.EventBuffer, AsyncEventID.CreateAsyncContext, MaxEventPayloadSize)) - { - context.EventBuffer.Index += Serializer.WriteCompressedUInt64(context.EventBuffer.Data.AsSpan(context.EventBuffer.Index, MaxEventPayloadSize), id); - } - } - - [MethodImpl(MethodImplOptions.AggressiveInlining)] public static void EmitEvent(AsyncThreadContext context, long currentTimestamp, ulong id) { const int MaxEventPayloadSize = Serializer.MaxCompressedUInt64Size; - if (Serializer.AsyncEventHeader(context, ref context.EventBuffer, currentTimestamp, AsyncEventID.CreateAsyncContext, MaxEventPayloadSize)) + ref EventBuffer eventBuffer = ref context.EventBuffer; + if (Serializer.AsyncEventHeader(context, ref eventBuffer, currentTimestamp, AsyncEventID.CreateAsyncContext, MaxEventPayloadSize)) { - context.EventBuffer.Index += Serializer.WriteCompressedUInt64(context.EventBuffer.Data.AsSpan(context.EventBuffer.Index, MaxEventPayloadSize), id); + eventBuffer.Index += Serializer.WriteCompressedUInt64(eventBuffer.Data.AsSpan(eventBuffer.Index, MaxEventPayloadSize), id); } } } internal static partial class ResumeAsyncContext { - [MethodImpl(MethodImplOptions.AggressiveInlining)] - public static void EmitEvent(AsyncThreadContext context, ulong id) - { - const int MaxEventPayloadSize = Serializer.MaxCompressedUInt64Size; - - if (Serializer.AsyncEventHeader(context, ref context.EventBuffer, AsyncEventID.ResumeAsyncContext, MaxEventPayloadSize)) - { - context.EventBuffer.Index += Serializer.WriteCompressedUInt64(context.EventBuffer.Data.AsSpan(context.EventBuffer.Index, MaxEventPayloadSize), id); - } - } - - [MethodImpl(MethodImplOptions.AggressiveInlining)] public static void EmitEvent(AsyncThreadContext context, long currentTimestamp, ulong id) { const int MaxEventPayloadSize = Serializer.MaxCompressedUInt64Size; - if (Serializer.AsyncEventHeader(context, ref context.EventBuffer, currentTimestamp, AsyncEventID.ResumeAsyncContext, MaxEventPayloadSize)) + ref EventBuffer eventBuffer = ref context.EventBuffer; + if (Serializer.AsyncEventHeader(context, ref eventBuffer, currentTimestamp, AsyncEventID.ResumeAsyncContext, MaxEventPayloadSize)) { - context.EventBuffer.Index += Serializer.WriteCompressedUInt64(context.EventBuffer.Data.AsSpan(context.EventBuffer.Index, MaxEventPayloadSize), id); + eventBuffer.Index += Serializer.WriteCompressedUInt64(eventBuffer.Data.AsSpan(eventBuffer.Index, MaxEventPayloadSize), id); } } } internal static partial class SuspendAsyncContext { - [MethodImpl(MethodImplOptions.AggressiveInlining)] public static void EmitEvent(AsyncThreadContext context, long currentTimestamp) { Serializer.AsyncEventHeader(context, ref context.EventBuffer, currentTimestamp, AsyncEventID.SuspendAsyncContext, 0); @@ -770,7 +748,6 @@ public static void Complete(ref Info info) AsyncThreadContext.Release(context); } - [MethodImpl(MethodImplOptions.AggressiveInlining)] public static void EmitEvent(AsyncThreadContext context, long currentTimestamp) { Serializer.AsyncEventHeader(context, ref context.EventBuffer, currentTimestamp, AsyncEventID.CompleteAsyncContext, 0); @@ -816,15 +793,15 @@ public static void Handled(ref Info info, uint unwindedFrames) AsyncThreadContext.Release(context); } - [MethodImpl(MethodImplOptions.AggressiveInlining)] public static void EmitEvent(AsyncThreadContext context, long currentTimestamp, uint unwindedFrames) { // unwinded frames const int MaxEventPayloadSize = Serializer.MaxCompressedUInt32Size; - if (Serializer.AsyncEventHeader(context, ref context.EventBuffer, currentTimestamp, AsyncEventID.UnwindAsyncException, MaxEventPayloadSize)) + ref EventBuffer eventBuffer = ref context.EventBuffer; + if (Serializer.AsyncEventHeader(context, ref eventBuffer, currentTimestamp, AsyncEventID.UnwindAsyncException, MaxEventPayloadSize)) { - context.EventBuffer.Index += Serializer.WriteCompressedUInt32(context.EventBuffer.Data.AsSpan(context.EventBuffer.Index, MaxEventPayloadSize), unwindedFrames); + eventBuffer.Index += Serializer.WriteCompressedUInt32(eventBuffer.Data.AsSpan(eventBuffer.Index, MaxEventPayloadSize), unwindedFrames); } } } @@ -844,7 +821,6 @@ public static void Resume(ref Info info) AsyncThreadContext.Release(context); } - [MethodImpl(MethodImplOptions.AggressiveInlining)] public static void EmitEvent(AsyncThreadContext context) { Serializer.AsyncEventHeader(context, ref context.EventBuffer, AsyncEventID.ResumeAsyncMethod, 0); @@ -866,7 +842,6 @@ public static void Complete(ref Info info) AsyncThreadContext.Release(context); } - [MethodImpl(MethodImplOptions.AggressiveInlining)] public static void EmitEvent(AsyncThreadContext context) { Serializer.AsyncEventHeader(context, ref context.EventBuffer, AsyncEventID.CompleteAsyncMethod, 0); @@ -888,7 +863,6 @@ public static void IncrementIndex(ref Info info) } } - [MethodImpl(MethodImplOptions.AggressiveInlining)] public static void UnwindIndex(ref Info info, uint unwindedFrames) { uint oldIndex = info.ContinuationIndex; @@ -913,7 +887,6 @@ private static void ResetIndex(ref Info info) AsyncThreadContext.Release(context); } - [MethodImpl(MethodImplOptions.AggressiveInlining)] private static void EmitEvent(AsyncThreadContext context) { Serializer.AsyncEventHeader(context, ref context.EventBuffer, AsyncEventID.ResetAsyncContinuationWrapperIndex, 0); @@ -949,7 +922,6 @@ private static void ResetContext(AsyncThreadContext context) ResumeAsyncCallstacks(context); } - [MethodImpl(MethodImplOptions.AggressiveInlining)] private static void EmitEvent(AsyncThreadContext context) { Serializer.AsyncEventHeader(context, ref context.EventBuffer, AsyncEventID.ResetAsyncThreadContext, 0); @@ -1161,6 +1133,5 @@ public AsyncThreadContextHolder(AsyncThreadContext context, Thread ownerThread) private static List s_cache = new List(); } - } } From b45d09f314d176bc35b954b076f722d347d1f3c2 Mon Sep 17 00:00:00 2001 From: lateralusX Date: Tue, 28 Apr 2026 18:51:41 +0200 Subject: [PATCH 22/22] Review feedback. --- .../AsyncProfilerTests.cs | 90 +++++++++++++++---- .../RuntimeAsyncTests.cs | 7 +- 2 files changed, 77 insertions(+), 20 deletions(-) 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 index e9a594ac20aa91..0f1ab34662abe7 100644 --- 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 @@ -69,6 +69,12 @@ public class AsyncProfilerTests 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() { @@ -1295,13 +1301,10 @@ public void RuntimeAsync_WrapperIndexMatchesCallstack() 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}"); - MethodInfo getMethodFromIP = typeof(System.Diagnostics.StackFrame).GetMethod("GetMethodFromNativeIP", BindingFlags.Static | BindingFlags.NonPublic)!; - Assert.NotNull(getMethodFromIP); - var resolvedNames = new List(); foreach (var (nativeIP, _) in chainStack.Value.Frames) { - var method = (MethodBase?)getMethodFromIP.Invoke(null, new object[] { (IntPtr)nativeIP }); + var method = GetMethodFromNativeIP(nativeIP); resolvedNames.Add(method?.Name ?? ""); } @@ -1402,7 +1405,9 @@ public void RuntimeAsync_DeadThreadFlush() thread.IsBackground = true; thread.Start(); - thread.Join(TimeSpan.FromSeconds(10)); + bool joined = thread.Join(TimeSpan.FromSeconds(10)); + + 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. @@ -1590,6 +1595,66 @@ public void RuntimeAsync_MetadataEventEmittedOnceAcrossThreads() Assert.True(metadataList.Count == 1, $"Expected exactly 1 metadata event across {threadCount} threads, got {metadataList.Count}"); } + [ConditionalFact(typeof(AsyncProfilerTests), nameof(IsRuntimeAsyncSupported))] + [ActiveIssue("https://github.com/dotnet/runtime/issues/124072", typeof(PlatformDetection), nameof(PlatformDetection.IsInterpreter))] + 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(AsyncProfilerTests), nameof(IsRuntimeAsyncSupported))] [ActiveIssue("https://github.com/dotnet/runtime/issues/124072", typeof(PlatformDetection), nameof(PlatformDetection.IsInterpreter))] public void RuntimeAsync_CallstackStressWithVaryingDepths() @@ -1635,9 +1700,6 @@ public void RuntimeAsync_CallstackStressWithVaryingDepths() callstacksWithTimestamp.Sort((a, b) => a.Timestamp.CompareTo(b.Timestamp)); // Verify all callstacks have valid frame data that resolves to managed methods. - MethodInfo getMethodFromIP = typeof(System.Diagnostics.StackFrame).GetMethod("GetMethodFromNativeIP", BindingFlags.Static | BindingFlags.NonPublic)!; - Assert.NotNull(getMethodFromIP); - foreach (var cs in callstacksWithTimestamp) { Assert.True(cs.FrameCount > 0, "Callstack has 0 frames"); @@ -1646,7 +1708,7 @@ public void RuntimeAsync_CallstackStressWithVaryingDepths() { var (nativeIP, _) = cs.Frames[f]; Assert.True(nativeIP != 0, $"Frame {f} has zero NativeIP"); - var method = (MethodBase?)getMethodFromIP.Invoke(null, new object[] { (IntPtr)nativeIP }); + var method = GetMethodFromNativeIP(nativeIP); Assert.True(method is not null, $"Frame {f}: NativeIP 0x{nativeIP:X} does not resolve to a managed method"); } @@ -1681,9 +1743,6 @@ public void RuntimeAsync_CallstackOverflowPathProducesValidFrames() // 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. - MethodInfo getMethodFromIP = typeof(System.Diagnostics.StackFrame).GetMethod("GetMethodFromNativeIP", BindingFlags.Static | BindingFlags.NonPublic)!; - Assert.NotNull(getMethodFromIP); - bool overflowDetected = false; var rng = new Random(42); @@ -1730,7 +1789,7 @@ public void RuntimeAsync_CallstackOverflowPathProducesValidFrames() { var (nativeIP, _) = frames[f]; Assert.True(nativeIP != 0, $"Overflow callstack frame {f} has zero NativeIP"); - var method = (MethodBase?)getMethodFromIP.Invoke(null, new object[] { (IntPtr)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"); } @@ -1772,13 +1831,10 @@ public void RuntimeAsync_CallstackDepthCappedAtMaxFrames() Assert.Equal(deepest.FrameCount, deepest.Frames.Count); // Verify all frames are valid. - MethodInfo getMethodFromIP = typeof(System.Diagnostics.StackFrame).GetMethod("GetMethodFromNativeIP", BindingFlags.Static | BindingFlags.NonPublic)!; - Assert.NotNull(getMethodFromIP); - foreach (var (nativeIP, _) in deepest.Frames) { Assert.True(nativeIP != 0, "Frame has zero NativeIP"); - var method = (MethodBase?)getMethodFromIP.Invoke(null, new object[] { (IntPtr)nativeIP }); + var method = GetMethodFromNativeIP(nativeIP); Assert.True(method is not null, $"NativeIP 0x{nativeIP:X} does not resolve to a managed method"); } 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 e48bdea01dfb09..aa24fe1a5bbdae 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 @@ -38,7 +38,7 @@ private static void AttachDebugger() Assert.True(flags == SynchronizeInstrumentationFlags || flags == DisabledInstrumentationFlags, $"ActiveFlags equals {flags}, expected {SynchronizeInstrumentationFlags} || {DisabledInstrumentationFlags}"); s_asyncDebuggingEnabledField.SetValue(null, true); - s_activeFlagsField.SetValue(null, (uint)s_activeFlagsField.GetValue(null) | SynchronizeInstrumentationFlags); + s_activeFlagsField.SetValue(null, flags | SynchronizeInstrumentationFlags); // Run an async method to trigger SyncActiveFlags which will pick up the Synchronize bit. Func().GetAwaiter().GetResult(); @@ -62,14 +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_activeFlagsField.SetValue(null, (uint)s_activeFlagsField.GetValue(null) | SynchronizeInstrumentationFlags); + s_activeFlagsField.SetValue(null, flags | SynchronizeInstrumentationFlags); // Run an async method to trigger SyncActiveFlags which will detect // s_asyncDebuggingEnabled is false and clear the debugger flags. Func().GetAwaiter().GetResult(); - uint flags = Convert.ToUInt32(s_activeFlagsField.GetValue(null)); + flags = Convert.ToUInt32(s_activeFlagsField.GetValue(null)); Assert.True(flags == DisabledInstrumentationFlags, $"ActiveFlags equals {flags}, expected {DisabledInstrumentationFlags}"); } }