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 ce2c1251f9da81..d176067a553366 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 @@ -93,15 +93,16 @@ public static void Suspend(ref AsyncDispatcherInfo info, Continuation nextContin 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); } + + if (IsEnabled.SuspendAsyncContextEvent(activeEventKeywords)) + { + EmitEvent(context, currentTimestamp); + } } AsyncThreadContext.Release(context); @@ -147,12 +148,6 @@ internal static partial class ContinuationWrapper /// public const string NameTemplate = "Continuation_Wrapper_{0}"; - /// - /// Number of distinct wrapper methods. The wrapper index rotates modulo this value. - /// - public const byte COUNT = 32; - public const byte COUNT_MASK = COUNT - 1; - public static void InitInfo(ref Info info) { info.ContinuationTable = ref Unsafe.As(ref s_continuationWrappers); @@ -433,23 +428,48 @@ private static unsafe void ResumeRuntimeAsyncCallstacks(AsyncDispatcherInfo* inf private static partial class AsyncCallstack { - private const int MaxAsyncMethodFrameSize = Serializer.MaxCompressedUInt64Size + Serializer.MaxCompressedUInt32Size; + private const int MaxRuntimeAsyncMethodFrameSize = Serializer.MaxCompressedUInt64Size + Serializer.MaxCompressedUInt32Size; - public ref struct CaptureRuntimeAsyncCallstackState + private ref struct CaptureRuntimeAsyncCallstackState : ICaptureAsyncCallstack { public Continuation? Continuation; public ulong LastNativeIP; public byte Count; + + public bool Capture(byte[] buffer, ref int index, out byte count) + { + bool result = CaptureRuntimeAsyncCallstack(buffer, ref index, ref this); + count = Count; + return result; + } + + public static AsyncCallstackType CallstackType => AsyncCallstackType.Runtime; + public static int MaxAsyncMethodFrameSize => MaxRuntimeAsyncMethodFrameSize; } - public static bool CaptureRuntimeAsyncCallstack(byte[] buffer, ref int index, ref CaptureRuntimeAsyncCallstackState state) + public static void EmitEvent(AsyncThreadContext context, long currentTimestamp, ulong id, Continuation? asyncCallstack) + { + EmitEvent(context, currentTimestamp, AsyncEventID.ResumeAsyncCallstack, id, asyncCallstack); + } + + public static void EmitEvent(AsyncThreadContext context, long currentTimestamp, AsyncEventID eventID, ulong id, Continuation? asyncCallstack) + { + if (asyncCallstack != null) + { + CaptureRuntimeAsyncCallstackState state = default; + state.Continuation = asyncCallstack; + EmitAsyncCallstack(context, currentTimestamp, currentTimestamp - context.LastEventTimestamp, eventID, id, ref state); + } + } + + private static bool CaptureRuntimeAsyncCallstack(byte[] buffer, ref int index, ref CaptureRuntimeAsyncCallstackState state) { if (index > buffer.Length || state.Continuation == null) { return false; } - byte maxAsyncCallstackFrames = (byte)Math.Min(byte.MaxValue, (buffer.Length - index) / MaxAsyncMethodFrameSize); + byte maxAsyncCallstackFrames = (byte)Math.Min(byte.MaxValue, (buffer.Length - index) / MaxRuntimeAsyncMethodFrameSize); if (maxAsyncCallstackFrames == 0) { return false; @@ -505,130 +525,6 @@ public static bool CaptureRuntimeAsyncCallstack(byte[] buffer, ref int index, re return state.Continuation == null || state.Count == byte.MaxValue; } - - public static void EmitEvent(AsyncThreadContext context, long currentTimestamp, ulong id, Continuation? asyncCallstack) - { - EmitEvent(context, currentTimestamp, AsyncEventID.ResumeAsyncCallstack, id, AsyncCallstackType.Runtime, asyncCallstack); - } - - public static void EmitEvent(AsyncThreadContext context, long currentTimestamp, AsyncEventID eventID, ulong id, Continuation? asyncCallstack) - { - EmitEvent(context, currentTimestamp, eventID, id, AsyncCallstackType.Runtime, asyncCallstack); - } - - public static void EmitEvent(AsyncThreadContext context, long currentTimestamp, AsyncEventID eventID, ulong id, AsyncCallstackType type, Continuation? asyncCallstack) - { - EmitEvent(context, currentTimestamp, currentTimestamp - context.LastEventTimestamp, eventID, id, type, asyncCallstack); - } - - public static void EmitEvent(AsyncThreadContext context, long currentTimestamp, long delta, AsyncEventID eventID, ulong id, AsyncCallstackType type, Continuation? asyncCallstack) - { - if (asyncCallstack != null) - { - ref EventBuffer eventBuffer = ref context.EventBuffer; - - // Max callstack data that can fit in the buffer after flush. - int maxCallstackBytes = Math.Min( - byte.MaxValue * MaxAsyncMethodFrameSize, - eventBuffer.Data.Length); - - CaptureRuntimeAsyncCallstackState state = default; - state.Continuation = asyncCallstack; - - // Static callstack payload: type (1) + callstackId (1) + frameCount (1) + id (max 10 bytes compressed). - const int MaxStaticEventPayloadSize = sizeof(byte) + sizeof(byte) + sizeof(byte) + Serializer.MaxCompressedUInt64Size; - - if (Serializer.AsyncEventHeader(context, ref eventBuffer, currentTimestamp, delta, eventID, MaxStaticEventPayloadSize, out Serializer.AsyncEventHeaderRollbackData rollbackData)) - { - int frameCountOffset = CallstackHeader(ref eventBuffer, id, type, 0); - - byte[] buffer = eventBuffer.Data; - int startIndex = eventBuffer.Index; - int currentIndex = startIndex; - - if (!CaptureRuntimeAsyncCallstack(buffer, ref currentIndex, ref state)) - { - byte[]? rentedArray = RentArray(maxCallstackBytes); - if (rentedArray != null) - { - int length = currentIndex - startIndex; - int index = length; - - Buffer.BlockCopy(buffer, startIndex, rentedArray, 0, length); - CaptureRuntimeAsyncCallstack(rentedArray, ref index, ref state); - - // Rollback async event header before flushing. - Serializer.RollbackAsyncEventHeader(context, in rollbackData); - context.Flush(); - - // Write the callstack again. - if (Serializer.AsyncEventHeader(context, ref eventBuffer, context.LastEventTimestamp, 0, eventID, MaxStaticEventPayloadSize + index)) - { - CallstackHeader(ref eventBuffer, id, type, state.Count); - CallstackData(ref eventBuffer, rentedArray, index); - } - - ArrayPool.Shared.Return(rentedArray); - } - else - { - // Rollback async event header since we can't write the callstack. - Serializer.RollbackAsyncEventHeader(context, in rollbackData); - } - } - else - { - // Patch frame count in the event buffer using the offset from CallstackHeader. - eventBuffer.Data[frameCountOffset] = state.Count; - eventBuffer.Index += currentIndex - startIndex; - } - } - } - } - - private static int CallstackHeader(ref EventBuffer eventBuffer, ulong id, AsyncCallstackType type, byte callstackFrameCount) - { - // Callstack header layout: type (1 byte) + callstackId (1 byte, reserved for future use) + frameCount (1 byte) + id (max 10 bytes compressed). - const int MaxCallstackHeaderSize = sizeof(byte) + sizeof(byte) + sizeof(byte) + Serializer.MaxCompressedUInt64Size; - - ref int index = ref eventBuffer.Index; - - Span callstackHeaderSpan = eventBuffer.Data.AsSpan(index, MaxCallstackHeaderSize); - int spanIndex = 0; - - callstackHeaderSpan[spanIndex++] = (byte)type; - callstackHeaderSpan[spanIndex++] = 0; // Reserved callstack ID for future callstack interning. - - int frameCountOffset = index + spanIndex; - callstackHeaderSpan[spanIndex++] = callstackFrameCount; - - spanIndex += Serializer.WriteCompressedUInt64(callstackHeaderSpan.Slice(spanIndex), id); - eventBuffer.Index += spanIndex; - - return frameCountOffset; - } - - private static void CallstackData(ref EventBuffer eventBuffer, byte[] callstackData, int callstackDataByteCount) - { - ref int index = ref eventBuffer.Index; - Buffer.BlockCopy(callstackData, 0, eventBuffer.Data, index, callstackDataByteCount); - index += callstackDataByteCount; - } - - private static byte[]? RentArray(int minimumLength) - { - byte[]? rentedArray = null; - try - { - rentedArray = ArrayPool.Shared.Rent(minimumLength); - } - catch - { - //AsyncProfiler can't throw, return null if renting fails. - } - - return rentedArray; - } } } } diff --git a/src/libraries/System.Private.CoreLib/src/System.Private.CoreLib.Shared.projitems b/src/libraries/System.Private.CoreLib/src/System.Private.CoreLib.Shared.projitems index 3668b72acffbf9..d52b66b657cc17 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 @@ -836,12 +836,13 @@ + - + @@ -943,8 +944,9 @@ - - + + + diff --git a/src/libraries/System.Private.CoreLib/src/System/Runtime/CompilerServices/AsyncMethodBuilderCore.cs b/src/libraries/System.Private.CoreLib/src/System/Runtime/CompilerServices/AsyncMethodBuilderCore.cs index 7cb85c290c7555..3c066512eac256 100644 --- a/src/libraries/System.Private.CoreLib/src/System/Runtime/CompilerServices/AsyncMethodBuilderCore.cs +++ b/src/libraries/System.Private.CoreLib/src/System/Runtime/CompilerServices/AsyncMethodBuilderCore.cs @@ -127,6 +127,20 @@ internal static Action TryGetStateMachineForDebugger(Action action) // debugger wrapper._innerTask : // A wrapped continuation, created by an awaiter continuation.Target as Task; // The continuation targets a task directly, such as with AsyncStateMachineBox + internal static IAsyncStateMachineBox? TryGetStateMachineBox(Action continuation) + { + object? target = continuation.Target; + if (target is IAsyncStateMachineBox box) + { + return box; + } + if (target is ContinuationWrapper cw) + { + return TryGetStateMachineBox(cw._continuation); + } + return null; + } + /// /// Logically we pass just an Action (delegate) to a task for its action to 'ContinueWith' when it completes. /// However debuggers and profilers need more information about what that action is. (In particular what 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 151365ecf071a2..6920fec6a326ff 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 @@ -1,12 +1,16 @@ // 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.Buffers.Binary; using System.Collections.Generic; using System.Diagnostics; using System.Diagnostics.Tracing; using System.Runtime.InteropServices; using System.Threading; +using System.Threading.Tasks; +using static System.Runtime.CompilerServices.AsyncInstrumentation; +using static System.Runtime.CompilerServices.AsyncProfiler; using static System.Runtime.CompilerServices.AsyncProfilerEventSource; using Serializer = System.Runtime.CompilerServices.AsyncProfiler.EventBuffer.Serializer; @@ -37,7 +41,8 @@ internal enum AsyncEventID : byte ResetAsyncThreadContext = 11, ResetAsyncContinuationWrapperIndex = 12, AsyncProfilerMetadata = 13, - AsyncProfilerSyncClock = 14 + AsyncProfilerSyncClock = 14, + AppendAsyncCallstack = 15 } internal ref struct Info @@ -50,7 +55,6 @@ internal ref struct Info internal static void InitInfo(ref Info info) { info.Context = null; - info.ContinuationIndex = 0; ContinuationWrapper.InitInfo(ref info); } @@ -696,6 +700,45 @@ private static AsyncThreadContext CreateAsyncThreadContext() internal static partial class CreateAsyncContext { + public static void Create(AsyncTaskDispatcher dispatcher, ref Info info, ulong id) + { + AsyncThreadContext context = AsyncThreadContext.Acquire(ref info); + + SyncPoint.Check(context); + + EventKeywords activeEventKeywords = context.ActiveEventKeywords; + if (IsEnabled.AnyAsyncEvents(activeEventKeywords)) + { + long currentTimestamp = Stopwatch.GetTimestamp(); + if (IsEnabled.ResumeAsyncCallstackEvent(activeEventKeywords)) + { + ResumeAsyncContext.Append(dispatcher, context, currentTimestamp); + } + + if (IsEnabled.CreateAsyncContextEvent(activeEventKeywords)) + { + EmitEvent(context, currentTimestamp, id); + } + } + + AsyncThreadContext.Release(context); + } + + public static void Create(ulong id) + { + Info info = default; + AsyncThreadContext context = AsyncThreadContext.Acquire(ref info); + + SyncPoint.Check(context); + + if (IsEnabled.CreateAsyncContextEvent(context.ActiveEventKeywords)) + { + EmitEvent(context, Stopwatch.GetTimestamp(), id); + } + + AsyncThreadContext.Release(context); + } + public static void EmitEvent(AsyncThreadContext context, long currentTimestamp, ulong id) { const int MaxEventPayloadSize = Serializer.MaxCompressedUInt64Size; @@ -710,6 +753,64 @@ public static void EmitEvent(AsyncThreadContext context, long currentTimestamp, internal static partial class ResumeAsyncContext { + public static ulong GetId(AsyncTaskDispatcher dispatcher) => dispatcher.ContextId; + + public static ulong GetId(ref AsyncTaskDispatcherInfo info) + { + if (info.Dispatcher != null) + { + return info.Dispatcher.ContextId; + } + + return 0; + } + + public static void Resume(ref AsyncTaskDispatcherInfo info) + { + AsyncThreadContext context = AsyncThreadContext.Acquire(ref info.AsyncProfilerInfo); + + // TODO: SyncPoint.Check needs to re-emit V1 contexts (like V2 does). + // Temporarily bypass so Resume always fires. + SyncPoint.Check(context); + + EventKeywords activeEventKeywords = context.ActiveEventKeywords; + if (IsEnabled.AnyAsyncEvents(activeEventKeywords)) + { + long currentTimestamp = Stopwatch.GetTimestamp(); + ulong id = GetId(ref info); + if (IsEnabled.ResumeAsyncContextEvent(activeEventKeywords)) + { + EmitEvent(context, currentTimestamp, id); + } + + if (IsEnabled.ResumeAsyncCallstackEvent(activeEventKeywords) && info.Dispatcher != null) + { + AsyncCallstack.EmitEvent(info.Dispatcher, context, currentTimestamp, id); + } + } + + AsyncThreadContext.Release(context); + } + + public static void Append(AsyncTaskDispatcher dispatcher, AsyncThreadContext context, long currentTimestamp) + { + if (IsEnabled.ResumeAsyncCallstackEvent(context.ActiveEventKeywords) && dispatcher.ContinuationChainChanged) + { + AsyncCallstack.EmitEvent(dispatcher, context, dispatcher.LastContinuation?.ContinuationForDiagnostics, currentTimestamp, AsyncEventID.AppendAsyncCallstack, GetId(dispatcher)); + } + } + + public static void Append(AsyncTaskDispatcher dispatcher, IAsyncStateMachineBox enteringBox, AsyncThreadContext context, long currentTimestamp) + { + if (IsEnabled.ResumeAsyncCallstackEvent(context.ActiveEventKeywords) && dispatcher.ReachedLastContinuation) + { + if (!ReferenceEquals(enteringBox, dispatcher.LastContinuation)) + { + AsyncCallstack.EmitEvent(dispatcher, context, enteringBox, currentTimestamp, AsyncEventID.AppendAsyncCallstack, GetId(dispatcher)); + } + } + } + public static void EmitEvent(AsyncThreadContext context, long currentTimestamp, ulong id) { const int MaxEventPayloadSize = Serializer.MaxCompressedUInt64Size; @@ -732,6 +833,30 @@ public static void EmitEvent(AsyncThreadContext context, long currentTimestamp) internal static partial class CompleteAsyncContext { + public static void Complete(AsyncTaskDispatcher dispatcher, ref Info info) + { + AsyncThreadContext context = AsyncThreadContext.Acquire(ref info); + + SyncPoint.Check(context); + + EventKeywords activeEventKeywords = context.ActiveEventKeywords; + if (IsEnabled.AnyAsyncEvents(activeEventKeywords)) + { + long currentTimestamp = Stopwatch.GetTimestamp(); + if (IsEnabled.ResumeAsyncCallstackEvent(activeEventKeywords)) + { + ResumeAsyncContext.Append(dispatcher, context, currentTimestamp); + } + + if (IsEnabled.CompleteAsyncContextEvent(activeEventKeywords)) + { + EmitEvent(context, currentTimestamp); + } + } + + AsyncThreadContext.Release(context); + } + public static void Complete(ref Info info) { AsyncThreadContext context = AsyncThreadContext.Acquire(ref info); @@ -779,6 +904,11 @@ public static void Unhandled(ref Info info, uint unwindedFrames) } public static void Handled(ref Info info, uint unwindedFrames) + { + UnwindFrames(ref info, unwindedFrames); + } + + public static void UnwindFrames(ref Info info, uint unwindedFrames) { AsyncThreadContext context = AsyncThreadContext.Acquire(ref info); @@ -806,6 +936,30 @@ public static void EmitEvent(AsyncThreadContext context, long currentTimestamp, internal static partial class ResumeAsyncMethod { + public static void Resume(AsyncTaskDispatcher dispatcher, IAsyncStateMachineBox box, ref Info info) + { + AsyncThreadContext context = AsyncThreadContext.Acquire(ref info); + + SyncPoint.Check(context); + + EventKeywords activeEventKeywords = context.ActiveEventKeywords; + if (IsEnabled.AnyAsyncEvents(activeEventKeywords)) + { + long currentTimestamp = Stopwatch.GetTimestamp(); + if (IsEnabled.ResumeAsyncCallstackEvent(activeEventKeywords)) + { + ResumeAsyncContext.Append(dispatcher, box, context, currentTimestamp); + } + + if (IsEnabled.ResumeAsyncMethodEvent(activeEventKeywords)) + { + EmitEvent(context, currentTimestamp); + } + } + + AsyncThreadContext.Release(context); + } + public static void Resume(ref Info info) { AsyncThreadContext context = AsyncThreadContext.Acquire(ref info); @@ -823,6 +977,11 @@ public static void EmitEvent(AsyncThreadContext context) { Serializer.AsyncEventHeader(context, ref context.EventBuffer, AsyncEventID.ResumeAsyncMethod, 0); } + + public static void EmitEvent(AsyncThreadContext context, long currentTimestamp) + { + Serializer.AsyncEventHeader(context, ref context.EventBuffer, currentTimestamp, AsyncEventID.ResumeAsyncMethod, 0); + } } internal static partial class CompleteAsyncMethod @@ -848,6 +1007,20 @@ public static void EmitEvent(AsyncThreadContext context) internal static partial class ContinuationWrapper { + /// + /// Number of distinct wrapper methods. The wrapper index rotates modulo this value. + /// + public const byte COUNT = 32; + public const byte COUNT_MASK = COUNT - 1; + +#if MONO + public static void InitInfo(ref Info info) + { + info.ContinuationTable = 0; + info.ContinuationIndex = 0; + } +#endif + [MethodImpl(MethodImplOptions.AggressiveInlining)] public static void IncrementIndex(ref Info info) { @@ -913,8 +1086,9 @@ private static void ResetContext(AsyncThreadContext context) Config.EmitAsyncProfilerMetadataIfNeeded(context); EmitEvent(context); } - +#if !MONO ResumeAsyncCallstacks(context); +#endif } private static void EmitEvent(AsyncThreadContext context) @@ -1128,5 +1302,284 @@ public AsyncThreadContextHolder(AsyncThreadContext context, Thread ownerThread) private static List s_cache = new List(); } + + private static partial class AsyncCallstack + { + private const int MaxTaskAsyncMethodFrameSize = Serializer.MaxCompressedUInt64Size + Serializer.MaxCompressedUInt32Size; + + private interface ICaptureAsyncCallstack + { + bool Capture(byte[] buffer, ref int index, out byte count); + + static abstract AsyncCallstackType CallstackType { get; } + static abstract int MaxAsyncMethodFrameSize { get; } + } + + private ref struct CaptureTaskAsyncCallstackState : ICaptureAsyncCallstack + { + public object? Continuation; + public object? LastContinuation; + public ulong LastMethodId; + public byte Count; + + public bool Capture(byte[] buffer, ref int index, out byte count) + { + bool result = CaptureTaskAsyncCallstack(buffer, ref index, ref this); + count = Count; + return result; + } + + public static AsyncCallstackType CallstackType => AsyncCallstackType.Compiler; + public static int MaxAsyncMethodFrameSize => MaxTaskAsyncMethodFrameSize; + } + + public static void EmitEvent(AsyncTaskDispatcher dispatcher, AsyncThreadContext context, long currentTimestamp, ulong id) + { + EmitEvent(dispatcher, context, dispatcher.InnerBox, currentTimestamp, AsyncEventID.ResumeAsyncCallstack, id); + } + + public static void EmitEvent(AsyncTaskDispatcher dispatcher, AsyncThreadContext context, object? continuation, long currentTimestamp, AsyncEventID eventID, ulong id) + { + if (continuation != null) + { + IAsyncStateMachineBox? box = ResolveAsyncStateMachineBox(continuation); + if (box != null) + { + CaptureTaskAsyncCallstackState state = default; + state.Continuation = box; + + EmitAsyncCallstack(context, currentTimestamp, currentTimestamp - context.LastEventTimestamp, eventID, id, ref state); + + box = ResolveAsyncStateMachineBox(state.LastContinuation); + if (box != null) + { + Debug.Assert(box is Task); + dispatcher.LastContinuation = Unsafe.As(box); + } + else + { + dispatcher.LastContinuation = null; + } + } + else + { + dispatcher.LastContinuation = null; + } + + dispatcher.ReachedLastContinuation = false; + } + } + + private static bool CaptureTaskAsyncCallstack(byte[] buffer, ref int index, ref CaptureTaskAsyncCallstackState state) + { + if (index > buffer.Length || state.Continuation == null) + { + return false; + } + + byte maxAsyncCallstackFrames = (byte)Math.Min(byte.MaxValue, (buffer.Length - index) / MaxTaskAsyncMethodFrameSize); + if (maxAsyncCallstackFrames == 0) + { + return false; + } + + Span callstackSpan = buffer.AsSpan(index); + int callstackSpanIndex = 0; + ulong previousMethodId = state.LastMethodId; + + state.LastContinuation = state.Continuation; + + if (!GetFrameDiagnosticsData(state.Continuation, out ulong currentMethodId, out int methodState, out object? nextContinuation)) + { + return false; + } + + state.Continuation = nextContinuation; + + if (state.Count == 0) + { + callstackSpanIndex += Serializer.WriteCompressedUInt64(callstackSpan.Slice(callstackSpanIndex, Serializer.MaxCompressedUInt64Size), currentMethodId); + } + else + { + callstackSpanIndex += Serializer.WriteCompressedInt64(callstackSpan.Slice(callstackSpanIndex, Serializer.MaxCompressedInt64Size), (long)(currentMethodId - previousMethodId)); + } + + callstackSpanIndex += Serializer.WriteCompressedInt32(callstackSpan.Slice(callstackSpanIndex, Serializer.MaxCompressedInt32Size), methodState); + state.Count++; + + while (state.Count < maxAsyncCallstackFrames && state.Continuation != null) + { + previousMethodId = currentMethodId; + state.LastContinuation = state.Continuation; + + if (!GetFrameDiagnosticsData(state.Continuation, out currentMethodId, out methodState, out nextContinuation)) + { + state.Continuation = nextContinuation; + continue; + } + + callstackSpanIndex += Serializer.WriteCompressedInt64(callstackSpan.Slice(callstackSpanIndex, Serializer.MaxCompressedInt64Size), (long)(currentMethodId - previousMethodId)); + callstackSpanIndex += Serializer.WriteCompressedInt32(callstackSpan.Slice(callstackSpanIndex, Serializer.MaxCompressedInt32Size), methodState); + + state.Count++; + state.Continuation = nextContinuation; + } + + state.LastMethodId = currentMethodId; + index += callstackSpanIndex; + + return state.Continuation == null || state.Count == byte.MaxValue; + } + + private static bool GetFrameDiagnosticsData(object? continuationObject, out ulong methodId, out int state, out object? nextContinuation) + { + IAsyncStateMachineBox? box = ResolveAsyncStateMachineBox(continuationObject); + if (box != null) + { + return box.GetDiagnosticData(out methodId, out state, out nextContinuation); + } + + methodId = 0; + state = -1; + nextContinuation = null; + + return false; + } + + private static IAsyncStateMachineBox? ResolveAsyncStateMachineBox(object? continuationObject) + { + if (continuationObject == null) + { + return null; + } + + if (continuationObject is IAsyncStateMachineBox box) + { + return box; + } + + if (continuationObject is Action action) + { + return AsyncMethodBuilderCore.TryGetStateMachineBox(action); + } + + if (continuationObject is List list) + { + for (int i = 0; i < list.Count; i++) + { + IAsyncStateMachineBox? resolved = ResolveAsyncStateMachineBox(list[i]); + if (resolved is not null) + { + return resolved; + } + } + } + + return null; + } + + private static void EmitAsyncCallstack(AsyncThreadContext context, long currentTimestamp, long delta, AsyncEventID eventID, ulong id, ref T captureCallstack) + where T : ICaptureAsyncCallstack, allows ref struct + { + ref EventBuffer eventBuffer = ref context.EventBuffer; + + // Max callstack data that can fit in the buffer after flush. + int maxCallstackBytes = Math.Min(byte.MaxValue * T.MaxAsyncMethodFrameSize, eventBuffer.Data.Length); + + // Static callstack payload: type (1) + callstackId (1) + frameCount (1) + id (max 10 bytes compressed). + const int MaxStaticEventPayloadSize = sizeof(byte) + sizeof(byte) + sizeof(byte) + Serializer.MaxCompressedUInt64Size; + + if (Serializer.AsyncEventHeader(context, ref eventBuffer, currentTimestamp, delta, eventID, MaxStaticEventPayloadSize, out Serializer.AsyncEventHeaderRollbackData rollbackData)) + { + int frameCountOffset = CallstackHeader(ref eventBuffer, id, T.CallstackType, 0); + + byte[] buffer = eventBuffer.Data; + int startIndex = eventBuffer.Index; + int currentIndex = startIndex; + + if (!captureCallstack.Capture(buffer, ref currentIndex, out byte count)) + { + byte[]? rentedArray = RentArray(maxCallstackBytes); + if (rentedArray != null) + { + int length = currentIndex - startIndex; + int index = length; + + Buffer.BlockCopy(buffer, startIndex, rentedArray, 0, length); + captureCallstack.Capture(rentedArray, ref index, out count); + + // Rollback async event header before flushing. + Serializer.RollbackAsyncEventHeader(context, in rollbackData); + context.Flush(); + + // Write the callstack again. + if (Serializer.AsyncEventHeader(context, ref eventBuffer, context.LastEventTimestamp, 0, eventID, MaxStaticEventPayloadSize + index)) + { + CallstackHeader(ref eventBuffer, id, T.CallstackType, count); + CallstackData(ref eventBuffer, rentedArray, index); + } + + ArrayPool.Shared.Return(rentedArray); + } + else + { + // Rollback async event header since we can't write the callstack. + Serializer.RollbackAsyncEventHeader(context, in rollbackData); + } + } + else + { + // Patch frame count in the event buffer using the offset from CallstackHeader. + eventBuffer.Data[frameCountOffset] = count; + eventBuffer.Index += currentIndex - startIndex; + } + } + } + + private static int CallstackHeader(ref EventBuffer eventBuffer, ulong id, AsyncCallstackType type, byte callstackFrameCount) + { + // Callstack header layout: type (1 byte) + callstackId (1 byte, reserved for future use) + frameCount (1 byte) + id (max 10 bytes compressed). + const int MaxCallstackHeaderSize = sizeof(byte) + sizeof(byte) + sizeof(byte) + Serializer.MaxCompressedUInt64Size; + + ref int index = ref eventBuffer.Index; + + Span callstackHeaderSpan = eventBuffer.Data.AsSpan(index, MaxCallstackHeaderSize); + int spanIndex = 0; + + callstackHeaderSpan[spanIndex++] = (byte)type; + callstackHeaderSpan[spanIndex++] = 0; // Reserved callstack ID for future callstack interning. + + int frameCountOffset = index + spanIndex; + callstackHeaderSpan[spanIndex++] = callstackFrameCount; + + spanIndex += Serializer.WriteCompressedUInt64(callstackHeaderSpan.Slice(spanIndex), id); + eventBuffer.Index += spanIndex; + + return frameCountOffset; + } + + private static void CallstackData(ref EventBuffer eventBuffer, byte[] callstackData, int callstackDataByteCount) + { + ref int index = ref eventBuffer.Index; + Buffer.BlockCopy(callstackData, 0, eventBuffer.Data, index, callstackDataByteCount); + index += callstackDataByteCount; + } + + private static byte[]? RentArray(int minimumLength) + { + byte[]? rentedArray = null; + try + { + rentedArray = ArrayPool.Shared.Rent(minimumLength); + } + catch + { + //AsyncProfiler can't throw, return null if renting fails. + } + + return rentedArray; + } + } } } diff --git a/src/libraries/System.Private.CoreLib/src/System/Runtime/CompilerServices/AsyncStateMachineDiagnostics.cs b/src/libraries/System.Private.CoreLib/src/System/Runtime/CompilerServices/AsyncStateMachineDiagnostics.cs new file mode 100644 index 00000000000000..59f5e2c481c7d0 --- /dev/null +++ b/src/libraries/System.Private.CoreLib/src/System/Runtime/CompilerServices/AsyncStateMachineDiagnostics.cs @@ -0,0 +1,94 @@ +// 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; +using System.Diagnostics.CodeAnalysis; +using System.Reflection; + +namespace System.Runtime.CompilerServices +{ + internal static class AsyncStateMachineDiagnostics + where TStateMachine : IAsyncStateMachine + { +#if NATIVEAOT + // In NativeAOT we don't have reflection to resolve the method handle and state field offset. + // Due to the way the state machine is constructed, we can't get a direct pointer to its MoveNext method + // and using the interface dispatch to locate the method at slot 0 is unreliable due to Native AOT optimizations. + // The state field is also not guaranteed to be at a specific offset due to auto layout and Native AOT optimizations. + // To support this on Native AOT we would need to precompute this information in ILC and emit a + // hash table keyed by state machine MethodTable. At runtime we would still need to cache + // this data in static fields to avoid lookup cost when walking each continuation frame. + // On JIT these static fields are lazy evaluated and cached on initial access, but on Native AOT + // they will be pre-allocated, so code should be linked out when diagnostics is not supported. + // Given the added complexity on Native AOT, the fact that this is only used for diagnostics, + // and that Native AOT currently have limited asyncv1 diagnostics support in tooling, we can + // postpone the support until proven needed. + public static ulong MethodId + { + [MethodImpl(MethodImplOptions.AggressiveInlining)] + get => 0; + } + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static int GetState(ref TStateMachine? _) => -1; +#else + private static readonly ulong s_methodId = ResolveMethodId(); + private static readonly int s_resolveStateFieldOffset = ResolveStateFieldOffset(); + + public static ulong MethodId + { + [MethodImpl(MethodImplOptions.AggressiveInlining)] + get => s_methodId; + } + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static int GetState(ref TStateMachine? stateMachine) + { + if (typeof(TStateMachine).IsValueType) + { + // Struct: state field is inline at offset within the struct + return Unsafe.As(ref Unsafe.AddByteOffset(ref Unsafe.As(ref stateMachine), (nint)s_resolveStateFieldOffset)); + } + else + { + // Class (debug builds): StateMachine is a reference, dereference to get object data + if (stateMachine is not null) + { + return Unsafe.As(ref Unsafe.AddByteOffset(ref RuntimeHelpers.GetRawData(stateMachine), (nint)s_resolveStateFieldOffset)); + } + } + + return -1; + } + + [UnconditionalSuppressMessage("ReflectionAnalysis", "IL2090", Justification = "State machine types are always preserved.")] + private static ulong ResolveMethodId() + { + MethodInfo? methodInfo = typeof(TStateMachine).GetMethod("MoveNext", BindingFlags.Instance | BindingFlags.Public | BindingFlags.NonPublic); + if (methodInfo is not null) + { + return (ulong)methodInfo.MethodHandle.Value; + } + + return 0; + } + + [UnconditionalSuppressMessage("ReflectionAnalysis", "IL2090", Justification = "State machine types are always preserved.")] + private static int ResolveStateFieldOffset() + { + FieldInfo? stateField = typeof(TStateMachine).GetField("<>1__state", BindingFlags.Instance | BindingFlags.Public | BindingFlags.NonPublic); + if (stateField is not null) + { +#if MONO + return stateField.GetFieldOffset(); +#else + Debug.Assert(stateField is RtFieldInfo, $"Expected RtFieldInfo but got {stateField.GetType().Name}"); + return RuntimeFieldHandle.GetInstanceFieldOffset((RtFieldInfo)stateField); +#endif + } + + return 0; + } +#endif + } +} diff --git a/src/libraries/System.Private.CoreLib/src/System/Runtime/CompilerServices/AsyncTaskDispatcher.cs b/src/libraries/System.Private.CoreLib/src/System/Runtime/CompilerServices/AsyncTaskDispatcher.cs new file mode 100644 index 00000000000000..716b25de5c5848 --- /dev/null +++ b/src/libraries/System.Private.CoreLib/src/System/Runtime/CompilerServices/AsyncTaskDispatcher.cs @@ -0,0 +1,250 @@ +// 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; +using System.Runtime.InteropServices; +using System.Threading; +using System.Threading.Tasks; +using static System.Runtime.CompilerServices.AsyncInstrumentation; +using static System.Runtime.CompilerServices.AsyncProfiler; + +namespace System.Runtime.CompilerServices +{ + [StructLayout(LayoutKind.Explicit)] + internal unsafe ref struct AsyncTaskDispatcherInfo + { + [FieldOffset(0)] + public AsyncTaskDispatcherInfo* Next; + +#if TARGET_64BIT + [FieldOffset(8)] +#else + [FieldOffset(4)] +#endif + public AsyncTaskDispatcher? Dispatcher; + +#if TARGET_64BIT + [FieldOffset(16)] +#else + [FieldOffset(8)] +#endif + public AsyncProfiler.Info AsyncProfilerInfo; + + [ThreadStatic] + internal static unsafe AsyncTaskDispatcherInfo* t_current; + + public static bool InstrumentCheckPoint + { + [MethodImpl(MethodImplOptions.AggressiveInlining)] + get => AsyncInstrumentation.IsSupported && AsyncInstrumentation.ActiveFlags != AsyncInstrumentation.Flags.Disabled; + } + + public static bool AsyncProfilerInstrumentCheckPoint + { + [MethodImpl(MethodImplOptions.AggressiveInlining)] + get => InstrumentCheckPoint && AsyncInstrumentation.IsEnabled.AsyncProfiler(AsyncInstrumentation.SyncActiveFlags()); + } + + internal static unsafe AsyncTaskDispatcher? GetActiveDispatcher() + { + AsyncTaskDispatcherInfo* current = AsyncTaskDispatcherInfo.t_current; + if (current != null && current->Dispatcher is AsyncTaskDispatcher activeDispatcher) + { + // V1 dispatchers emit only Resume/Complete per MoveNext invocation — no Suspend + // events. Each dispatcher MoveNext is treated as a discrete unit. When a child + // wrapper is created here (parent box yielded), the parent's MoveNext will simply + // emit Complete on return; the child's lifecycle (Resume + Complete) fires later + // when its continuation runs. The logical context spans multiple dispatchers via + // shared contextId, with Resume count == Complete count as the balance invariant. + return activeDispatcher; + } + + return null; + } + + internal static unsafe void UnwindAsyncFrame() + { + AsyncTaskDispatcherInfo* current = t_current; + if (current != null) + { + AsyncProfiler.AsyncMethodException.UnwindFrames(ref current->AsyncProfilerInfo, 1); + } + } + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + internal static unsafe void TryFireResumeAsyncMethod(IAsyncStateMachineBox box, AsyncInstrumentation.Flags flags) + { + AsyncTaskDispatcherInfo* current = t_current; + if (current == null || current->Dispatcher is not AsyncTaskDispatcher activeDispatcher) + { + return; + } + + bool methodEventEnabled = AsyncInstrumentation.IsEnabled.ResumeAsyncMethod(flags); + bool callstackEnabled = AsyncInstrumentation.IsEnabled.ResumeAsyncContext(flags); + if (!methodEventEnabled && !(callstackEnabled && activeDispatcher.LastContinuation != null)) + { + return; + } + + ResumeAsyncMethod(activeDispatcher, current, box, methodEventEnabled, callstackEnabled); + } + + private static unsafe void ResumeAsyncMethod(AsyncTaskDispatcher activeDispatcher, AsyncTaskDispatcherInfo* info, IAsyncStateMachineBox box, bool methodEventEnabled, bool callstackEnabled) + { + bool callstackEventEnabled = callstackEnabled && activeDispatcher.ReachedLastContinuation; + + if (!activeDispatcher.ReachedLastContinuation && ReferenceEquals(activeDispatcher.LastContinuation, box)) + { + activeDispatcher.ReachedLastContinuation = true; + } + + if (methodEventEnabled || callstackEventEnabled) + { + AsyncProfiler.ResumeAsyncMethod.Resume(activeDispatcher, box, ref info->AsyncProfilerInfo); + } + } + + internal static unsafe void CompleteAsyncMethod() + { + AsyncTaskDispatcherInfo* current = t_current; + if (current != null) + { + AsyncProfiler.CompleteAsyncMethod.Complete(ref current->AsyncProfilerInfo); + } + } + } + + internal sealed class AsyncTaskDispatcher : Task, IAsyncStateMachineBox + { + private IAsyncStateMachineBox? _inner; + private Action? _moveNextAction; + private ulong _contextId; + + internal IAsyncStateMachineBox? InnerBox => _inner; + + internal Task? LastContinuation; + + internal bool ReachedLastContinuation; + + internal AsyncTaskDispatcher(IAsyncStateMachineBox inner) : base() + { + _inner = inner; + _contextId = 0; + } + + internal AsyncTaskDispatcher(IAsyncStateMachineBox inner, AsyncTaskDispatcher parent) : base() + { + _inner = inner; + _contextId = parent.ContextId; + } + + internal ulong ContextId + { + get + { + if (_contextId == 0) + { + return (ulong)this.Id; + } + + return _contextId; + } + } + + internal bool ContinuationChainChanged => LastContinuation?.ContinuationForDiagnostics != null; + + internal static unsafe AsyncTaskDispatcher Create(IAsyncStateMachineBox box) + { + AsyncTaskDispatcherInfo* activeInfo = AsyncTaskDispatcherInfo.t_current; + AsyncTaskDispatcher? activeDispatcher = (activeInfo != null && activeInfo->Dispatcher is AsyncTaskDispatcher d) ? d : null; + AsyncTaskDispatcher dispatcher = activeDispatcher != null + ? new AsyncTaskDispatcher(box, activeDispatcher) + : new AsyncTaskDispatcher(box); + + AsyncInstrumentation.Flags flags = AsyncInstrumentation.ActiveFlags; + if (AsyncInstrumentation.IsEnabled.CreateAsyncContext(flags) || AsyncInstrumentation.IsEnabled.ResumeAsyncContext(flags)) + { + if (activeDispatcher is not null) + { + AsyncProfiler.CreateAsyncContext.Create(activeDispatcher, ref activeInfo->AsyncProfilerInfo, (ulong)dispatcher.ContextId); + } + else + { + AsyncProfiler.CreateAsyncContext.Create((ulong)dispatcher.ContextId); + } + } + + return dispatcher; + } + + internal sealed override void ExecuteDirectly(Thread? threadPoolThread) => MoveNext(); + + public unsafe void MoveNext() + { + IAsyncStateMachineBox? inner = _inner; + if (inner is null) + { + return; + } + + AsyncTaskDispatcherInfo dispatcherInfo; + ref AsyncTaskDispatcherInfo* refCurrent = ref AsyncTaskDispatcherInfo.t_current; + AsyncTaskDispatcherInfo* previous = refCurrent; + refCurrent = &dispatcherInfo; + dispatcherInfo.Next = previous; + + dispatcherInfo.Dispatcher = this; + + AsyncInstrumentation.Flags flags = AsyncInstrumentation.SyncActiveFlags(); + AsyncProfiler.InitInfo(ref dispatcherInfo.AsyncProfilerInfo); + + if (AsyncInstrumentation.IsEnabled.ResumeAsyncContext(flags)) + { + AsyncProfiler.ResumeAsyncContext.Resume(ref dispatcherInfo); + } + + try + { + inner.MoveNext(); + } + finally + { + if (AsyncInstrumentation.IsEnabled.CompleteAsyncContext(flags)) + { + AsyncProfiler.CompleteAsyncContext.Complete(this, ref dispatcherInfo.AsyncProfilerInfo); + } + + refCurrent = dispatcherInfo.Next; + } + } + + public Action MoveNextAction => _moveNextAction ??= MoveNext; + + public IAsyncStateMachine GetStateMachineObject() + { + IAsyncStateMachineBox? inner = _inner; + return inner is not null ? inner.GetStateMachineObject() : null!; + } + + public void ClearStateUponCompletion() + { + _inner?.ClearStateUponCompletion(); + _inner = null; + } + + public bool GetDiagnosticData(out ulong methodId, out int state, out object? nextContinuation) + { + IAsyncStateMachineBox? inner = _inner; + if (inner != null) + { + return inner.GetDiagnosticData(out methodId, out state, out nextContinuation); + } + + methodId = 0; + state = -1; + nextContinuation = null; + return false; + } + } +} diff --git a/src/libraries/System.Private.CoreLib/src/System/Runtime/CompilerServices/AsyncTaskMethodBuilderT.cs b/src/libraries/System.Private.CoreLib/src/System/Runtime/CompilerServices/AsyncTaskMethodBuilderT.cs index 8d3e233865c558..c1ede9d94cd785 100644 --- a/src/libraries/System.Private.CoreLib/src/System/Runtime/CompilerServices/AsyncTaskMethodBuilderT.cs +++ b/src/libraries/System.Private.CoreLib/src/System/Runtime/CompilerServices/AsyncTaskMethodBuilderT.cs @@ -353,6 +353,11 @@ private void MoveNext(Thread? threadPoolThread) { Debug.Assert(!IsCompleted); + if (AsyncTaskDispatcherInfo.AsyncProfilerInstrumentCheckPoint) + { + AsyncTaskDispatcherInfo.TryFireResumeAsyncMethod(this, AsyncInstrumentation.ActiveFlags); + } + bool loggingOn = TplEventSource.Log.IsEnabled(); if (loggingOn) { @@ -421,6 +426,23 @@ public void ClearStateUponCompletion() /// Gets the state machine as a boxed object. This should only be used for debugging purposes. IAsyncStateMachine IAsyncStateMachineBox.GetStateMachineObject() => StateMachine!; // likely boxes, only use for debugging + + bool IAsyncStateMachineBox.GetDiagnosticData(out ulong methodId, out int state, out object? nextContinuation) + { + if (AsyncTaskDispatcherInfo.InstrumentCheckPoint) + { + methodId = AsyncStateMachineDiagnostics.MethodId; + state = AsyncStateMachineDiagnostics.GetState(ref StateMachine); + nextContinuation = this.ContinuationForDiagnostics; + return true; + } + + methodId = 0; + state = -1; + nextContinuation = null; + return false; + } + } /// Gets the for this builder. @@ -486,6 +508,14 @@ internal static void SetExistingTaskResult(Task task, TResult? result) { Debug.Assert(task != null, "Expected non-null task"); + if (AsyncTaskDispatcherInfo.AsyncProfilerInstrumentCheckPoint) + { + if (AsyncInstrumentation.IsEnabled.CompleteAsyncMethod(AsyncInstrumentation.ActiveFlags)) + { + AsyncTaskDispatcherInfo.CompleteAsyncMethod(); + } + } + if (TplEventSource.Log.IsEnabled()) { TplEventSource.Log.TraceOperationEnd(task.Id, AsyncCausalityStatus.Completed); @@ -516,6 +546,14 @@ internal static void SetException(Exception exception, ref Task? taskFi // Get the task, forcing initialization if it hasn't already been initialized. Task task = (taskField ??= new Task()); + if (AsyncTaskDispatcherInfo.AsyncProfilerInstrumentCheckPoint) + { + if (AsyncInstrumentation.IsEnabled.UnwindAsyncException(AsyncInstrumentation.ActiveFlags)) + { + AsyncTaskDispatcherInfo.UnwindAsyncFrame(); + } + } + // If the exception represents cancellation, cancel the task. Otherwise, fault the task. bool successfullySet = exception is OperationCanceledException oce ? task.TrySetCanceled(oce.CancellationToken, oce) : diff --git a/src/libraries/System.Private.CoreLib/src/System/Runtime/CompilerServices/ConfiguredValueTaskAwaitable.cs b/src/libraries/System.Private.CoreLib/src/System/Runtime/CompilerServices/ConfiguredValueTaskAwaitable.cs index 3ed0a443e72d17..390d26bf518054 100644 --- a/src/libraries/System.Private.CoreLib/src/System/Runtime/CompilerServices/ConfiguredValueTaskAwaitable.cs +++ b/src/libraries/System.Private.CoreLib/src/System/Runtime/CompilerServices/ConfiguredValueTaskAwaitable.cs @@ -102,6 +102,11 @@ void IStateMachineBoxAwareAwaiter.AwaitUnsafeOnCompleted(IAsyncStateMachineBox b } else if (obj != null) { + if (AsyncTaskDispatcherInfo.AsyncProfilerInstrumentCheckPoint) + { + box = AsyncTaskDispatcher.Create(box); + } + Unsafe.As(obj).OnCompleted(ThreadPool.s_invokeAsyncStateMachineBox, box, _value._token, _value._continueOnCapturedContext ? ValueTaskSourceOnCompletedFlags.UseSchedulingContext : ValueTaskSourceOnCompletedFlags.None); } @@ -207,6 +212,11 @@ void IStateMachineBoxAwareAwaiter.AwaitUnsafeOnCompleted(IAsyncStateMachineBox b } else if (obj != null) { + if (AsyncTaskDispatcherInfo.AsyncProfilerInstrumentCheckPoint) + { + box = AsyncTaskDispatcher.Create(box); + } + Unsafe.As>(obj).OnCompleted(ThreadPool.s_invokeAsyncStateMachineBox, box, _value._token, _value._continueOnCapturedContext ? ValueTaskSourceOnCompletedFlags.UseSchedulingContext : ValueTaskSourceOnCompletedFlags.None); } diff --git a/src/libraries/System.Private.CoreLib/src/System/Runtime/CompilerServices/IAsyncStateMachineBox.cs b/src/libraries/System.Private.CoreLib/src/System/Runtime/CompilerServices/IAsyncStateMachineBox.cs index b42401600c91b0..834fa016e5e7fd 100644 --- a/src/libraries/System.Private.CoreLib/src/System/Runtime/CompilerServices/IAsyncStateMachineBox.cs +++ b/src/libraries/System.Private.CoreLib/src/System/Runtime/CompilerServices/IAsyncStateMachineBox.cs @@ -22,5 +22,8 @@ internal interface IAsyncStateMachineBox /// Clears the state of the box. void ClearStateUponCompletion(); + + /// Gets the state machine diagnostic data. + bool GetDiagnosticData(out ulong methodId, out int state, out object? nextContinuation); } } diff --git a/src/libraries/System.Private.CoreLib/src/System/Runtime/CompilerServices/PoolingAsyncValueTaskMethodBuilderT.cs b/src/libraries/System.Private.CoreLib/src/System/Runtime/CompilerServices/PoolingAsyncValueTaskMethodBuilderT.cs index 2c68e487d93c73..6cc757078054d5 100644 --- a/src/libraries/System.Private.CoreLib/src/System/Runtime/CompilerServices/PoolingAsyncValueTaskMethodBuilderT.cs +++ b/src/libraries/System.Private.CoreLib/src/System/Runtime/CompilerServices/PoolingAsyncValueTaskMethodBuilderT.cs @@ -442,6 +442,15 @@ void IValueTaskSource.GetResult(short token) /// Gets the state machine as a boxed object. This should only be used for debugging purposes. IAsyncStateMachine IAsyncStateMachineBox.GetStateMachineObject() => StateMachine!; // likely boxes, only use for debugging + + bool IAsyncStateMachineBox.GetDiagnosticData(out ulong methodId, out int state, out object? nextContinuation) + { + // TODO-AsyncProfiler: Implement when pooling async builders are fully supported in AsyncProfiler. + methodId = 0; + state = -1; + nextContinuation = null; + return false; + } } } } diff --git a/src/libraries/System.Private.CoreLib/src/System/Runtime/CompilerServices/TaskAwaiter.cs b/src/libraries/System.Private.CoreLib/src/System/Runtime/CompilerServices/TaskAwaiter.cs index 4a07ab7eed5b99..b357126c6eb80e 100644 --- a/src/libraries/System.Private.CoreLib/src/System/Runtime/CompilerServices/TaskAwaiter.cs +++ b/src/libraries/System.Private.CoreLib/src/System/Runtime/CompilerServices/TaskAwaiter.cs @@ -194,6 +194,23 @@ internal static void UnsafeOnCompletedInternal(Task task, IAsyncStateMachineBox { Debug.Assert(stateMachineBox != null); + if (AsyncTaskDispatcherInfo.AsyncProfilerInstrumentCheckPoint) + { + if (task is not IAsyncStateMachineBox) + { + stateMachineBox = AsyncTaskDispatcher.Create(stateMachineBox); + } + else if (continueOnCapturedContext) + { + bool customSyncContext = SynchronizationContext.Current is SynchronizationContext syncCtx && syncCtx.GetType() != typeof(SynchronizationContext); + bool customTaskScheduler = TaskScheduler.InternalCurrent is TaskScheduler scheduler && scheduler != TaskScheduler.Default; + if (customSyncContext || customTaskScheduler) + { + stateMachineBox = AsyncTaskDispatcher.Create(stateMachineBox); + } + } + } + // If TaskWait* ETW events are enabled, trace a beginning event for this await // and set up an ending event to be traced when the asynchronous await completes. if (TplEventSource.Log.IsEnabled() || Task.s_asyncDebuggingEnabled) diff --git a/src/libraries/System.Private.CoreLib/src/System/Runtime/CompilerServices/ValueTaskAwaiter.cs b/src/libraries/System.Private.CoreLib/src/System/Runtime/CompilerServices/ValueTaskAwaiter.cs index c242ad3f2ad203..5c9ace7fe0d4e9 100644 --- a/src/libraries/System.Private.CoreLib/src/System/Runtime/CompilerServices/ValueTaskAwaiter.cs +++ b/src/libraries/System.Private.CoreLib/src/System/Runtime/CompilerServices/ValueTaskAwaiter.cs @@ -94,6 +94,11 @@ void IStateMachineBoxAwareAwaiter.AwaitUnsafeOnCompleted(IAsyncStateMachineBox b } else if (obj != null) { + if (AsyncTaskDispatcherInfo.AsyncProfilerInstrumentCheckPoint) + { + box = AsyncTaskDispatcher.Create(box); + } + Unsafe.As(obj).OnCompleted(ThreadPool.s_invokeAsyncStateMachineBox, box, _value._token, ValueTaskSourceOnCompletedFlags.UseSchedulingContext); } else @@ -176,6 +181,11 @@ void IStateMachineBoxAwareAwaiter.AwaitUnsafeOnCompleted(IAsyncStateMachineBox b } else if (obj != null) { + if (AsyncTaskDispatcherInfo.AsyncProfilerInstrumentCheckPoint) + { + box = AsyncTaskDispatcher.Create(box); + } + Unsafe.As>(obj).OnCompleted(ThreadPool.s_invokeAsyncStateMachineBox, box, _value._token, ValueTaskSourceOnCompletedFlags.UseSchedulingContext); } else diff --git a/src/libraries/System.Private.CoreLib/src/System/Runtime/CompilerServices/YieldAwaitable.cs b/src/libraries/System.Private.CoreLib/src/System/Runtime/CompilerServices/YieldAwaitable.cs index d9bea6410babbb..26a5cb3db94c11 100644 --- a/src/libraries/System.Private.CoreLib/src/System/Runtime/CompilerServices/YieldAwaitable.cs +++ b/src/libraries/System.Private.CoreLib/src/System/Runtime/CompilerServices/YieldAwaitable.cs @@ -116,6 +116,11 @@ void IStateMachineBoxAwareAwaiter.AwaitUnsafeOnCompleted(IAsyncStateMachineBox b { Debug.Assert(box != null); + if (AsyncTaskDispatcherInfo.AsyncProfilerInstrumentCheckPoint) + { + box = AsyncTaskDispatcher.Create(box); + } + // If tracing is enabled, delegate the Action-based implementation. if (TplEventSource.Log.IsEnabled()) { diff --git a/src/libraries/System.Private.CoreLib/src/System/Threading/Tasks/Task.cs b/src/libraries/System.Private.CoreLib/src/System/Threading/Tasks/Task.cs index 5381d2d4418d0c..dd55f66a70dc7f 100644 --- a/src/libraries/System.Private.CoreLib/src/System/Threading/Tasks/Task.cs +++ b/src/libraries/System.Private.CoreLib/src/System/Threading/Tasks/Task.cs @@ -7224,6 +7224,8 @@ internal static Task CreateUnwrapPromise(Task outerTask, bool return new UnwrapPromise(outerTask, lookForOce); } + internal object? ContinuationForDiagnostics => m_continuationObject; + internal virtual Delegate[]? GetDelegateContinuationsForDebugger() { // Avoid an infinite loop by making sure the continuation object is not a reference to istelf. diff --git a/src/libraries/System.Private.CoreLib/src/System/Threading/Tasks/TaskContinuation.cs b/src/libraries/System.Private.CoreLib/src/System/Threading/Tasks/TaskContinuation.cs index a99b3425f9b4d9..0488aec05cf836 100644 --- a/src/libraries/System.Private.CoreLib/src/System/Threading/Tasks/TaskContinuation.cs +++ b/src/libraries/System.Private.CoreLib/src/System/Threading/Tasks/TaskContinuation.cs @@ -768,6 +768,11 @@ internal static void RunOrScheduleAction(IAsyncStateMachineBox box, bool allowIn // If we're not allowed to run here, schedule the action if (!allowInlining || !IsValidLocationForInlining) { + if (AsyncTaskDispatcherInfo.AsyncProfilerInstrumentCheckPoint) + { + box = AsyncTaskDispatcher.Create(box); + } + // If logging is disabled, we can simply queue the box itself as a custom work // item, and its work item execution will just invoke its MoveNext. However, if // logging is enabled, there is pre/post-work we need to do around logging to 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 65e376c0282e48..0918a9c5807040 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 @@ -30,10 +30,19 @@ public enum AsyncEventID : byte ResetAsyncContinuationWrapperIndex = 12, AsyncProfilerMetadata = 13, AsyncProfilerSyncClock = 14, + AppendAsyncCallstack = 15, + } + + //Mirrors AsyncProfiler.AsyncCallstackType from the runtime (which is internal and inaccessible from tests). + public enum AsyncCallstackType : byte + { + Compiler = 0x1, + Runtime = 0x2, + Cached = 0x80 } [ActiveIssue("https://github.com/dotnet/runtime/issues/127951", TestPlatforms.Android | TestPlatforms.iOS | TestPlatforms.tvOS | TestPlatforms.MacCatalyst)] - public class AsyncProfilerTests + public partial class AsyncProfilerTests { // The test scenarios drive async work via Task.Run(...).GetAwaiter().GetResult() (see // RunScenarioAndFlush / RunScenario), which requires synchronous blocking waits. On @@ -85,161 +94,57 @@ public class AsyncProfilerTests CompleteAsyncContextKeyword | CompleteAsyncMethodKeyword | UnwindAsyncExceptionKeyword; // CoreCLR has StackFrame.GetMethodFromNativeIP (static, non-public). - // NativeAOT lacks that but has an internal StackFrame(IntPtr, bool) constructor; - // we resolve the name via DiagnosticMethodInfo.Create(frame). private static readonly MethodInfo? s_getMethodFromNativeIPMethod = typeof(StackFrame).GetMethod("GetMethodFromNativeIP", BindingFlags.Static | BindingFlags.NonPublic); + // NativeAOT has DiagnosticMethodInfo.Create(StackFrame) (instance, non-public). private static readonly ConstructorInfo? s_stackFrameFromIPCtor = s_getMethodFromNativeIPMethod is null ? typeof(StackFrame).GetConstructor(BindingFlags.Instance | BindingFlags.NonPublic, null, new[] { typeof(IntPtr), typeof(bool) }, null) : null; - internal static string? GetMethodNameFromNativeIP(ulong nativeIP) - { - if (s_getMethodFromNativeIPMethod is not null) - { - var method = (MethodBase?)s_getMethodFromNativeIPMethod.Invoke(null, new object[] { (IntPtr)nativeIP }); - return method?.Name; - } - - if (s_stackFrameFromIPCtor is not null) - { - var frame = (StackFrame)s_stackFrameFromIPCtor.Invoke(new object[] { (IntPtr)nativeIP, false })!; - var diagInfo = DiagnosticMethodInfo.Create(frame); - return diagInfo?.Name; - } - - return null; - } - - [System.Runtime.CompilerServices.RuntimeAsyncMethodGeneration(true)] - [System.Runtime.CompilerServices.MethodImpl(System.Runtime.CompilerServices.MethodImplOptions.NoInlining)] - static async Task SingleAsyncYield() - { - await Task.Yield(); - } - - [System.Runtime.CompilerServices.RuntimeAsyncMethodGeneration(true)] - [System.Runtime.CompilerServices.MethodImpl(System.Runtime.CompilerServices.MethodImplOptions.NoInlining)] - static async Task ChainedAsyncYield() - { - await InnerAsyncYield(); - } - - [System.Runtime.CompilerServices.RuntimeAsyncMethodGeneration(true)] - [System.Runtime.CompilerServices.MethodImpl(System.Runtime.CompilerServices.MethodImplOptions.NoInlining)] - static async Task InnerAsyncYield() - { - await Task.Yield(); - } - - [System.Runtime.CompilerServices.RuntimeAsyncMethodGeneration(true)] - [System.Runtime.CompilerServices.MethodImpl(System.Runtime.CompilerServices.MethodImplOptions.NoInlining)] - static async Task OuterCatches() - { - try - { - await InnerThrows(); - } - catch (InvalidOperationException) - { - } - await Task.Yield(); - } - - [System.Runtime.CompilerServices.RuntimeAsyncMethodGeneration(true)] - [System.Runtime.CompilerServices.MethodImpl(System.Runtime.CompilerServices.MethodImplOptions.NoInlining)] - static async Task InnerThrows() - { - await Task.Yield(); - throw new InvalidOperationException("inner"); - } - - [System.Runtime.CompilerServices.RuntimeAsyncMethodGeneration(true)] - [System.Runtime.CompilerServices.MethodImpl(System.Runtime.CompilerServices.MethodImplOptions.NoInlining)] - static async Task DeepOuterCatches() + private static string? GetMethodNameFromMethodId(AsyncCallstackType callstackType, ulong methodId) { - try - { - await DeepMiddle(); - } - catch (InvalidOperationException) + if (methodId != 0) { - } - } - - [System.Runtime.CompilerServices.RuntimeAsyncMethodGeneration(true)] - [System.Runtime.CompilerServices.MethodImpl(System.Runtime.CompilerServices.MethodImplOptions.NoInlining)] - static async Task DeepMiddle() - { - await DeepInnerThrows(); - } - - [System.Runtime.CompilerServices.RuntimeAsyncMethodGeneration(true)] - [System.Runtime.CompilerServices.MethodImpl(System.Runtime.CompilerServices.MethodImplOptions.NoInlining)] - static async Task DeepInnerThrows() - { - await Task.Yield(); - throw new InvalidOperationException("deep inner"); - } + if (callstackType == AsyncCallstackType.Runtime) + { + if (s_getMethodFromNativeIPMethod is not null) + { + MethodBase? method = (MethodBase?)s_getMethodFromNativeIPMethod.Invoke(null, new object[] { (IntPtr)methodId }); + return method?.Name; + } - [System.Runtime.CompilerServices.RuntimeAsyncMethodGeneration(true)] - [System.Runtime.CompilerServices.MethodImpl(System.Runtime.CompilerServices.MethodImplOptions.NoInlining)] - static async Task DeepUnhandledOuter() - { - await DeepUnhandledMiddle(); - } + if (s_stackFrameFromIPCtor is not null) + { + StackFrame frame = (StackFrame)s_stackFrameFromIPCtor.Invoke(new object[] { (IntPtr)methodId, false })!; + DiagnosticMethodInfo? diagInfo = DiagnosticMethodInfo.Create(frame); + return diagInfo?.Name; + } + } + else if (callstackType == AsyncCallstackType.Compiler) + { + System.RuntimeMethodHandle handle = RuntimeMethodHandle.FromIntPtr((IntPtr)methodId); + MethodBase? method = MethodBase.GetMethodFromHandle(handle); + if (method != null) + { + string methodName = method.DeclaringType.Name; - [System.Runtime.CompilerServices.RuntimeAsyncMethodGeneration(true)] - [System.Runtime.CompilerServices.MethodImpl(System.Runtime.CompilerServices.MethodImplOptions.NoInlining)] - static async Task DeepUnhandledMiddle() - { - await DeepUnhandledInnerThrows(); - } + int start = methodName.IndexOf('<'); + int end = methodName.IndexOf('>'); - [System.Runtime.CompilerServices.RuntimeAsyncMethodGeneration(true)] - [System.Runtime.CompilerServices.MethodImpl(System.Runtime.CompilerServices.MethodImplOptions.NoInlining)] - static async Task DeepUnhandledInnerThrows() - { - await Task.Yield(); - throw new InvalidOperationException("deep unhandled"); - } + start++; + if (start > 0 && end > start) + { + methodName = methodName.Substring(start, end - start); + } - [System.Runtime.CompilerServices.RuntimeAsyncMethodGeneration(true)] - [System.Runtime.CompilerServices.MethodImpl(System.Runtime.CompilerServices.MethodImplOptions.NoInlining)] - static async Task RecursiveAsyncChain(int depth) - { - if (depth <= 1) - { - await Task.Yield(); - return; + return methodName; + } + } } - await RecursiveAsyncChain(depth - 1); - } - - [System.Runtime.CompilerServices.RuntimeAsyncMethodGeneration(true)] - [System.Runtime.CompilerServices.MethodImpl(System.Runtime.CompilerServices.MethodImplOptions.NoInlining)] - 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)] - [System.Runtime.CompilerServices.MethodImpl(System.Runtime.CompilerServices.MethodImplOptions.NoInlining)] - 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)] - [System.Runtime.CompilerServices.MethodImpl(System.Runtime.CompilerServices.MethodImplOptions.NoInlining)] - static async Task WrapperTestC(List<(string MethodName, int WrapperSlot)> captures) - { - await Task.Yield(); - captures.Add((nameof(WrapperTestC), GetCurrentWrapperSlot(nameof(WrapperTestC)))); + return null; } private static TestEventListener CreateListener(EventKeywords keywords) @@ -277,52 +182,63 @@ private static int GetCurrentWrapperSlot(string resumedMethodName) string? name = GetFrameMethodName(st.GetFrame(i)); if (name is not null && name.Contains(resumedMethodName)) { - // Scan subsequent frames, skipping unresolvable stubs (e.g. delegate invoke thunks on NativeAOT). for (int j = i + 1; j < st.FrameCount; j++) { string? wrapperName = GetFrameMethodName(st.GetFrame(j)); if (wrapperName is null) + { continue; + } + if (wrapperName.StartsWith(WrapperNamePrefix, StringComparison.Ordinal)) { string wrapperSuffix = wrapperName.Substring(WrapperNamePrefix.Length); return int.TryParse(wrapperSuffix, out int wrapperSlot) ? wrapperSlot : -1; } + break; } + return -1; } } + return -1; } private static string? GetFrameMethodName(StackFrame? frame) { if (frame is null) + { return null; + } + string? name = frame.GetMethod()?.Name; if (name is null) { name = DiagnosticMethodInfo.Create(frame)?.Name; } + return name; } 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 delegate bool EventVisitorWithTimestamp(AsyncEventID eventId, long timestamp, ReadOnlySpan buffer, ref int index); + 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; @@ -330,7 +246,9 @@ private static void ParseEventBuffer(ReadOnlySpan buffer, EventVisitorWith while (index < buffer.Length) { if (index + 2 > buffer.Length) + { break; + } AsyncEventID eventId = (AsyncEventID)buffer[index++]; @@ -338,7 +256,9 @@ private static void ParseEventBuffer(ReadOnlySpan buffer, EventVisitorWith baseTimestamp += delta; if (!visitor(eventId, baseTimestamp, buffer, ref index)) + { break; + } } } @@ -348,44 +268,59 @@ private static bool SkipEventPayload(AsyncEventID eventId, ReadOnlySpan bu { case AsyncEventID.CreateAsyncContext: case AsyncEventID.ResumeAsyncContext: + { ReadCompressedUInt64(buffer, ref index); return true; + } case AsyncEventID.SuspendAsyncContext: case AsyncEventID.CompleteAsyncContext: case AsyncEventID.ResumeAsyncMethod: case AsyncEventID.CompleteAsyncMethod: case AsyncEventID.ResetAsyncThreadContext: case AsyncEventID.ResetAsyncContinuationWrapperIndex: + { return true; + } case AsyncEventID.AsyncProfilerMetadata: + { SkipMetadataPayload(buffer, ref index); return true; + } case AsyncEventID.AsyncProfilerSyncClock: + { ReadCompressedUInt64(buffer, ref index); // qpcSync ReadCompressedUInt64(buffer, ref index); // utcSync return true; + } case AsyncEventID.UnwindAsyncException: + { ReadCompressedUInt32(buffer, ref index); return true; + } case AsyncEventID.CreateAsyncCallstack: case AsyncEventID.ResumeAsyncCallstack: case AsyncEventID.SuspendAsyncCallstack: + case AsyncEventID.AppendAsyncCallstack: + { 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); + 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); + Deserializer.ReadCompressedUInt64(buffer, ref index, out ulong value); return value; } @@ -395,45 +330,47 @@ private static void SkipCallstackPayload(ReadOnlySpan buffer, ref int inde } private static void ReadCallstackPayload(ReadOnlySpan buffer, ref int index, - out byte frameCount, out List<(ulong NativeIP, int State)> frames) + out byte frameCount, out List<(ulong MethodId, int State)> frames) { - ReadCallstackPayload(buffer, ref index, out _, out frameCount, out frames); + ReadCallstackPayload(buffer, ref index, out _, 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) + out ulong taskId, out AsyncCallstackType callstackType, out byte frameCount, out List<(ulong MethodId, int State)> frames) { - index++; // type - index++; // callstack ID (reserved) + callstackType = (AsyncCallstackType)buffer[index++]; + index++; frameCount = buffer[index++]; taskId = ReadCompressedUInt64(buffer, ref index); frames = new List<(ulong, int)>(frameCount); if (frameCount == 0) + { return; + } - ulong currentNativeIP = ReadCompressedUInt64(buffer, ref index); + ulong currentMethodId = ReadCompressedUInt64(buffer, ref index); int state = ReadCompressedInt32(buffer, ref index); - frames.Add((currentNativeIP, state)); + frames.Add((currentMethodId, 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)); + currentMethodId = (ulong)((long)currentMethodId + delta); + frames.Add((currentMethodId, state)); } } private static int ReadCompressedInt32(ReadOnlySpan buffer, ref int index) { - EventBuffer.Deserializer.ReadCompressedInt32(buffer, ref index, out int value); + 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); + Deserializer.ReadCompressedInt64(buffer, ref index, out long value); return value; } @@ -459,15 +396,17 @@ private record struct MetadataFromBuffer(ulong QpcFrequency, ulong QpcSync, ulon 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); + Deserializer.ReadUInt32(buffer, ref index, out uint totalSize); + Deserializer.ReadUInt32(buffer, ref index, out uint contextId); + Deserializer.ReadUInt64(buffer, ref index, out ulong threadId); + Deserializer.ReadUInt32(buffer, ref index, out uint eventCount); + Deserializer.ReadUInt64(buffer, ref index, out ulong startTs); + Deserializer.ReadUInt64(buffer, ref index, out ulong endTs); return new EventBufferHeader(buffer[0], totalSize, contextId, threadId, eventCount, startTs, endTs); } @@ -478,41 +417,35 @@ private sealed class ParsedEvent public long Timestamp { get; init; } public ulong OsThreadId { get; init; } - /// - /// The Task.Id associated with this event. For context events (Create/Resume), - /// this is the payload ID. For callstack events, this is the ID from the callstack - /// header. For other events, this is the active task ID at the time of emission. - /// public ulong TaskId { get; init; } - // Callstack events (Create/Resume/Suspend): frames + public AsyncCallstackType CallstackType { get; init; } public byte FrameCount { get; init; } - public List<(ulong NativeIP, int State)> Frames { get; init; } = []; + public List<(ulong MethodId, int State)> Frames { get; init; } = []; - // UnwindAsyncException: frame count unwound public uint UnwindFrameCount { get; init; } - // Metadata public MetadataFromBuffer? Metadata { get; init; } - // SyncClock public ulong SyncClockQpc { get; init; } public ulong SyncClockUtc { get; init; } - /// - /// Returns true if any frame in this event's callstack resolves to a method - /// whose name contains the specified marker string. - /// public bool HasMarkerFrame(string markerMethodName) { if (Frames.Count == 0) + { return false; - foreach (var (nativeIP, _) in Frames) + } + + foreach (var (methodId, _) in Frames) { - var methodName = GetMethodNameFromNativeIP(nativeIP); + string? methodName = GetMethodNameFromMethodId(CallstackType, methodId); if (methodName is not null && methodName.Contains(markerMethodName, StringComparison.Ordinal)) + { return true; + } } + return false; } } @@ -528,77 +461,160 @@ public ParsedEventStream(List events) _events.Sort((a, b) => a.Timestamp.CompareTo(b.Timestamp)); } - /// All events in timestamp order. + // All events in timestamp order. public IReadOnlyList All => _events; - /// All distinct event IDs present in the stream. + // All distinct event IDs present in the stream. public IEnumerable EventIds => _events.Select(e => e.EventId).Distinct(); - /// Filter events by event ID, in timestamp order. + // Filter events by event ID, in timestamp order. public IEnumerable OfType(AsyncEventID eventId) => _events.Where(e => e.EventId == eventId); - /// Filter events by multiple event IDs, in timestamp order. + // Filter events by multiple event IDs, in timestamp order. public IEnumerable OfTypes(params AsyncEventID[] eventIds) { var set = new HashSet(eventIds); return _events.Where(e => set.Contains(e.EventId)); } - /// Get events grouped by Task.Id, each group in timestamp order. + // Get events grouped by Task.Id, each group in timestamp order. public Dictionary> ByTaskId() { if (_byTaskId is not null) + { return _byTaskId; + } _byTaskId = new Dictionary>(); foreach (var evt in _events) { if (evt.TaskId == 0) + { continue; + } + if (!_byTaskId.TryGetValue(evt.TaskId, out var list)) { list = new List(); _byTaskId[evt.TaskId] = list; } + list.Add(evt); } + return _byTaskId; } - /// Get events for a specific Task.Id in timestamp order. + // Get events for a specific Task.Id in timestamp order. public List ForTask(ulong taskId) => ByTaskId().TryGetValue(taskId, out var list) ? list : new List(); - /// - /// Get callstack events (of specified type) that contain the marker method in their frames. - /// Results are in timestamp order. - /// + // Get callstack events (of specified type) that contain the marker method in their frames. + // Results are in timestamp order. public List CallstacksWithMarker(AsyncEventID callstackEventId, string markerMethodName) => _events.Where(e => e.EventId == callstackEventId && e.HasMarkerFrame(markerMethodName)).ToList(); - /// - /// Get callstack events (of specified type) that contain the marker method, - /// taking only the first match per Task.Id (deepest chain by timestamp). - /// + // Reconstructs full resume callstacks by merging each ResumeAsyncCallstack with any subsequent + // AppendAsyncCallstack events for the same context, up until the next Suspend/Complete on that + // context. + // + // V1 dispatchers may emit a partial Resume callstack when the parent continuation hasn't yet + // registered (race between dispatcher pickup and parent's AwaitUnsafeOnCompleted). Frames that + // register later are emitted as AppendAsyncCallstack at the next hook point. Merging produces + // the complete chain that was observable during the dispatcher's lifetime. + // + // Returns one ParsedEvent per Resume, with Frames and FrameCount reflecting the merged total. + public List MergedResumeCallstacks() + { + var result = new List(); + var openByTaskId = new Dictionary(); + + foreach (var evt in _events) + { + switch (evt.EventId) + { + case AsyncEventID.ResumeAsyncCallstack: + { + var merged = new ParsedEvent + { + EventId = evt.EventId, + Timestamp = evt.Timestamp, + OsThreadId = evt.OsThreadId, + TaskId = evt.TaskId, + CallstackType = evt.CallstackType, + FrameCount = evt.FrameCount, + Frames = new List<(ulong MethodId, int State)>(evt.Frames), + }; + + openByTaskId[evt.TaskId] = result.Count; + result.Add(merged); + + break; + } + case AsyncEventID.AppendAsyncCallstack: + { + if (openByTaskId.TryGetValue(evt.TaskId, out int idx)) + { + ParsedEvent existing = result[idx]; + var combinedFrames = new List<(ulong MethodId, int State)>(existing.Frames); + combinedFrames.AddRange(evt.Frames); + + result[idx] = new ParsedEvent + { + EventId = existing.EventId, + Timestamp = existing.Timestamp, + OsThreadId = existing.OsThreadId, + TaskId = existing.TaskId, + CallstackType = existing.CallstackType, + FrameCount = (byte)Math.Min(combinedFrames.Count, byte.MaxValue), + Frames = combinedFrames, + }; + } + + break; + } + case AsyncEventID.SuspendAsyncContext: + case AsyncEventID.CompleteAsyncContext: + { + openByTaskId.Remove(evt.TaskId); + break; + } + } + } + + return result; + } + + // Get merged resume callstacks (Resume + subsequent Appends) that contain the marker method + // in any of their merged frames. + public List MergedResumeCallstacksWithMarker(string markerMethodName) => + MergedResumeCallstacks().Where(e => e.HasMarkerFrame(markerMethodName)).ToList(); + + // Get callstack events (of specified type) that contain the marker method, + // taking only the first match per Task.Id (deepest chain by timestamp). public List CallstacksWithMarkerFirstPerTask(AsyncEventID callstackEventId, string markerMethodName) { - var matched = CallstacksWithMarker(callstackEventId, markerMethodName); + List matched = CallstacksWithMarker(callstackEventId, markerMethodName); var seen = new HashSet(); var result = new List(); + foreach (var evt in matched) { if (evt.TaskId != 0 && seen.Add(evt.TaskId)) + { result.Add(evt); + } } + return result; } - /// Get all metadata events. + // Get all metadata events. public List MetadataEvents => _events.Where(e => e.Metadata.HasValue).Select(e => e.Metadata!.Value).ToList(); - /// Get distinct OS thread IDs across all events. + // Get distinct OS thread IDs across all events. public HashSet OsThreadIds => new(_events.Select(e => e.OsThreadId).Where(id => id != 0)); } @@ -610,17 +626,22 @@ private static ParsedEventStream ParseAllEvents(CollectedEvents events) { EventBufferHeader? header = ParseEventBufferHeader(buffer); if (header is null) + { return; + } ulong osThreadId = header.Value.OsThreadId; ulong currentTaskId = 0; + var taskIdStack = new Stack(); int index = HeaderSize; long baseTimestamp = (long)header.Value.StartTimestamp; while (index < buffer.Length) { if (index + 2 > buffer.Length) + { break; + } AsyncEventID eventId = (AsyncEventID)buffer[index++]; long delta = (long)ReadCompressedUInt64(buffer, ref index); @@ -629,9 +650,12 @@ private static ParsedEventStream ParseAllEvents(CollectedEvents events) ParsedEvent evt = eventId switch { AsyncEventID.CreateAsyncContext or AsyncEventID.ResumeAsyncContext => - ParseContextEvent(eventId, baseTimestamp, osThreadId, buffer, ref index, ref currentTaskId), + ParseContextEvent(eventId, baseTimestamp, osThreadId, buffer, ref index, ref currentTaskId, taskIdStack), + + AsyncEventID.CompleteAsyncContext => + ParseCompleteContextEvent(baseTimestamp, osThreadId, ref currentTaskId, taskIdStack), - AsyncEventID.SuspendAsyncContext or AsyncEventID.CompleteAsyncContext or + AsyncEventID.SuspendAsyncContext or AsyncEventID.ResumeAsyncMethod or AsyncEventID.CompleteAsyncMethod => new ParsedEvent { @@ -645,8 +669,8 @@ AsyncEventID.SuspendAsyncContext or AsyncEventID.CompleteAsyncContext or ParseResetEvent(eventId, baseTimestamp, osThreadId, ref currentTaskId), AsyncEventID.CreateAsyncCallstack or AsyncEventID.ResumeAsyncCallstack or - AsyncEventID.SuspendAsyncCallstack => - ParseCallstackEvent(eventId, baseTimestamp, osThreadId, buffer, ref index), + AsyncEventID.SuspendAsyncCallstack or AsyncEventID.AppendAsyncCallstack => + ParseCallstackEvent(eventId, baseTimestamp, osThreadId, buffer, ref index, ref currentTaskId, taskIdStack), AsyncEventID.UnwindAsyncException => ParseUnwindEvent(baseTimestamp, osThreadId, currentTaskId, buffer, ref index), @@ -667,10 +691,16 @@ AsyncEventID.CreateAsyncCallstack or AsyncEventID.ResumeAsyncCallstack or return new ParsedEventStream(allEvents); static ParsedEvent ParseContextEvent(AsyncEventID eventId, long timestamp, ulong osThreadId, - ReadOnlySpan buffer, ref int index, ref ulong currentTaskId) + ReadOnlySpan buffer, ref int index, ref ulong currentTaskId, Stack taskIdStack) { ulong id = ReadCompressedUInt64(buffer, ref index); + if (eventId == AsyncEventID.ResumeAsyncContext && id != currentTaskId) + { + taskIdStack.Push(currentTaskId); + } + currentTaskId = id; + return new ParsedEvent { EventId = eventId, @@ -680,11 +710,29 @@ static ParsedEvent ParseContextEvent(AsyncEventID eventId, long timestamp, ulong }; } + static ParsedEvent ParseCompleteContextEvent(long timestamp, ulong osThreadId, + ref ulong currentTaskId, Stack taskIdStack) + { + ulong completedTaskId = currentTaskId; + currentTaskId = taskIdStack.Count > 0 ? taskIdStack.Pop() : 0; + + return new ParsedEvent + { + EventId = AsyncEventID.CompleteAsyncContext, + Timestamp = timestamp, + OsThreadId = osThreadId, + TaskId = completedTaskId + }; + } + static ParsedEvent ParseResetEvent(AsyncEventID eventId, long timestamp, ulong osThreadId, ref ulong currentTaskId) { ulong prevTaskId = currentTaskId; if (eventId == AsyncEventID.ResetAsyncThreadContext) + { currentTaskId = 0; + } + return new ParsedEvent { EventId = eventId, @@ -695,15 +743,22 @@ static ParsedEvent ParseResetEvent(AsyncEventID eventId, long timestamp, ulong o } static ParsedEvent ParseCallstackEvent(AsyncEventID eventId, long timestamp, ulong osThreadId, - ReadOnlySpan buffer, ref int index) + ReadOnlySpan buffer, ref int index, ref ulong currentTaskId, Stack taskIdStack) { - ReadCallstackPayload(buffer, ref index, out ulong taskId, out byte frameCount, out var frames); + ReadCallstackPayload(buffer, ref index, out ulong taskId, out AsyncCallstackType callstackType, out byte frameCount, out var frames); + if (taskId != currentTaskId) + { + taskIdStack.Push(currentTaskId); + currentTaskId = taskId; + } + return new ParsedEvent { EventId = eventId, Timestamp = timestamp, OsThreadId = osThreadId, TaskId = taskId, + CallstackType = callstackType, FrameCount = frameCount, Frames = frames }; @@ -713,6 +768,7 @@ static ParsedEvent ParseUnwindEvent(long timestamp, ulong osThreadId, ulong curr ReadOnlySpan buffer, ref int index) { uint unwindCount = ReadCompressedUInt32(buffer, ref index); + return new ParsedEvent { EventId = AsyncEventID.UnwindAsyncException, @@ -727,6 +783,7 @@ static ParsedEvent ParseMetadataEvent(long timestamp, ulong osThreadId, ulong cu ReadOnlySpan buffer, ref int index) { ReadMetadataPayload(buffer, ref index, out ulong freq, out ulong qpcSync, out ulong utcSync, out uint bufSize, out byte wrapperCount); + return new ParsedEvent { EventId = AsyncEventID.AsyncProfilerMetadata, @@ -742,6 +799,7 @@ static ParsedEvent ParseSyncClockEvent(long timestamp, ulong osThreadId, ulong c { ulong qpcSync = ReadCompressedUInt64(buffer, ref index); ulong utcSync = ReadCompressedUInt64(buffer, ref index); + return new ParsedEvent { EventId = AsyncEventID.AsyncProfilerSyncClock, @@ -757,6 +815,7 @@ static ParsedEvent ParseUnknownEvent(AsyncEventID eventId, long timestamp, ulong ulong currentTaskId, ReadOnlySpan buffer, ref int index) { SkipEventPayload(eventId, buffer, ref index); + return new ParsedEvent { EventId = eventId, @@ -788,13 +847,41 @@ private static void ForEachEventBufferPayload(ConcurrentQueue EventBuffer.OutputEventBuffer(buffer)); + EventBuffer.DumpAllEvents(events); } private static void RunScenarioAndFlush(Func scenario) { - Task.Run(scenario).GetAwaiter().GetResult(); - SendFlushCommand(); + // V1 (task-based) async: the dispatcher's finally block emits CompleteAsyncContext + // after inner.MoveNext() returns, but MoveNext() already set the task result which + // unblocks this thread. Brief sleep ensures the pool thread's finally completes. + // V2 (runtime-async) does not have this issue -- Complete fires inside the dispatch + // loop before the task is signaled. + // + // Clear SynchronizationContext so RuntimeAsync continuations don't capture + // xunit's context, which would cause per-frame re-queuing instead of inlining. + SynchronizationContext? prevCtx = SynchronizationContext.Current; + int originalThreadId = Environment.CurrentManagedThreadId; + SynchronizationContext.SetSynchronizationContext(null); + + try + { + Task.Run(scenario).GetAwaiter().GetResult(); + } + finally + { + Thread.Sleep(50); + + // Only restore the SynchronizationContext if we're still on the same thread. + // ConfigureAwait(false) may resume on a different thread pool thread, and + // setting the original thread's context there would be incorrect. + if (Environment.CurrentManagedThreadId == originalThreadId) + { + SynchronizationContext.SetSynchronizationContext(prevCtx); + } + + SendFlushCommand(); + } } private static void RunScenario(Func scenario) @@ -816,11 +903,13 @@ await listener.RunWithCallbackAsync(result.Events.Enqueue, async () => { SendFlushCommand(); result.Events.Clear(); + // Clear SynchronizationContext so RuntimeAsync continuations don't capture // xunit's context, which would cause per-frame re-queuing instead of inlining. - var prevCtx = SynchronizationContext.Current; + SynchronizationContext? prevCtx = SynchronizationContext.Current; int originalThreadId = Environment.CurrentManagedThreadId; SynchronizationContext.SetSynchronizationContext(null); + try { await scenario().ConfigureAwait(false); @@ -834,8 +923,12 @@ await listener.RunWithCallbackAsync(result.Events.Enqueue, async () => { SynchronizationContext.SetSynchronizationContext(prevCtx); } + + // Post-flush inside finally so buffered events from before an exception + // still reach the listener (otherwise on a scenario throw the trace would + // be truncated, hiding what happened up to the failure point). + SendFlushCommand(); } - SendFlushCommand(); }).ConfigureAwait(false); } return result; @@ -855,45 +948,44 @@ private static CollectedEvents CollectEvents(EventKeywords keywords, Action - /// Returns true if any callstack event contains all expected method names as frames, - /// appearing in the given order (index 0 = innermost/deepest frame). - /// private static bool HasCallstackWithExpectedFrames(List callstacks, string[] expectedFrames) { foreach (var cs in callstacks) { var resolvedNames = cs.Frames - .Select(f => GetMethodNameFromNativeIP(f.NativeIP)) + .Select(f => GetMethodNameFromMethodId(cs.CallstackType, f.MethodId)) .ToList(); int matchIndex = 0; for (int i = 0; i < resolvedNames.Count && matchIndex < expectedFrames.Length; i++) { if (resolvedNames[i] is not null && resolvedNames[i]!.Contains(expectedFrames[matchIndex], StringComparison.Ordinal)) + { matchIndex++; + } } if (matchIndex == expectedFrames.Length) + { return true; + } } return false; } - /// - /// For a given context, simulates the async callstack depth by walking events in order: - /// ResumeAsyncCallstack sets the depth to frame count, CompleteAsyncMethod decrements, - /// UnwindAsyncException subtracts unwound frames. Asserts depth reaches zero. - /// + // For a given context, simulates the async callstack depth by walking events in order: + // ResumeAsyncCallstack sets the depth to frame count, CompleteAsyncMethod decrements, + // UnwindAsyncException subtracts unwound frames. Asserts depth reaches zero. private static void AssertCallstackSimulationReachesZero(ParsedEventStream stream, string markerMethodName) { - // Find context ID via marker on a resume callstack var resumeStacks = stream.CallstacksWithMarker(AsyncEventID.ResumeAsyncCallstack, markerMethodName); Assert.True(resumeStacks.Count >= 1, $"Expected at least one resume callstack with marker '{markerMethodName}'"); @@ -907,1723 +999,424 @@ private static void AssertCallstackSimulationReachesZero(ParsedEventStream strea switch (evt.EventId) { case AsyncEventID.ResumeAsyncCallstack: + { stackDepth = (int)evt.FrameCount; break; + } case AsyncEventID.CompleteAsyncMethod: + { if (stackDepth > 0) + { stackDepth--; + } + break; + } case AsyncEventID.UnwindAsyncException: + { stackDepth = Math.Max(0, stackDepth - (int)evt.UnwindFrameCount); break; + } } } Assert.Equal(0, stackDepth); } - [ConditionalFact(typeof(AsyncProfilerTests), nameof(IsRuntimeAsyncSupported))] - public async Task RuntimeAsync_EventBufferHeaderFormat() - { - var events = await CollectEventsAsync(CoreKeywords, SingleAsyncYield); - - // DumpAllEvents(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))] - public async Task RuntimeAsync_EventsEmitted() + private static void AssertExactlyOneCreateAndComplete(ParsedEventStream stream, ulong taskId, string chainName) { - var events = await CollectEventsAsync(AllKeywords, SingleAsyncYield); - - // DumpAllEvents(events); - - Assert.True(events.Events.Count > 0, "Expected at least one AsyncEvents event to be emitted"); - Assert.Contains(events.Events, e => e.EventId == AsyncEventsId); + var ids = stream.ForTask(taskId).Select(e => e.EventId).ToList(); + int creates = ids.Count(id => id == AsyncEventID.CreateAsyncContext); + int completes = ids.Count(id => id == AsyncEventID.CompleteAsyncContext); + Assert.True(creates == 1, $"Expected exactly 1 CreateAsyncContext for {chainName} (TaskId {taskId}), got {creates}"); + Assert.True(completes == 1, $"Expected exactly 1 CompleteAsyncContext for {chainName} (TaskId {taskId}), got {completes}"); } - [System.Runtime.CompilerServices.RuntimeAsyncMethodGeneration(true)] - [System.Runtime.CompilerServices.MethodImpl(System.Runtime.CompilerServices.MethodImplOptions.NoInlining)] - static async Task SuspendResumeCompleteMarker() + // V1-friendly variant: V1's per-MoveNext dispatcher model emits one Create per await + // suspension, so a method with N awaits produces N dispatchers / N Creates on the same + // TaskId. The invariant we can still assert is creates == completes (both >= 1). + private static void AssertCreateEqualsCompleteForTask(ParsedEventStream stream, ulong taskId, string chainName) { - await Task.Yield(); - await SingleAsyncYield(); + var ids = stream.ForTask(taskId).Select(e => e.EventId).ToList(); + int creates = ids.Count(id => id == AsyncEventID.CreateAsyncContext); + int completes = ids.Count(id => id == AsyncEventID.CompleteAsyncContext); + Assert.True(creates >= 1, $"Expected at least 1 CreateAsyncContext for {chainName} (TaskId {taskId}), got {creates}"); + Assert.True(creates == completes, $"Expected CreateAsyncContext count == CompleteAsyncContext count for {chainName} (TaskId {taskId}), got {creates} creates and {completes} completes"); } - [ConditionalFact(typeof(AsyncProfilerTests), nameof(IsRuntimeAsyncSupported))] - public async Task RuntimeAsync_SuspendResumeCompleteEvents() + private sealed class InlinePostSynchronizationContext : SynchronizationContext { - var events = await CollectEventsAsync(CallstackKeywords, SuspendResumeCompleteMarker); - - // DumpAllEvents(events); - - var stream = ParseAllEvents(events); + private int _postCount; + public int PostCount => _postCount; - // Find our context via marker callstack. - var markerCallstacks = stream.CallstacksWithMarker(AsyncEventID.ResumeAsyncCallstack, nameof(SuspendResumeCompleteMarker)); - Assert.True(markerCallstacks.Count > 0, "Expected at least one callstack with SuspendResumeCompleteMarker"); - - ulong taskId = markerCallstacks[0].TaskId; - var taskEvts = stream.ForTask(taskId); - var ids = taskEvts.Select(e => e.EventId).ToList(); - - Assert.Contains(AsyncEventID.ResumeAsyncContext, ids); - Assert.Contains(AsyncEventID.SuspendAsyncContext, ids); - Assert.Contains(AsyncEventID.CompleteAsyncContext, ids); - } - - - [System.Runtime.CompilerServices.RuntimeAsyncMethodGeneration(true)] - [System.Runtime.CompilerServices.MethodImpl(System.Runtime.CompilerServices.MethodImplOptions.NoInlining)] - static async Task ContextLifecycleMarker() - { - await Task.Yield(); - await SingleAsyncYield(); + public override void Post(SendOrPostCallback d, object? state) + { + Interlocked.Increment(ref _postCount); + d(state); + } } - [ConditionalFact(typeof(AsyncProfilerTests), nameof(IsRuntimeAsyncSupported))] - public async Task RuntimeAsync_ContextEventIdLifecycle() + private sealed class InlineRunTaskScheduler : TaskScheduler { - var events = await CollectEventsAsync(CallstackKeywords, ContextLifecycleMarker); + private int _queuedCount; + public int QueuedCount => _queuedCount; - // DumpAllEvents(events); - - var stream = ParseAllEvents(events); - - // Find events in the context that contains our marker method. - var markerCallstacks = stream.CallstacksWithMarker(AsyncEventID.ResumeAsyncCallstack, nameof(ContextLifecycleMarker)); - Assert.True(markerCallstacks.Count > 0, "Expected at least one callstack with ContextLifecycleMarker"); - - ulong taskId = markerCallstacks[0].TaskId; - Assert.True(taskId > 0, "Context ID should be non-zero"); + protected override void QueueTask(Task task) + { + Interlocked.Increment(ref _queuedCount); + TryExecuteTask(task); + } - var taskEvts = stream.ForTask(taskId); - var ids = taskEvts.Select(e => e.EventId).ToList(); + protected override bool TryExecuteTaskInline(Task task, bool taskWasPreviouslyQueued) => false; - int createIdx = ids.IndexOf(AsyncEventID.CreateAsyncContext); - int resumeIdx = ids.IndexOf(AsyncEventID.ResumeAsyncContext); - Assert.True(createIdx >= 0, "Expected CreateAsyncContext in context events"); - Assert.True(resumeIdx > createIdx, "Expected ResumeAsyncContext after CreateAsyncContext"); + protected override IEnumerable? GetScheduledTasks() => null; } - [ConditionalFact(typeof(AsyncProfilerTests), nameof(IsRuntimeAsyncSupported))] - public async Task RuntimeAsync_ResumeCompleteMethodEvents() + private static class Deserializer { - var events = await CollectEventsAsync(MethodKeywords, ChainedAsyncYield); - - // DumpAllEvents(events); - - var ids = ParseAllEvents(events).EventIds; - - Assert.Contains(AsyncEventID.ResumeAsyncMethod, ids); - Assert.Contains(AsyncEventID.CompleteAsyncMethod, ids); - } - - [System.Runtime.CompilerServices.RuntimeAsyncMethodGeneration(true)] - [System.Runtime.CompilerServices.MethodImpl(System.Runtime.CompilerServices.MethodImplOptions.NoInlining)] - static async Task EventSequenceOrderMarker() - { - await Task.Yield(); - await SingleAsyncYield(); - } - - [ConditionalFact(typeof(AsyncProfilerTests), nameof(IsRuntimeAsyncSupported))] - public async Task RuntimeAsync_EventSequenceOrder() - { - var events = await CollectEventsAsync(CallstackKeywords, EventSequenceOrderMarker); - - // DumpAllEvents(events); - - var stream = ParseAllEvents(events); - - // Find our context via marker callstack. - var markerCallstacks = stream.CallstacksWithMarker(AsyncEventID.ResumeAsyncCallstack, nameof(EventSequenceOrderMarker)); - Assert.True(markerCallstacks.Count > 0, "Expected at least one callstack with EventSequenceOrderMarker"); - - ulong taskId = markerCallstacks[0].TaskId; - var taskEvts = stream.ForTask(taskId); - var ids = taskEvts.Select(e => e.EventId).ToList(); - - // Verify the expected lifecycle sequence exists in order. - int resumeIdx1 = ids.IndexOf(AsyncEventID.ResumeAsyncContext); - Assert.True(resumeIdx1 >= 0, "Expected first ResumeAsyncContext"); - - int suspendIdx = ids.IndexOf(AsyncEventID.SuspendAsyncContext, resumeIdx1 + 1); - Assert.True(suspendIdx > resumeIdx1, "Expected SuspendAsyncContext after first Resume"); - - int resumeIdx2 = ids.IndexOf(AsyncEventID.ResumeAsyncContext, suspendIdx + 1); - Assert.True(resumeIdx2 > suspendIdx, "Expected second ResumeAsyncContext after Suspend"); - - int completeIdx = ids.IndexOf(AsyncEventID.CompleteAsyncContext, resumeIdx2 + 1); - Assert.True(completeIdx > resumeIdx2, "Expected CompleteAsyncContext after second Resume"); - } - - [ConditionalFact(typeof(AsyncProfilerTests), nameof(IsRuntimeAsyncSupported))] - public async Task RuntimeAsync_CreateAsyncContextEmittedOnFirstAwait() - { - var events = await CollectEventsAsync(CreateAsyncContextKeyword | CompleteAsyncContextKeyword, SingleAsyncYield); - - // DumpAllEvents(events); - - var ids = ParseAllEvents(events).EventIds; - Assert.Contains(AsyncEventID.CreateAsyncContext, ids); - } - - [System.Runtime.CompilerServices.RuntimeAsyncMethodGeneration(true)] - [System.Runtime.CompilerServices.MethodImpl(System.Runtime.CompilerServices.MethodImplOptions.NoInlining)] - static async Task CreateCallstackMarker() - { - await SingleAsyncYield(); - } - - [ConditionalFact(typeof(AsyncProfilerTests), nameof(IsRuntimeAsyncSupported))] - public async Task RuntimeAsync_CreateAsyncCallstackEmittedOnFirstAwait() - { - var events = await CollectEventsAsync(CallstackKeywords, CreateCallstackMarker); - - // DumpAllEvents(events); - - var stream = ParseAllEvents(events); - var createCallstacks = stream.CallstacksWithMarker(AsyncEventID.CreateAsyncCallstack, nameof(CreateCallstackMarker)); - - Assert.NotEmpty(createCallstacks); - Assert.All(createCallstacks, 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"); - }); - } - - - [System.Runtime.CompilerServices.RuntimeAsyncMethodGeneration(true)] - [System.Runtime.CompilerServices.MethodImpl(System.Runtime.CompilerServices.MethodImplOptions.NoInlining)] - static async Task CreateCallstackDepthMarker() - { - await ChainedAsyncYield(); - } - - [ConditionalFact(typeof(AsyncProfilerTests), nameof(IsRuntimeAsyncSupported))] - public async Task RuntimeAsync_CreateCallstackDepthMatchesChain() - { - var events = await CollectEventsAsync(CallstackKeywords, CreateCallstackDepthMarker); - - // DumpAllEvents(events); - - var stream = ParseAllEvents(events); - var createCallstacks = stream.CallstacksWithMarker(AsyncEventID.CreateAsyncCallstack, nameof(CreateCallstackDepthMarker)); - - // The expected [NoInlining] frames in order (innermost first): - // InnerAsyncYield -> ChainedAsyncYield -> CreateCallstackDepthMarker - Assert.NotEmpty(createCallstacks); - string[] expectedFrames = [nameof(InnerAsyncYield), nameof(ChainedAsyncYield), nameof(CreateCallstackDepthMarker)]; - Assert.True( - HasCallstackWithExpectedFrames(createCallstacks, expectedFrames), - $"Expected callstack to contain frames [{string.Join(", ", expectedFrames)}] in order"); - } - - [System.Runtime.CompilerServices.RuntimeAsyncMethodGeneration(true)] - [System.Runtime.CompilerServices.MethodImpl(System.Runtime.CompilerServices.MethodImplOptions.NoInlining)] - static async Task SuspendCallstackMarker() - { - await Task.Yield(); - await SingleAsyncYield(); - } - - [ConditionalFact(typeof(AsyncProfilerTests), nameof(IsRuntimeAsyncSupported))] - public async Task RuntimeAsync_SuspendAsyncCallstackEmittedOnAwait() - { - var events = await CollectEventsAsync(CallstackKeywords, SuspendCallstackMarker); - - // DumpAllEvents(events); - - var stream = ParseAllEvents(events); - var suspendCallstacks = stream.CallstacksWithMarker(AsyncEventID.SuspendAsyncCallstack, nameof(SuspendCallstackMarker)); - - Assert.NotEmpty(suspendCallstacks); - Assert.All(suspendCallstacks, 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"); - }); - } - - [System.Runtime.CompilerServices.RuntimeAsyncMethodGeneration(true)] - [System.Runtime.CompilerServices.MethodImpl(System.Runtime.CompilerServices.MethodImplOptions.NoInlining)] - static async Task SuspendDepthMarker() - { - await Task.Yield(); - await ChainedAsyncYield(); - } - - [ConditionalFact(typeof(AsyncProfilerTests), nameof(IsRuntimeAsyncSupported))] - public async Task RuntimeAsync_SuspendCallstackDepthMatchesChain() - { - var events = await CollectEventsAsync(CallstackKeywords, SuspendDepthMarker); - - // DumpAllEvents(events); - - var stream = ParseAllEvents(events); - var suspendCallstacks = stream.CallstacksWithMarker(AsyncEventID.SuspendAsyncCallstack, nameof(SuspendDepthMarker)); - - // The expected [NoInlining] frames in order (innermost first): - // InnerAsyncYield -> ChainedAsyncYield -> SuspendDepthMarker - Assert.NotEmpty(suspendCallstacks); - string[] expectedFrames = [nameof(InnerAsyncYield), nameof(ChainedAsyncYield), nameof(SuspendDepthMarker)]; - Assert.True( - HasCallstackWithExpectedFrames(suspendCallstacks, expectedFrames), - $"Expected callstack to contain frames [{string.Join(", ", expectedFrames)}] in order"); - } - - [System.Runtime.CompilerServices.RuntimeAsyncMethodGeneration(true)] - [System.Runtime.CompilerServices.MethodImpl(System.Runtime.CompilerServices.MethodImplOptions.NoInlining)] - static async Task SuspendPrecedesCompleteMarker() - { - await Task.Yield(); - await InnerAsyncYield(); - } - - [ConditionalFact(typeof(AsyncProfilerTests), nameof(IsRuntimeAsyncSupported))] - public async Task RuntimeAsync_SuspendCallstackPrecedesComplete() - { - var events = await CollectEventsAsync(CallstackKeywords, SuspendPrecedesCompleteMarker); - - // DumpAllEvents(events); - - var stream = ParseAllEvents(events); - - // Find the suspend callstack via marker to get the context ID - var suspendStacks = stream.CallstacksWithMarker(AsyncEventID.SuspendAsyncCallstack, nameof(SuspendPrecedesCompleteMarker)); - Assert.True(suspendStacks.Count >= 1, $"Expected at least one suspend callstack with marker, got {suspendStacks.Count}"); - - ulong taskId = suspendStacks[0].TaskId; - Assert.True(taskId > 0, "Expected non-zero context ID"); - - var taskEvts = stream.ForTask(taskId); - var ids = taskEvts.Select(e => e.EventId).ToList(); - - int suspendIdx = ids.IndexOf(AsyncEventID.SuspendAsyncCallstack); - int completeIdx = ids.IndexOf(AsyncEventID.CompleteAsyncContext); - - Assert.True(suspendIdx >= 0, "Expected SuspendAsyncCallstack in context events"); - Assert.True(completeIdx >= 0, "Expected CompleteAsyncContext in context events"); - Assert.True(suspendIdx < completeIdx, $"SuspendAsyncCallstack (index {suspendIdx}) should precede CompleteAsyncContext (index {completeIdx})"); - } - - [System.Runtime.CompilerServices.RuntimeAsyncMethodGeneration(true)] - [System.Runtime.CompilerServices.MethodImpl(System.Runtime.CompilerServices.MethodImplOptions.NoInlining)] - static async Task SuspendDeeperMarker() - { - await Task.Yield(); - await InnerAsyncYield(); - } - - [ConditionalFact(typeof(AsyncProfilerTests), nameof(IsRuntimeAsyncSupported))] - public async Task RuntimeAsync_SuspendCallstackDeeperThanInitialResume() - { - var events = await CollectEventsAsync(CallstackKeywords, SuspendDeeperMarker); - - // DumpAllEvents(events); - - var stream = ParseAllEvents(events); - var resumeStacks = stream.CallstacksWithMarker(AsyncEventID.ResumeAsyncCallstack, nameof(SuspendDeeperMarker)); - var suspendStacks = stream.CallstacksWithMarker(AsyncEventID.SuspendAsyncCallstack, nameof(SuspendDeeperMarker)); - - Assert.True(resumeStacks.Count >= 1, $"Expected at least one resume callstack with marker, got {resumeStacks.Count}"); - Assert.True(suspendStacks.Count >= 1, $"Expected at least one suspend callstack with marker, got {suspendStacks.Count}"); - - // First resume (after initial Yield) should be shallow, first suspend (InnerAsyncYield's Yield) should be deeper - var firstResume = resumeStacks[0]; - var firstSuspend = suspendStacks[0]; - - Assert.True(firstSuspend.FrameCount > firstResume.FrameCount, $"First suspend callstack depth ({firstSuspend.FrameCount}) should be deeper than first resume callstack depth ({firstResume.FrameCount})"); - } - - [System.Runtime.CompilerServices.RuntimeAsyncMethodGeneration(true)] - [System.Runtime.CompilerServices.MethodImpl(System.Runtime.CompilerServices.MethodImplOptions.NoInlining)] - static async Task CreatePrecedesResumeMarker() - { - await Task.Yield(); - await InnerAsyncYield(); - } - - [ConditionalFact(typeof(AsyncProfilerTests), nameof(IsRuntimeAsyncSupported))] - public async Task RuntimeAsync_CreateCallstackPrecedesResumeCallstack() - { - var events = await CollectEventsAsync(CallstackKeywords, CreatePrecedesResumeMarker); - - // DumpAllEvents(events); - - var stream = ParseAllEvents(events); - var createStacks = stream.CallstacksWithMarker(AsyncEventID.CreateAsyncCallstack, nameof(CreatePrecedesResumeMarker)); - var resumeStacks = stream.CallstacksWithMarker(AsyncEventID.ResumeAsyncCallstack, nameof(CreatePrecedesResumeMarker)); - - Assert.NotEmpty(createStacks); - Assert.NotEmpty(resumeStacks); - - // For each task that has both Create and Resume callstacks, verify Create timestamp precedes Resume. - int matchedPairs = 0; - foreach (var create in createStacks) - { - var matchingResume = resumeStacks.FirstOrDefault(r => r.TaskId == create.TaskId); - if (matchingResume is null) - continue; - - matchedPairs++; - Assert.True(create.Timestamp <= matchingResume.Timestamp, $"For task {create.TaskId}: CreateAsyncCallstack (ts {create.Timestamp}) should precede ResumeAsyncCallstack (ts {matchingResume.Timestamp})"); - } - - Assert.True(matchedPairs >= 1, $"Expected at least one matching Create/Resume callstack pair, but found {matchedPairs}"); - } - - [System.Runtime.CompilerServices.RuntimeAsyncMethodGeneration(true)] - [System.Runtime.CompilerServices.MethodImpl(System.Runtime.CompilerServices.MethodImplOptions.NoInlining)] - static async Task CreateResumeMatchMarker() - { - await Task.Yield(); - await InnerAsyncYield(); - } - - [ConditionalFact(typeof(AsyncProfilerTests), nameof(IsRuntimeAsyncSupported))] - public async Task RuntimeAsync_CreateAndFirstResumeCallstacksMatch() - { - var events = await CollectEventsAsync(CallstackKeywords, CreateResumeMatchMarker); - - // DumpAllEvents(events); - - var stream = ParseAllEvents(events); - var createStacks = stream.CallstacksWithMarker(AsyncEventID.CreateAsyncCallstack, nameof(CreateResumeMatchMarker)); - var resumeStacks = stream.CallstacksWithMarker(AsyncEventID.ResumeAsyncCallstack, nameof(CreateResumeMatchMarker)); - - Assert.NotEmpty(createStacks); - Assert.NotEmpty(resumeStacks); - - // For each create callstack, find the first resume with the same task ID and verify frames match. - int matchedPairs = 0; - foreach (var create in createStacks) + public static void ReadInt32(ReadOnlySpan buffer, ref int index, out int value) { - var matchingResume = resumeStacks.FirstOrDefault(r => r.TaskId == create.TaskId); - if (matchingResume is null) - continue; - - matchedPairs++; - Assert.Equal(create.Frames.Count, matchingResume.Frames.Count); - for (int i = 0; i < create.Frames.Count; i++) - { - Assert.Equal(create.Frames[i].NativeIP, matchingResume.Frames[i].NativeIP); - } + uint uValue; + ReadUInt32(buffer, ref index, out uValue); + value = (int)uValue; } - Assert.True(matchedPairs >= 1, $"Expected at least one matching Create/Resume callstack pair, but found {matchedPairs}"); - } - - [System.Runtime.CompilerServices.RuntimeAsyncMethodGeneration(true)] - [System.Runtime.CompilerServices.MethodImpl(System.Runtime.CompilerServices.MethodImplOptions.NoInlining)] - static async Task CallstackOnResumeMarker() - { - await Task.Yield(); - await InnerAsyncYield(); - } - - [ConditionalFact(typeof(AsyncProfilerTests), nameof(IsRuntimeAsyncSupported))] - public async Task RuntimeAsync_CallstackEmittedOnResume() - { - var events = await CollectEventsAsync(CallstackKeywords, CallstackOnResumeMarker); - - // DumpAllEvents(events); - - var stream = ParseAllEvents(events); - var callstacks = stream.CallstacksWithMarker(AsyncEventID.ResumeAsyncCallstack, nameof(CallstackOnResumeMarker)); - - 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"); - }); - } - - [System.Runtime.CompilerServices.RuntimeAsyncMethodGeneration(true)] - [System.Runtime.CompilerServices.MethodImpl(System.Runtime.CompilerServices.MethodImplOptions.NoInlining)] - static async Task CallstackDepthMarker() - { - await Task.Yield(); - await InnerAsyncYield(); - } - - [ConditionalFact(typeof(AsyncProfilerTests), nameof(IsRuntimeAsyncSupported))] - public async Task RuntimeAsync_CallstackDepthMatchesChain() - { - var events = await CollectEventsAsync(CallstackKeywords, CallstackDepthMarker); - - // DumpAllEvents(events); - - var stream = ParseAllEvents(events); - var callstacks = stream.CallstacksWithMarker(AsyncEventID.ResumeAsyncCallstack, nameof(CallstackDepthMarker)); - - // The expected [NoInlining] frames in order (innermost first): - // InnerAsyncYield -> CallstackDepthMarker - Assert.NotEmpty(callstacks); - string[] expectedFrames = [nameof(InnerAsyncYield), nameof(CallstackDepthMarker)]; - Assert.True( - HasCallstackWithExpectedFrames(callstacks, expectedFrames), - $"Expected callstack to contain frames [{string.Join(", ", expectedFrames)}] in order"); - } - - [System.Runtime.CompilerServices.RuntimeAsyncMethodGeneration(true)] - [System.Runtime.CompilerServices.MethodImpl(System.Runtime.CompilerServices.MethodImplOptions.NoInlining)] - static async Task SimulationNormalMarker() - { - await Task.Yield(); - await InnerAsyncYield(); - } - - [ConditionalFact(typeof(AsyncProfilerTests), nameof(IsRuntimeAsyncSupported))] - public async Task RuntimeAsync_CallstackSimulation_NormalCompletion() - { - var events = await CollectEventsAsync(CallstackKeywords, SimulationNormalMarker); - - // DumpAllEvents(events); - - var stream = ParseAllEvents(events); - AssertCallstackSimulationReachesZero(stream, nameof(SimulationNormalMarker)); - } - - [System.Runtime.CompilerServices.RuntimeAsyncMethodGeneration(true)] - [System.Runtime.CompilerServices.MethodImpl(System.Runtime.CompilerServices.MethodImplOptions.NoInlining)] - static async Task SimulationHandledMarker() - { - await DeepOuterCatches(); - } - - [ConditionalFact(typeof(AsyncProfilerTests), nameof(IsRuntimeAsyncSupported))] - public async Task RuntimeAsync_CallstackSimulation_HandledException() - { - var events = await CollectEventsAsync(CallstackKeywords, SimulationHandledMarker); - - // DumpAllEvents(events); - - var stream = ParseAllEvents(events); - AssertCallstackSimulationReachesZero(stream, nameof(SimulationHandledMarker)); - } - - [System.Runtime.CompilerServices.RuntimeAsyncMethodGeneration(true)] - [System.Runtime.CompilerServices.MethodImpl(System.Runtime.CompilerServices.MethodImplOptions.NoInlining)] - static async Task SimulationUnhandledMarker() - { - await DeepUnhandledOuter(); - } - - [System.Runtime.CompilerServices.RuntimeAsyncMethodGeneration(false)] - [System.Runtime.CompilerServices.MethodImpl(System.Runtime.CompilerServices.MethodImplOptions.NoInlining)] - static async Task SimulationUnhandledMarkerCatcher() - { - try - { - await SimulationUnhandledMarker(); - } - catch (InvalidOperationException) + public static void ReadCompressedInt32(ReadOnlySpan buffer, ref int index, out int value) { + uint uValue; + ReadCompressedUInt32(buffer, ref index, out uValue); + value = ZigzagDecodeInt32(uValue); } - } - - [ConditionalFact(typeof(AsyncProfilerTests), nameof(IsRuntimeAsyncSupported))] - public async Task RuntimeAsync_CallstackSimulation_UnhandledException() - { - var events = await CollectEventsAsync(CallstackKeywords, SimulationUnhandledMarkerCatcher); - - // DumpAllEvents(events); - - var stream = ParseAllEvents(events); - AssertCallstackSimulationReachesZero(stream, nameof(SimulationUnhandledMarker)); - } - - [System.Runtime.CompilerServices.RuntimeAsyncMethodGeneration(true)] - [System.Runtime.CompilerServices.MethodImpl(System.Runtime.CompilerServices.MethodImplOptions.NoInlining)] - static async Task UnhandledUnwindMarker() - { - await DeepUnhandledOuter(); - } - [System.Runtime.CompilerServices.RuntimeAsyncMethodGeneration(false)] - [System.Runtime.CompilerServices.MethodImpl(System.Runtime.CompilerServices.MethodImplOptions.NoInlining)] - static async Task UnhandledUnwindCatcher() - { - try - { - await UnhandledUnwindMarker(); - } - catch (InvalidOperationException) - { - } - } - - [ConditionalFact(typeof(AsyncProfilerTests), nameof(IsRuntimeAsyncSupported))] - public async Task RuntimeAsync_UnhandledExceptionUnwind() - { - var events = await CollectEventsAsync(CallstackKeywords, UnhandledUnwindCatcher); - - // DumpAllEvents(events); - - var stream = ParseAllEvents(events); - var resumeStacks = stream.CallstacksWithMarker(AsyncEventID.ResumeAsyncCallstack, nameof(UnhandledUnwindMarker)); - Assert.True(resumeStacks.Count >= 1, $"Expected at least one resume callstack with marker '{nameof(UnhandledUnwindMarker)}'"); - - ulong taskId = resumeStacks[0].TaskId; - - var taskEvts = stream.ForTask(taskId); - var eventIds = taskEvts.Select(e => e.EventId).ToList(); - - Assert.Contains(AsyncEventID.ResumeAsyncContext, eventIds); - Assert.Contains(AsyncEventID.UnwindAsyncException, eventIds); - Assert.Contains(AsyncEventID.CompleteAsyncContext, eventIds); - - // Verify unwind frame count for this task - // UnhandledUnwindMarker -> DeepUnhandledOuter -> DeepUnhandledMiddle -> DeepUnhandledInnerThrows, 4 frames deep after the initial resume. - var unwindEvents = taskEvts.Where(e => e.EventId == AsyncEventID.UnwindAsyncException).ToList(); - Assert.NotEmpty(unwindEvents); - Assert.All(unwindEvents, e => Assert.Equal(4u, e.UnwindFrameCount)); - } - - [System.Runtime.CompilerServices.RuntimeAsyncMethodGeneration(true)] - [System.Runtime.CompilerServices.MethodImpl(System.Runtime.CompilerServices.MethodImplOptions.NoInlining)] - static async Task HandledUnwindMarker() - { - await DeepOuterCatches(); - } - - [ConditionalFact(typeof(AsyncProfilerTests), nameof(IsRuntimeAsyncSupported))] - public async Task RuntimeAsync_HandledExceptionUnwind() - { - var events = await CollectEventsAsync(CallstackKeywords, HandledUnwindMarker); - - // DumpAllEvents(events); - - var stream = ParseAllEvents(events); - var resumeStacks = stream.CallstacksWithMarker(AsyncEventID.ResumeAsyncCallstack, nameof(HandledUnwindMarker)); - Assert.True(resumeStacks.Count >= 1, $"Expected at least one resume callstack with marker '{nameof(HandledUnwindMarker)}'"); - - ulong taskId = resumeStacks[0].TaskId; - - var taskEvts = stream.ForTask(taskId); - var eventIds = taskEvts.Select(e => e.EventId).ToList(); - - Assert.Contains(AsyncEventID.ResumeAsyncContext, eventIds); - Assert.Contains(AsyncEventID.UnwindAsyncException, eventIds); - Assert.Contains(AsyncEventID.CompleteAsyncContext, eventIds); - - // Verify unwind frame count for this task - // DeepMiddle -> DeepInnerThrows, 2 frames deep after the initial resume. - var unwindEvents = taskEvts.Where(e => e.EventId == AsyncEventID.UnwindAsyncException).ToList(); - Assert.NotEmpty(unwindEvents); - Assert.All(unwindEvents, e => Assert.Equal(2u, e.UnwindFrameCount)); - } - - // Requires threading: - // Wrapper index tests use RunScenarioAndFlush (Task.Run) to escape xunit's - // AsyncTestSyncContext. With a SynchronizationContext present, each level in the - // async chain re-dispatches through the sync context, creating a separate - // DispatchContinuations call that resets the wrapper index to 0. Task.Run ensures - // the entire continuation chain executes in a single dispatch loop where the - // wrapper index increments sequentially across resumptions. - [ConditionalFact(typeof(AsyncProfilerTests), nameof(IsRuntimeAsyncAndThreadingSupported))] - public void RuntimeAsync_WrapperIndexMatchesCallstack() - { - var captures = new List<(string MethodName, int WrapperSlot)>(); - - var events = CollectEvents(ResumeAsyncCallstackKeyword, () => - { - RunScenarioAndFlush(async () => - { - await WrapperTestA(captures); - }); - }); - - // DumpAllEvents(events); - - 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 wrapper frame 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); - } - - // Requires threading: - // Same comment as RuntimeAsync_WrapperIndexMatchesCallstack regarding Task.Run and wrapper index behavior. - [ConditionalFact(typeof(AsyncProfilerTests), nameof(IsRuntimeAsyncAndThreadingSupported))] - 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 RecursiveAsyncChain(34); - }); - }); - - // DumpAllEvents(events); - - var ids = ParseAllEvents(events).EventIds; - - Assert.Contains(AsyncEventID.ResetAsyncContinuationWrapperIndex, ids); - } - - // Requires threading: - // Same comment as RuntimeAsync_WrapperIndexMatchesCallstack regarding Task.Run and wrapper index behavior. - [ConditionalFact(typeof(AsyncProfilerTests), nameof(IsRuntimeAsyncAndThreadingSupported))] - 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 RecursiveAsyncChain(2); - }); - }); - - // DumpAllEvents(events); - - var ids = ParseAllEvents(events).EventIds; - - Assert.DoesNotContain(AsyncEventID.ResetAsyncContinuationWrapperIndex, ids); - } - - // Requires threading: - // The periodic flush timer runs on a background thread. - // On single-threaded runtimes there is no background thread to fire the timer. - [ConditionalFact(typeof(AsyncProfilerTests), nameof(IsRuntimeAsyncAndThreadingSupported))] - public void RuntimeAsync_PeriodicTimerFlush() - { - static bool IsRequestedEvent(AsyncEventID id) => - id == AsyncEventID.CreateAsyncContext || - id == AsyncEventID.ResumeAsyncContext || - id == AsyncEventID.SuspendAsyncContext || - id == AsyncEventID.CompleteAsyncContext; - - var events = CollectEvents(CoreKeywords, (collectedEvents, _) => - { - // Run scenario - do NOT flush explicitly afterwards. - RunScenario(async () => - { - await SingleAsyncYield(); - }); - - // Wait for the periodic flush timer (1s interval) to detect the idle buffer and flush it automatically. - Thread.Sleep(1000); - - // Poll to make sure the expected buffer got flush. - bool flushed = SpinWait.SpinUntil(() => - { - var stream = ParseAllEvents(collectedEvents); - return stream.EventIds.Any(id => IsRequestedEvent(id)); - }, TimeSpan.FromSeconds(20)); - - Assert.True(flushed, "Expected periodic timer to flush buffer with core lifecycle events within timeout"); - }); - - // DumpAllEvents(events); - - var stream = ParseAllEvents(events); - int coreEventCount = stream.EventIds.Count(id => IsRequestedEvent(id)); - - Assert.True(coreEventCount > 0, "Expected periodic timer to flush buffer with core lifecycle events"); - } - - // Requires threading: - // Verifies the background flush timer preserves the owning thread's OS thread ID, - // which needs both a dedicated worker thread and the timer thread. - [ConditionalFact(typeof(AsyncProfilerTests), nameof(IsRuntimeAsyncAndThreadingSupported))] - public void RuntimeAsync_PeriodicTimerFlush_PreservesOwnerThreadId() - { - // This test verifies that when the background flush timer flushes a thread's buffer, - // the new header written afterwards preserves the owning thread's OS thread ID - // (not the timer thread's ID). - // - // Strategy: run async work on a dedicated thread so its profiler context gets events. - // Between two batches of work, wait for the flush timer to fire. Both buffer flushes - // from the dedicated thread should carry the same OsThreadId. - - ulong workerOsThreadId = 0; - var workerIdReady = new ManualResetEventSlim(false); - var firstBatchDone = new ManualResetEventSlim(false); - var firstFlushSeen = new ManualResetEventSlim(false); - var events = new CollectedEvents(); - - using (var listener = CreateListener(CoreKeywords)) + public static void ReadUInt32(ReadOnlySpan buffer, ref int index, out uint value) { - listener.RunWithCallback(e => - { - if (!workerIdReady.IsSet) - return; - if (e.EventId != AsyncEventsId || e.Payload is null || e.Payload.Count == 0) - return; - if (e.Payload[0] is not byte[] payload) - return; - EventBufferHeader? header = ParseEventBufferHeader(payload); - if (header is not null && header.Value.OsThreadId == workerOsThreadId) - events.Events.Enqueue(e); - }, () => - { - SendFlushCommand(); - - var thread = new Thread(() => - { - workerOsThreadId = GetCurrentOSThreadId(); - workerIdReady.Set(); - - // First batch: generate events on this thread's profiler context. - SingleAsyncYield().GetAwaiter().GetResult(); - firstBatchDone.Set(); - - // Wait for the flush to deliver our first buffer before generating more events. - bool flushed = firstFlushSeen.Wait(TimeSpan.FromSeconds(20)); - Assert.True(flushed, "Expected first flush of core lifecycle events within timeout"); - - // Second batch: generate more events on the same thread's context. - SingleAsyncYield().GetAwaiter().GetResult(); - }); - - thread.IsBackground = true; - thread.Start(); - - // Wait for the worker to finish its first batch, then force flush. - firstBatchDone.Wait(TimeSpan.FromSeconds(20)); - SendFlushCommand(); - - // Poll for first buffer from our worker thread. - bool firstFlush = SpinWait.SpinUntil(() => events.Events.Count >= 1, TimeSpan.FromSeconds(20)); - Assert.True(firstFlush, "Expected periodic timer to flush core lifecycle events within timeout"); - - firstFlushSeen.Set(); - - // Wait for the worker to finish its second batch. - bool joined = thread.Join(TimeSpan.FromSeconds(20)); - Assert.True(joined, "Expected worker thread to terminate within timeout after second batch of work"); - - // Force a flush to deliver the second batch. - SendFlushCommand(); - - // Poll for second buffer from our worker thread. - bool secondFlush = SpinWait.SpinUntil(() => events.Events.Count >= 2, TimeSpan.FromSeconds(20)); - Assert.True(secondFlush, "Expected periodic timer to flush core lifecycle events within timeout"); - }); + value = BinaryPrimitives.ReadUInt32LittleEndian(buffer.Slice(index)); + index += 4; } - // DumpAllEvents(events); - - Assert.True(workerOsThreadId != 0, "Failed to capture worker OS thread ID"); - - // The key assertion: find buffers that contain CreateAsyncContext events (our work batches). - // There must be at least 2 such buffers (one per SingleAsyncYield() call), and ALL of them must - // have the worker's OsThreadId - proving the timer flush didn't corrupt the header. - var stream = ParseAllEvents(events); - var createEvents = stream.OfType(AsyncEventID.CreateAsyncContext).ToList(); - Assert.True(createEvents.Count >= 2, $"Expected at least 2 CreateAsyncContext events from the worker thread, got {createEvents.Count}"); - Assert.All(createEvents, e => Assert.Equal(workerOsThreadId, e.OsThreadId)); - } - - // Requires threading: - // Spawns a dedicated thread that exits, then waits for the background flush timer - // to detect and flush the orphaned thread-local buffer. - [ConditionalFact(typeof(AsyncProfilerTests), nameof(IsRuntimeAsyncAndThreadingSupported))] - public void RuntimeAsync_DeadThreadFlush() - { - static bool IsRequestedEvent(AsyncEventID id) => - id == AsyncEventID.CreateAsyncContext || - id == AsyncEventID.ResumeAsyncContext || - id == AsyncEventID.SuspendAsyncContext || - id == AsyncEventID.CompleteAsyncContext; - - var events = CollectEvents(CoreKeywords, (collectedEvents, _) => + public static void ReadCompressedUInt32(ReadOnlySpan buffer, ref int index, out uint value) { - // 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 SingleAsyncYield(); - }); - }); - - thread.IsBackground = true; - thread.Start(); - bool joined = thread.Join(TimeSpan.FromSeconds(20)); - Assert.True(joined, "Expected worker thread to terminate within timeout before waiting for orphaned buffer flush"); - - // Do NOT send a flush command. - // Wait for the periodic flush timer to detect the dead thread and flush its orphaned buffer. - Thread.Sleep(1000); + int shift = 0; + byte b; - // Poll to make sure the expected buffer got flush. - bool flushed = SpinWait.SpinUntil(() => + value = 0; + do { - var stream = ParseAllEvents(collectedEvents); - return stream.EventIds.Any(id => IsRequestedEvent(id)); - }, TimeSpan.FromSeconds(20)); - - Assert.True(flushed, "Expected periodic timer to flush buffer with core lifecycle events within timeout"); - }); - - // DumpAllEvents(events); - - var stream = ParseAllEvents(events); - int coreEventCount = stream.EventIds.Count(id => IsRequestedEvent(id)); - - Assert.True(coreEventCount > 0, "Expected periodic timer to flush dead thread's buffer"); - } - - // This test is sensitive to event noise - it asserts a specific clock event is absent. - // It cannot run in parallel with other async profiler scenarios that might produce - // clock events. Test parallelization is disabled via XunitAssemblyAttributes.cs. - [ConditionalFact(typeof(AsyncProfilerTests), nameof(IsRuntimeAsyncSupported))] - public async Task RuntimeAsync_NoSyncClockEventBeforeInterval() - { - var events = await CollectEventsAsync(CoreKeywords, SingleAsyncYield); - - var ids = ParseAllEvents(events).EventIds; - - Assert.DoesNotContain(AsyncEventID.AsyncProfilerSyncClock, ids); - } - - // This test is sensitive to event noise - it asserts zero context events appear - // after enabling the listener. It cannot run in parallel with other async profiler - // scenarios that might produce events on the same thread context. - // Test parallelization is already disabled via XunitAssemblyAttributes.cs. - [ConditionalFact(typeof(AsyncProfilerTests), nameof(IsRuntimeAsyncSupported))] - public async Task RuntimeAsync_NoEventsWhenDisabled() - { - // Run async work WITHOUT a listener attached - for (int i = 0; i < 50; i++) - { - await SingleAsyncYield(); + b = buffer[index++]; + value |= (uint)(b & 0x7F) << shift; + shift += 7; + } while ((b & 0x80) != 0); } - // Now attach listener but don't run any RuntimeAsync work inside — - // just call a synchronous no-op. Verify no stale events from above leak through. - var events = await CollectEventsAsync(CoreKeywords, () => Task.CompletedTask); - - // There may be meta data related events, but there should be no suspend/resume/complete events from the earlier work. - var ids = ParseAllEvents(events).EventIds; - - int contextEvents = ids.Count(id => id == AsyncEventID.ResumeAsyncContext || id == AsyncEventID.SuspendAsyncContext || id == AsyncEventID.CompleteAsyncContext); - - Assert.Equal(0, contextEvents); - } - - public static IEnumerable KeywordGatekeepingData() - { - yield return new object[] { (long)CreateAsyncContextKeyword, new AsyncEventID[] { AsyncEventID.ResetAsyncThreadContext, AsyncEventID.CreateAsyncContext, AsyncEventID.AsyncProfilerMetadata } }; - yield return new object[] { (long)ResumeAsyncContextKeyword, new AsyncEventID[] { AsyncEventID.ResetAsyncThreadContext, AsyncEventID.ResumeAsyncContext, AsyncEventID.AsyncProfilerMetadata } }; - yield return new object[] { (long)SuspendAsyncContextKeyword, new AsyncEventID[] { AsyncEventID.ResetAsyncThreadContext, AsyncEventID.SuspendAsyncContext, AsyncEventID.AsyncProfilerMetadata } }; - yield return new object[] { (long)CompleteAsyncContextKeyword, new AsyncEventID[] { AsyncEventID.ResetAsyncThreadContext, AsyncEventID.CompleteAsyncContext, AsyncEventID.AsyncProfilerMetadata } }; - yield return new object[] { (long)UnwindAsyncExceptionKeyword, new AsyncEventID[] { AsyncEventID.ResetAsyncThreadContext, AsyncEventID.UnwindAsyncException, AsyncEventID.AsyncProfilerMetadata } }; - yield return new object[] { (long)CreateAsyncCallstackKeyword, new AsyncEventID[] { AsyncEventID.ResetAsyncThreadContext, AsyncEventID.CreateAsyncCallstack, AsyncEventID.AsyncProfilerMetadata } }; - yield return new object[] { (long)ResumeAsyncCallstackKeyword, new AsyncEventID[] { AsyncEventID.ResetAsyncThreadContext, AsyncEventID.ResumeAsyncCallstack, AsyncEventID.AsyncProfilerMetadata } }; - yield return new object[] { (long)SuspendAsyncCallstackKeyword, new AsyncEventID[] { AsyncEventID.ResetAsyncThreadContext, AsyncEventID.SuspendAsyncCallstack, AsyncEventID.AsyncProfilerMetadata } }; - yield return new object[] { (long)ResumeAsyncMethodKeyword, new AsyncEventID[] { AsyncEventID.ResetAsyncThreadContext, AsyncEventID.ResumeAsyncMethod, AsyncEventID.AsyncProfilerMetadata } }; - yield return new object[] { (long)CompleteAsyncMethodKeyword, new AsyncEventID[] { AsyncEventID.ResetAsyncThreadContext, AsyncEventID.CompleteAsyncMethod, AsyncEventID.AsyncProfilerMetadata } }; - } - - [System.Runtime.CompilerServices.RuntimeAsyncMethodGeneration(true)] - [System.Runtime.CompilerServices.MethodImpl(System.Runtime.CompilerServices.MethodImplOptions.NoInlining)] - static async Task KeywordGatekeepingMarker() - { - await OuterCatches(); - await ChainedAsyncYield(); - } - - // This test is sensitive to event noise - it asserts that ONLY the expected event - // types appear for a given keyword. It cannot run in parallel with other async - // profiler scenarios that might produce events on the same thread context. - // Test parallelization is already disabled via XunitAssemblyAttributes.cs. - [ConditionalTheory(typeof(AsyncProfilerTests), nameof(IsRuntimeAsyncSupported))] - [MemberData(nameof(KeywordGatekeepingData))] - public async Task RuntimeAsync_KeywordGatekeeping(long keywordValue, AsyncEventID[] allowedEventIds) - { - EventKeywords kw = (EventKeywords)keywordValue; - var allowed = new HashSet(allowedEventIds); - - // 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. - var events = await CollectEventsAsync(kw, KeywordGatekeepingMarker); - - var stream = ParseAllEvents(events); - var unexpected = stream.EventIds.Where(id => !allowed.Contains(id)).ToList(); - - 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))] - public async Task RuntimeAsync_ResetAsyncThreadContextEvent() - { - var events = await CollectEventsAsync(CoreKeywords, SingleAsyncYield); - - // DumpAllEvents(events); - - var ids = ParseAllEvents(events).EventIds; - - Assert.Contains(AsyncEventID.ResetAsyncThreadContext, ids); - } - - [ConditionalFact(typeof(AsyncProfilerTests), nameof(IsRuntimeAsyncSupported))] - public async Task RuntimeAsync_MetadataEventEmittedOnEnable() - { - var events = await CollectEventsAsync(AllKeywords, SingleAsyncYield); - - // DumpAllEvents(events); - - var stream = ParseAllEvents(events); - var metadataList = stream.MetadataEvents; - Assert.True(metadataList.Count >= 1, "Expected at least one metadata event in buffer"); - - MetadataFromBuffer meta = metadataList[0]; - Assert.True(meta.QpcFrequency > 0, $"QPC frequency should be positive, got {meta.QpcFrequency}"); - Assert.True(meta.QpcSync > 0, $"QPC sync timestamp should be positive, got {meta.QpcSync}"); - Assert.True(meta.UtcSync > 0, $"UTC sync timestamp should be positive, got {meta.UtcSync}"); - Assert.True(meta.EventBufferSize > 0, $"Event buffer size should be positive, got {meta.EventBufferSize}"); - Assert.True(meta.WrapperCount > 0, "Wrapper count should be positive"); - } - - // Requires threading: - // Spawns 8 threads with a Barrier to verify metadata is - // emitted exactly once under concurrent enable pressure. - [ConditionalFact(typeof(AsyncProfilerTests), nameof(IsRuntimeAsyncAndThreadingSupported))] - 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(); - SingleAsyncYield().GetAwaiter().GetResult(); - }, TaskCreationOptions.LongRunning); - } - Task.WaitAll(tasks); - SendFlushCommand(); - }); - - // DumpAllEvents(events); - - var stream = ParseAllEvents(events); - var metadataList = stream.MetadataEvents; - Assert.True(metadataList.Count == 1, $"Expected exactly 1 metadata event across {threadCount} threads, got {metadataList.Count}"); - } - - [System.Runtime.CompilerServices.RuntimeAsyncMethodGeneration(true)] - [System.Runtime.CompilerServices.MethodImpl(System.Runtime.CompilerServices.MethodImplOptions.NoInlining)] - static async Task NativeIPDeltaRoundtripMarker() - { - await ChainedAsyncYield(); - await DeepOuterCatches(); - await RecursiveAsyncChain(10); - } - - [ConditionalFact(typeof(AsyncProfilerTests), nameof(IsRuntimeAsyncSupported))] - public async Task 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 = await CollectEventsAsync(CallstackKeywords, NativeIPDeltaRoundtripMarker); - - var stream = ParseAllEvents(events); - var callstacks = stream.CallstacksWithMarker(AsyncEventID.ResumeAsyncCallstack, nameof(NativeIPDeltaRoundtripMarker)); - 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"); + public static void ReadInt64(ReadOnlySpan buffer, ref int index, out long value) + { + ulong uValue; + ReadUInt64(buffer, ref index, out uValue); + value = (long)uValue; + } - bool hasPositiveDelta = false; - bool hasNegativeDelta = false; + public static void ReadCompressedInt64(ReadOnlySpan buffer, ref int index, out long value) + { + ulong uValue; + ReadCompressedUInt64(buffer, ref index, out uValue); + value = ZigzagDecodeInt64(uValue); + } - foreach (var cs in deepCallstacks) + public static void ReadUInt64(ReadOnlySpan buffer, ref int index, out ulong value) { - for (int i = 0; i < cs.Frames.Count; i++) - { - var (nativeIP, _) = cs.Frames[i]; - Assert.True(nativeIP != 0, $"Frame {i} has zero NativeIP"); + value = BinaryPrimitives.ReadUInt64LittleEndian(buffer.Slice(index)); + index += 8; + } - var method = GetMethodNameFromNativeIP(nativeIP); - Assert.True(method is not null, $"Frame {i}: NativeIP 0x{nativeIP:X} does not resolve to a managed method"); + public static void ReadCompressedUInt64(ReadOnlySpan buffer, ref int index, out ulong value) + { + int shift = 0; + byte b; - 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; - } - } + value = 0; + do + { + b = buffer[index++]; + value |= (ulong)(b & 0x7F) << shift; + shift += 7; + } while ((b & 0x80) != 0); } - // 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"); - } + private static int ZigzagDecodeInt32(uint value) => (int)((value >> 1) ^ (~(value & 1) + 1)); - [System.Runtime.CompilerServices.RuntimeAsyncMethodGeneration(true)] - [System.Runtime.CompilerServices.MethodImpl(System.Runtime.CompilerServices.MethodImplOptions.NoInlining)] - static async Task CallstackStressMarker(int depth) - { - await RecursiveAsyncChain(depth); + private static long ZigzagDecodeInt64(ulong value) => (long)((value >> 1) ^ (~(value & 1) + 1)); } - // Requires threading: - // The recursive async chain must execute in a single dispatch - // loop (no sync context) to produce full-depth callstacks. Under xunit's - // AsyncTestSyncContext, each await re-dispatches, fragmenting the chain. - [ConditionalFact(typeof(AsyncProfilerTests), nameof(IsRuntimeAsyncAndThreadingSupported))] - public void RuntimeAsync_CallstackStressWithVaryingDepths() + private static class EventBuffer { - // 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. - // lambda -> CallstackStressMarker(d) -> RecursiveAsyncChain(d) produces d + 2 frames. - 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 CallstackStressMarker(depths[i]); - }); - }); - - // DumpAllEvents(events); - - var stream = ParseAllEvents(events); - var callstacks = stream.CallstacksWithMarker(AsyncEventID.ResumeAsyncCallstack, nameof(CallstackStressMarker)); - - // Verify all callstacks have valid frame data that resolves to managed methods. - foreach (var cs in callstacks) + public static void DumpAllEvents(CollectedEvents events) { - Assert.True(cs.FrameCount > 0, "Callstack has 0 frames"); - Assert.Equal((int)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 = GetMethodNameFromNativeIP(nativeIP); - Assert.True(method is not null, $"Frame {f}: NativeIP 0x{nativeIP:X} does not resolve to a managed method"); - } + ForEachEventBufferPayload(events.Events, buffer => EventBuffer.OutputEventBuffer(buffer)); + OutputFooter(); } - // One resume callstack per iteration (marker filters out noise). - // lambda -> CallstackStressMarker -> RecursiveAsyncChain(d) produces d + 2 frames. - Assert.True(callstacks.Count >= iterations, $"Expected at least {iterations} callstacks with marker, got {callstacks.Count}"); - - for (int i = 0; i < iterations; i++) + private static int OutputEventBuffer(ReadOnlySpan buffer) { - // lambda + CallstackStressMarker + RecursiveAsyncChain(d) = d + 2 - int expected = depths[i] + 2; - int actual = callstacks[i].FrameCount; - Assert.True(actual == expected, $"Iteration {i}: expected depth {expected} (lambda -> CallstackStressMarker -> RecursiveAsyncChain({depths[i]})), got {actual}"); - } + OutputHeader("Async Event Buffer"); - // Verify multiple buffer flushes occurred. - int bufferCount = 0; - ForEachEventBufferPayload(events, _ => bufferCount++); - Assert.True(bufferCount >= 3, $"Expected at least 3 buffer flushes, got {bufferCount}"); - } - - // Requires threading: - // Deep recursive chains must execute in a single dispatch loop (no sync context) - // to produce full-depth callstacks that trigger overflow. - [ConditionalFact(typeof(AsyncProfilerTests), nameof(IsRuntimeAsyncAndThreadingSupported))] - 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 the partial buffer, - // and re-writes the callstack as the first event in a fresh buffer. - // - // To prove overflow occurred we check consecutive buffer pairs: - // Buffer N: not full (has remaining capacity, but not enough for the next callstack) - // Buffer N+1: first event is a large callstack - // This proves the runtime detected insufficient space, rewound, and flushed. - 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); + int index = 0; - var events = CollectEvents(AllKeywords, () => + if ((uint)buffer.Length < 1) { - RunScenarioAndFlush(async () => - { - for (int i = 0; i < iterations; i++) - await RecursiveAsyncChain(depths[i]); - }); - }); + Console.WriteLine("Buffer too small."); + OutputFooter(); + return index; + } - // Get buffer capacity from metadata. - var stream = ParseAllEvents(events); - var metadataList = stream.MetadataEvents; - if (metadataList.Count == 0) - continue; - uint bufferCapacity = metadataList[0].EventBufferSize; - - // Collect per-buffer info grouped by async thread context. - // Consecutive buffers from the same context represent the overflow sequence. - var buffersByContext = new Dictionary>(); - ForEachEventBufferPayload(events, buffer => - { - EventBufferHeader? header = ParseEventBufferHeader(buffer); - if (header is null) - return; + byte version = buffer[index++]; + Console.WriteLine($"Version: {version}"); - uint contextId = header.Value.AsyncThreadContextId; + if (version != 1) + { + Console.WriteLine($"Unsupported version: {version}"); + OutputFooter(); + return index; + } - // Parse the first event in this buffer. - int index = HeaderSize; - if (index >= buffer.Length) - return; + 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); - AsyncEventID firstId = (AsyncEventID)buffer[index++]; - // Skip timestamp delta - ReadCompressedUInt64(buffer, ref index); + Console.WriteLine($"TotalSize: {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}"); - byte frameCount = 0; - if (firstId == AsyncEventID.ResumeAsyncCallstack || - firstId == AsyncEventID.CreateAsyncCallstack || - firstId == AsyncEventID.SuspendAsyncCallstack) - { - // Callstack payload: type(1) + callstackId(1) + frameCount(1) + compressed taskId + frames... - index++; // type byte - index++; // callstack ID (reserved) - if (index < buffer.Length) - frameCount = buffer[index]; - } + int eventCount = 0; + ulong currentTimestamp = startTimestamp; - if (!buffersByContext.TryGetValue(contextId, out var list)) + while (index < buffer.Length) + { + if (index + 2 > buffer.Length) { - list = new List<(int, AsyncEventID, byte)>(); - buffersByContext[contextId] = list; + Console.WriteLine($"Trailing bytes: {buffer.Length - index} (incomplete entry header)."); + break; } - list.Add((buffer.Length, firstId, frameCount)); - }); - // Look for overflow evidence within the same thread context: - // buffer N not full, buffer N+1 starts with large callstack. - foreach (var bufferInfos in buffersByContext.Values) - { - for (int i = 0; i < bufferInfos.Count - 1; i++) - { - var current = bufferInfos[i]; - var next = bufferInfos[i + 1]; + AsyncEventID eventId = (AsyncEventID)buffer[index++]; - Assert.True((uint)current.UsedSize <= bufferCapacity, $"Buffer used size {current.UsedSize} exceeds capacity {bufferCapacity}."); + Deserializer.ReadCompressedUInt64(buffer, ref index, out ulong delta); + currentTimestamp += delta; - uint remaining = bufferCapacity - (uint)current.UsedSize; - bool currentNotFull = remaining > 0; - bool nextStartsWithLargeCallstack = - (next.FirstEventId == AsyncEventID.ResumeAsyncCallstack || - next.FirstEventId == AsyncEventID.CreateAsyncCallstack || - next.FirstEventId == AsyncEventID.SuspendAsyncCallstack) && - next.FirstFrameCount > 30; + OutputHeader(eventCount, eventId, currentTimestamp); - if (currentNotFull && nextStartsWithLargeCallstack) + int payloadStart = index; + try + { + index += eventId switch { - overflowDetected = true; - break; - } + 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(buffer.Slice(index)), + AsyncEventID.ResumeAsyncCallstack => OutputAsyncCallstackEvent(buffer.Slice(index)), + AsyncEventID.SuspendAsyncCallstack => OutputAsyncCallstackEvent(buffer.Slice(index)), + AsyncEventID.AppendAsyncCallstack => OutputAsyncCallstackEvent(buffer.Slice(index)), + AsyncEventID.ResumeAsyncMethod => OutputResumeAsyncMethodEvent(), + AsyncEventID.CompleteAsyncMethod => OutputCompleteAsyncMethodEvent(), + AsyncEventID.ResetAsyncThreadContext => OutputResetAsyncThreadContextEvent(), + AsyncEventID.ResetAsyncContinuationWrapperIndex => OutputResetAsyncContinuationWrapperIndexEvent(), + AsyncEventID.AsyncProfilerMetadata => OutputAsyncProfilerMetadataEvent(buffer.Slice(index)), + AsyncEventID.AsyncProfilerSyncClock => OutputAsyncProfilerSyncClockEvent(buffer.Slice(index)), + _ => throw new InvalidOperationException($"Unknown eventId {eventId}."), + }; } - - if (overflowDetected) - break; - } - - // Validate all large callstacks in the stream have correct frames. - if (overflowDetected) - { - var largeCallstacks = stream.OfType(AsyncEventID.ResumeAsyncCallstack) - .Where(e => e.FrameCount > 30) - .ToList(); - - foreach (var cs in largeCallstacks) + catch (Exception ex) { - Assert.Equal((int)cs.FrameCount, cs.Frames.Count); - for (int f = 0; f < cs.Frames.Count; f++) - { - var (nativeIP, _) = cs.Frames[f]; - Assert.True(nativeIP != 0, $"Overflow callstack frame {f} has zero NativeIP"); - - var method = GetMethodNameFromNativeIP(nativeIP); - Assert.True(method is not null, $"Overflow callstack frame {f}: NativeIP 0x{nativeIP:X} does not resolve to a managed method"); - } + Console.WriteLine($" Failed decoding entry payload at offset {payloadStart}: {ex.GetType().Name}: {ex.Message}"); + break; } + + eventCount++; } - } - Assert.True(overflowDetected, "Failed to trigger callstack buffer overflow after 10 attempts — " + - "no consecutive buffer pair found where buffer N has remaining capacity and buffer N+1 starts with a large callstack"); - } + return index; + } - // Requires threading: - // Deep recursive chains must execute in a single dispatch - // loop (no sync context) to produce chains exceeding the 255-frame cap. - [ConditionalFact(typeof(AsyncProfilerTests), nameof(IsRuntimeAsyncAndThreadingSupported))] - public void RuntimeAsync_CallstackDepthCappedAtMaxFrames() - { - // Verify that callstack depth is capped when the continuation chain - // exceeds the maximum frame count (255, limited by byte storage). - // RecursiveAsyncChain(300) produces a 300-deep chain + 1 lambda = 301 frames. - const int requestedDepth = 300; + private const int OutputEventSeparatorWidth = 80; - var events = CollectEvents(ResumeAsyncCallstackKeyword, () => + private static void OutputHeader() { - RunScenarioAndFlush(async () => - { - await RecursiveAsyncChain(requestedDepth); - }); - }); - - // DumpAllEvents(events); + Console.WriteLine($"{new string('-', OutputEventSeparatorWidth)}"); + } - var stream = ParseAllEvents(events); - var callstacks = stream.OfType(AsyncEventID.ResumeAsyncCallstack).ToList(); - Assert.True(callstacks.Count >= 1, "Expected at least one callstack"); + private static void OutputHeader(string header) => Console.WriteLine($"{FormatCenteredLabel(header)}"); - // Find the callstack from our deep RecursiveAsyncChain call. - // The max frame count is capped at 255 (byte.MaxValue) since the - // CaptureRuntimeAsyncCallstackState.Count is a byte. - // RecursiveAsyncChain(300) + 1 lambda = 301 frames, capped to 255. - var deepest = callstacks.MaxBy(cs => cs.FrameCount); - Assert.Equal(255, (int)deepest!.FrameCount); - Assert.Equal((int)deepest.FrameCount, deepest.Frames.Count); + private static void OutputHeader(int eventCount, AsyncEventID id, ulong timestamp) => + Console.WriteLine($"[{eventCount}] {id} (0x{timestamp:X16})"); - // Verify all frames are valid. - foreach (var (nativeIP, _) in deepest.Frames) + private static string FormatCenteredLabel(string label) { - Assert.True(nativeIP != 0, "Frame has zero NativeIP"); - var method = GetMethodNameFromNativeIP(nativeIP); - Assert.True(method is not null, $"NativeIP 0x{nativeIP:X} does not resolve to a managed method"); + int totalDashes = Math.Max(6, OutputEventSeparatorWidth - label.Length - 2); + int leftDashes = totalDashes / 2; + int rightDashes = totalDashes - leftDashes; + return $"{new string('-', leftDashes)} {label} {new string('-', rightDashes)}"; } - } - - [ConditionalFact(typeof(AsyncProfilerTests), nameof(IsRuntimeAsyncSupported))] - public async Task RuntimeAsync_MetadataMatchesWrapperMethods() - { - var events = await CollectEventsAsync(AllKeywords, SingleAsyncYield); - var stream = ParseAllEvents(events); - var metadataList = stream.MetadataEvents; - Assert.True(metadataList.Count >= 1, "Expected at least one metadata event in buffer"); - - MetadataFromBuffer meta = metadataList[0]; - Assert.True(meta.WrapperCount > 0, "Expected positive wrapper count in metadata"); - - // On CoreCLR, verify via reflection that the contract-defined template produces names matching real methods. - // This catches accidental renames of wrapper methods without updating the contract. - if (PlatformDetection.IsCoreCLR) + private static void OutputFooter() { - Type? wrapperType = typeof(System.Runtime.CompilerServices.AsyncTaskMethodBuilder) - .Assembly.GetType("System.Runtime.CompilerServices.AsyncProfiler+ContinuationWrapper"); - Assert.NotNull(wrapperType); - for (int i = 0; i < meta.WrapperCount; i++) - { - string expectedName = string.Format(WrapperNameTemplate, i); - var method = wrapperType.GetMethod(expectedName, - System.Reflection.BindingFlags.Static | System.Reflection.BindingFlags.NonPublic | System.Reflection.BindingFlags.Public); - Assert.True(method is not null, $"Expected method '{expectedName}' not found on ContinuationWrapper type"); - } - - // Verify that the wrapper count matches the actual number of wrapper methods on the type. - int actualCount = wrapperType.GetMethods(System.Reflection.BindingFlags.Static | System.Reflection.BindingFlags.NonPublic | System.Reflection.BindingFlags.Public) - .Count(m => m.Name.StartsWith(WrapperNamePrefix, StringComparison.Ordinal)); - Assert.Equal(meta.WrapperCount, actualCount); + Console.WriteLine(new string('-', OutputEventSeparatorWidth)); } - } - } - - internal static class EventBuffer - { - public static int OutputEventBuffer(ReadOnlySpan buffer) - { - Console.WriteLine("--- AsyncEvents ---"); - int index = 0; - - if ((uint)buffer.Length < 1) + private static int OutputCreateAsyncContextEvent(ReadOnlySpan buffer) { - Console.WriteLine("Buffer too small."); - Console.WriteLine("----------------------------------"); + int index = 0; + Deserializer.ReadCompressedUInt64(buffer, ref index, out ulong id); + Console.WriteLine($" ID: {id}"); return index; } - byte version = buffer[index++]; - Console.WriteLine($"Version: {version}"); - - if (version != 1) + private static int OutputResumeAsyncContextEvent(ReadOnlySpan buffer) { - Console.WriteLine($"Unsupported version: {version}"); - Console.WriteLine("----------------------------------"); + int index = 0; + Deserializer.ReadCompressedUInt64(buffer, ref index, out ulong id); + Console.WriteLine($" ID: {id}"); 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) + private static int OutputSuspendAsyncContextEvent() { - if (index + 2 > buffer.Length) - { - Console.WriteLine($"Trailing bytes: {buffer.Length - index} (incomplete entry header)."); - break; - } - - AsyncEventID eventId = (AsyncEventID)buffer[index++]; - - Deserializer.ReadCompressedUInt64(buffer, ref index, out ulong delta); - currentTimestamp += delta; - - Console.WriteLine($"Entry[{eventCount}]: Timestamp=0x{currentTimestamp:X16}, EventId={eventId}"); - - int payloadStart = index; - try - { - index += eventId switch - { - AsyncEventID.CreateAsyncContext => OutputCreateAsyncContextEvent(buffer.Slice(index)), - AsyncEventID.ResumeAsyncContext => OutputResumeAsyncContextEvent(buffer.Slice(index)), - AsyncEventID.SuspendAsyncContext => OutputSuspendAsyncContextEvent(), - AsyncEventID.CompleteAsyncContext => OutputCompleteAsyncContextEvent(), - AsyncEventID.UnwindAsyncException => OutputUnwindAsyncExceptionEvent(buffer.Slice(index)), - AsyncEventID.CreateAsyncCallstack => OutputAsyncCallstackEvent("CreateAsyncCallstack", buffer.Slice(index)), - AsyncEventID.ResumeAsyncCallstack => OutputAsyncCallstackEvent("ResumeAsyncCallstack", buffer.Slice(index)), - AsyncEventID.SuspendAsyncCallstack => OutputAsyncCallstackEvent("SuspendAsyncCallstack", buffer.Slice(index)), - AsyncEventID.ResumeAsyncMethod => OutputResumeAsyncMethodEvent(), - AsyncEventID.CompleteAsyncMethod => OutputCompleteAsyncMethodEvent(), - AsyncEventID.ResetAsyncThreadContext => OutputResetAsyncThreadContextEvent(), - AsyncEventID.ResetAsyncContinuationWrapperIndex => OutputResetAsyncContinuationWrapperIndexEvent(), - AsyncEventID.AsyncProfilerMetadata => OutputAsyncProfilerMetadataEvent(buffer.Slice(index)), - _ => throw new InvalidOperationException($"Unknown eventId {eventId}."), - }; - } - catch (Exception ex) - { - Console.WriteLine($" Failed decoding entry payload at offset {payloadStart}: {ex.GetType().Name}: {ex.Message}"); - break; - } - - eventCount++; + return 0; } - 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; + private static int OutputCompleteAsyncContextEvent() + { + return 0; + } - Deserializer.ReadCompressedUInt32(buffer, ref index, out unwindedFrames); - index += OutputUnwindAsyncExceptionEvent(unwindedFrames); + private static int OutputUnwindAsyncExceptionEvent(ReadOnlySpan buffer) + { + uint unwindedFrames; + int index = 0; - return index; - } + Deserializer.ReadCompressedUInt32(buffer, ref index, out unwindedFrames); + index += OutputUnwindAsyncExceptionEvent(unwindedFrames); - private static int OutputUnwindAsyncExceptionEvent(uint unwindedFrames) - { - Console.WriteLine("--- UnwindAsyncException ---"); - Console.WriteLine($"Unwinded Frames: {unwindedFrames}"); - Console.WriteLine("----------------------------"); - return 0; - } + return index; + } - private static int OutputResumeAsyncMethodEvent() - { - Console.WriteLine("--- ResumeAsyncMethod ---"); - Console.WriteLine("----------------------------"); - return 0; - } + private static int OutputUnwindAsyncExceptionEvent(uint unwindedFrames) + { + Console.WriteLine($" Unwinded Frames: {unwindedFrames}"); + return 0; + } - private static int OutputCompleteAsyncMethodEvent() - { - Console.WriteLine("--- CompleteAsyncMethod ---"); - Console.WriteLine("----------------------------"); - return 0; - } + private static int OutputResumeAsyncMethodEvent() + { + return 0; + } - private static int OutputResetAsyncContinuationWrapperIndexEvent() - { - Console.WriteLine("--- ResetAsyncContinuationWrapperIndex ---"); - Console.WriteLine("----------------------------"); - return 0; - } + private static int OutputCompleteAsyncMethodEvent() + { + return 0; + } - private static int OutputResetAsyncThreadContextEvent() - { - Console.WriteLine("--- ResetAsyncThreadContext ---"); - Console.WriteLine("----------------------------"); - return 0; - } + private static int OutputResetAsyncContinuationWrapperIndexEvent() + { + return 0; + } - private static int OutputAsyncProfilerMetadataEvent(ReadOnlySpan buffer) - { - int index = 0; - Console.WriteLine("--- AsyncProfilerMetadata ---"); + private static int OutputResetAsyncThreadContextEvent() + { + return 0; + } - Deserializer.ReadCompressedUInt64(buffer, ref index, out ulong qpcFrequency); - Console.WriteLine($" QPCFrequency: {qpcFrequency}"); + private static int OutputAsyncProfilerMetadataEvent(ReadOnlySpan buffer) + { + int index = 0; - Deserializer.ReadCompressedUInt64(buffer, ref index, out ulong qpcSync); - Console.WriteLine($" QPCSync: {qpcSync}"); + Deserializer.ReadCompressedUInt64(buffer, ref index, out ulong qpcFrequency); + Console.WriteLine($" QPCFrequency: {qpcFrequency}"); - Deserializer.ReadCompressedUInt64(buffer, ref index, out ulong utcSync); - Console.WriteLine($" UTCSync: {utcSync}"); + Deserializer.ReadCompressedUInt64(buffer, ref index, out ulong qpcSync); + Console.WriteLine($" QPCSync: {qpcSync}"); - Deserializer.ReadCompressedUInt32(buffer, ref index, out uint eventBufferSize); - Console.WriteLine($" EventBufferSize: {eventBufferSize}"); + Deserializer.ReadCompressedUInt64(buffer, ref index, out ulong utcSync); + Console.WriteLine($" UTCSync: {utcSync}"); - byte wrapperCount = buffer[index++]; - Console.WriteLine($" WrapperCount: {wrapperCount}"); + Deserializer.ReadCompressedUInt32(buffer, ref index, out uint eventBufferSize); + Console.WriteLine($" EventBufferSize: {eventBufferSize}"); - Console.WriteLine("----------------------------"); - return index; - } + byte wrapperCount = buffer[index++]; + Console.WriteLine($" WrapperCount: {wrapperCount}"); - private static int OutputAsyncCallstackEvent(string eventName, ReadOnlySpan buffer) - { - ulong id; - byte type; - byte callstackId; - byte asyncCallstackLength; - int index = 0; - - type = buffer[index++]; - callstackId = buffer[index++]; - asyncCallstackLength = buffer[index++]; - Deserializer.ReadCompressedUInt64(buffer, ref index, out id); - - Console.WriteLine($"--- {eventName} ---"); - Console.WriteLine($"ID: {id}"); - Console.WriteLine($"Type: {type}"); - Console.WriteLine($"CallstackId: {callstackId}"); - Console.WriteLine($"Length: {asyncCallstackLength}"); - - if (asyncCallstackLength == 0) - { return index; } - ulong previousNativeIP; - ulong currentNativeIP; - int state; - - Deserializer.ReadCompressedUInt64(buffer, ref index, out currentNativeIP); - Deserializer.ReadCompressedInt32(buffer, ref index, out state); - - OutputAsyncFrame(currentNativeIP, state, 0); - - for (int i = 1; i < asyncCallstackLength; i++) + private static int OutputAsyncProfilerSyncClockEvent(ReadOnlySpan buffer) { - 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); - } + int index = 0; - return index; - } + Deserializer.ReadCompressedUInt64(buffer, ref index, out ulong qpcSync); + Console.WriteLine($" QPCSync: {qpcSync}"); - private static void OutputAsyncFrame(ulong nativeIP, int state, int frameIndex) - { - string asyncMethodName = AsyncProfilerTests.GetMethodNameFromNativeIP(nativeIP) ?? "??"; - string nativeIPString = $"0x{nativeIP:X}"; - Console.WriteLine($" Frame {frameIndex}: AsyncMethod = {asyncMethodName}, NativeIP = {nativeIPString}, State = {state}"); - } + Deserializer.ReadCompressedUInt64(buffer, ref index, out ulong utcSync); + Console.WriteLine($" UTCSync: {utcSync}"); - 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; + return index; } - public static void ReadCompressedInt32(ReadOnlySpan buffer, ref int index, out int value) + private static int OutputAsyncCallstackEvent(ReadOnlySpan buffer) { - uint uValue; - ReadCompressedUInt32(buffer, ref index, out uValue); - value = ZigzagDecodeInt32(uValue); - } + ulong id; + byte type; + byte callstackId; + byte asyncCallstackLength; + int index = 0; - public static void ReadUInt32(ReadOnlySpan buffer, ref int index, out uint value) - { - value = BinaryPrimitives.ReadUInt32LittleEndian(buffer.Slice(index)); - index += 4; - } + type = buffer[index++]; + callstackId = buffer[index++]; + asyncCallstackLength = buffer[index++]; + Deserializer.ReadCompressedUInt64(buffer, ref index, out id); - public static void ReadCompressedUInt32(ReadOnlySpan buffer, ref int index, out uint value) - { - int shift = 0; - byte b; + Console.WriteLine($" ID: {id}"); + Console.WriteLine($" Type: {type}"); + Console.WriteLine($" CallstackId: {callstackId}"); + Console.WriteLine($" Length: {asyncCallstackLength}"); - value = 0; - do + if (asyncCallstackLength == 0) { - 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; - } + return index; + } - public static void ReadCompressedInt64(ReadOnlySpan buffer, ref int index, out long value) - { - ulong uValue; - ReadCompressedUInt64(buffer, ref index, out uValue); - value = ZigzagDecodeInt64(uValue); - } + ulong previousMethodId; + ulong currentMethodId; + int state; - public static void ReadUInt64(ReadOnlySpan buffer, ref int index, out ulong value) - { - value = BinaryPrimitives.ReadUInt64LittleEndian(buffer.Slice(index)); - index += 8; - } + Deserializer.ReadCompressedUInt64(buffer, ref index, out currentMethodId); + Deserializer.ReadCompressedInt32(buffer, ref index, out state); - public static void ReadCompressedUInt64(ReadOnlySpan buffer, ref int index, out ulong value) - { - int shift = 0; - byte b; + OutputAsyncFrame((AsyncCallstackType)type, currentMethodId, state, 0); - value = 0; - do + for (int i = 1; i < asyncCallstackLength; i++) { - b = buffer[index++]; - value |= (ulong)(b & 0x7F) << shift; - shift += 7; - } while ((b & 0x80) != 0); - } + previousMethodId = currentMethodId; + Deserializer.ReadCompressedInt64(buffer, ref index, out long methodIdDelta); + Deserializer.ReadCompressedInt32(buffer, ref index, out state); + currentMethodId = previousMethodId + (ulong)methodIdDelta; + OutputAsyncFrame((AsyncCallstackType)type, currentMethodId, state, i); + } - private static int ZigzagDecodeInt32(uint value) => (int)((value >> 1) ^ (~(value & 1) + 1)); + return index; + } - private static long ZigzagDecodeInt64(ulong value) => (long)((value >> 1) ^ (~(value & 1) + 1)); + private static void OutputAsyncFrame(AsyncCallstackType type, ulong methodId, int state, int frameIndex) + { + string asyncMethodName = GetMethodNameFromMethodId(type, methodId) ?? "??"; + Console.WriteLine($" [{frameIndex}] {asyncMethodName} (0x{methodId:X}) (state={state})"); + } } } } diff --git a/src/libraries/System.Runtime/tests/System.Threading.Tasks.Tests/System.Runtime.CompilerServices/AsyncProfilerV1Tests.cs b/src/libraries/System.Runtime/tests/System.Threading.Tasks.Tests/System.Runtime.CompilerServices/AsyncProfilerV1Tests.cs new file mode 100644 index 00000000000000..88370dbd8ddf11 --- /dev/null +++ b/src/libraries/System.Runtime/tests/System.Threading.Tasks.Tests/System.Runtime.CompilerServices/AsyncProfilerV1Tests.cs @@ -0,0 +1,1713 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +using System.Collections.Generic; +using System.Diagnostics.Tracing; +using System.Linq; +using System.Runtime.CompilerServices; +using Microsoft.DotNet.XUnitExtensions; +using Xunit; + +namespace System.Threading.Tasks.Tests +{ + // Tests for V1 (Task-based AsyncStateMachineBox) async profiler event emission. + // All scenario methods use [RuntimeAsyncMethodGeneration(false)] to ensure they + // exercise the legacy Task-based async path even if the default changes in the future. + // Most tests use sync CollectEvents with RunScenarioAndFlush to isolate the V1 chain + // on a threadpool thread, ensuring dispatcher finally blocks complete before flush. + // Requires threading support. + public partial class AsyncProfilerTests + { + [RuntimeAsyncMethodGeneration(false)] + [MethodImpl(MethodImplOptions.NoInlining)] + private static async Task TaskAsync_SingleYield() + { + await Task.Yield(); + } + + [RuntimeAsyncMethodGeneration(false)] + [MethodImpl(MethodImplOptions.NoInlining)] + private static async Task TaskAsync_DeepChain() + { + await TaskAsync_Level1(); + } + + [RuntimeAsyncMethodGeneration(false)] + [MethodImpl(MethodImplOptions.NoInlining)] + private static async Task TaskAsync_Level1() + { + await TaskAsync_Level2(); + } + + [RuntimeAsyncMethodGeneration(false)] + [MethodImpl(MethodImplOptions.NoInlining)] + private static async Task TaskAsync_Level2() + { + await TaskAsync_Level3(); + } + + [RuntimeAsyncMethodGeneration(false)] + [MethodImpl(MethodImplOptions.NoInlining)] + private static async Task TaskAsync_Level3() + { + await Task.Delay(100); + } + + [RuntimeAsyncMethodGeneration(false)] + [MethodImpl(MethodImplOptions.NoInlining)] + private static async Task TaskAsync_ExceptionHandled() + { + try + { + await TaskAsync_InnerThrows(); + } + catch (InvalidOperationException) + { + } + } + + [RuntimeAsyncMethodGeneration(false)] + [MethodImpl(MethodImplOptions.NoInlining)] + private static async Task TaskAsync_InnerThrows() + { + await Task.Delay(100); + throw new InvalidOperationException("inner"); + } + + [RuntimeAsyncMethodGeneration(false)] + [MethodImpl(MethodImplOptions.NoInlining)] + private static async Task TaskAsync_UnhandledExceptionOuter() + { + await TaskAsync_UnhandledExceptionInner(); + } + + [RuntimeAsyncMethodGeneration(false)] + [MethodImpl(MethodImplOptions.NoInlining)] + private static async Task TaskAsync_UnhandledExceptionInner() + { + await Task.Delay(100); + throw new InvalidOperationException("unhandled inner"); + } + + [RuntimeAsyncMethodGeneration(false)] + [MethodImpl(MethodImplOptions.NoInlining)] + private static async Task TaskAsync_RecursiveChain(int depth) + { + if (depth <= 1) + { + await Task.Delay(100); + return; + } + await TaskAsync_RecursiveChain(depth - 1); + } + + [RuntimeAsyncMethodGeneration(false)] + [MethodImpl(MethodImplOptions.NoInlining)] + private static async ValueTask ValueTaskAsync_Level1() + { + await ValueTaskAsync_Level2(); + } + + [RuntimeAsyncMethodGeneration(false)] + [MethodImpl(MethodImplOptions.NoInlining)] + private static async ValueTask ValueTaskAsync_Level2() + { + await ValueTaskAsync_Level3(); + } + + [RuntimeAsyncMethodGeneration(false)] + [MethodImpl(MethodImplOptions.NoInlining)] + private static async ValueTask ValueTaskAsync_Level3() + { + await Task.Delay(100); + } + + [ConditionalFact(typeof(AsyncProfilerTests), nameof(IsRuntimeAsyncAndThreadingSupported))] + public void TaskAsync_EventsEmitted() + { + var events = CollectEvents(CoreKeywords, () => + { + RunScenarioAndFlush(() => TaskAsync_SingleYield()); + }); + + // DumpAllEvents(events); + + Assert.True(events.Events.Count > 0, "Expected at least one AsyncEvents event to be emitted"); + Assert.Contains(events.Events, e => e.EventId == AsyncEventsId); + } + + [ConditionalFact(typeof(AsyncProfilerTests), nameof(IsRuntimeAsyncAndThreadingSupported))] + public void TaskAsync_CreateAsyncContextEmittedOnFirstAwait() + { + var events = CollectEvents(CoreKeywords, () => + { + RunScenarioAndFlush(() => TaskAsync_SingleYield()); + }); + + // DumpAllEvents(events); + + var stream = ParseAllEvents(events); + + var creates = stream.OfType(AsyncEventID.CreateAsyncContext).ToList(); + Assert.True(creates.Count >= 1, $"Expected at least 1 CreateAsyncContext, got {creates.Count}"); + } + + [RuntimeAsyncMethodGeneration(false)] + [MethodImpl(MethodImplOptions.NoInlining)] + private static async Task TaskAsync_EventSequenceOrder_Marker() + { + await Task.Delay(100); + } + + [ConditionalFact(typeof(AsyncProfilerTests), nameof(IsRuntimeAsyncAndThreadingSupported))] + public void TaskAsync_EventSequenceOrder() + { + var events = CollectEvents(ResumeAsyncCallstackKeyword | CoreKeywords, () => + { + RunScenarioAndFlush(() => TaskAsync_EventSequenceOrder_Marker()); + }); + + // DumpAllEvents(events); + + var stream = ParseAllEvents(events); + + var markerCallstacks = stream.CallstacksWithMarker(AsyncEventID.ResumeAsyncCallstack, nameof(TaskAsync_EventSequenceOrder_Marker)); + Assert.NotEmpty(markerCallstacks); + + ulong taskId = markerCallstacks[0].TaskId; + var ids = stream.ForTask(taskId).Select(e => e.EventId).ToList(); + + int createIdx = ids.IndexOf(AsyncEventID.CreateAsyncContext); + Assert.True(createIdx >= 0, "Expected CreateAsyncContext"); + + int resumeIdx = ids.IndexOf(AsyncEventID.ResumeAsyncContext, createIdx + 1); + Assert.True(resumeIdx > createIdx, "Expected ResumeAsyncContext after Create"); + + int completeIdx = ids.IndexOf(AsyncEventID.CompleteAsyncContext, resumeIdx + 1); + Assert.True(completeIdx > resumeIdx, "Expected CompleteAsyncContext after Resume"); + } + + [RuntimeAsyncMethodGeneration(false)] + [MethodImpl(MethodImplOptions.NoInlining)] + private static async Task TaskAsync_SuspendResumeCompleteEvents_Marker() + { + await Task.Delay(100); + await Task.Delay(100); + await Task.Delay(100); + } + + [ConditionalFact(typeof(AsyncProfilerTests), nameof(IsRuntimeAsyncAndThreadingSupported))] + public void TaskAsync_SuspendResumeCompleteEvents() + { + var events = CollectEvents(ResumeAsyncCallstackKeyword | CoreKeywords, () => + { + RunScenarioAndFlush(() => TaskAsync_SuspendResumeCompleteEvents_Marker()); + }); + + // DumpAllEvents(events); + + var stream = ParseAllEvents(events); + + var markerCallstacks = stream.CallstacksWithMarker(AsyncEventID.ResumeAsyncCallstack, nameof(TaskAsync_SuspendResumeCompleteEvents_Marker)); + Assert.NotEmpty(markerCallstacks); + + ulong taskId = markerCallstacks[0].TaskId; + var ids = stream.ForTask(taskId).Select(e => e.EventId).ToList(); + + int createIdx = ids.IndexOf(AsyncEventID.CreateAsyncContext); + Assert.True(createIdx >= 0, "Expected CreateAsyncContext"); + + int resumeCount = ids.Count(id => id == AsyncEventID.ResumeAsyncContext); + Assert.True(resumeCount >= 1, "Expected at least one ResumeAsyncContext"); + + int completeCount = ids.Count(id => id == AsyncEventID.CompleteAsyncContext); + Assert.True(completeCount >= 1, "Expected at least one CompleteAsyncContext"); + + // Expected ResumeAsyncContext and CompleteAsyncContext counts to match. + Assert.Equal(resumeCount, completeCount); + + // Expected no SuspendAsyncContext events. + Assert.DoesNotContain(AsyncEventID.SuspendAsyncContext, ids); + } + + [ConditionalFact(typeof(AsyncProfilerTests), nameof(IsRuntimeAsyncAndThreadingSupported))] + public void TaskAsync_ResumeCompleteMethodEvents() + { + var events = CollectEvents(MethodKeywords, () => + { + RunScenarioAndFlush(() => TaskAsync_SingleYield()); + }); + + // DumpAllEvents(events); + + var stream = ParseAllEvents(events); + var ids = stream.EventIds; + + Assert.Contains(AsyncEventID.ResumeAsyncMethod, ids); + Assert.Contains(AsyncEventID.CompleteAsyncMethod, ids); + } + + [RuntimeAsyncMethodGeneration(false)] + [MethodImpl(MethodImplOptions.NoInlining)] + private static async Task TaskAsync_HandledException_EmitsUnwindAndComplete_Marker() + { + await TaskAsync_ExceptionHandled(); + } + + [ConditionalFact(typeof(AsyncProfilerTests), nameof(IsRuntimeAsyncAndThreadingSupported))] + public void TaskAsync_HandledException_EmitsUnwindAndComplete() + { + var events = CollectEvents(ResumeAsyncCallstackKeyword | CoreKeywords | UnwindAsyncExceptionKeyword, () => + { + RunScenarioAndFlush(() => TaskAsync_HandledException_EmitsUnwindAndComplete_Marker()); + }); + + //DumpAllEvents(events); + + var stream = ParseAllEvents(events); + + var markerCallstacks = stream.CallstacksWithMarker(AsyncEventID.ResumeAsyncCallstack, nameof(TaskAsync_HandledException_EmitsUnwindAndComplete_Marker)); + Assert.NotEmpty(markerCallstacks); + + ulong taskId = markerCallstacks[0].TaskId; + var ids = stream.ForTask(taskId).Select(e => e.EventId).ToList(); + + int createIdx = ids.IndexOf(AsyncEventID.CreateAsyncContext); + Assert.True(createIdx >= 0, "Expected CreateAsyncContext"); + + int resumeIdx = ids.IndexOf(AsyncEventID.ResumeAsyncContext, createIdx + 1); + Assert.True(resumeIdx > createIdx, "Expected ResumeAsyncContext after Create"); + + int unwindIdx = ids.IndexOf(AsyncEventID.UnwindAsyncException, resumeIdx + 1); + Assert.True(unwindIdx > resumeIdx, "Expected UnwindAsyncException after Resume"); + + int completeIdx = ids.IndexOf(AsyncEventID.CompleteAsyncContext, unwindIdx + 1); + Assert.True(completeIdx > unwindIdx, "Expected CompleteAsyncContext after Unwind"); + } + + [RuntimeAsyncMethodGeneration(false)] + [MethodImpl(MethodImplOptions.NoInlining)] + private static async Task TaskAsync_UnhandledException_EmitsUnwindAndComplete_Marker() + { + await TaskAsync_UnhandledExceptionOuter(); + } + + [ConditionalFact(typeof(AsyncProfilerTests), nameof(IsRuntimeAsyncAndThreadingSupported))] + public void TaskAsync_UnhandledException_EmitsUnwindAndComplete() + { + var events = CollectEvents(ResumeAsyncCallstackKeyword | CoreKeywords | UnwindAsyncExceptionKeyword, () => + { + try + { + RunScenarioAndFlush(() => TaskAsync_UnhandledException_EmitsUnwindAndComplete_Marker()); + } + catch (InvalidOperationException) + { + } + }); + + // DumpAllEvents(events); + + var stream = ParseAllEvents(events); + + var markerCallstacks = stream.CallstacksWithMarker(AsyncEventID.ResumeAsyncCallstack, nameof(TaskAsync_UnhandledException_EmitsUnwindAndComplete_Marker)); + Assert.NotEmpty(markerCallstacks); + + ulong taskId = markerCallstacks[0].TaskId; + var ids = stream.ForTask(taskId).Select(e => e.EventId).ToList(); + + int createIdx = ids.IndexOf(AsyncEventID.CreateAsyncContext); + Assert.True(createIdx >= 0, "Expected CreateAsyncContext"); + + int resumeIdx = ids.IndexOf(AsyncEventID.ResumeAsyncContext, createIdx + 1); + Assert.True(resumeIdx > createIdx, "Expected ResumeAsyncContext after Create"); + + int unwindIdx1 = ids.IndexOf(AsyncEventID.UnwindAsyncException, resumeIdx + 1); + Assert.True(unwindIdx1 > resumeIdx, "Expected first UnwindAsyncException after Resume"); + + int unwindIdx2 = ids.IndexOf(AsyncEventID.UnwindAsyncException, unwindIdx1 + 1); + Assert.True(unwindIdx2 > unwindIdx1, "Expected second UnwindAsyncException after first Unwind"); + + int completeIdx = ids.IndexOf(AsyncEventID.CompleteAsyncContext, unwindIdx2 + 1); + Assert.True(completeIdx > unwindIdx2, "Expected CompleteAsyncContext after second Unwind"); + } + + + [RuntimeAsyncMethodGeneration(false)] + [MethodImpl(MethodImplOptions.NoInlining)] + private static async Task TaskAsync_MethodEventCountMatchesChainDepth_Marker() + { + await TaskAsync_DeepChain(); + } + + [ConditionalFact(typeof(AsyncProfilerTests), nameof(IsRuntimeAsyncAndThreadingSupported))] + public void TaskAsync_MethodEventCountMatchesChainDepth() + { + var events = CollectEvents(ResumeAsyncCallstackKeyword | MethodKeywords | CoreKeywords, () => + { + RunScenarioAndFlush(() => TaskAsync_MethodEventCountMatchesChainDepth_Marker()); + }); + + // DumpAllEvents(events); + + var stream = ParseAllEvents(events); + + // Marker -> DeepChain -> Level1 -> Level2 -> Level3 + const int ExpectedChainDepth = 5; + + var markerCallstacks = stream.CallstacksWithMarker(AsyncEventID.ResumeAsyncCallstack, nameof(TaskAsync_MethodEventCountMatchesChainDepth_Marker)); + Assert.NotEmpty(markerCallstacks); + + Assert.Equal(ExpectedChainDepth, markerCallstacks[0].Frames.Count); + + ulong chainTaskId = markerCallstacks[0].TaskId; + var chainEvents = stream.ForTask(chainTaskId); + + int resumeCount = chainEvents.Count(e => e.EventId == AsyncEventID.ResumeAsyncMethod); + Assert.Equal(ExpectedChainDepth, resumeCount); + + int completeCount = chainEvents.Count(e => e.EventId == AsyncEventID.CompleteAsyncMethod); + Assert.Equal(ExpectedChainDepth, completeCount); + } + + [RuntimeAsyncMethodGeneration(false)] + [MethodImpl(MethodImplOptions.NoInlining)] + private static async Task TaskAsync_HandledException_MethodEventsWithUnwind_Marker() + { + await TaskAsync_ExceptionHandled(); + } + + [ConditionalFact(typeof(AsyncProfilerTests), nameof(IsRuntimeAsyncAndThreadingSupported))] + public void TaskAsync_HandledException_MethodEventsWithUnwind() + { + var events = CollectEvents(ResumeAsyncCallstackKeyword | MethodKeywords | CoreKeywords | UnwindAsyncExceptionKeyword, () => + { + RunScenarioAndFlush(() => TaskAsync_HandledException_MethodEventsWithUnwind_Marker()); + }); + + // DumpAllEvents(events); + + var stream = ParseAllEvents(events); + + var markerCallstacks = stream.CallstacksWithMarker(AsyncEventID.ResumeAsyncCallstack, nameof(TaskAsync_HandledException_MethodEventsWithUnwind_Marker)); + + Assert.NotEmpty(markerCallstacks); + + ulong chainTaskId = markerCallstacks[0].TaskId; + var chainEvents = stream.ForTask(chainTaskId); + + var sequence = chainEvents + .Where(e => e.EventId is AsyncEventID.ResumeAsyncMethod + or AsyncEventID.CompleteAsyncMethod + or AsyncEventID.UnwindAsyncException) + .Select(e => e.EventId) + .ToList(); + + // Exactly one Unwind expected on the chain's context (InnerThrows throws once). + Assert.Equal(1, sequence.Count(id => id == AsyncEventID.UnwindAsyncException)); + + // Around the Unwind: the throwing method's Resume precedes it, and the catching + // method's Resume -> Complete pair follows. + int unwindIdx = sequence.IndexOf(AsyncEventID.UnwindAsyncException); + Assert.Equal(AsyncEventID.ResumeAsyncMethod, sequence[unwindIdx - 1]); + Assert.Equal(AsyncEventID.ResumeAsyncMethod, sequence[unwindIdx + 1]); + Assert.Equal(AsyncEventID.CompleteAsyncMethod, sequence[unwindIdx + 2]); + } + + [RuntimeAsyncMethodGeneration(false)] + [MethodImpl(MethodImplOptions.NoInlining)] + private static async Task TaskAsync_UnhandledException_MethodEventsWithUnwind_Marker() + { + await TaskAsync_UnhandledExceptionOuter(); + } + + [ConditionalFact(typeof(AsyncProfilerTests), nameof(IsRuntimeAsyncAndThreadingSupported))] + public void TaskAsync_UnhandledException_MethodEventsWithUnwind() + { + var events = CollectEvents(ResumeAsyncCallstackKeyword | MethodKeywords | CoreKeywords | UnwindAsyncExceptionKeyword, () => + { + try + { + RunScenarioAndFlush(() => TaskAsync_UnhandledException_MethodEventsWithUnwind_Marker()); + } + catch (InvalidOperationException) + { + } + }); + + // DumpAllEvents(events); + + var stream = ParseAllEvents(events); + + // Marker -> UnhandledExceptionOuter -> UnhandledExceptionInner + const int ExpectedChainDepth = 3; + + var markerCallstacks = stream.CallstacksWithMarker(AsyncEventID.ResumeAsyncCallstack, nameof(TaskAsync_UnhandledException_MethodEventsWithUnwind_Marker)); + Assert.NotEmpty(markerCallstacks); + + ulong chainTaskId = markerCallstacks[0].TaskId; + var chainEvents = stream.ForTask(chainTaskId); + + var sequence = chainEvents + .Where(e => e.EventId is AsyncEventID.ResumeAsyncMethod + or AsyncEventID.CompleteAsyncMethod + or AsyncEventID.UnwindAsyncException) + .Select(e => e.EventId) + .ToList(); + + // Every method in the chain unwinds (no catch); no CompleteAsyncMethod expected. + Assert.Equal(ExpectedChainDepth, sequence.Count(id => id == AsyncEventID.ResumeAsyncMethod)); + Assert.Equal(ExpectedChainDepth, sequence.Count(id => id == AsyncEventID.UnwindAsyncException)); + Assert.Equal(0, sequence.Count(id => id == AsyncEventID.CompleteAsyncMethod)); + + // Per-method ordering: each Resume is immediately followed by its Unwind. + for (int i = 0; i < sequence.Count; i += 2) + { + Assert.Equal(AsyncEventID.ResumeAsyncMethod, sequence[i]); + Assert.Equal(AsyncEventID.UnwindAsyncException, sequence[i + 1]); + } + } + + [RuntimeAsyncMethodGeneration(false)] + [MethodImpl(MethodImplOptions.NoInlining)] + private static async Task TaskAsync_ResumeAsyncCallstackEmitted_Marker() + { + await TaskAsync_DeepChain(); + } + + [ConditionalFact(typeof(AsyncProfilerTests), nameof(IsRuntimeAsyncAndThreadingSupported))] + public void TaskAsync_ResumeAsyncCallstackEmitted() + { + var events = CollectEvents(ResumeAsyncCallstackKeyword | CoreKeywords, () => + { + RunScenarioAndFlush(() => TaskAsync_ResumeAsyncCallstackEmitted_Marker()); + }); + + // DumpAllEvents(events); + + var stream = ParseAllEvents(events); + + var markerCallstacks = stream.CallstacksWithMarker(AsyncEventID.ResumeAsyncCallstack, nameof(TaskAsync_ResumeAsyncCallstackEmitted_Marker)); + Assert.NotEmpty(markerCallstacks); + + Assert.All(markerCallstacks, cs => + { + Assert.True(cs.FrameCount > 0, "Expected at least one frame in resume callstack"); + Assert.True(cs.Frames[0].MethodId != 0, "Expected non-zero methodId in first frame"); + }); + } + + [RuntimeAsyncMethodGeneration(false)] + [MethodImpl(MethodImplOptions.NoInlining)] + private static async Task TaskAsync_CallstackDepthMatchesChainDepth_Marker() + { + await TaskAsync_Level1(); + } + + [ConditionalFact(typeof(AsyncProfilerTests), nameof(IsRuntimeAsyncAndThreadingSupported))] + public void TaskAsync_CallstackDepthMatchesChainDepth() + { + var events = CollectEvents(ResumeAsyncCallstackKeyword | CoreKeywords, () => + { + RunScenarioAndFlush(() => TaskAsync_CallstackDepthMatchesChainDepth_Marker()); + }); + + // DumpAllEvents(events); + + var stream = ParseAllEvents(events); + + var markerCallstacks = stream.CallstacksWithMarker(AsyncEventID.ResumeAsyncCallstack, nameof(TaskAsync_CallstackDepthMatchesChainDepth_Marker)); + Assert.NotEmpty(markerCallstacks); + + // TaskAsync_CallstackDepthMarker -> Level1 -> Level2 -> Level3: deepest callstack should have exactly 4 frames + Assert.Equal(4, markerCallstacks[0].FrameCount); + } + + [RuntimeAsyncMethodGeneration(false)] + [MethodImpl(MethodImplOptions.NoInlining)] + private static async Task TaskAsync_CallstackFramesHaveDistinctMethodIds_Marker() + { + await TaskAsync_Level1(); + } + + [ConditionalFact(typeof(AsyncProfilerTests), nameof(IsRuntimeAsyncAndThreadingSupported))] + public void TaskAsync_CallstackFramesHaveDistinctMethodIds() + { + var events = CollectEvents(ResumeAsyncCallstackKeyword | CoreKeywords, () => + { + RunScenarioAndFlush(() => TaskAsync_CallstackFramesHaveDistinctMethodIds_Marker()); + }); + + // DumpAllEvents(events); + + var stream = ParseAllEvents(events); + + var markerCallstacks = stream.CallstacksWithMarker(AsyncEventID.ResumeAsyncCallstack, nameof(TaskAsync_CallstackFramesHaveDistinctMethodIds_Marker)); + Assert.NotEmpty(markerCallstacks); + + // Frames in the same callstack should have distinct methodIds (different async methods) + var methodIds = markerCallstacks[0].Frames.Select(f => f.MethodId).ToList(); + Assert.Equal(methodIds.Count, methodIds.Distinct().Count()); + } + + [RuntimeAsyncMethodGeneration(false)] + [MethodImpl(MethodImplOptions.NoInlining)] + private static async Task TaskAsync_CallstackFramesHaveDistinctStates_Root_Marker() + { + await Task.Yield(); + await TaskAsync_CallstackFramesHaveDistinctStates_Middle_Marker(); + } + + [RuntimeAsyncMethodGeneration(false)] + [MethodImpl(MethodImplOptions.NoInlining)] + private static async Task TaskAsync_CallstackFramesHaveDistinctStates_Middle_Marker() + { + await Task.Yield(); + await Task.Yield(); + await TaskAsync_CallstackFramesHaveDistinctStates_Leaf_Marker(); + } + + [RuntimeAsyncMethodGeneration(false)] + [MethodImpl(MethodImplOptions.NoInlining)] + private static async Task TaskAsync_CallstackFramesHaveDistinctStates_Leaf_Marker() + { + await Task.Delay(100); + } + + [RuntimeAsyncMethodGeneration(false)] + [MethodImpl(MethodImplOptions.NoInlining)] + private static async Task TaskAsync_CallstackFramesHaveDistinctStates_Marker() + { + await TaskAsync_CallstackFramesHaveDistinctStates_Root_Marker(); + } + + [ConditionalFact(typeof(AsyncProfilerTests), nameof(IsRuntimeAsyncAndThreadingSupported))] + public void TaskAsync_CallstackFramesHaveDistinctStates() + { + var events = CollectEvents(ResumeAsyncCallstackKeyword | CoreKeywords, () => + { + RunScenarioAndFlush(() => TaskAsync_CallstackFramesHaveDistinctStates_Marker()); + }); + + // DumpAllEvents(events); + + var stream = ParseAllEvents(events); + + var markerCallstacks = stream.CallstacksWithMarker(AsyncEventID.ResumeAsyncCallstack, nameof(TaskAsync_CallstackFramesHaveDistinctStates_Marker)); + Assert.NotEmpty(markerCallstacks); + + // The deepest callstack (on the final Delay resume) should have 4 frames: + // Leaf (state=3), Middle (state=2), Root (state=1), Marker (state=0) + var deepest = markerCallstacks.MaxBy(cs => cs.FrameCount)!; + Assert.Equal(4, deepest.FrameCount); + + // Each frame should have a different state reflecting its suspend point + var states = deepest.Frames.Select(f => f.State).ToList(); + Assert.Equal(0, states[0]); // Leaf: suspended at 1st await (state=0) + Assert.Equal(2, states[1]); // Middle: suspended at 3rd await (state=2) + Assert.Equal(1, states[2]); // Root: suspended at 2nd await (state=1) + Assert.Equal(0, states[3]); // Marker: suspended at 1st await (state=0) + } + + [RuntimeAsyncMethodGeneration(false)] + [MethodImpl(MethodImplOptions.NoInlining)] + private static async Task TaskAsync_YieldAtEachLevel_CallstackShrinks_Level1_Marker() + { + await TaskAsync_YieldAtEachLevel_CallstackShrinks_Level2_Marker(); + await Task.Yield(); + } + + [RuntimeAsyncMethodGeneration(false)] + [MethodImpl(MethodImplOptions.NoInlining)] + private static async Task TaskAsync_YieldAtEachLevel_CallstackShrinks_Level2_Marker() + { + await TaskAsync_YieldAtEachLevel_CallstackShrinks_Level3_Marker(); + await Task.Yield(); + } + + [RuntimeAsyncMethodGeneration(false)] + [MethodImpl(MethodImplOptions.NoInlining)] + private static async Task TaskAsync_YieldAtEachLevel_CallstackShrinks_Level3_Marker() + { + await Task.Delay(100); + await Task.Yield(); + } + + [RuntimeAsyncMethodGeneration(false)] + [MethodImpl(MethodImplOptions.NoInlining)] + private static async Task TaskAsync_YieldAtEachLevel_CallstackShrinks_Marker() + { + await TaskAsync_YieldAtEachLevel_CallstackShrinks_Level1_Marker(); + } + + [ConditionalFact(typeof(AsyncProfilerTests), nameof(IsRuntimeAsyncAndThreadingSupported))] + public void TaskAsync_YieldAtEachLevel_CallstackShrinks() + { + var events = CollectEvents(ResumeAsyncCallstackKeyword | CoreKeywords, () => + { + RunScenarioAndFlush(() => TaskAsync_YieldAtEachLevel_CallstackShrinks_Marker()); + }); + + // DumpAllEvents(events); + + var stream = ParseAllEvents(events); + + var markerCallstacks = stream.CallstacksWithMarker(AsyncEventID.ResumeAsyncCallstack, nameof(TaskAsync_YieldAtEachLevel_CallstackShrinks_Marker)); + + // After Task.Delay resumes: full chain (Level3, Level2, Level1, Marker) = 4 frames + // After Level3's yield resumes: Level3 completes, chain is (Level2, Level1, Marker) = 3 frames + // After Level2's yield resumes: Level2 completes, chain is (Level1, Marker) = 2 frames + Assert.Contains(markerCallstacks, cs => cs.FrameCount == 4); + Assert.Contains(markerCallstacks, cs => cs.FrameCount == 3); + Assert.Contains(markerCallstacks, cs => cs.FrameCount == 2); + } + + private static SemaphoreSlim s_appendRace_proceed; + + [RuntimeAsyncMethodGeneration(false)] + [MethodImpl(MethodImplOptions.NoInlining)] + private static async Task TaskAsync_AppendCallstack_FiresOnLateParentRegistration_Child_Marker() + { + await Task.Yield(); + s_appendRace_proceed.Release(); + Thread.Sleep(200); + await Task.Yield(); + } + + [RuntimeAsyncMethodGeneration(false)] + [MethodImpl(MethodImplOptions.NoInlining)] + private static async Task TaskAsync_AppendCallstack_FiresOnLateParentRegistration_Parent_Marker() + { + Task t = TaskAsync_AppendCallstack_FiresOnLateParentRegistration_Child_Marker(); + s_appendRace_proceed.Wait(); + await t; + } + + [ConditionalFact(typeof(AsyncProfilerTests), nameof(IsRuntimeAsyncAndThreadingSupported))] + public void TaskAsync_AppendCallstack_FiresOnLateParentRegistration() + { + s_appendRace_proceed = new SemaphoreSlim(0, 1); + + var events = CollectEvents(ResumeAsyncCallstackKeyword | CoreKeywords, () => + { + RunScenarioAndFlush(() => TaskAsync_AppendCallstack_FiresOnLateParentRegistration_Parent_Marker()); + }); + + // DumpAllEvents(events); + + var stream = ParseAllEvents(events); + + // The initial Resume should only include TaskAsync_AppendRace_Child (race: Parent not registered yet). + var childOnlyResumes = stream.CallstacksWithMarker(AsyncEventID.ResumeAsyncCallstack, nameof(TaskAsync_AppendCallstack_FiresOnLateParentRegistration_Child_Marker)); + Assert.NotEmpty(childOnlyResumes); + + // After Parent registers and Child hits its next complete hook, + // an AppendAsyncCallstack should fire with the Parent frame. + var appendsWithParent = stream.CallstacksWithMarker(AsyncEventID.AppendAsyncCallstack, nameof(TaskAsync_AppendCallstack_FiresOnLateParentRegistration_Parent_Marker)); + Assert.NotEmpty(appendsWithParent); + } + + [RuntimeAsyncMethodGeneration(false)] + [MethodImpl(MethodImplOptions.NoInlining)] + private static async Task TaskAsync_CompleteChain_DoesNotEmitAppendEvents_Marker() + { + await TaskAsync_DeepChain(); + } + + [ConditionalFact(typeof(AsyncProfilerTests), nameof(IsRuntimeAsyncAndThreadingSupported))] + public void TaskAsync_CompleteChain_DoesNotEmitAppendEvents() + { + var events = CollectEvents(ResumeAsyncCallstackKeyword | CoreKeywords, () => + { + RunScenarioAndFlush(() => TaskAsync_CompleteChain_DoesNotEmitAppendEvents_Marker()); + }); + + // DumpAllEvents(events); + + var stream = ParseAllEvents(events); + + // Sanity: the marker frame must appear in the initial Resume callstack (full chain captured). + var markerCallstacks = stream.CallstacksWithMarker(AsyncEventID.ResumeAsyncCallstack, nameof(TaskAsync_CompleteChain_DoesNotEmitAppendEvents_Marker)); + Assert.NotEmpty(markerCallstacks); + + // No Append events should fire on this context -- the chain was complete at Resume time. + ulong chainTaskId = markerCallstacks[0].TaskId; + var appendEvents = stream.ForTask(chainTaskId) + .Where(e => e.EventId == AsyncEventID.AppendAsyncCallstack) + .ToList(); + Assert.Empty(appendEvents); + } + + private static InlinePostSynchronizationContext? s_taskAsyncSyncContextCtx; + + [RuntimeAsyncMethodGeneration(false)] + [MethodImpl(MethodImplOptions.NoInlining)] + private static async Task TaskAsync_CustomSyncContext_EmitsContextEventsAndCallstack_Marker() + { + // Install a non-default SynchronizationContext on this thread so the await captures it. + // The await's continuation will be routed via SynchronizationContextAwaitTaskContinuation, + // which wraps the box in an AsyncTaskDispatcher and posts back to the context. + int callerThreadId = Environment.CurrentManagedThreadId; + SynchronizationContext? prev = SynchronizationContext.Current; + SynchronizationContext.SetSynchronizationContext(s_taskAsyncSyncContextCtx); + try + { + await Task.Delay(100); + } + finally + { + if (Environment.CurrentManagedThreadId == callerThreadId) + { + SynchronizationContext.SetSynchronizationContext(prev); + } + } + } + + [ConditionalFact(typeof(AsyncProfilerTests), nameof(IsRuntimeAsyncAndThreadingSupported))] + public void TaskAsync_CustomSyncContext_EmitsContextEventsAndCallstack() + { + s_taskAsyncSyncContextCtx = new InlinePostSynchronizationContext(); + + var events = CollectEvents(ResumeAsyncCallstackKeyword | CoreKeywords, () => + { + RunScenarioAndFlush(() => TaskAsync_CustomSyncContext_EmitsContextEventsAndCallstack_Marker()); + }); + + // DumpAllEvents(events); + + // The custom SyncContext should have received at least one Post for the await continuation. + Assert.True(s_taskAsyncSyncContextCtx.PostCount > 0, + $"Expected custom SynchronizationContext to receive at least one Post, got {s_taskAsyncSyncContextCtx.PostCount}"); + + var stream = ParseAllEvents(events); + + // The marker frame should appear in the Resume callstack. + var markerCallstacks = stream.CallstacksWithMarker(AsyncEventID.ResumeAsyncCallstack, nameof(TaskAsync_CustomSyncContext_EmitsContextEventsAndCallstack_Marker)); + Assert.NotEmpty(markerCallstacks); + + // Verify the standard Create -> Resume -> Complete sequence fired for our context. + ulong taskId = markerCallstacks[0].TaskId; + var ids = stream.ForTask(taskId).Select(e => e.EventId).ToList(); + + int createIdx = ids.IndexOf(AsyncEventID.CreateAsyncContext); + Assert.True(createIdx >= 0, "Expected CreateAsyncContext for the custom SyncContext scenario"); + + int resumeIdx = ids.IndexOf(AsyncEventID.ResumeAsyncContext, createIdx + 1); + Assert.True(resumeIdx > createIdx, "Expected ResumeAsyncContext after Create"); + + int completeIdx = ids.IndexOf(AsyncEventID.CompleteAsyncContext, resumeIdx + 1); + Assert.True(completeIdx > resumeIdx, "Expected CompleteAsyncContext after Resume"); + } + + [RuntimeAsyncMethodGeneration(false)] + [MethodImpl(MethodImplOptions.NoInlining)] + private static async Task TaskAsync_CustomTaskScheduler_EmitsContextEventsAndCallstack_Marker() + { + await Task.Delay(100); + } + + [ConditionalFact(typeof(AsyncProfilerTests), nameof(IsRuntimeAsyncAndThreadingSupported))] + public void TaskAsync_CustomTaskScheduler_EmitsContextEventsAndCallstack() + { + var scheduler = new InlineRunTaskScheduler(); + + var events = CollectEvents(ResumeAsyncCallstackKeyword | CoreKeywords, () => + { + try + { + Task.Factory.StartNew( + () => TaskAsync_CustomTaskScheduler_EmitsContextEventsAndCallstack_Marker(), + CancellationToken.None, + TaskCreationOptions.None, + scheduler).Unwrap().GetAwaiter().GetResult(); + } + finally + { + Thread.Sleep(50); + SendFlushCommand(); + } + }); + + // DumpAllEvents(events); + + // The custom scheduler must have received at least one QueueTask call. + Assert.True(scheduler.QueuedCount >= 1, + $"Expected custom TaskScheduler to receive at least one QueueTask call, got {scheduler.QueuedCount}"); + + var stream = ParseAllEvents(events); + + var markerCallstacks = stream.CallstacksWithMarker(AsyncEventID.ResumeAsyncCallstack, nameof(TaskAsync_CustomTaskScheduler_EmitsContextEventsAndCallstack_Marker)); + Assert.NotEmpty(markerCallstacks); + + // Verify standard Create -> Resume -> Complete sequence for our context. + ulong taskId = markerCallstacks[0].TaskId; + var ids = stream.ForTask(taskId).Select(e => e.EventId).ToList(); + + int createIdx = ids.IndexOf(AsyncEventID.CreateAsyncContext); + Assert.True(createIdx >= 0, "Expected CreateAsyncContext for the custom TaskScheduler scenario"); + + int resumeIdx = ids.IndexOf(AsyncEventID.ResumeAsyncContext, createIdx + 1); + Assert.True(resumeIdx > createIdx, "Expected ResumeAsyncContext after Create"); + + int completeIdx = ids.IndexOf(AsyncEventID.CompleteAsyncContext, resumeIdx + 1); + Assert.True(completeIdx > resumeIdx, "Expected CompleteAsyncContext after Resume"); + } + + [RuntimeAsyncMethodGeneration(false)] + [MethodImpl(MethodImplOptions.NoInlining)] + private static async Task TaskAsync_NoEventsWhenDisabled_Marker() + { + await Task.Delay(50); + } + + [ConditionalFact(typeof(AsyncProfilerTests), nameof(IsRuntimeAsyncAndThreadingSupported))] + public void TaskAsync_NoEventsWhenDisabled() + { + for (int i = 0; i < 50; i++) + { + RunScenario(() => TaskAsync_NoEventsWhenDisabled_Marker()); + } + + // Now attach a listener but don't perform any V1 async work. + var events = CollectEvents(CoreKeywords, () => { }); + + var ids = ParseAllEvents(events).EventIds; + int contextEvents = ids.Count(id => + id == AsyncEventID.CreateAsyncContext || + id == AsyncEventID.ResumeAsyncContext || + id == AsyncEventID.SuspendAsyncContext || + id == AsyncEventID.CompleteAsyncContext); + + Assert.Equal(0, contextEvents); + } + + public static IEnumerable TaskAsyncKeywordGatekeepingData() + { + yield return new object[] { (long)CreateAsyncContextKeyword, new AsyncEventID[] { AsyncEventID.ResetAsyncThreadContext, AsyncEventID.AsyncProfilerMetadata, AsyncEventID.CreateAsyncContext } }; + yield return new object[] { (long)ResumeAsyncContextKeyword, new AsyncEventID[] { AsyncEventID.ResetAsyncThreadContext, AsyncEventID.AsyncProfilerMetadata, AsyncEventID.ResumeAsyncContext } }; + yield return new object[] { (long)CompleteAsyncContextKeyword, new AsyncEventID[] { AsyncEventID.ResetAsyncThreadContext, AsyncEventID.AsyncProfilerMetadata, AsyncEventID.CompleteAsyncContext } }; + yield return new object[] { (long)UnwindAsyncExceptionKeyword, new AsyncEventID[] { AsyncEventID.ResetAsyncThreadContext, AsyncEventID.AsyncProfilerMetadata, AsyncEventID.UnwindAsyncException } }; + yield return new object[] { (long)ResumeAsyncCallstackKeyword, new AsyncEventID[] { AsyncEventID.ResetAsyncThreadContext, AsyncEventID.AsyncProfilerMetadata, AsyncEventID.ResumeAsyncCallstack, AsyncEventID.AppendAsyncCallstack } }; + yield return new object[] { (long)ResumeAsyncMethodKeyword, new AsyncEventID[] { AsyncEventID.ResetAsyncThreadContext, AsyncEventID.AsyncProfilerMetadata, AsyncEventID.ResumeAsyncMethod } }; + yield return new object[] { (long)CompleteAsyncMethodKeyword, new AsyncEventID[] { AsyncEventID.ResetAsyncThreadContext, AsyncEventID.AsyncProfilerMetadata, AsyncEventID.CompleteAsyncMethod } }; + } + + [RuntimeAsyncMethodGeneration(false)] + [MethodImpl(MethodImplOptions.NoInlining)] + private static async Task TaskAsync_KeywordGatekeeping_Marker() + { + // Exercise multiple event types: exception unwind, multiple completes, method invocations. + try + { + await TaskAsync_InnerThrows(); + } + catch (InvalidOperationException) { } + await Task.Delay(50); + } + + [ConditionalTheory(typeof(AsyncProfilerTests), nameof(IsRuntimeAsyncAndThreadingSupported))] + [MemberData(nameof(TaskAsyncKeywordGatekeepingData))] + public void TaskAsync_KeywordGatekeeping(long keywordValue, AsyncEventID[] allowedEventIds) + { + EventKeywords kw = (EventKeywords)keywordValue; + var allowed = new HashSet(allowedEventIds); + + var events = CollectEvents(kw, () => + { + RunScenarioAndFlush(() => TaskAsync_NoEventsWhenDisabled_Marker()); + }); + + var stream = ParseAllEvents(events); + var unexpected = stream.EventIds.Where(id => !allowed.Contains(id)).ToList(); + + Assert.True(unexpected.Count == 0, + $"Keyword 0x{(long)kw:X}: unexpected event IDs [{string.Join(", ", unexpected)}], allowed [{string.Join(", ", allowed)}]"); + } + + [RuntimeAsyncMethodGeneration(false)] + [MethodImpl(MethodImplOptions.NoInlining)] + private static async Task TaskAsync_WhenAll_TracksAllBranches_BranchA_Marker() + { + await Task.Delay(100); + } + + [RuntimeAsyncMethodGeneration(false)] + [MethodImpl(MethodImplOptions.NoInlining)] + private static async Task TaskAsync_WhenAll_TracksAllBranches_BranchB_Marker() + { + await Task.Delay(120); + } + + [RuntimeAsyncMethodGeneration(false)] + [MethodImpl(MethodImplOptions.NoInlining)] + private static async Task TaskAsync_WhenAll_TracksAllBranches_BranchC_Marker() + { + await Task.Delay(140); + } + + [RuntimeAsyncMethodGeneration(false)] + [MethodImpl(MethodImplOptions.NoInlining)] + private static async Task TaskAsync_WhenAll_TracksAllBranches_Marker() + { + await Task.WhenAll( + TaskAsync_WhenAll_TracksAllBranches_BranchA_Marker(), + TaskAsync_WhenAll_TracksAllBranches_BranchB_Marker(), + TaskAsync_WhenAll_TracksAllBranches_BranchC_Marker()); + } + + [ConditionalFact(typeof(AsyncProfilerTests), nameof(IsRuntimeAsyncAndThreadingSupported))] + public void TaskAsync_WhenAll_TracksAllBranches() + { + var events = CollectEvents(ResumeAsyncCallstackKeyword | CoreKeywords, () => + { + RunScenarioAndFlush(() => TaskAsync_WhenAll_TracksAllBranches_Marker()); + }); + + // DumpAllEvents(events); + + var stream = ParseAllEvents(events); + + var markerCallstacks = stream.CallstacksWithMarker(AsyncEventID.ResumeAsyncCallstack, nameof(TaskAsync_WhenAll_TracksAllBranches_Marker)); + Assert.NotEmpty(markerCallstacks); + + // Each branch is its own async chain; its inner await of Task.Delay produces a Resume callstack containing the branch frame. + var branchACallstacks = stream.CallstacksWithMarker(AsyncEventID.ResumeAsyncCallstack, nameof(TaskAsync_WhenAll_TracksAllBranches_BranchA_Marker)); + var branchBCallstacks = stream.CallstacksWithMarker(AsyncEventID.ResumeAsyncCallstack, nameof(TaskAsync_WhenAll_TracksAllBranches_BranchB_Marker)); + var branchCCallstacks = stream.CallstacksWithMarker(AsyncEventID.ResumeAsyncCallstack, nameof(TaskAsync_WhenAll_TracksAllBranches_BranchC_Marker)); + Assert.NotEmpty(branchACallstacks); + Assert.NotEmpty(branchBCallstacks); + Assert.NotEmpty(branchCCallstacks); + + // Each tracked chain (3 branches + outer marker) must see exactly one Create and one Complete on its own TaskId. + AssertExactlyOneCreateAndComplete(stream, branchACallstacks[0].TaskId, nameof(TaskAsync_WhenAll_TracksAllBranches_BranchA_Marker)); + AssertExactlyOneCreateAndComplete(stream, branchBCallstacks[0].TaskId, nameof(TaskAsync_WhenAll_TracksAllBranches_BranchB_Marker)); + AssertExactlyOneCreateAndComplete(stream, branchCCallstacks[0].TaskId, nameof(TaskAsync_WhenAll_TracksAllBranches_BranchC_Marker)); + AssertExactlyOneCreateAndComplete(stream, markerCallstacks[0].TaskId, nameof(TaskAsync_WhenAll_TracksAllBranches_Marker)); + + // The outer marker's chain should fire the standard Create -> Resume -> Complete sequence on its own TaskId, in that order. + ulong markerTaskId = markerCallstacks[0].TaskId; + var markerIds = stream.ForTask(markerTaskId).Select(e => e.EventId).ToList(); + + int createIdx = markerIds.IndexOf(AsyncEventID.CreateAsyncContext); + Assert.True(createIdx >= 0, "Expected CreateAsyncContext for the WhenAll outer marker"); + + int resumeIdx = markerIds.IndexOf(AsyncEventID.ResumeAsyncContext, createIdx + 1); + Assert.True(resumeIdx > createIdx, "Expected ResumeAsyncContext after Create on the outer marker"); + + int completeIdx = markerIds.IndexOf(AsyncEventID.CompleteAsyncContext, resumeIdx + 1); + Assert.True(completeIdx > resumeIdx, "Expected CompleteAsyncContext after Resume on the outer marker"); + + // The outer should be created exactly once. + int createCountForMarker = markerIds.Count(id => id == AsyncEventID.CreateAsyncContext); + Assert.Equal(1, createCountForMarker); + } + + [RuntimeAsyncMethodGeneration(false)] + [MethodImpl(MethodImplOptions.NoInlining)] + private static async Task TaskAsync_WhenAny_TracksAllBranches_Fast_Marker() + { + await Task.Delay(50); + } + + [RuntimeAsyncMethodGeneration(false)] + [MethodImpl(MethodImplOptions.NoInlining)] + private static async Task TaskAsync_WhenAny_TracksAllBranches_Slow1_Marker() + { + await Task.Delay(400); + } + + [RuntimeAsyncMethodGeneration(false)] + [MethodImpl(MethodImplOptions.NoInlining)] + private static async Task TaskAsync_WhenAny_TracksAllBranches_Slow2_Marker() + { + await Task.Delay(600); + } + + [RuntimeAsyncMethodGeneration(false)] + [MethodImpl(MethodImplOptions.NoInlining)] + private static async Task TaskAsync_WhenAny_TracksAllBranches_Marker() + { + Task fast = TaskAsync_WhenAny_TracksAllBranches_Fast_Marker(); + Task slow1 = TaskAsync_WhenAny_TracksAllBranches_Slow1_Marker(); + Task slow2 = TaskAsync_WhenAny_TracksAllBranches_Slow2_Marker(); + + await Task.WhenAny(fast, slow1, slow2); + await Task.WhenAll(slow1, slow2); + } + + [ConditionalFact(typeof(AsyncProfilerTests), nameof(IsRuntimeAsyncAndThreadingSupported))] + public void TaskAsync_WhenAny_TracksAllBranches() + { + var events = CollectEvents(ResumeAsyncCallstackKeyword | CoreKeywords, () => + { + RunScenarioAndFlush(() => TaskAsync_WhenAny_TracksAllBranches_Marker()); + }); + + // DumpAllEvents(events); + + var stream = ParseAllEvents(events); + + var markerCallstacks = stream.CallstacksWithMarker(AsyncEventID.ResumeAsyncCallstack, nameof(TaskAsync_WhenAny_TracksAllBranches_Marker)); + Assert.NotEmpty(markerCallstacks); + + // All branches - including the slow ones whose completion the outer is no longer + // strictly waiting on after WhenAny returned -- must produce their own Resume + // callstacks. This proves their dispatcher lifetimes are tracked independently. + var fastCallstacks = stream.CallstacksWithMarker(AsyncEventID.ResumeAsyncCallstack, nameof(TaskAsync_WhenAny_TracksAllBranches_Fast_Marker)); + var slow1Callstacks = stream.CallstacksWithMarker(AsyncEventID.ResumeAsyncCallstack, nameof(TaskAsync_WhenAny_TracksAllBranches_Slow1_Marker)); + var slow2Callstacks = stream.CallstacksWithMarker(AsyncEventID.ResumeAsyncCallstack, nameof(TaskAsync_WhenAny_TracksAllBranches_Slow2_Marker)); + Assert.NotEmpty(fastCallstacks); + Assert.NotEmpty(slow1Callstacks); + Assert.NotEmpty(slow2Callstacks); + + // Each tracked chain (3 branches + outer marker) must see exactly one Create and one Complete on its own TaskId. + AssertExactlyOneCreateAndComplete(stream, fastCallstacks[0].TaskId, nameof(TaskAsync_WhenAny_TracksAllBranches_Fast_Marker)); + AssertExactlyOneCreateAndComplete(stream, slow1Callstacks[0].TaskId, nameof(TaskAsync_WhenAny_TracksAllBranches_Slow1_Marker)); + AssertExactlyOneCreateAndComplete(stream, slow2Callstacks[0].TaskId, nameof(TaskAsync_WhenAny_TracksAllBranches_Slow2_Marker)); + AssertCreateEqualsCompleteForTask(stream, markerCallstacks[0].TaskId, nameof(TaskAsync_WhenAny_TracksAllBranches_Marker)); + + // The outer marker's chain: exactly one Create, at least two Resumes (one after + // WhenAny, one after WhenAll on the slow branches), then Complete. + ulong markerTaskId = markerCallstacks[0].TaskId; + var markerEvents = stream.ForTask(markerTaskId); + var markerIds = markerEvents.Select(e => e.EventId).ToList(); + + int resumeCountForMarker = markerIds.Count(id => id == AsyncEventID.ResumeAsyncContext); + Assert.True(resumeCountForMarker >= 1, + $"Expected outer marker to be resumed at least once, got {resumeCountForMarker}"); + + int completeCountForMarker = markerIds.Count(id => id == AsyncEventID.CompleteAsyncContext); + Assert.True(completeCountForMarker >= 1, "Expected at least one CompleteAsyncContext for the outer marker"); + } + + [RuntimeAsyncMethodGeneration(false)] + [MethodImpl(MethodImplOptions.NoInlining)] + private static async Task TaskAsync_CallstackDepthCappedAtMaxFrames_Marker(int depth) + { + await TaskAsync_RecursiveChain(depth); + } + + [ConditionalFact(typeof(AsyncProfilerTests), nameof(IsRuntimeAsyncAndThreadingSupported))] + public void TaskAsync_CallstackDepthCappedAtMaxFrames() + { + // Build a chain deeper than the 255-frame cap (byte FrameCount). The deepest + // ResumeAsyncCallstack should clamp at byte.MaxValue without crashing. + const int requestedDepth = 300; + + var events = CollectEvents(ResumeAsyncCallstackKeyword | CoreKeywords, () => + { + RunScenarioAndFlush(() => TaskAsync_CallstackDepthCappedAtMaxFrames_Marker(requestedDepth)); + }); + + // DumpAllEvents(events); + + var stream = ParseAllEvents(events); + + // With the cap at byte.MaxValue, the initial Resume walks the first 255 frames from + // the leaf. The marker sits at the top of the chain (deeper than 255), so it appears + // in a subsequent AppendAsyncCallstack carrying the remaining frames. Use the merged + // view to validate the chain as a whole. + var mergedMarker = stream.MergedResumeCallstacksWithMarker(nameof(TaskAsync_CallstackDepthCappedAtMaxFrames_Marker)); + Assert.NotEmpty(mergedMarker); + + var deepest = mergedMarker.MaxBy(cs => cs.Frames.Count); + Assert.NotNull(deepest); + + // Walker should have captured at least the full requested depth across Resume+Appends. + Assert.True(deepest.Frames.Count >= requestedDepth, + $"Expected merged frame count >= {requestedDepth}, got {deepest.Frames.Count}"); + + // Each individual event clamps its own FrameCount at byte.MaxValue (wire format limit). + ulong chainTaskId = deepest.TaskId; + var perEventCallstacks = stream.ForTask(chainTaskId) + .Where(e => e.EventId is AsyncEventID.ResumeAsyncCallstack or AsyncEventID.AppendAsyncCallstack) + .ToList(); + Assert.NotEmpty(perEventCallstacks); + Assert.All(perEventCallstacks, cs => Assert.True(cs.FrameCount <= byte.MaxValue)); + + // At least one event must have hit the cap (otherwise the test isn't exercising it). + Assert.Contains(perEventCallstacks, cs => cs.FrameCount == byte.MaxValue); + + // Every captured frame should resolve to a managed method. + foreach (var (methodId, _) in deepest.Frames) + { + Assert.True(methodId != 0, "Frame has zero MethodId"); + var method = GetMethodNameFromMethodId(deepest.CallstackType, methodId); + Assert.True(method is not null, $"MethodId 0x{methodId:X} does not resolve to a managed method"); + } + } + + [RuntimeAsyncMethodGeneration(false)] + [MethodImpl(MethodImplOptions.NoInlining)] + private static async Task TaskAsync_CallstackStressWithVaryingDepths_Marker(int depth) + { + await TaskAsync_RecursiveChain(depth); + } + + [ConditionalFact(typeof(AsyncProfilerTests), nameof(IsRuntimeAsyncAndThreadingSupported))] + public void TaskAsync_CallstackStressWithVaryingDepths() + { + const int iterations = 50; + int[] depths = new int[iterations]; + var rng = new Random(42); + for (int i = 0; i < iterations; i++) + depths[i] = rng.Next(1, 60); + + var events = CollectEvents(ResumeAsyncCallstackKeyword | CoreKeywords, () => + { + RunScenarioAndFlush(async () => + { + for (int i = 0; i < iterations; i++) + await TaskAsync_CallstackStressWithVaryingDepths_Marker(depths[i]); + }); + }); + + // DumpAllEvents(events); + + var stream = ParseAllEvents(events); + var markerCallstacks = stream.CallstacksWithMarker(AsyncEventID.ResumeAsyncCallstack, nameof(TaskAsync_CallstackStressWithVaryingDepths_Marker)); + Assert.NotEmpty(markerCallstacks); + + // Every emitted callstack must have valid frame data. + foreach (var cs in markerCallstacks) + { + Assert.True(cs.FrameCount > 0, "Callstack has 0 frames"); + Assert.Equal((int)cs.FrameCount, cs.Frames.Count); + for (int f = 0; f < cs.Frames.Count; f++) + { + var (methodId, _) = cs.Frames[f]; + Assert.True(methodId != 0, $"Frame {f} has zero MethodId"); + var method = GetMethodNameFromMethodId(cs.CallstackType, methodId); + Assert.True(method is not null, $"Frame {f}: MethodId 0x{methodId:X} does not resolve to a managed method"); + } + } + + // We expect at least one marker callstack per iteration. + Assert.True(markerCallstacks.Count >= iterations, + $"Expected at least {iterations} callstacks with marker, got {markerCallstacks.Count}"); + + // Verify multiple buffer flushes occurred -- proves the buffer machinery is exercised. + int bufferCount = 0; + ForEachEventBufferPayload(events, _ => bufferCount++); + Assert.True(bufferCount >= 3, $"Expected at least 3 buffer flushes, got {bufferCount}"); + } + + [RuntimeAsyncMethodGeneration(false)] + [MethodImpl(MethodImplOptions.NoInlining)] + private static async Task TaskAsync_WaitThenYield_BalancesResumeAndComplete_WaitYield_Marker(Task gate) + { + await gate; + await Task.Yield(); + } + + [RuntimeAsyncMethodGeneration(false)] + [MethodImpl(MethodImplOptions.NoInlining)] + private static async Task TaskAsync_WaitThenYield_BalancesResumeAndComplete_Marker() + { + await Task.Yield(); + + var tcs = new TaskCompletionSource(); + Task b1 = TaskAsync_WaitThenYield_BalancesResumeAndComplete_WaitYield_Marker(tcs.Task); + Task b2 = TaskAsync_WaitThenYield_BalancesResumeAndComplete_WaitYield_Marker(tcs.Task); + + tcs.SetResult(); + + await Task.WhenAll(b1, b2); + } + + [ConditionalFact(typeof(AsyncProfilerTests), nameof(IsRuntimeAsyncAndThreadingSupported))] + public void TaskAsync_WaitThenYield_BalancesResumeAndComplete() + { + var events = CollectEvents(CoreKeywords | ResumeAsyncCallstackKeyword, () => + { + RunScenarioAndFlush(() => TaskAsync_WaitThenYield_BalancesResumeAndComplete_Marker()); + }); + + // DumpAllEvents(events); + + var stream = ParseAllEvents(events); + + int createCount = stream.OfType(AsyncEventID.CreateAsyncContext).Count(); + int completeCount = stream.OfType(AsyncEventID.CompleteAsyncContext).Count(); + int resumeCount = stream.OfType(AsyncEventID.ResumeAsyncContext).Count(); + int suspendCount = stream.OfType(AsyncEventID.SuspendAsyncContext).Count(); + + // At least one root Create event. + Assert.True(createCount >= 1, + $"Expected at least one CreateAsyncContext event, got {createCount}"); + + Assert.Equal(createCount, completeCount); + Assert.Equal(resumeCount, completeCount); + + // Does not emit Suspend events. + Assert.Equal(0, suspendCount); + + Assert.True(createCount >= 3, + $"Expected fan-out chain to produce at least 3 CreateAsyncContext events (root + 2 child wraps), got {createCount}"); + + var markerCallstacks = stream.CallstacksWithMarker(AsyncEventID.ResumeAsyncCallstack, nameof(TaskAsync_WaitThenYield_BalancesResumeAndComplete_Marker)); + Assert.NotEmpty(markerCallstacks); + } + + [RuntimeAsyncMethodGeneration(false)] + [MethodImpl(MethodImplOptions.NoInlining)] + private static async Task TaskAsync_ConfigureAwaitFalse_Leaf_Marker() + { + await Task.Delay(100).ConfigureAwait(false); + } + + [RuntimeAsyncMethodGeneration(false)] + [MethodImpl(MethodImplOptions.NoInlining)] + private static async Task TaskAsync_ConfigureAwaitFalse_Mid_Marker() + { + await TaskAsync_ConfigureAwaitFalse_Leaf_Marker().ConfigureAwait(false); + } + + [RuntimeAsyncMethodGeneration(false)] + [MethodImpl(MethodImplOptions.NoInlining)] + private static async Task TaskAsync_ConfigureAwaitFalse_Marker() + { + await TaskAsync_ConfigureAwaitFalse_Mid_Marker().ConfigureAwait(false); + } + + [ConditionalFact(typeof(AsyncProfilerTests), nameof(IsRuntimeAsyncAndThreadingSupported))] + public void TaskAsync_ConfigureAwaitFalse() + { + var events = CollectEvents(ResumeAsyncCallstackKeyword | CoreKeywords, () => + { + RunScenarioAndFlush(() => TaskAsync_ConfigureAwaitFalse_Marker()); + }); + + // DumpAllEvents(events); + + var stream = ParseAllEvents(events); + + var markerCallstacks = stream.CallstacksWithMarker(AsyncEventID.ResumeAsyncCallstack, nameof(TaskAsync_ConfigureAwaitFalse_Marker)); + Assert.NotEmpty(markerCallstacks); + + var frameNames = markerCallstacks[0].Frames + .Select(f => GetMethodNameFromMethodId(markerCallstacks[0].CallstackType, f.MethodId)) + .Where(n => n is not null) + .ToList(); + + Assert.Contains(nameof(TaskAsync_ConfigureAwaitFalse_Leaf_Marker), frameNames); + Assert.Contains(nameof(TaskAsync_ConfigureAwaitFalse_Mid_Marker), frameNames); + Assert.Contains(nameof(TaskAsync_ConfigureAwaitFalse_Marker), frameNames); + + // ConfigureAwait(false) on a sequential await chain collapses Leaf -> Mid -> Marker into one + // continuation chain, so exactly one Create / one Complete is expected on the marker's TaskId. + AssertExactlyOneCreateAndComplete(stream, markerCallstacks[0].TaskId, nameof(TaskAsync_ConfigureAwaitFalse_Marker)); + } + + [RuntimeAsyncMethodGeneration(false)] + [MethodImpl(MethodImplOptions.NoInlining)] + private static async Task TaskAsync_FaultedTask_Inner_Marker() + { + await Task.Delay(50); + throw new InvalidOperationException("test fault"); + } + + [RuntimeAsyncMethodGeneration(false)] + [MethodImpl(MethodImplOptions.NoInlining)] + private static async Task TaskAsync_FaultedTask_Marker() + { + try + { + await TaskAsync_FaultedTask_Inner_Marker(); + } + catch (InvalidOperationException) + { + } + } + + [ConditionalFact(typeof(AsyncProfilerTests), nameof(IsRuntimeAsyncAndThreadingSupported))] + public void TaskAsync_FaultedTask() + { + var events = CollectEvents(ResumeAsyncCallstackKeyword | UnwindAsyncExceptionKeyword | CoreKeywords, () => + { + RunScenarioAndFlush(() => TaskAsync_FaultedTask_Marker()); + }); + + // DumpAllEvents(events); + + var stream = ParseAllEvents(events); + + var markerCallstacks = stream.CallstacksWithMarker(AsyncEventID.ResumeAsyncCallstack, nameof(TaskAsync_FaultedTask_Marker)); + Assert.NotEmpty(markerCallstacks); + + var innerCallstacks = stream.CallstacksWithMarker(AsyncEventID.ResumeAsyncCallstack, nameof(TaskAsync_FaultedTask_Inner_Marker)); + Assert.NotEmpty(innerCallstacks); + + // Every dispatcher that was Created must Complete, even on the fault path. + int createCount = stream.OfType(AsyncEventID.CreateAsyncContext).Count(); + int completeCount = stream.OfType(AsyncEventID.CompleteAsyncContext).Count(); + Assert.Equal(createCount, completeCount); + + // Both inner (faulting) and outer marker chains must see exactly one Create and one Complete on their own TaskId. + AssertExactlyOneCreateAndComplete(stream, innerCallstacks[0].TaskId, nameof(TaskAsync_FaultedTask_Inner_Marker)); + AssertExactlyOneCreateAndComplete(stream, markerCallstacks[0].TaskId, nameof(TaskAsync_FaultedTask_Marker)); + + // The unwind must be attributed to the inner faulting chain. + ulong innerTaskId = innerCallstacks[0].TaskId; + int unwindCountForInner = stream.ForTask(innerTaskId).Count(e => e.EventId == AsyncEventID.UnwindAsyncException); + Assert.True(unwindCountForInner >= 1, + $"Expected at least one UnwindAsyncException on the faulted inner chain (TaskId {innerTaskId}), got {unwindCountForInner}"); + } + + [RuntimeAsyncMethodGeneration(false)] + [MethodImpl(MethodImplOptions.NoInlining)] + private static async Task TaskAsync_TaskCancellation_Inner_Marker(CancellationToken ct) + { + await Task.Delay(5000, ct); + } + + [RuntimeAsyncMethodGeneration(false)] + [MethodImpl(MethodImplOptions.NoInlining)] + private static async Task TaskAsync_TaskCancellation_Marker() + { + using var cts = new CancellationTokenSource(); + Task inner = TaskAsync_TaskCancellation_Inner_Marker(cts.Token); + cts.CancelAfter(50); + try + { + await inner; + } + catch (OperationCanceledException) + { + } + } + + [ConditionalFact(typeof(AsyncProfilerTests), nameof(IsRuntimeAsyncAndThreadingSupported))] + public void TaskAsync_TaskCancellation() + { + var events = CollectEvents(ResumeAsyncCallstackKeyword | UnwindAsyncExceptionKeyword | CoreKeywords, () => + { + RunScenarioAndFlush(() => TaskAsync_TaskCancellation_Marker()); + }); + + // DumpAllEvents(events); + + var stream = ParseAllEvents(events); + + var markerCallstacks = stream.CallstacksWithMarker(AsyncEventID.ResumeAsyncCallstack, nameof(TaskAsync_TaskCancellation_Marker)); + Assert.NotEmpty(markerCallstacks); + + var innerCallstacks = stream.CallstacksWithMarker(AsyncEventID.ResumeAsyncCallstack, nameof(TaskAsync_TaskCancellation_Inner_Marker)); + Assert.NotEmpty(innerCallstacks); + + // Inner cancelled task + outer marker must each see exactly one Create and one Complete on their own TaskId. + AssertExactlyOneCreateAndComplete(stream, innerCallstacks[0].TaskId, nameof(TaskAsync_TaskCancellation_Inner_Marker)); + AssertExactlyOneCreateAndComplete(stream, markerCallstacks[0].TaskId, nameof(TaskAsync_TaskCancellation_Marker)); + } + + [RuntimeAsyncMethodGeneration(false)] + [MethodImpl(MethodImplOptions.NoInlining)] + private static async ValueTask ValueTaskAsync_EventSequenceOrder_Marker() + { + await ValueTaskAsync_Level1(); + } + + [ConditionalFact(typeof(AsyncProfilerTests), nameof(IsRuntimeAsyncAndThreadingSupported))] + public void ValueTaskAsync_EventSequenceOrder() + { + var events = CollectEvents(ResumeAsyncCallstackKeyword | CoreKeywords, () => + { + RunScenarioAndFlush(() => ValueTaskAsync_EventSequenceOrder_Marker().AsTask()); + }); + + // DumpAllEvents(events); + + var stream = ParseAllEvents(events); + + var markerCallstacks = stream.CallstacksWithMarker(AsyncEventID.ResumeAsyncCallstack, nameof(ValueTaskAsync_EventSequenceOrder_Marker)); + Assert.NotEmpty(markerCallstacks); + + ulong taskId = markerCallstacks[0].TaskId; + var ids = stream.ForTask(taskId).Select(e => e.EventId).ToList(); + + int createIdx = ids.IndexOf(AsyncEventID.CreateAsyncContext); + Assert.True(createIdx >= 0, "Expected CreateAsyncContext"); + + int resumeIdx = ids.IndexOf(AsyncEventID.ResumeAsyncContext, createIdx + 1); + Assert.True(resumeIdx > createIdx, "Expected ResumeAsyncContext after Create"); + + int completeIdx = ids.IndexOf(AsyncEventID.CompleteAsyncContext, resumeIdx + 1); + Assert.True(completeIdx > resumeIdx, "Expected CompleteAsyncContext after Resume"); + } + + [RuntimeAsyncMethodGeneration(false)] + [MethodImpl(MethodImplOptions.NoInlining)] + private static async ValueTask ValueTaskAsync_MethodEventsEmitted_Marker() + { + await ValueTaskAsync_Level1(); + } + + [ConditionalFact(typeof(AsyncProfilerTests), nameof(IsRuntimeAsyncAndThreadingSupported))] + public void ValueTaskAsync_MethodEventsEmitted() + { + var events = CollectEvents(MethodKeywords | CoreKeywords, () => + { + RunScenarioAndFlush(() => ValueTaskAsync_MethodEventsEmitted_Marker().AsTask()); + }); + + // DumpAllEvents(events); + + var stream = ParseAllEvents(events); + + var methodEvents = stream.All + .Where(e => e.EventId is AsyncEventID.ResumeAsyncMethod or AsyncEventID.CompleteAsyncMethod) + .Select(e => e.EventId) + .ToList(); + + int resumeCount = methodEvents.Count(id => id == AsyncEventID.ResumeAsyncMethod); + int completeCount = methodEvents.Count(id => id == AsyncEventID.CompleteAsyncMethod); + + // Marker -> Level1 -> Level2 -> Level3 + Assert.True(resumeCount >= 4, $"Expected at least 4 ResumeAsyncMethod events for ValueTask chain, got {resumeCount}"); + Assert.True(completeCount >= 4, $"Expected at least 4 CompleteAsyncMethod events for ValueTask chain, got {completeCount}"); + } + + + [RuntimeAsyncMethodGeneration(false)] + [MethodImpl(MethodImplOptions.NoInlining)] + private static async ValueTask ValueTaskAsync_CallstackDepthMatchesChainDepth_Marker() + { + await ValueTaskAsync_Level1(); + } + + [ConditionalFact(typeof(AsyncProfilerTests), nameof(IsRuntimeAsyncAndThreadingSupported))] + public void ValueTaskAsync_CallstackDepthMatchesChainDepth() + { + var events = CollectEvents(ResumeAsyncCallstackKeyword | CoreKeywords, () => + { + RunScenarioAndFlush(() => ValueTaskAsync_CallstackDepthMatchesChainDepth_Marker().AsTask()); + }); + + // DumpAllEvents(events); + + var stream = ParseAllEvents(events); + + var markerCallstacks = stream.CallstacksWithMarker(AsyncEventID.ResumeAsyncCallstack, nameof(ValueTaskAsync_CallstackDepthMatchesChainDepth_Marker)); + Assert.NotEmpty(markerCallstacks); + + // Marker -> Level1 -> Level2 -> Level3 + Assert.Equal(4, markerCallstacks[0].FrameCount); + } + + [RuntimeAsyncMethodGeneration(false)] + [MethodImpl(MethodImplOptions.NoInlining)] + private static async ValueTask ValueTaskAsync_CallstackFramesHaveDistinctMethodIds_Marker() + { + await ValueTaskAsync_Level1(); + } + + [ConditionalFact(typeof(AsyncProfilerTests), nameof(IsRuntimeAsyncAndThreadingSupported))] + public void ValueTaskAsync_CallstackFramesHaveDistinctMethodIds() + { + var events = CollectEvents(ResumeAsyncCallstackKeyword | CoreKeywords, () => + { + RunScenarioAndFlush(() => ValueTaskAsync_CallstackFramesHaveDistinctMethodIds_Marker().AsTask()); + }); + + // DumpAllEvents(events); + + var stream = ParseAllEvents(events); + + var markerCallstacks = stream.CallstacksWithMarker(AsyncEventID.ResumeAsyncCallstack, nameof(ValueTaskAsync_CallstackFramesHaveDistinctMethodIds_Marker)); + Assert.NotEmpty(markerCallstacks); + + var methodIds = markerCallstacks[0].Frames.Select(f => f.MethodId).ToList(); + Assert.Equal(methodIds.Count, methodIds.Distinct().Count()); + } + + [RuntimeAsyncMethodGeneration(false)] + [MethodImpl(MethodImplOptions.NoInlining)] + private static async ValueTask ValueTaskAsync_HandledException_EmitsUnwindAndComplete_InnerThrows_Marker() + { + await Task.Delay(100); + throw new InvalidOperationException("valuetask inner throw"); + } + + [RuntimeAsyncMethodGeneration(false)] + [MethodImpl(MethodImplOptions.NoInlining)] + private static async ValueTask ValueTaskAsync_HandledException_EmitsUnwindAndComplete_Handled_Marker() + { + try + { + await ValueTaskAsync_HandledException_EmitsUnwindAndComplete_InnerThrows_Marker(); + } + catch (InvalidOperationException) + { + } + } + + [RuntimeAsyncMethodGeneration(false)] + [MethodImpl(MethodImplOptions.NoInlining)] + private static async ValueTask ValueTaskAsync_HandledException_EmitsUnwindAndComplete_Marker() + { + await ValueTaskAsync_HandledException_EmitsUnwindAndComplete_Handled_Marker(); + } + + [ConditionalFact(typeof(AsyncProfilerTests), nameof(IsRuntimeAsyncAndThreadingSupported))] + public void ValueTaskAsync_HandledException_EmitsUnwindAndComplete() + { + var events = CollectEvents(ResumeAsyncCallstackKeyword | CoreKeywords | UnwindAsyncExceptionKeyword, () => + { + RunScenarioAndFlush(() => ValueTaskAsync_HandledException_EmitsUnwindAndComplete_Marker().AsTask()); + }); + + // DumpAllEvents(events); + + var stream = ParseAllEvents(events); + + var markerCallstacks = stream.CallstacksWithMarker(AsyncEventID.ResumeAsyncCallstack, nameof(ValueTaskAsync_HandledException_EmitsUnwindAndComplete_Marker)); + Assert.NotEmpty(markerCallstacks); + + ulong taskId = markerCallstacks[0].TaskId; + var ids = stream.ForTask(taskId).Select(e => e.EventId).ToList(); + + int createIdx = ids.IndexOf(AsyncEventID.CreateAsyncContext); + Assert.True(createIdx >= 0, "Expected CreateAsyncContext"); + + int resumeIdx = ids.IndexOf(AsyncEventID.ResumeAsyncContext, createIdx + 1); + Assert.True(resumeIdx > createIdx, "Expected ResumeAsyncContext after Create"); + + int unwindIdx = ids.IndexOf(AsyncEventID.UnwindAsyncException, resumeIdx + 1); + Assert.True(unwindIdx > resumeIdx, "Expected UnwindAsyncException after Resume"); + + int completeIdx = ids.IndexOf(AsyncEventID.CompleteAsyncContext, unwindIdx + 1); + Assert.True(completeIdx > unwindIdx, "Expected CompleteAsyncContext after Unwind"); + } + + + [RuntimeAsyncMethodGeneration(false)] + [MethodImpl(MethodImplOptions.NoInlining)] + private static async ValueTask ValueTaskAsync_UnhandledException_EmitsUnwindAndComplete_UnhandledOuter_Marker() + { + await ValueTaskAsync_UnhandledException_EmitsUnwindAndComplete_UnhandledInner_Marker(); + } + + [RuntimeAsyncMethodGeneration(false)] + [MethodImpl(MethodImplOptions.NoInlining)] + private static async ValueTask ValueTaskAsync_UnhandledException_EmitsUnwindAndComplete_UnhandledInner_Marker() + { + await Task.Delay(100); + throw new InvalidOperationException("valuetask unhandled inner"); + } + + [RuntimeAsyncMethodGeneration(false)] + [MethodImpl(MethodImplOptions.NoInlining)] + private static async ValueTask ValueTaskAsync_UnhandledException_EmitsUnwindAndComplete_Marker() + { + await ValueTaskAsync_UnhandledException_EmitsUnwindAndComplete_UnhandledOuter_Marker(); + } + + [ConditionalFact(typeof(AsyncProfilerTests), nameof(IsRuntimeAsyncAndThreadingSupported))] + public void ValueTaskAsync_UnhandledException_EmitsUnwindAndComplete() + { + var events = CollectEvents(ResumeAsyncCallstackKeyword | CoreKeywords | UnwindAsyncExceptionKeyword, () => + { + try + { + RunScenarioAndFlush(() => ValueTaskAsync_UnhandledException_EmitsUnwindAndComplete_Marker().AsTask()); + } + catch (InvalidOperationException) + { + } + }); + + // DumpAllEvents(events); + + var stream = ParseAllEvents(events); + + var markerCallstacks = stream.CallstacksWithMarker(AsyncEventID.ResumeAsyncCallstack, nameof(ValueTaskAsync_UnhandledException_EmitsUnwindAndComplete_Marker)); + Assert.NotEmpty(markerCallstacks); + + ulong taskId = markerCallstacks[0].TaskId; + var ids = stream.ForTask(taskId).Select(e => e.EventId).ToList(); + + int createIdx = ids.IndexOf(AsyncEventID.CreateAsyncContext); + Assert.True(createIdx >= 0, "Expected CreateAsyncContext"); + + int resumeIdx = ids.IndexOf(AsyncEventID.ResumeAsyncContext, createIdx + 1); + Assert.True(resumeIdx > createIdx, "Expected ResumeAsyncContext after Create"); + + int unwindIdx1 = ids.IndexOf(AsyncEventID.UnwindAsyncException, resumeIdx + 1); + Assert.True(unwindIdx1 > resumeIdx, "Expected first UnwindAsyncException after Resume"); + + int unwindIdx2 = ids.IndexOf(AsyncEventID.UnwindAsyncException, unwindIdx1 + 1); + Assert.True(unwindIdx2 > unwindIdx1, "Expected second UnwindAsyncException after first Unwind"); + + int completeIdx = ids.IndexOf(AsyncEventID.CompleteAsyncContext, unwindIdx2 + 1); + Assert.True(completeIdx > unwindIdx2, "Expected CompleteAsyncContext after second Unwind"); + } + + [RuntimeAsyncMethodGeneration(false)] + [MethodImpl(MethodImplOptions.NoInlining)] + private static async Task TaskAsync_SingleThread_ChainEventsAndCallstack_Inner_Marker(Task gate) + { + await gate; + } + + [RuntimeAsyncMethodGeneration(false)] + [MethodImpl(MethodImplOptions.NoInlining)] + private static async Task TaskAsync_SingleThread_ChainEventsAndCallstack_Mid_Marker(Task gate) + { + await TaskAsync_SingleThread_ChainEventsAndCallstack_Inner_Marker(gate); + } + + [RuntimeAsyncMethodGeneration(false)] + [MethodImpl(MethodImplOptions.NoInlining)] + private static async Task TaskAsync_SingleThread_ChainEventsAndCallstack_Marker(Task gate) + { + await TaskAsync_SingleThread_ChainEventsAndCallstack_Mid_Marker(gate); + } + + [ConditionalFact(typeof(AsyncProfilerTests), nameof(IsRuntimeAsyncSupported))] + public async Task TaskAsync_SingleThread_ChainEventsAndCallstack() + { + var events = await CollectEventsAsync(ResumeAsyncCallstackKeyword | CoreKeywords | MethodKeywords, async () => + { + var tcs = new TaskCompletionSource(); + Task chain = TaskAsync_SingleThread_ChainEventsAndCallstack_Marker(tcs.Task); + tcs.SetResult(); + await chain; + }); + + // DumpAllEvents(events); + + var stream = ParseAllEvents(events); + + // The marker frame must appear in a Resume callstack -- proves the chain was walkable. + var markerCallstacks = stream.CallstacksWithMarker(AsyncEventID.ResumeAsyncCallstack, nameof(TaskAsync_SingleThread_ChainEventsAndCallstack_Marker)); + Assert.NotEmpty(markerCallstacks); + + var deepest = markerCallstacks.MaxBy(cs => cs.FrameCount)!; + var frameNames = deepest.Frames + .Select(f => GetMethodNameFromMethodId(deepest.CallstackType, f.MethodId)) + .Where(n => n is not null) + .ToList(); + Assert.Contains(nameof(TaskAsync_SingleThread_ChainEventsAndCallstack_Inner_Marker), frameNames); + Assert.Contains(nameof(TaskAsync_SingleThread_ChainEventsAndCallstack_Mid_Marker), frameNames); + Assert.Contains(nameof(TaskAsync_SingleThread_ChainEventsAndCallstack_Marker), frameNames); + + // Create balanced by Complete on the synchronous cascade path. + int createCount = stream.OfType(AsyncEventID.CreateAsyncContext).Count(); + int completeCount = stream.OfType(AsyncEventID.CompleteAsyncContext).Count(); + Assert.Equal(createCount, completeCount); + + // Inline-cascade optimization: exactly 1 Create for the entire chain. + Assert.Equal(1, createCount); + + int methodResumeCount = stream.OfType(AsyncEventID.ResumeAsyncMethod).Count(); + int methodCompleteCount = stream.OfType(AsyncEventID.CompleteAsyncMethod).Count(); + + // Method-level events should be balanced. + Assert.Equal(methodResumeCount, methodCompleteCount); + + // No AppendAsyncCallstack events should be emitted in this scenario. + int appendCount = stream.OfType(AsyncEventID.AppendAsyncCallstack).Count(); + Assert.Equal(0, appendCount); + } + } +} diff --git a/src/libraries/System.Runtime/tests/System.Threading.Tasks.Tests/System.Runtime.CompilerServices/AsyncProfilerV2Tests.cs b/src/libraries/System.Runtime/tests/System.Threading.Tasks.Tests/System.Runtime.CompilerServices/AsyncProfilerV2Tests.cs new file mode 100644 index 00000000000000..54723cb2164db3 --- /dev/null +++ b/src/libraries/System.Runtime/tests/System.Threading.Tasks.Tests/System.Runtime.CompilerServices/AsyncProfilerV2Tests.cs @@ -0,0 +1,2255 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +using System.Collections.Generic; +using System.Diagnostics; +using System.Diagnostics.Tracing; +using System.Linq; +using System.Reflection; +using System.Runtime.CompilerServices; +using Microsoft.DotNet.XUnitExtensions; +using Xunit; + +namespace System.Threading.Tasks.Tests +{ + // Tests for V2 (runtime-async) async profiler event emission. All scenario methods use + // [RuntimeAsyncMethodGeneration(true)] to ensure they exercise the runtime-async path. + // V2 emits Create/Resume/Suspend/Complete callstacks natively from the runtime dispatch + // loop, unlike V1 which uses the AsyncStateMachineBox dispatcher wrapper. + public partial class AsyncProfilerTests + { + [RuntimeAsyncMethodGeneration(true)] + [MethodImpl(MethodImplOptions.NoInlining)] + private static async Task RuntimeAsync_SingleYield() + { + await Task.Yield(); + } + + [RuntimeAsyncMethodGeneration(true)] + [MethodImpl(MethodImplOptions.NoInlining)] + private static async Task RuntimeAsync_ChainedYield() + { + await RuntimeAsync_InnerYield(); + } + + [RuntimeAsyncMethodGeneration(true)] + [MethodImpl(MethodImplOptions.NoInlining)] + private static async Task RuntimeAsync_InnerYield() + { + await Task.Yield(); + } + + [RuntimeAsyncMethodGeneration(true)] + [MethodImpl(MethodImplOptions.NoInlining)] + private static async Task RuntimeAsync_DeepChain_Level3() + { + await Task.Yield(); + } + + [RuntimeAsyncMethodGeneration(true)] + [MethodImpl(MethodImplOptions.NoInlining)] + private static async Task RuntimeAsync_DeepChain_Level2() + { + await RuntimeAsync_DeepChain_Level3(); + } + + [RuntimeAsyncMethodGeneration(true)] + [MethodImpl(MethodImplOptions.NoInlining)] + private static async Task RuntimeAsync_DeepChain_Level1() + { + await RuntimeAsync_DeepChain_Level2(); + } + + [RuntimeAsyncMethodGeneration(true)] + [MethodImpl(MethodImplOptions.NoInlining)] + private static async Task RuntimeAsync_DeepChain() + { + await RuntimeAsync_DeepChain_Level1(); + } + + [RuntimeAsyncMethodGeneration(true)] + [MethodImpl(MethodImplOptions.NoInlining)] + private static async Task RuntimeAsync_OuterCatches() + { + try + { + await RuntimeAsync_InnerThrows(); + } + catch (InvalidOperationException) + { + } + await Task.Yield(); + } + + [RuntimeAsyncMethodGeneration(true)] + [MethodImpl(MethodImplOptions.NoInlining)] + private static async Task RuntimeAsync_InnerThrows() + { + await Task.Yield(); + throw new InvalidOperationException("inner"); + } + + [RuntimeAsyncMethodGeneration(true)] + [MethodImpl(MethodImplOptions.NoInlining)] + private static async Task RuntimeAsync_DeepOuterCatches() + { + try + { + await RuntimeAsync_DeepMiddle(); + } + catch (InvalidOperationException) + { + } + } + + [RuntimeAsyncMethodGeneration(true)] + [MethodImpl(MethodImplOptions.NoInlining)] + private static async Task RuntimeAsync_DeepMiddle() + { + await RuntimeAsync_DeepInnerThrows(); + } + + [RuntimeAsyncMethodGeneration(true)] + [MethodImpl(MethodImplOptions.NoInlining)] + private static async Task RuntimeAsync_DeepInnerThrows() + { + await Task.Yield(); + throw new InvalidOperationException("deep inner"); + } + + [RuntimeAsyncMethodGeneration(true)] + [MethodImpl(MethodImplOptions.NoInlining)] + private static async Task RuntimeAsync_DeepUnhandledOuter() + { + await RuntimeAsync_DeepUnhandledMiddle(); + } + + [RuntimeAsyncMethodGeneration(true)] + [MethodImpl(MethodImplOptions.NoInlining)] + private static async Task RuntimeAsync_DeepUnhandledMiddle() + { + await RuntimeAsync_DeepUnhandledInnerThrows(); + } + + [RuntimeAsyncMethodGeneration(true)] + [MethodImpl(MethodImplOptions.NoInlining)] + private static async Task RuntimeAsync_DeepUnhandledInnerThrows() + { + await Task.Yield(); + throw new InvalidOperationException("deep unhandled"); + } + + [RuntimeAsyncMethodGeneration(true)] + [MethodImpl(MethodImplOptions.NoInlining)] + private static async Task RuntimeAsync_RecursiveChain(int depth) + { + if (depth <= 1) + { + await Task.Yield(); + return; + } + await RuntimeAsync_RecursiveChain(depth - 1); + } + + [RuntimeAsyncMethodGeneration(true)] + [MethodImpl(MethodImplOptions.NoInlining)] + private static async Task RuntimeAsync_WrapperTestA(List<(string MethodName, int WrapperSlot)> captures) + { + await RuntimeAsync_WrapperTestB(captures); + captures.Add((nameof(RuntimeAsync_WrapperTestA), GetCurrentWrapperSlot(nameof(RuntimeAsync_WrapperTestA)))); + } + + [RuntimeAsyncMethodGeneration(true)] + [MethodImpl(MethodImplOptions.NoInlining)] + private static async Task RuntimeAsync_WrapperTestB(List<(string MethodName, int WrapperSlot)> captures) + { + await RuntimeAsync_WrapperTestC(captures); + captures.Add((nameof(RuntimeAsync_WrapperTestB), GetCurrentWrapperSlot(nameof(RuntimeAsync_WrapperTestB)))); + } + + [RuntimeAsyncMethodGeneration(true)] + [MethodImpl(MethodImplOptions.NoInlining)] + private static async Task RuntimeAsync_WrapperTestC(List<(string MethodName, int WrapperSlot)> captures) + { + await Task.Yield(); + captures.Add((nameof(RuntimeAsync_WrapperTestC), GetCurrentWrapperSlot(nameof(RuntimeAsync_WrapperTestC)))); + } + + [System.Runtime.CompilerServices.RuntimeAsyncMethodGeneration(true)] + [System.Runtime.CompilerServices.MethodImpl(System.Runtime.CompilerServices.MethodImplOptions.NoInlining)] + private static async ValueTask RuntimeAsync_ValueTask_Level1() + { + await RuntimeAsync_ValueTask_Level2(); + } + + [System.Runtime.CompilerServices.RuntimeAsyncMethodGeneration(true)] + [System.Runtime.CompilerServices.MethodImpl(System.Runtime.CompilerServices.MethodImplOptions.NoInlining)] + private static async ValueTask RuntimeAsync_ValueTask_Level2() + { + await RuntimeAsync_ValueTask_Level3(); + } + + [System.Runtime.CompilerServices.RuntimeAsyncMethodGeneration(true)] + [System.Runtime.CompilerServices.MethodImpl(System.Runtime.CompilerServices.MethodImplOptions.NoInlining)] + private static async ValueTask RuntimeAsync_ValueTask_Level3() + { + await Task.Yield(); + } + + [ConditionalFact(typeof(AsyncProfilerTests), nameof(IsRuntimeAsyncSupported))] + public async Task RuntimeAsync_EventBufferHeaderFormat() + { + var events = await CollectEventsAsync(CoreKeywords, RuntimeAsync_SingleYield); + + // DumpAllEvents(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))] + public async Task RuntimeAsync_EventsEmitted() + { + var events = await CollectEventsAsync(AllKeywords, RuntimeAsync_SingleYield); + + // DumpAllEvents(events); + + Assert.True(events.Events.Count > 0, "Expected at least one AsyncEvents event to be emitted"); + Assert.Contains(events.Events, e => e.EventId == AsyncEventsId); + } + + [System.Runtime.CompilerServices.RuntimeAsyncMethodGeneration(true)] + [System.Runtime.CompilerServices.MethodImpl(System.Runtime.CompilerServices.MethodImplOptions.NoInlining)] + private static async Task RuntimeAsync_SuspendResumeCompleteEvents_Marker() + { + await Task.Yield(); + await RuntimeAsync_SingleYield(); + } + + [ConditionalFact(typeof(AsyncProfilerTests), nameof(IsRuntimeAsyncSupported))] + public async Task RuntimeAsync_SuspendResumeCompleteEvents() + { + var events = await CollectEventsAsync(CallstackKeywords, RuntimeAsync_SuspendResumeCompleteEvents_Marker); + + // DumpAllEvents(events); + + var stream = ParseAllEvents(events); + + // Find our context via marker callstack. + var markerCallstacks = stream.CallstacksWithMarker(AsyncEventID.ResumeAsyncCallstack, nameof(RuntimeAsync_SuspendResumeCompleteEvents_Marker)); + Assert.NotEmpty(markerCallstacks); + + ulong taskId = markerCallstacks[0].TaskId; + var taskEvts = stream.ForTask(taskId); + var ids = taskEvts.Select(e => e.EventId).ToList(); + + Assert.Contains(AsyncEventID.ResumeAsyncContext, ids); + Assert.Contains(AsyncEventID.SuspendAsyncContext, ids); + Assert.Contains(AsyncEventID.CompleteAsyncContext, ids); + } + + + [System.Runtime.CompilerServices.RuntimeAsyncMethodGeneration(true)] + [System.Runtime.CompilerServices.MethodImpl(System.Runtime.CompilerServices.MethodImplOptions.NoInlining)] + private static async Task RuntimeAsync_ContextEventIdLifecycle_Marker() + { + await Task.Yield(); + await RuntimeAsync_SingleYield(); + } + + [ConditionalFact(typeof(AsyncProfilerTests), nameof(IsRuntimeAsyncSupported))] + public async Task RuntimeAsync_ContextEventIdLifecycle() + { + var events = await CollectEventsAsync(CallstackKeywords, RuntimeAsync_ContextEventIdLifecycle_Marker); + + // DumpAllEvents(events); + + var stream = ParseAllEvents(events); + + // Find events in the context that contains our marker method. + var markerCallstacks = stream.CallstacksWithMarker(AsyncEventID.ResumeAsyncCallstack, nameof(RuntimeAsync_ContextEventIdLifecycle_Marker)); + Assert.NotEmpty(markerCallstacks); + + ulong taskId = markerCallstacks[0].TaskId; + Assert.True(taskId > 0, "Context ID should be non-zero"); + + var taskEvts = stream.ForTask(taskId); + var ids = taskEvts.Select(e => e.EventId).ToList(); + + int createIdx = ids.IndexOf(AsyncEventID.CreateAsyncContext); + int resumeIdx = ids.IndexOf(AsyncEventID.ResumeAsyncContext); + Assert.True(createIdx >= 0, "Expected CreateAsyncContext in context events"); + Assert.True(resumeIdx > createIdx, "Expected ResumeAsyncContext after CreateAsyncContext"); + } + + [ConditionalFact(typeof(AsyncProfilerTests), nameof(IsRuntimeAsyncSupported))] + public async Task RuntimeAsync_ResumeCompleteMethodEvents() + { + var events = await CollectEventsAsync(MethodKeywords, RuntimeAsync_ChainedYield); + + // DumpAllEvents(events); + + var ids = ParseAllEvents(events).EventIds; + + Assert.Contains(AsyncEventID.ResumeAsyncMethod, ids); + Assert.Contains(AsyncEventID.CompleteAsyncMethod, ids); + } + + [System.Runtime.CompilerServices.RuntimeAsyncMethodGeneration(true)] + [System.Runtime.CompilerServices.MethodImpl(System.Runtime.CompilerServices.MethodImplOptions.NoInlining)] + private static async Task RuntimeAsync_EventSequenceOrder_Marker() + { + await Task.Yield(); + await RuntimeAsync_SingleYield(); + } + + [ConditionalFact(typeof(AsyncProfilerTests), nameof(IsRuntimeAsyncSupported))] + public async Task RuntimeAsync_EventSequenceOrder() + { + var events = await CollectEventsAsync(CallstackKeywords, RuntimeAsync_EventSequenceOrder_Marker); + + // DumpAllEvents(events); + + var stream = ParseAllEvents(events); + + // Find our context via marker callstack. + var markerCallstacks = stream.CallstacksWithMarker(AsyncEventID.ResumeAsyncCallstack, nameof(RuntimeAsync_EventSequenceOrder_Marker)); + Assert.NotEmpty(markerCallstacks); + + ulong taskId = markerCallstacks[0].TaskId; + var taskEvts = stream.ForTask(taskId); + var ids = taskEvts.Select(e => e.EventId).ToList(); + + // Verify the expected lifecycle sequence exists in order. + int resumeIdx1 = ids.IndexOf(AsyncEventID.ResumeAsyncContext); + Assert.True(resumeIdx1 >= 0, "Expected first ResumeAsyncContext"); + + int suspendIdx = ids.IndexOf(AsyncEventID.SuspendAsyncContext, resumeIdx1 + 1); + Assert.True(suspendIdx > resumeIdx1, "Expected SuspendAsyncContext after first Resume"); + + int resumeIdx2 = ids.IndexOf(AsyncEventID.ResumeAsyncContext, suspendIdx + 1); + Assert.True(resumeIdx2 > suspendIdx, "Expected second ResumeAsyncContext after Suspend"); + + int completeIdx = ids.IndexOf(AsyncEventID.CompleteAsyncContext, resumeIdx2 + 1); + Assert.True(completeIdx > resumeIdx2, "Expected CompleteAsyncContext after second Resume"); + } + + [ConditionalFact(typeof(AsyncProfilerTests), nameof(IsRuntimeAsyncSupported))] + public async Task RuntimeAsync_CreateAsyncContextEmittedOnFirstAwait() + { + var events = await CollectEventsAsync(CreateAsyncContextKeyword | CompleteAsyncContextKeyword, RuntimeAsync_SingleYield); + + // DumpAllEvents(events); + + var ids = ParseAllEvents(events).EventIds; + Assert.Contains(AsyncEventID.CreateAsyncContext, ids); + } + + [System.Runtime.CompilerServices.RuntimeAsyncMethodGeneration(true)] + [System.Runtime.CompilerServices.MethodImpl(System.Runtime.CompilerServices.MethodImplOptions.NoInlining)] + private static async Task RuntimeAsync_CreateAsyncCallstackEmittedOnFirstAwait_Marker() + { + await RuntimeAsync_SingleYield(); + } + + [ConditionalFact(typeof(AsyncProfilerTests), nameof(IsRuntimeAsyncSupported))] + public async Task RuntimeAsync_CreateAsyncCallstackEmittedOnFirstAwait() + { + var events = await CollectEventsAsync(CallstackKeywords, RuntimeAsync_CreateAsyncCallstackEmittedOnFirstAwait_Marker); + + // DumpAllEvents(events); + + var stream = ParseAllEvents(events); + var createCallstacks = stream.CallstacksWithMarker(AsyncEventID.CreateAsyncCallstack, nameof(RuntimeAsync_CreateAsyncCallstackEmittedOnFirstAwait_Marker)); + + Assert.NotEmpty(createCallstacks); + Assert.All(createCallstacks, 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].MethodId != 0, "Expected non-zero MethodId in first frame"); + }); + } + + + [System.Runtime.CompilerServices.RuntimeAsyncMethodGeneration(true)] + [System.Runtime.CompilerServices.MethodImpl(System.Runtime.CompilerServices.MethodImplOptions.NoInlining)] + private static async Task RuntimeAsync_CreateCallstackDepthMatchesChain_Marker() + { + await RuntimeAsync_ChainedYield(); + } + + [ConditionalFact(typeof(AsyncProfilerTests), nameof(IsRuntimeAsyncSupported))] + public async Task RuntimeAsync_CreateCallstackDepthMatchesChain() + { + var events = await CollectEventsAsync(CallstackKeywords, RuntimeAsync_CreateCallstackDepthMatchesChain_Marker); + + // DumpAllEvents(events); + + var stream = ParseAllEvents(events); + var createCallstacks = stream.CallstacksWithMarker(AsyncEventID.CreateAsyncCallstack, nameof(RuntimeAsync_CreateCallstackDepthMatchesChain_Marker)); + + // The expected [NoInlining] frames in order (innermost first): + // RuntimeAsync_InnerYield -> RuntimeAsync_ChainedYield -> RuntimeAsync_CreateCallstackDepthMatchesChain_Marker + Assert.NotEmpty(createCallstacks); + string[] expectedFrames = [nameof(RuntimeAsync_InnerYield), nameof(RuntimeAsync_ChainedYield), nameof(RuntimeAsync_CreateCallstackDepthMatchesChain_Marker)]; + Assert.True( + HasCallstackWithExpectedFrames(createCallstacks, expectedFrames), + $"Expected callstack to contain frames [{string.Join(", ", expectedFrames)}] in order"); + } + + [System.Runtime.CompilerServices.RuntimeAsyncMethodGeneration(true)] + [System.Runtime.CompilerServices.MethodImpl(System.Runtime.CompilerServices.MethodImplOptions.NoInlining)] + private static async Task RuntimeAsync_SuspendAsyncCallstackEmittedOnAwait_Marker() + { + await Task.Yield(); + await RuntimeAsync_SingleYield(); + } + + [ConditionalFact(typeof(AsyncProfilerTests), nameof(IsRuntimeAsyncSupported))] + public async Task RuntimeAsync_SuspendAsyncCallstackEmittedOnAwait() + { + var events = await CollectEventsAsync(CallstackKeywords, RuntimeAsync_SuspendAsyncCallstackEmittedOnAwait_Marker); + + // DumpAllEvents(events); + + var stream = ParseAllEvents(events); + var suspendCallstacks = stream.CallstacksWithMarker(AsyncEventID.SuspendAsyncCallstack, nameof(RuntimeAsync_SuspendAsyncCallstackEmittedOnAwait_Marker)); + + Assert.NotEmpty(suspendCallstacks); + Assert.All(suspendCallstacks, 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].MethodId != 0, "Expected non-zero MethodId in first frame"); + }); + } + + [System.Runtime.CompilerServices.RuntimeAsyncMethodGeneration(true)] + [System.Runtime.CompilerServices.MethodImpl(System.Runtime.CompilerServices.MethodImplOptions.NoInlining)] + private static async Task RuntimeAsync_SuspendCallstackDepthMatchesChain_Marker() + { + await Task.Yield(); + await RuntimeAsync_ChainedYield(); + } + + [ConditionalFact(typeof(AsyncProfilerTests), nameof(IsRuntimeAsyncSupported))] + public async Task RuntimeAsync_SuspendCallstackDepthMatchesChain() + { + var events = await CollectEventsAsync(CallstackKeywords, RuntimeAsync_SuspendCallstackDepthMatchesChain_Marker); + + // DumpAllEvents(events); + + var stream = ParseAllEvents(events); + var suspendCallstacks = stream.CallstacksWithMarker(AsyncEventID.SuspendAsyncCallstack, nameof(RuntimeAsync_SuspendCallstackDepthMatchesChain_Marker)); + + // The expected [NoInlining] frames in order (innermost first): + // RuntimeAsync_InnerYield -> RuntimeAsync_ChainedYield -> RuntimeAsync_SuspendCallstackDepthMatchesChain_Marker + Assert.NotEmpty(suspendCallstacks); + string[] expectedFrames = [nameof(RuntimeAsync_InnerYield), nameof(RuntimeAsync_ChainedYield), nameof(RuntimeAsync_SuspendCallstackDepthMatchesChain_Marker)]; + Assert.True( + HasCallstackWithExpectedFrames(suspendCallstacks, expectedFrames), + $"Expected callstack to contain frames [{string.Join(", ", expectedFrames)}] in order"); + } + + [System.Runtime.CompilerServices.RuntimeAsyncMethodGeneration(true)] + [System.Runtime.CompilerServices.MethodImpl(System.Runtime.CompilerServices.MethodImplOptions.NoInlining)] + private static async Task RuntimeAsync_SuspendCallstackPrecedesComplete_Marker() + { + await Task.Yield(); + await RuntimeAsync_InnerYield(); + } + + [ConditionalFact(typeof(AsyncProfilerTests), nameof(IsRuntimeAsyncSupported))] + public async Task RuntimeAsync_SuspendCallstackPrecedesComplete() + { + var events = await CollectEventsAsync(CallstackKeywords, RuntimeAsync_SuspendCallstackPrecedesComplete_Marker); + + // DumpAllEvents(events); + + var stream = ParseAllEvents(events); + + // Find the suspend callstack via marker to get the context ID + var suspendStacks = stream.CallstacksWithMarker(AsyncEventID.SuspendAsyncCallstack, nameof(RuntimeAsync_SuspendCallstackPrecedesComplete_Marker)); + Assert.NotEmpty(suspendStacks); + + ulong taskId = suspendStacks[0].TaskId; + Assert.True(taskId > 0, "Expected non-zero context ID"); + + var taskEvts = stream.ForTask(taskId); + var ids = taskEvts.Select(e => e.EventId).ToList(); + + int suspendIdx = ids.IndexOf(AsyncEventID.SuspendAsyncCallstack); + int completeIdx = ids.IndexOf(AsyncEventID.CompleteAsyncContext); + + Assert.True(suspendIdx >= 0, "Expected SuspendAsyncCallstack in context events"); + Assert.True(completeIdx >= 0, "Expected CompleteAsyncContext in context events"); + Assert.True(suspendIdx < completeIdx, $"SuspendAsyncCallstack (index {suspendIdx}) should precede CompleteAsyncContext (index {completeIdx})"); + } + + [System.Runtime.CompilerServices.RuntimeAsyncMethodGeneration(true)] + [System.Runtime.CompilerServices.MethodImpl(System.Runtime.CompilerServices.MethodImplOptions.NoInlining)] + private static async Task RuntimeAsync_SuspendCallstackDeeperThanInitialResume_Marker() + { + await Task.Yield(); + await RuntimeAsync_InnerYield(); + } + + [ConditionalFact(typeof(AsyncProfilerTests), nameof(IsRuntimeAsyncSupported))] + public async Task RuntimeAsync_SuspendCallstackDeeperThanInitialResume() + { + var events = await CollectEventsAsync(CallstackKeywords, RuntimeAsync_SuspendCallstackDeeperThanInitialResume_Marker); + + // DumpAllEvents(events); + + var stream = ParseAllEvents(events); + var resumeStacks = stream.CallstacksWithMarker(AsyncEventID.ResumeAsyncCallstack, nameof(RuntimeAsync_SuspendCallstackDeeperThanInitialResume_Marker)); + var suspendStacks = stream.CallstacksWithMarker(AsyncEventID.SuspendAsyncCallstack, nameof(RuntimeAsync_SuspendCallstackDeeperThanInitialResume_Marker)); + + Assert.NotEmpty(resumeStacks); + Assert.NotEmpty(suspendStacks); + + // First resume (after initial Yield) should be shallow, first suspend (RuntimeAsync_InnerYield's Yield) should be deeper + var firstResume = resumeStacks[0]; + var firstSuspend = suspendStacks[0]; + + Assert.True(firstSuspend.FrameCount > firstResume.FrameCount, $"First suspend callstack depth ({firstSuspend.FrameCount}) should be deeper than first resume callstack depth ({firstResume.FrameCount})"); + } + + [System.Runtime.CompilerServices.RuntimeAsyncMethodGeneration(true)] + [System.Runtime.CompilerServices.MethodImpl(System.Runtime.CompilerServices.MethodImplOptions.NoInlining)] + private static async Task RuntimeAsync_CreateCallstackPrecedesResumeCallstack_Marker() + { + await Task.Yield(); + await RuntimeAsync_InnerYield(); + } + + [ConditionalFact(typeof(AsyncProfilerTests), nameof(IsRuntimeAsyncSupported))] + public async Task RuntimeAsync_CreateCallstackPrecedesResumeCallstack() + { + var events = await CollectEventsAsync(CallstackKeywords, RuntimeAsync_CreateCallstackPrecedesResumeCallstack_Marker); + + // DumpAllEvents(events); + + var stream = ParseAllEvents(events); + var createStacks = stream.CallstacksWithMarker(AsyncEventID.CreateAsyncCallstack, nameof(RuntimeAsync_CreateCallstackPrecedesResumeCallstack_Marker)); + var resumeStacks = stream.CallstacksWithMarker(AsyncEventID.ResumeAsyncCallstack, nameof(RuntimeAsync_CreateCallstackPrecedesResumeCallstack_Marker)); + + Assert.NotEmpty(createStacks); + Assert.NotEmpty(resumeStacks); + + // For each task that has both Create and Resume callstacks, verify Create timestamp precedes Resume. + int matchedPairs = 0; + foreach (var create in createStacks) + { + var matchingResume = resumeStacks.FirstOrDefault(r => r.TaskId == create.TaskId); + if (matchingResume is null) + continue; + + matchedPairs++; + Assert.True(create.Timestamp <= matchingResume.Timestamp, $"For task {create.TaskId}: CreateAsyncCallstack (ts {create.Timestamp}) should precede ResumeAsyncCallstack (ts {matchingResume.Timestamp})"); + } + + Assert.True(matchedPairs >= 1, $"Expected at least one matching Create/Resume callstack pair, but found {matchedPairs}"); + } + + [System.Runtime.CompilerServices.RuntimeAsyncMethodGeneration(true)] + [System.Runtime.CompilerServices.MethodImpl(System.Runtime.CompilerServices.MethodImplOptions.NoInlining)] + private static async Task RuntimeAsync_CreateAndFirstResumeCallstacksMatch_Marker() + { + await Task.Yield(); + await RuntimeAsync_InnerYield(); + } + + [ConditionalFact(typeof(AsyncProfilerTests), nameof(IsRuntimeAsyncSupported))] + public async Task RuntimeAsync_CreateAndFirstResumeCallstacksMatch() + { + var events = await CollectEventsAsync(CallstackKeywords, RuntimeAsync_CreateAndFirstResumeCallstacksMatch_Marker); + + // DumpAllEvents(events); + + var stream = ParseAllEvents(events); + var createStacks = stream.CallstacksWithMarker(AsyncEventID.CreateAsyncCallstack, nameof(RuntimeAsync_CreateAndFirstResumeCallstacksMatch_Marker)); + var resumeStacks = stream.CallstacksWithMarker(AsyncEventID.ResumeAsyncCallstack, nameof(RuntimeAsync_CreateAndFirstResumeCallstacksMatch_Marker)); + + Assert.NotEmpty(createStacks); + Assert.NotEmpty(resumeStacks); + + // For each create callstack, find the first resume with the same task ID and verify frames match. + int matchedPairs = 0; + foreach (var create in createStacks) + { + var matchingResume = resumeStacks.FirstOrDefault(r => r.TaskId == create.TaskId); + if (matchingResume is null) + continue; + + matchedPairs++; + Assert.Equal(create.Frames.Count, matchingResume.Frames.Count); + for (int i = 0; i < create.Frames.Count; i++) + { + Assert.Equal(create.Frames[i].MethodId, matchingResume.Frames[i].MethodId); + } + } + + Assert.True(matchedPairs >= 1, $"Expected at least one matching Create/Resume callstack pair, but found {matchedPairs}"); + } + + [System.Runtime.CompilerServices.RuntimeAsyncMethodGeneration(true)] + [System.Runtime.CompilerServices.MethodImpl(System.Runtime.CompilerServices.MethodImplOptions.NoInlining)] + private static async Task RuntimeAsync_CallstackEmittedOnResume_Marker() + { + await Task.Yield(); + await RuntimeAsync_InnerYield(); + } + + [ConditionalFact(typeof(AsyncProfilerTests), nameof(IsRuntimeAsyncSupported))] + public async Task RuntimeAsync_CallstackEmittedOnResume() + { + var events = await CollectEventsAsync(CallstackKeywords, RuntimeAsync_CallstackEmittedOnResume_Marker); + + // DumpAllEvents(events); + + var stream = ParseAllEvents(events); + var callstacks = stream.CallstacksWithMarker(AsyncEventID.ResumeAsyncCallstack, nameof(RuntimeAsync_CallstackEmittedOnResume_Marker)); + + 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].MethodId != 0, "Expected non-zero MethodId in first frame"); + }); + } + + [System.Runtime.CompilerServices.RuntimeAsyncMethodGeneration(true)] + [System.Runtime.CompilerServices.MethodImpl(System.Runtime.CompilerServices.MethodImplOptions.NoInlining)] + private static async Task RuntimeAsync_CallstackDepthMatchesChain_Marker() + { + await Task.Yield(); + await RuntimeAsync_InnerYield(); + } + + [ConditionalFact(typeof(AsyncProfilerTests), nameof(IsRuntimeAsyncSupported))] + public async Task RuntimeAsync_CallstackDepthMatchesChain() + { + var events = await CollectEventsAsync(CallstackKeywords, RuntimeAsync_CallstackDepthMatchesChain_Marker); + + // DumpAllEvents(events); + + var stream = ParseAllEvents(events); + var callstacks = stream.CallstacksWithMarker(AsyncEventID.ResumeAsyncCallstack, nameof(RuntimeAsync_CallstackDepthMatchesChain_Marker)); + + // The expected [NoInlining] frames in order (innermost first): + // RuntimeAsync_InnerYield -> RuntimeAsync_CallstackDepthMatchesChain_Marker + Assert.NotEmpty(callstacks); + string[] expectedFrames = [nameof(RuntimeAsync_InnerYield), nameof(RuntimeAsync_CallstackDepthMatchesChain_Marker)]; + Assert.True( + HasCallstackWithExpectedFrames(callstacks, expectedFrames), + $"Expected callstack to contain frames [{string.Join(", ", expectedFrames)}] in order"); + } + + [RuntimeAsyncMethodGeneration(true)] + [MethodImpl(MethodImplOptions.NoInlining)] + private static async Task RuntimeAsync_MethodEventCountMatchesChainDepth_Marker() + { + await RuntimeAsync_DeepChain(); + } + + [ConditionalFact(typeof(AsyncProfilerTests), nameof(IsRuntimeAsyncSupported))] + public async Task RuntimeAsync_MethodEventCountMatchesChainDepth() + { + var events = await CollectEventsAsync(CallstackKeywords | MethodKeywords, RuntimeAsync_MethodEventCountMatchesChainDepth_Marker); + + // DumpAllEvents(events); + + var stream = ParseAllEvents(events); + + // Marker -> DeepChain -> Level1 -> Level2 -> Level3 + const int ExpectedChainDepth = 5; + + var markerCallstacks = stream.CallstacksWithMarker(AsyncEventID.ResumeAsyncCallstack, nameof(RuntimeAsync_MethodEventCountMatchesChainDepth_Marker)); + Assert.NotEmpty(markerCallstacks); + + Assert.Equal(ExpectedChainDepth, markerCallstacks[0].Frames.Count); + + ulong chainTaskId = markerCallstacks[0].TaskId; + var chainEvents = stream.ForTask(chainTaskId); + + int resumeCount = chainEvents.Count(e => e.EventId == AsyncEventID.ResumeAsyncMethod); + Assert.Equal(ExpectedChainDepth, resumeCount); + + int completeCount = chainEvents.Count(e => e.EventId == AsyncEventID.CompleteAsyncMethod); + Assert.Equal(ExpectedChainDepth, completeCount); + } + + [RuntimeAsyncMethodGeneration(true)] + [MethodImpl(MethodImplOptions.NoInlining)] + private static async Task RuntimeAsync_CallstackFramesHaveDistinctMethodIds_Marker() + { + await RuntimeAsync_DeepChain(); + } + + [ConditionalFact(typeof(AsyncProfilerTests), nameof(IsRuntimeAsyncSupported))] + public async Task RuntimeAsync_CallstackFramesHaveDistinctMethodIds() + { + var events = await CollectEventsAsync(CallstackKeywords, RuntimeAsync_CallstackFramesHaveDistinctMethodIds_Marker); + + // DumpAllEvents(events); + + var stream = ParseAllEvents(events); + + var markerCallstacks = stream.CallstacksWithMarker(AsyncEventID.ResumeAsyncCallstack, nameof(RuntimeAsync_CallstackFramesHaveDistinctMethodIds_Marker)); + Assert.NotEmpty(markerCallstacks); + + // Frames in the same callstack should have distinct methodIds (one per async method in the chain). + var deepest = markerCallstacks.MaxBy(cs => cs.FrameCount)!; + var methodIds = deepest.Frames.Select(f => f.MethodId).ToList(); + Assert.Equal(methodIds.Count, methodIds.Distinct().Count()); + } + + [RuntimeAsyncMethodGeneration(true)] + [MethodImpl(MethodImplOptions.NoInlining)] + private static async Task RuntimeAsync_YieldAtEachLevel_CallstackShrinks_Level3() + { + await Task.Delay(100); + await Task.Yield(); + } + + [RuntimeAsyncMethodGeneration(true)] + [MethodImpl(MethodImplOptions.NoInlining)] + private static async Task RuntimeAsync_YieldAtEachLevel_CallstackShrinks_Level2() + { + await RuntimeAsync_YieldAtEachLevel_CallstackShrinks_Level3(); + await Task.Yield(); + } + + [RuntimeAsyncMethodGeneration(true)] + [MethodImpl(MethodImplOptions.NoInlining)] + private static async Task RuntimeAsync_YieldAtEachLevel_CallstackShrinks_Level1() + { + await RuntimeAsync_YieldAtEachLevel_CallstackShrinks_Level2(); + await Task.Yield(); + } + + [RuntimeAsyncMethodGeneration(true)] + [MethodImpl(MethodImplOptions.NoInlining)] + private static async Task RuntimeAsync_YieldAtEachLevel_CallstackShrinks_Marker() + { + await RuntimeAsync_YieldAtEachLevel_CallstackShrinks_Level1(); + } + + [ConditionalFact(typeof(AsyncProfilerTests), nameof(IsRuntimeAsyncSupported))] + public async Task RuntimeAsync_YieldAtEachLevel_CallstackShrinks() + { + var events = await CollectEventsAsync(CallstackKeywords, RuntimeAsync_YieldAtEachLevel_CallstackShrinks_Marker); + + // DumpAllEvents(events); + + var stream = ParseAllEvents(events); + + var markerCallstacks = stream.CallstacksWithMarker(AsyncEventID.ResumeAsyncCallstack, nameof(RuntimeAsync_YieldAtEachLevel_CallstackShrinks_Marker)); + + // After Task.Delay resumes: full chain (Level3, Level2, Level1, Marker) = 4 frames + // After Level3's yield resumes: Level3 completes, chain is (Level2, Level1, Marker) = 3 frames + // After Level2's yield resumes: Level2 completes, chain is (Level1, Marker) = 2 frames + Assert.Contains(markerCallstacks, cs => cs.FrameCount == 4); + Assert.Contains(markerCallstacks, cs => cs.FrameCount == 3); + Assert.Contains(markerCallstacks, cs => cs.FrameCount == 2); + } + + [System.Runtime.CompilerServices.RuntimeAsyncMethodGeneration(true)] + [System.Runtime.CompilerServices.MethodImpl(System.Runtime.CompilerServices.MethodImplOptions.NoInlining)] + private static async Task RuntimeAsync_CallstackSimulation_NormalCompletion_Marker() + { + await Task.Yield(); + await RuntimeAsync_InnerYield(); + } + + [ConditionalFact(typeof(AsyncProfilerTests), nameof(IsRuntimeAsyncSupported))] + public async Task RuntimeAsync_CallstackSimulation_NormalCompletion() + { + var events = await CollectEventsAsync(CallstackKeywords, RuntimeAsync_CallstackSimulation_NormalCompletion_Marker); + + // DumpAllEvents(events); + + var stream = ParseAllEvents(events); + AssertCallstackSimulationReachesZero(stream, nameof(RuntimeAsync_CallstackSimulation_NormalCompletion_Marker)); + } + + [System.Runtime.CompilerServices.RuntimeAsyncMethodGeneration(true)] + [System.Runtime.CompilerServices.MethodImpl(System.Runtime.CompilerServices.MethodImplOptions.NoInlining)] + private static async Task RuntimeAsync_CallstackSimulation_HandledException_Marker() + { + await RuntimeAsync_DeepOuterCatches(); + } + + [ConditionalFact(typeof(AsyncProfilerTests), nameof(IsRuntimeAsyncSupported))] + public async Task RuntimeAsync_CallstackSimulation_HandledException() + { + var events = await CollectEventsAsync(CallstackKeywords, RuntimeAsync_CallstackSimulation_HandledException_Marker); + + // DumpAllEvents(events); + + var stream = ParseAllEvents(events); + AssertCallstackSimulationReachesZero(stream, nameof(RuntimeAsync_CallstackSimulation_HandledException_Marker)); + } + + [System.Runtime.CompilerServices.RuntimeAsyncMethodGeneration(true)] + [System.Runtime.CompilerServices.MethodImpl(System.Runtime.CompilerServices.MethodImplOptions.NoInlining)] + private static async Task RuntimeAsync_CallstackSimulation_UnhandledException_Marker() + { + await RuntimeAsync_DeepUnhandledOuter(); + } + + [System.Runtime.CompilerServices.RuntimeAsyncMethodGeneration(false)] + [System.Runtime.CompilerServices.MethodImpl(System.Runtime.CompilerServices.MethodImplOptions.NoInlining)] + private static async Task RuntimeAsync_CallstackSimulation_UnhandledException_Catcher_Marker() + { + try + { + await RuntimeAsync_CallstackSimulation_UnhandledException_Marker(); + } + catch (InvalidOperationException) + { + } + } + + [ConditionalFact(typeof(AsyncProfilerTests), nameof(IsRuntimeAsyncSupported))] + public async Task RuntimeAsync_CallstackSimulation_UnhandledException() + { + var events = await CollectEventsAsync(CallstackKeywords, RuntimeAsync_CallstackSimulation_UnhandledException_Catcher_Marker); + + // DumpAllEvents(events); + + var stream = ParseAllEvents(events); + AssertCallstackSimulationReachesZero(stream, nameof(RuntimeAsync_CallstackSimulation_UnhandledException_Marker)); + } + + [System.Runtime.CompilerServices.RuntimeAsyncMethodGeneration(true)] + [System.Runtime.CompilerServices.MethodImpl(System.Runtime.CompilerServices.MethodImplOptions.NoInlining)] + private static async Task RuntimeAsync_UnhandledExceptionUnwind_Marker() + { + await RuntimeAsync_DeepUnhandledOuter(); + } + + [System.Runtime.CompilerServices.RuntimeAsyncMethodGeneration(false)] + [System.Runtime.CompilerServices.MethodImpl(System.Runtime.CompilerServices.MethodImplOptions.NoInlining)] + private static async Task RuntimeAsync_UnhandledExceptionUnwind_Catcher_Marker() + { + try + { + await RuntimeAsync_UnhandledExceptionUnwind_Marker(); + } + catch (InvalidOperationException) + { + } + } + + [ConditionalFact(typeof(AsyncProfilerTests), nameof(IsRuntimeAsyncSupported))] + public async Task RuntimeAsync_UnhandledExceptionUnwind() + { + var events = await CollectEventsAsync(CallstackKeywords, RuntimeAsync_UnhandledExceptionUnwind_Catcher_Marker); + + // DumpAllEvents(events); + + var stream = ParseAllEvents(events); + var resumeStacks = stream.CallstacksWithMarker(AsyncEventID.ResumeAsyncCallstack, nameof(RuntimeAsync_UnhandledExceptionUnwind_Marker)); + Assert.NotEmpty(resumeStacks); + + ulong taskId = resumeStacks[0].TaskId; + + var taskEvts = stream.ForTask(taskId); + var eventIds = taskEvts.Select(e => e.EventId).ToList(); + + Assert.Contains(AsyncEventID.ResumeAsyncContext, eventIds); + Assert.Contains(AsyncEventID.UnwindAsyncException, eventIds); + Assert.Contains(AsyncEventID.CompleteAsyncContext, eventIds); + + // Verify unwind frame count for this task + // Marker -> RuntimeAsync_DeepUnhandledOuter -> RuntimeAsync_DeepUnhandledMiddle -> RuntimeAsync_DeepUnhandledInnerThrows, 4 frames deep after the initial resume. + var unwindEvents = taskEvts.Where(e => e.EventId == AsyncEventID.UnwindAsyncException).ToList(); + Assert.NotEmpty(unwindEvents); + Assert.All(unwindEvents, e => Assert.Equal(4u, e.UnwindFrameCount)); + } + + [System.Runtime.CompilerServices.RuntimeAsyncMethodGeneration(true)] + [System.Runtime.CompilerServices.MethodImpl(System.Runtime.CompilerServices.MethodImplOptions.NoInlining)] + private static async Task RuntimeAsync_HandledExceptionUnwind_Marker() + { + await RuntimeAsync_DeepOuterCatches(); + } + + [ConditionalFact(typeof(AsyncProfilerTests), nameof(IsRuntimeAsyncSupported))] + public async Task RuntimeAsync_HandledExceptionUnwind() + { + var events = await CollectEventsAsync(CallstackKeywords, RuntimeAsync_HandledExceptionUnwind_Marker); + + // DumpAllEvents(events); + + var stream = ParseAllEvents(events); + var resumeStacks = stream.CallstacksWithMarker(AsyncEventID.ResumeAsyncCallstack, nameof(RuntimeAsync_HandledExceptionUnwind_Marker)); + Assert.NotEmpty(resumeStacks); + + ulong taskId = resumeStacks[0].TaskId; + + var taskEvts = stream.ForTask(taskId); + var eventIds = taskEvts.Select(e => e.EventId).ToList(); + + Assert.Contains(AsyncEventID.ResumeAsyncContext, eventIds); + Assert.Contains(AsyncEventID.UnwindAsyncException, eventIds); + Assert.Contains(AsyncEventID.CompleteAsyncContext, eventIds); + + // Verify unwind frame count for this task + // RuntimeAsync_DeepMiddle -> RuntimeAsync_DeepInnerThrows, 2 frames deep after the initial resume. + var unwindEvents = taskEvts.Where(e => e.EventId == AsyncEventID.UnwindAsyncException).ToList(); + Assert.NotEmpty(unwindEvents); + Assert.All(unwindEvents, e => Assert.Equal(2u, e.UnwindFrameCount)); + } + + // Requires threading: + // Wrapper index tests use RunScenarioAndFlush (Task.Run) to escape xunit's + // AsyncTestSyncContext. With a SynchronizationContext present, each level in the + // async chain re-dispatches through the sync context, creating a separate + // DispatchContinuations call that resets the wrapper index to 0. Task.Run ensures + // the entire continuation chain executes in a single dispatch loop where the + // wrapper index increments sequentially across resumptions. + [ConditionalFact(typeof(AsyncProfilerTests), nameof(IsRuntimeAsyncAndThreadingSupported))] + public void RuntimeAsync_WrapperIndexMatchesCallstack() + { + var captures = new List<(string MethodName, int WrapperSlot)>(); + + var events = CollectEvents(ResumeAsyncCallstackKeyword, () => + { + RunScenarioAndFlush(async () => + { + await RuntimeAsync_WrapperTestA(captures); + }); + }); + + // DumpAllEvents(events); + + 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 wrapper frame on stack (slot={c.WrapperSlot})")); + + int slotC = captures.First(c => c.MethodName == nameof(RuntimeAsync_WrapperTestC)).WrapperSlot; + int slotB = captures.First(c => c.MethodName == nameof(RuntimeAsync_WrapperTestB)).WrapperSlot; + int slotA = captures.First(c => c.MethodName == nameof(RuntimeAsync_WrapperTestA)).WrapperSlot; + + Assert.Equal(slotC + 1, slotB); + Assert.Equal(slotB + 1, slotA); + } + + // Requires threading: + // Same comment as RuntimeAsync_WrapperIndexMatchesCallstack regarding Task.Run and wrapper index behavior. + [ConditionalFact(typeof(AsyncProfilerTests), nameof(IsRuntimeAsyncAndThreadingSupported))] + 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 RuntimeAsync_RecursiveChain(34); + }); + }); + + // DumpAllEvents(events); + + var ids = ParseAllEvents(events).EventIds; + + Assert.Contains(AsyncEventID.ResetAsyncContinuationWrapperIndex, ids); + } + + // Requires threading: + // Same comment as RuntimeAsync_WrapperIndexMatchesCallstack regarding Task.Run and wrapper index behavior. + [ConditionalFact(typeof(AsyncProfilerTests), nameof(IsRuntimeAsyncAndThreadingSupported))] + 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 RuntimeAsync_RecursiveChain(2); + }); + }); + + // DumpAllEvents(events); + + var ids = ParseAllEvents(events).EventIds; + + Assert.DoesNotContain(AsyncEventID.ResetAsyncContinuationWrapperIndex, ids); + } + + // Requires threading: + // The periodic flush timer runs on a background thread. + // On single-threaded runtimes there is no background thread to fire the timer. + [ConditionalFact(typeof(AsyncProfilerTests), nameof(IsRuntimeAsyncAndThreadingSupported))] + public void RuntimeAsync_PeriodicTimerFlush() + { + static bool IsRequestedEvent(AsyncEventID id) => + id == AsyncEventID.CreateAsyncContext || + id == AsyncEventID.ResumeAsyncContext || + id == AsyncEventID.SuspendAsyncContext || + id == AsyncEventID.CompleteAsyncContext; + + var events = CollectEvents(CoreKeywords, (collectedEvents, _) => + { + // Run scenario - do NOT flush explicitly afterwards. + RunScenario(async () => + { + await RuntimeAsync_SingleYield(); + }); + + // Wait for the periodic flush timer (1s interval) to detect the idle buffer and flush it automatically. + Thread.Sleep(1000); + + // Poll to make sure the expected buffer got flush. + bool flushed = SpinWait.SpinUntil(() => + { + var stream = ParseAllEvents(collectedEvents); + return stream.EventIds.Any(id => IsRequestedEvent(id)); + }, TimeSpan.FromSeconds(20)); + + Assert.True(flushed, "Expected periodic timer to flush buffer with core lifecycle events within timeout"); + }); + + // DumpAllEvents(events); + + var stream = ParseAllEvents(events); + int coreEventCount = stream.EventIds.Count(id => IsRequestedEvent(id)); + + Assert.True(coreEventCount > 0, "Expected periodic timer to flush buffer with core lifecycle events"); + } + + // Requires threading: + // Verifies the background flush timer preserves the owning thread's OS thread ID, + // which needs both a dedicated worker thread and the timer thread. + [ConditionalFact(typeof(AsyncProfilerTests), nameof(IsRuntimeAsyncAndThreadingSupported))] + public void RuntimeAsync_PeriodicTimerFlush_PreservesOwnerThreadId() + { + // This test verifies that when the background flush timer flushes a thread's buffer, + // the new header written afterwards preserves the owning thread's OS thread ID + // (not the timer thread's ID). + // + // Strategy: run async work on a dedicated thread so its profiler context gets events. + // Between two batches of work, wait for the flush timer to fire. Both buffer flushes + // from the dedicated thread should carry the same OsThreadId. + + ulong workerOsThreadId = 0; + var workerIdReady = new ManualResetEventSlim(false); + var firstBatchDone = new ManualResetEventSlim(false); + var firstFlushSeen = new ManualResetEventSlim(false); + var events = new CollectedEvents(); + + using (var listener = CreateListener(CoreKeywords)) + { + listener.RunWithCallback(e => + { + if (!workerIdReady.IsSet) + { + return; + } + + if (e.EventId != AsyncEventsId || e.Payload is null || e.Payload.Count == 0) + { + return; + } + + if (e.Payload[0] is not byte[] payload) + { + return; + } + + EventBufferHeader? header = ParseEventBufferHeader(payload); + if (header is not null && header.Value.OsThreadId == workerOsThreadId) + { + events.Events.Enqueue(e); + } + }, () => + { + SendFlushCommand(); + + var thread = new Thread(() => + { + workerOsThreadId = GetCurrentOSThreadId(); + workerIdReady.Set(); + + // First batch: generate events on this thread's profiler context. + RuntimeAsync_SingleYield().GetAwaiter().GetResult(); + firstBatchDone.Set(); + + // Wait for the flush to deliver our first buffer before generating more events. + bool flushed = firstFlushSeen.Wait(TimeSpan.FromSeconds(20)); + Assert.True(flushed, "Expected first flush of core lifecycle events within timeout"); + + // Second batch: generate more events on the same thread's context. + RuntimeAsync_SingleYield().GetAwaiter().GetResult(); + }); + + thread.IsBackground = true; + thread.Start(); + + // Wait for the worker to finish its first batch, then force flush. + firstBatchDone.Wait(TimeSpan.FromSeconds(20)); + SendFlushCommand(); + + // Poll for first buffer from our worker thread. + bool firstFlush = SpinWait.SpinUntil(() => events.Events.Count >= 1, TimeSpan.FromSeconds(20)); + Assert.True(firstFlush, "Expected periodic timer to flush core lifecycle events within timeout"); + + firstFlushSeen.Set(); + + // Wait for the worker to finish its second batch. + bool joined = thread.Join(TimeSpan.FromSeconds(20)); + Assert.True(joined, "Expected worker thread to terminate within timeout after second batch of work"); + + // Force a flush to deliver the second batch. + SendFlushCommand(); + + // Poll for second buffer from our worker thread. + bool secondFlush = SpinWait.SpinUntil(() => events.Events.Count >= 2, TimeSpan.FromSeconds(20)); + Assert.True(secondFlush, "Expected periodic timer to flush core lifecycle events within timeout"); + }); + } + + // DumpAllEvents(events); + + Assert.True(workerOsThreadId != 0, "Failed to capture worker OS thread ID"); + + // The key assertion: find buffers that contain CreateAsyncContext events (our work batches). + // There must be at least 2 such buffers (one per RuntimeAsync_SingleYield() call), and ALL of them must + // have the worker's OsThreadId - proving the timer flush didn't corrupt the header. + var stream = ParseAllEvents(events); + var createEvents = stream.OfType(AsyncEventID.CreateAsyncContext).ToList(); + Assert.True(createEvents.Count >= 2, $"Expected at least 2 CreateAsyncContext events from the worker thread, got {createEvents.Count}"); + Assert.All(createEvents, e => Assert.Equal(workerOsThreadId, e.OsThreadId)); + } + + // Requires threading: + // Spawns a dedicated thread that exits, then waits for the background flush timer + // to detect and flush the orphaned thread-local buffer. + [ConditionalFact(typeof(AsyncProfilerTests), nameof(IsRuntimeAsyncAndThreadingSupported))] + public void RuntimeAsync_DeadThreadFlush() + { + static bool IsRequestedEvent(AsyncEventID id) => + id == AsyncEventID.CreateAsyncContext || + id == AsyncEventID.ResumeAsyncContext || + id == AsyncEventID.SuspendAsyncContext || + id == AsyncEventID.CompleteAsyncContext; + + var events = CollectEvents(CoreKeywords, (collectedEvents, _) => + { + // Spawn a dedicated thread that runs async work then exits. + // Its thread-local buffer becomes orphaned when the thread dies. + var thread = new Thread(() => + { + RunScenario(async () => + { + await RuntimeAsync_SingleYield(); + }); + }); + + thread.IsBackground = true; + thread.Start(); + bool joined = thread.Join(TimeSpan.FromSeconds(20)); + Assert.True(joined, "Expected worker thread to terminate within timeout before waiting for orphaned buffer flush"); + + // Do NOT send a flush command. + // Wait for the periodic flush timer to detect the dead thread and flush its orphaned buffer. + Thread.Sleep(1000); + + // Poll to make sure the expected buffer got flush. + bool flushed = SpinWait.SpinUntil(() => + { + var stream = ParseAllEvents(collectedEvents); + return stream.EventIds.Any(id => IsRequestedEvent(id)); + }, TimeSpan.FromSeconds(20)); + + Assert.True(flushed, "Expected periodic timer to flush buffer with core lifecycle events within timeout"); + }); + + // DumpAllEvents(events); + + var stream = ParseAllEvents(events); + int coreEventCount = stream.EventIds.Count(id => IsRequestedEvent(id)); + + Assert.True(coreEventCount > 0, "Expected periodic timer to flush dead thread's buffer"); + } + + // This test is sensitive to event noise - it asserts a specific clock event is absent. + // It cannot run in parallel with other async profiler scenarios that might produce + // clock events. Test parallelization is disabled via XunitAssemblyAttributes.cs. + [ConditionalFact(typeof(AsyncProfilerTests), nameof(IsRuntimeAsyncSupported))] + public async Task RuntimeAsync_NoSyncClockEventBeforeInterval() + { + var events = await CollectEventsAsync(CoreKeywords, RuntimeAsync_SingleYield); + + var ids = ParseAllEvents(events).EventIds; + + Assert.DoesNotContain(AsyncEventID.AsyncProfilerSyncClock, ids); + } + + // This test is sensitive to event noise - it asserts zero context events appear + // after enabling the listener. It cannot run in parallel with other async profiler + // scenarios that might produce events on the same thread context. + // Test parallelization is already disabled via XunitAssemblyAttributes.cs. + [ConditionalFact(typeof(AsyncProfilerTests), nameof(IsRuntimeAsyncSupported))] + public async Task RuntimeAsync_NoEventsWhenDisabled() + { + // Run async work WITHOUT a listener attached + for (int i = 0; i < 50; i++) + { + await RuntimeAsync_SingleYield(); + } + + // Now attach listener but don't run any RuntimeAsync work inside -- + // just call a synchronous no-op. Verify no stale events from above leak through. + var events = await CollectEventsAsync(CoreKeywords, () => Task.CompletedTask); + + // There may be meta data related events, but there should be no suspend/resume/complete events from the earlier work. + var ids = ParseAllEvents(events).EventIds; + + int contextEvents = ids.Count(id => id == AsyncEventID.ResumeAsyncContext || id == AsyncEventID.SuspendAsyncContext || id == AsyncEventID.CompleteAsyncContext); + + Assert.Equal(0, contextEvents); + } + + public static IEnumerable KeywordGatekeepingData() + { + yield return new object[] { (long)CreateAsyncContextKeyword, new AsyncEventID[] { AsyncEventID.ResetAsyncThreadContext, AsyncEventID.CreateAsyncContext, AsyncEventID.AsyncProfilerMetadata } }; + yield return new object[] { (long)ResumeAsyncContextKeyword, new AsyncEventID[] { AsyncEventID.ResetAsyncThreadContext, AsyncEventID.ResumeAsyncContext, AsyncEventID.AsyncProfilerMetadata } }; + yield return new object[] { (long)SuspendAsyncContextKeyword, new AsyncEventID[] { AsyncEventID.ResetAsyncThreadContext, AsyncEventID.SuspendAsyncContext, AsyncEventID.AsyncProfilerMetadata } }; + yield return new object[] { (long)CompleteAsyncContextKeyword, new AsyncEventID[] { AsyncEventID.ResetAsyncThreadContext, AsyncEventID.CompleteAsyncContext, AsyncEventID.AsyncProfilerMetadata } }; + yield return new object[] { (long)UnwindAsyncExceptionKeyword, new AsyncEventID[] { AsyncEventID.ResetAsyncThreadContext, AsyncEventID.UnwindAsyncException, AsyncEventID.AsyncProfilerMetadata } }; + yield return new object[] { (long)CreateAsyncCallstackKeyword, new AsyncEventID[] { AsyncEventID.ResetAsyncThreadContext, AsyncEventID.CreateAsyncCallstack, AsyncEventID.AsyncProfilerMetadata } }; + yield return new object[] { (long)ResumeAsyncCallstackKeyword, new AsyncEventID[] { AsyncEventID.ResetAsyncThreadContext, AsyncEventID.ResumeAsyncCallstack, AsyncEventID.AsyncProfilerMetadata } }; + yield return new object[] { (long)SuspendAsyncCallstackKeyword, new AsyncEventID[] { AsyncEventID.ResetAsyncThreadContext, AsyncEventID.SuspendAsyncCallstack, AsyncEventID.AsyncProfilerMetadata } }; + yield return new object[] { (long)ResumeAsyncMethodKeyword, new AsyncEventID[] { AsyncEventID.ResetAsyncThreadContext, AsyncEventID.ResumeAsyncMethod, AsyncEventID.AsyncProfilerMetadata } }; + yield return new object[] { (long)CompleteAsyncMethodKeyword, new AsyncEventID[] { AsyncEventID.ResetAsyncThreadContext, AsyncEventID.CompleteAsyncMethod, AsyncEventID.AsyncProfilerMetadata } }; + } + + [System.Runtime.CompilerServices.RuntimeAsyncMethodGeneration(true)] + [System.Runtime.CompilerServices.MethodImpl(System.Runtime.CompilerServices.MethodImplOptions.NoInlining)] + private static async Task RuntimeAsync_KeywordGatekeeping_Marker() + { + await RuntimeAsync_OuterCatches(); + await RuntimeAsync_ChainedYield(); + } + + // This test is sensitive to event noise - it asserts that ONLY the expected event + // types appear for a given keyword. It cannot run in parallel with other async + // profiler scenarios that might produce events on the same thread context. + // Test parallelization is already disabled via XunitAssemblyAttributes.cs. + [ConditionalTheory(typeof(AsyncProfilerTests), nameof(IsRuntimeAsyncSupported))] + [MemberData(nameof(KeywordGatekeepingData))] + public async Task RuntimeAsync_KeywordGatekeeping(long keywordValue, AsyncEventID[] allowedEventIds) + { + EventKeywords kw = (EventKeywords)keywordValue; + var allowed = new HashSet(allowedEventIds); + + // 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. + var events = await CollectEventsAsync(kw, RuntimeAsync_KeywordGatekeeping_Marker); + + // DumpAllEvents(events); + + var stream = ParseAllEvents(events); + var unexpected = stream.EventIds.Where(id => !allowed.Contains(id)).ToList(); + + 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))] + public async Task RuntimeAsync_ResetAsyncThreadContextEvent() + { + var events = await CollectEventsAsync(CoreKeywords, RuntimeAsync_SingleYield); + + // DumpAllEvents(events); + + var ids = ParseAllEvents(events).EventIds; + + Assert.Contains(AsyncEventID.ResetAsyncThreadContext, ids); + } + + [ConditionalFact(typeof(AsyncProfilerTests), nameof(IsRuntimeAsyncSupported))] + public async Task RuntimeAsync_MetadataEventEmittedOnEnable() + { + var events = await CollectEventsAsync(AllKeywords, RuntimeAsync_SingleYield); + + // DumpAllEvents(events); + + var stream = ParseAllEvents(events); + var metadataList = stream.MetadataEvents; + Assert.True(metadataList.Count >= 1, "Expected at least one metadata event in buffer"); + + MetadataFromBuffer meta = metadataList[0]; + Assert.True(meta.QpcFrequency > 0, $"QPC frequency should be positive, got {meta.QpcFrequency}"); + Assert.True(meta.QpcSync > 0, $"QPC sync timestamp should be positive, got {meta.QpcSync}"); + Assert.True(meta.UtcSync > 0, $"UTC sync timestamp should be positive, got {meta.UtcSync}"); + Assert.True(meta.EventBufferSize > 0, $"Event buffer size should be positive, got {meta.EventBufferSize}"); + Assert.True(meta.WrapperCount > 0, "Wrapper count should be positive"); + } + + // Requires threading: + // Spawns 8 threads with a Barrier to verify metadata is + // emitted exactly once under concurrent enable pressure. + [ConditionalFact(typeof(AsyncProfilerTests), nameof(IsRuntimeAsyncAndThreadingSupported))] + 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(); + RuntimeAsync_SingleYield().GetAwaiter().GetResult(); + }, TaskCreationOptions.LongRunning); + } + Task.WaitAll(tasks); + SendFlushCommand(); + }); + + // DumpAllEvents(events); + + var stream = ParseAllEvents(events); + var metadataList = stream.MetadataEvents; + Assert.True(metadataList.Count == 1, $"Expected exactly 1 metadata event across {threadCount} threads, got {metadataList.Count}"); + } + + [System.Runtime.CompilerServices.RuntimeAsyncMethodGeneration(true)] + [System.Runtime.CompilerServices.MethodImpl(System.Runtime.CompilerServices.MethodImplOptions.NoInlining)] + private static async Task RuntimeAsync_CallstackNativeIPDeltaRoundtrip_Marker() + { + await RuntimeAsync_ChainedYield(); + await RuntimeAsync_DeepOuterCatches(); + await RuntimeAsync_RecursiveChain(10); + } + + [ConditionalFact(typeof(AsyncProfilerTests), nameof(IsRuntimeAsyncSupported))] + public async Task 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 = await CollectEventsAsync(CallstackKeywords, RuntimeAsync_CallstackNativeIPDeltaRoundtrip_Marker); + + var stream = ParseAllEvents(events); + var callstacks = stream.CallstacksWithMarker(AsyncEventID.ResumeAsyncCallstack, nameof(RuntimeAsync_CallstackNativeIPDeltaRoundtrip_Marker)); + 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 (methodId, _) = cs.Frames[i]; + Assert.True(methodId != 0, $"Frame {i} has zero MethodId"); + + var method = GetMethodNameFromMethodId(cs.CallstackType, methodId); + Assert.True(method is not null, $"Frame {i}: MethodId 0x{methodId:X} does not resolve to a managed method"); + + if (i > 0) + { + long delta = (long)(cs.Frames[i].MethodId - cs.Frames[i - 1].MethodId); + 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"); + } + + [System.Runtime.CompilerServices.RuntimeAsyncMethodGeneration(true)] + [System.Runtime.CompilerServices.MethodImpl(System.Runtime.CompilerServices.MethodImplOptions.NoInlining)] + private static async Task RuntimeAsync_CallstackStressWithVaryingDepths_Marker(int depth) + { + await RuntimeAsync_RecursiveChain(depth); + } + + // Requires threading: + // The recursive async chain must execute in a single dispatch + // loop (no sync context) to produce full-depth callstacks. Under xunit's + // AsyncTestSyncContext, each await re-dispatches, fragmenting the chain. + [ConditionalFact(typeof(AsyncProfilerTests), nameof(IsRuntimeAsyncAndThreadingSupported))] + 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. + // lambda -> Marker(d) -> RuntimeAsync_RecursiveChain(d) produces d + 2 frames. + 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 RuntimeAsync_CallstackStressWithVaryingDepths_Marker(depths[i]); + }); + }); + + // DumpAllEvents(events); + + var stream = ParseAllEvents(events); + var callstacks = stream.CallstacksWithMarker(AsyncEventID.ResumeAsyncCallstack, nameof(RuntimeAsync_CallstackStressWithVaryingDepths_Marker)); + + // Verify all callstacks have valid frame data that resolves to managed methods. + foreach (var cs in callstacks) + { + Assert.True(cs.FrameCount > 0, "Callstack has 0 frames"); + Assert.Equal((int)cs.FrameCount, cs.Frames.Count); + for (int f = 0; f < cs.Frames.Count; f++) + { + var (methodId, _) = cs.Frames[f]; + Assert.True(methodId != 0, $"Frame {f} has zero MethodId"); + + var method = GetMethodNameFromMethodId(cs.CallstackType, methodId); + Assert.True(method is not null, $"Frame {f}: MethodId 0x{methodId:X} does not resolve to a managed method"); + } + } + + // One resume callstack per iteration (marker filters out noise). + // lambda -> Marker -> RuntimeAsync_RecursiveChain(d) produces d + 2 frames. + Assert.True(callstacks.Count >= iterations, $"Expected at least {iterations} callstacks with marker, got {callstacks.Count}"); + + for (int i = 0; i < iterations; i++) + { + // lambda + Marker + RuntimeAsync_RecursiveChain(d) = d + 2 + int expected = depths[i] + 2; + int actual = callstacks[i].FrameCount; + Assert.True(actual == expected, $"Iteration {i}: expected depth {expected} (lambda -> Marker -> RuntimeAsync_RecursiveChain({depths[i]})), 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}"); + } + + // Requires threading: + // Deep recursive chains must execute in a single dispatch loop (no sync context) + // to produce full-depth callstacks that trigger overflow. + [ConditionalFact(typeof(AsyncProfilerTests), nameof(IsRuntimeAsyncAndThreadingSupported))] + 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 the partial buffer, + // and re-writes the callstack as the first event in a fresh buffer. + // + // To prove overflow occurred we check consecutive buffer pairs: + // Buffer N: not full (has remaining capacity, but not enough for the next callstack) + // Buffer N+1: first event is a large callstack + // This proves the runtime detected insufficient space, rewound, and flushed. + 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(AllKeywords, () => + { + RunScenarioAndFlush(async () => + { + for (int i = 0; i < iterations; i++) + await RuntimeAsync_RecursiveChain(depths[i]); + }); + }); + + // Get buffer capacity from metadata. + var stream = ParseAllEvents(events); + var metadataList = stream.MetadataEvents; + if (metadataList.Count == 0) + continue; + uint bufferCapacity = metadataList[0].EventBufferSize; + + // Collect per-buffer info grouped by async thread context. + // Consecutive buffers from the same context represent the overflow sequence. + var buffersByContext = new Dictionary>(); + ForEachEventBufferPayload(events, buffer => + { + EventBufferHeader? header = ParseEventBufferHeader(buffer); + if (header is null) + return; + + uint contextId = header.Value.AsyncThreadContextId; + + // Parse the first event in this buffer. + int index = HeaderSize; + if (index >= buffer.Length) + return; + + AsyncEventID firstId = (AsyncEventID)buffer[index++]; + // Skip timestamp delta + ReadCompressedUInt64(buffer, ref index); + + byte frameCount = 0; + if (firstId == AsyncEventID.ResumeAsyncCallstack || + firstId == AsyncEventID.CreateAsyncCallstack || + firstId == AsyncEventID.SuspendAsyncCallstack) + { + // Callstack payload: type(1) + callstackId(1) + frameCount(1) + compressed taskId + frames... + index++; // type byte + index++; // callstack ID (reserved) + if (index < buffer.Length) + frameCount = buffer[index]; + } + + if (!buffersByContext.TryGetValue(contextId, out var list)) + { + list = new List<(int, AsyncEventID, byte)>(); + buffersByContext[contextId] = list; + } + list.Add((buffer.Length, firstId, frameCount)); + }); + + // Look for overflow evidence within the same thread context: + // buffer N not full, buffer N+1 starts with large callstack. + foreach (var bufferInfos in buffersByContext.Values) + { + for (int i = 0; i < bufferInfos.Count - 1; i++) + { + var current = bufferInfos[i]; + var next = bufferInfos[i + 1]; + + Assert.True((uint)current.UsedSize <= bufferCapacity, $"Buffer used size {current.UsedSize} exceeds capacity {bufferCapacity}."); + + uint remaining = bufferCapacity - (uint)current.UsedSize; + bool currentNotFull = remaining > 0; + bool nextStartsWithLargeCallstack = + (next.FirstEventId == AsyncEventID.ResumeAsyncCallstack || + next.FirstEventId == AsyncEventID.CreateAsyncCallstack || + next.FirstEventId == AsyncEventID.SuspendAsyncCallstack) && + next.FirstFrameCount > 30; + + if (currentNotFull && nextStartsWithLargeCallstack) + { + overflowDetected = true; + break; + } + } + + if (overflowDetected) + break; + } + + // Validate all large callstacks in the stream have correct frames. + if (overflowDetected) + { + var largeCallstacks = stream.OfType(AsyncEventID.ResumeAsyncCallstack) + .Where(e => e.FrameCount > 30) + .ToList(); + + foreach (var cs in largeCallstacks) + { + Assert.Equal((int)cs.FrameCount, cs.Frames.Count); + for (int f = 0; f < cs.Frames.Count; f++) + { + var (methodId, _) = cs.Frames[f]; + Assert.True(methodId != 0, $"Overflow callstack frame {f} has zero MethodId"); + + var method = GetMethodNameFromMethodId(cs.CallstackType, methodId); + Assert.True(method is not null, $"Overflow callstack frame {f}: MethodId 0x{methodId:X} does not resolve to a managed method"); + } + } + } + } + + Assert.True(overflowDetected, "Failed to trigger callstack buffer overflow after 10 attempts -- " + + "no consecutive buffer pair found where buffer N has remaining capacity and buffer N+1 starts with a large callstack"); + } + + // Requires threading: + // Deep recursive chains must execute in a single dispatch + // loop (no sync context) to produce chains exceeding the 255-frame cap. + [ConditionalFact(typeof(AsyncProfilerTests), nameof(IsRuntimeAsyncAndThreadingSupported))] + public void RuntimeAsync_CallstackDepthCappedAtMaxFrames() + { + // Verify that callstack depth is capped when the continuation chain + // exceeds the maximum frame count (255, limited by byte storage). + // RuntimeAsync_RecursiveChain(300) produces a 300-deep chain + 1 lambda = 301 frames. + const int requestedDepth = 300; + + var events = CollectEvents(ResumeAsyncCallstackKeyword, () => + { + RunScenarioAndFlush(async () => + { + await RuntimeAsync_RecursiveChain(requestedDepth); + }); + }); + + // DumpAllEvents(events); + + var stream = ParseAllEvents(events); + var callstacks = stream.OfType(AsyncEventID.ResumeAsyncCallstack).ToList(); + Assert.True(callstacks.Count >= 1, "Expected at least one callstack"); + + // Find the callstack from our deep RuntimeAsync_RecursiveChain call. + // The max frame count is capped at 255 (byte.MaxValue) since the + // CaptureRuntimeAsyncCallstackState.Count is a byte. + // RuntimeAsync_RecursiveChain(300) + 1 lambda = 301 frames, capped to 255. + var deepest = callstacks.MaxBy(cs => cs.FrameCount); + Assert.Equal(255, (int)deepest!.FrameCount); + Assert.Equal((int)deepest.FrameCount, deepest.Frames.Count); + + // Verify all frames are valid. + foreach (var (methodId, _) in deepest.Frames) + { + Assert.True(methodId != 0, "Frame has zero MethodId"); + var method = GetMethodNameFromMethodId(deepest.CallstackType, methodId); + Assert.True(method is not null, $"MethodId 0x{methodId:X} does not resolve to a managed method"); + } + } + + [ConditionalFact(typeof(AsyncProfilerTests), nameof(IsRuntimeAsyncSupported))] + public async Task RuntimeAsync_MetadataMatchesWrapperMethods() + { + var events = await CollectEventsAsync(AllKeywords, RuntimeAsync_SingleYield); + + var stream = ParseAllEvents(events); + var metadataList = stream.MetadataEvents; + Assert.True(metadataList.Count >= 1, "Expected at least one metadata event in buffer"); + + MetadataFromBuffer meta = metadataList[0]; + Assert.True(meta.WrapperCount > 0, "Expected positive wrapper count in metadata"); + + // On CoreCLR, verify via reflection that the contract-defined template produces names matching real methods. + // This catches accidental renames of wrapper methods without updating the contract. + if (PlatformDetection.IsCoreCLR) + { + Type? wrapperType = typeof(System.Runtime.CompilerServices.AsyncTaskMethodBuilder) + .Assembly.GetType("System.Runtime.CompilerServices.AsyncProfiler+ContinuationWrapper"); + Assert.NotNull(wrapperType); + for (int i = 0; i < meta.WrapperCount; i++) + { + string expectedName = string.Format(WrapperNameTemplate, i); + var method = wrapperType.GetMethod(expectedName, + System.Reflection.BindingFlags.Static | System.Reflection.BindingFlags.NonPublic | System.Reflection.BindingFlags.Public); + Assert.True(method is not null, $"Expected method '{expectedName}' not found on ContinuationWrapper type"); + } + + // Verify that the wrapper count matches the actual number of wrapper methods on the type. + int actualCount = wrapperType.GetMethods(System.Reflection.BindingFlags.Static | System.Reflection.BindingFlags.NonPublic | System.Reflection.BindingFlags.Public) + .Count(m => m.Name.StartsWith(WrapperNamePrefix, StringComparison.Ordinal)); + Assert.Equal(meta.WrapperCount, actualCount); + } + } + + + [System.Runtime.CompilerServices.RuntimeAsyncMethodGeneration(true)] + [System.Runtime.CompilerServices.MethodImpl(System.Runtime.CompilerServices.MethodImplOptions.NoInlining)] + private static async Task RuntimeAsync_WhenAll_TracksAllBranches_BranchA_Marker() + { + await Task.Yield(); + } + + [System.Runtime.CompilerServices.RuntimeAsyncMethodGeneration(true)] + [System.Runtime.CompilerServices.MethodImpl(System.Runtime.CompilerServices.MethodImplOptions.NoInlining)] + private static async Task RuntimeAsync_WhenAll_TracksAllBranches_BranchB_Marker() + { + await Task.Yield(); + } + + [System.Runtime.CompilerServices.RuntimeAsyncMethodGeneration(true)] + [System.Runtime.CompilerServices.MethodImpl(System.Runtime.CompilerServices.MethodImplOptions.NoInlining)] + private static async Task RuntimeAsync_WhenAll_TracksAllBranches_BranchC_Marker() + { + await Task.Yield(); + } + + [System.Runtime.CompilerServices.RuntimeAsyncMethodGeneration(true)] + [System.Runtime.CompilerServices.MethodImpl(System.Runtime.CompilerServices.MethodImplOptions.NoInlining)] + private static async Task RuntimeAsync_WhenAll_TracksAllBranches_Marker() + { + await Task.WhenAll( + RuntimeAsync_WhenAll_TracksAllBranches_BranchA_Marker(), + RuntimeAsync_WhenAll_TracksAllBranches_BranchB_Marker(), + RuntimeAsync_WhenAll_TracksAllBranches_BranchC_Marker()); + } + + [ConditionalFact(typeof(AsyncProfilerTests), nameof(IsRuntimeAsyncSupported))] + public async Task RuntimeAsync_WhenAll_TracksAllBranches() + { + var events = await CollectEventsAsync(CallstackKeywords, RuntimeAsync_WhenAll_TracksAllBranches_Marker); + + // DumpAllEvents(events); + + var stream = ParseAllEvents(events); + + var markerCallstacks = stream.CallstacksWithMarker(AsyncEventID.ResumeAsyncCallstack, nameof(RuntimeAsync_WhenAll_TracksAllBranches_Marker)); + Assert.NotEmpty(markerCallstacks); + + // Each branch is its own async chain; its inner await of Task.Yield produces a Resume callstack containing the branch frame. + var branchACallstacks = stream.CallstacksWithMarker(AsyncEventID.ResumeAsyncCallstack, nameof(RuntimeAsync_WhenAll_TracksAllBranches_BranchA_Marker)); + var branchBCallstacks = stream.CallstacksWithMarker(AsyncEventID.ResumeAsyncCallstack, nameof(RuntimeAsync_WhenAll_TracksAllBranches_BranchB_Marker)); + var branchCCallstacks = stream.CallstacksWithMarker(AsyncEventID.ResumeAsyncCallstack, nameof(RuntimeAsync_WhenAll_TracksAllBranches_BranchC_Marker)); + Assert.NotEmpty(branchACallstacks); + Assert.NotEmpty(branchBCallstacks); + Assert.NotEmpty(branchCCallstacks); + + // Each tracked chain (3 branches + outer marker) must see exactly one Create and one Complete on its own TaskId. + AssertExactlyOneCreateAndComplete(stream, branchACallstacks[0].TaskId, nameof(RuntimeAsync_WhenAll_TracksAllBranches_BranchA_Marker)); + AssertExactlyOneCreateAndComplete(stream, branchBCallstacks[0].TaskId, nameof(RuntimeAsync_WhenAll_TracksAllBranches_BranchB_Marker)); + AssertExactlyOneCreateAndComplete(stream, branchCCallstacks[0].TaskId, nameof(RuntimeAsync_WhenAll_TracksAllBranches_BranchC_Marker)); + AssertExactlyOneCreateAndComplete(stream, markerCallstacks[0].TaskId, nameof(RuntimeAsync_WhenAll_TracksAllBranches_Marker)); + + // The outer marker's chain should fire Create -> Resume -> Complete in that order. + ulong markerTaskId = markerCallstacks[0].TaskId; + var markerIds = stream.ForTask(markerTaskId).Select(e => e.EventId).ToList(); + + int createIdx = markerIds.IndexOf(AsyncEventID.CreateAsyncContext); + int resumeIdx = markerIds.IndexOf(AsyncEventID.ResumeAsyncContext, createIdx + 1); + Assert.True(resumeIdx > createIdx, "Expected ResumeAsyncContext after Create on the outer marker"); + + int completeIdx = markerIds.IndexOf(AsyncEventID.CompleteAsyncContext, resumeIdx + 1); + Assert.True(completeIdx > resumeIdx, "Expected CompleteAsyncContext after Resume on the outer marker"); + } + + [System.Runtime.CompilerServices.RuntimeAsyncMethodGeneration(true)] + [System.Runtime.CompilerServices.MethodImpl(System.Runtime.CompilerServices.MethodImplOptions.NoInlining)] + private static async Task RuntimeAsync_WhenAny_TracksAllBranches_Fast_Marker() + { + await Task.Yield(); + } + + [System.Runtime.CompilerServices.RuntimeAsyncMethodGeneration(true)] + [System.Runtime.CompilerServices.MethodImpl(System.Runtime.CompilerServices.MethodImplOptions.NoInlining)] + private static async Task RuntimeAsync_WhenAny_TracksAllBranches_Slow1_Marker() + { + await Task.Delay(200); + } + + [System.Runtime.CompilerServices.RuntimeAsyncMethodGeneration(true)] + [System.Runtime.CompilerServices.MethodImpl(System.Runtime.CompilerServices.MethodImplOptions.NoInlining)] + private static async Task RuntimeAsync_WhenAny_TracksAllBranches_Slow2_Marker() + { + await Task.Delay(300); + } + + [System.Runtime.CompilerServices.RuntimeAsyncMethodGeneration(true)] + [System.Runtime.CompilerServices.MethodImpl(System.Runtime.CompilerServices.MethodImplOptions.NoInlining)] + private static async Task RuntimeAsync_WhenAny_TracksAllBranches_Marker() + { + Task fast = RuntimeAsync_WhenAny_TracksAllBranches_Fast_Marker(); + Task slow1 = RuntimeAsync_WhenAny_TracksAllBranches_Slow1_Marker(); + Task slow2 = RuntimeAsync_WhenAny_TracksAllBranches_Slow2_Marker(); + + await Task.WhenAny(fast, slow1, slow2); + await Task.WhenAll(slow1, slow2); + } + + [ConditionalFact(typeof(AsyncProfilerTests), nameof(IsRuntimeAsyncSupported))] + public async Task RuntimeAsync_WhenAny_TracksAllBranches() + { + var events = await CollectEventsAsync(CallstackKeywords, RuntimeAsync_WhenAny_TracksAllBranches_Marker); + + // DumpAllEvents(events); + + var stream = ParseAllEvents(events); + + var markerCallstacks = stream.CallstacksWithMarker(AsyncEventID.ResumeAsyncCallstack, nameof(RuntimeAsync_WhenAny_TracksAllBranches_Marker)); + Assert.NotEmpty(markerCallstacks); + + // All branches - including the slow ones whose completion the outer is no longer + // strictly waiting on after WhenAny returned - must produce their own Resume callstacks. + var fastCallstacks = stream.CallstacksWithMarker(AsyncEventID.ResumeAsyncCallstack, nameof(RuntimeAsync_WhenAny_TracksAllBranches_Fast_Marker)); + var slow1Callstacks = stream.CallstacksWithMarker(AsyncEventID.ResumeAsyncCallstack, nameof(RuntimeAsync_WhenAny_TracksAllBranches_Slow1_Marker)); + var slow2Callstacks = stream.CallstacksWithMarker(AsyncEventID.ResumeAsyncCallstack, nameof(RuntimeAsync_WhenAny_TracksAllBranches_Slow2_Marker)); + Assert.NotEmpty(fastCallstacks); + Assert.NotEmpty(slow1Callstacks); + Assert.NotEmpty(slow2Callstacks); + + // Each tracked chain (3 branches + outer marker) must see exactly one Create and one Complete on its own TaskId. + AssertExactlyOneCreateAndComplete(stream, fastCallstacks[0].TaskId, nameof(RuntimeAsync_WhenAny_TracksAllBranches_Fast_Marker)); + AssertExactlyOneCreateAndComplete(stream, slow1Callstacks[0].TaskId, nameof(RuntimeAsync_WhenAny_TracksAllBranches_Slow1_Marker)); + AssertExactlyOneCreateAndComplete(stream, slow2Callstacks[0].TaskId, nameof(RuntimeAsync_WhenAny_TracksAllBranches_Slow2_Marker)); + AssertExactlyOneCreateAndComplete(stream, markerCallstacks[0].TaskId, nameof(RuntimeAsync_WhenAny_TracksAllBranches_Marker)); + + // The outer marker should also be resumed at least once. + ulong markerTaskId = markerCallstacks[0].TaskId; + var markerIds = stream.ForTask(markerTaskId).Select(e => e.EventId).ToList(); + int resumeCountForMarker = markerIds.Count(id => id == AsyncEventID.ResumeAsyncContext); + Assert.True(resumeCountForMarker >= 1, $"Expected outer marker to be resumed at least once, got {resumeCountForMarker}"); + } + + [System.Runtime.CompilerServices.RuntimeAsyncMethodGeneration(true)] + [System.Runtime.CompilerServices.MethodImpl(System.Runtime.CompilerServices.MethodImplOptions.NoInlining)] + private static async Task RuntimeAsync_ConfigureAwaitFalse_Leaf_Marker() + { + await Task.Yield(); + } + + [System.Runtime.CompilerServices.RuntimeAsyncMethodGeneration(true)] + [System.Runtime.CompilerServices.MethodImpl(System.Runtime.CompilerServices.MethodImplOptions.NoInlining)] + private static async Task RuntimeAsync_ConfigureAwaitFalse_Mid_Marker() + { + await RuntimeAsync_ConfigureAwaitFalse_Leaf_Marker().ConfigureAwait(false); + } + + [System.Runtime.CompilerServices.RuntimeAsyncMethodGeneration(true)] + [System.Runtime.CompilerServices.MethodImpl(System.Runtime.CompilerServices.MethodImplOptions.NoInlining)] + private static async Task RuntimeAsync_ConfigureAwaitFalse_Marker() + { + await RuntimeAsync_ConfigureAwaitFalse_Mid_Marker().ConfigureAwait(false); + } + + [ConditionalFact(typeof(AsyncProfilerTests), nameof(IsRuntimeAsyncSupported))] + public async Task RuntimeAsync_ConfigureAwaitFalse() + { + var events = await CollectEventsAsync(CallstackKeywords, RuntimeAsync_ConfigureAwaitFalse_Marker); + + // DumpAllEvents(events); + + var stream = ParseAllEvents(events); + + var markerCallstacks = stream.CallstacksWithMarker(AsyncEventID.ResumeAsyncCallstack, nameof(RuntimeAsync_ConfigureAwaitFalse_Marker)); + Assert.NotEmpty(markerCallstacks); + + var frameNames = markerCallstacks[0].Frames + .Select(f => GetMethodNameFromMethodId(markerCallstacks[0].CallstackType, f.MethodId)) + .Where(n => n is not null) + .ToList(); + + Assert.Contains(nameof(RuntimeAsync_ConfigureAwaitFalse_Leaf_Marker), frameNames); + Assert.Contains(nameof(RuntimeAsync_ConfigureAwaitFalse_Mid_Marker), frameNames); + Assert.Contains(nameof(RuntimeAsync_ConfigureAwaitFalse_Marker), frameNames); + + // ConfigureAwait(false) on a sequential await chain collapses Leaf -> Mid -> Marker into one + // continuation chain, so exactly one Create / one Complete is expected on the marker's TaskId. + AssertExactlyOneCreateAndComplete(stream, markerCallstacks[0].TaskId, nameof(RuntimeAsync_ConfigureAwaitFalse_Marker)); + } + + [System.Runtime.CompilerServices.RuntimeAsyncMethodGeneration(true)] + [System.Runtime.CompilerServices.MethodImpl(System.Runtime.CompilerServices.MethodImplOptions.NoInlining)] + private static async Task RuntimeAsync_TaskCancellation_Inner_Marker(CancellationToken ct) + { + await Task.Delay(5000, ct); + } + + [System.Runtime.CompilerServices.RuntimeAsyncMethodGeneration(true)] + [System.Runtime.CompilerServices.MethodImpl(System.Runtime.CompilerServices.MethodImplOptions.NoInlining)] + private static async Task RuntimeAsync_TaskCancellation_Marker() + { + using var cts = new CancellationTokenSource(); + Task inner = RuntimeAsync_TaskCancellation_Inner_Marker(cts.Token); + cts.CancelAfter(50); + try + { + await inner; + } + catch (OperationCanceledException) + { + } + } + + [ConditionalFact(typeof(AsyncProfilerTests), nameof(IsRuntimeAsyncSupported))] + public async Task RuntimeAsync_TaskCancellation() + { + var events = await CollectEventsAsync(CallstackKeywords, RuntimeAsync_TaskCancellation_Marker); + + // DumpAllEvents(events); + + var stream = ParseAllEvents(events); + + var markerCallstacks = stream.CallstacksWithMarker(AsyncEventID.ResumeAsyncCallstack, nameof(RuntimeAsync_TaskCancellation_Marker)); + Assert.NotEmpty(markerCallstacks); + + var innerCallstacks = stream.CallstacksWithMarker(AsyncEventID.ResumeAsyncCallstack, nameof(RuntimeAsync_TaskCancellation_Inner_Marker)); + Assert.NotEmpty(innerCallstacks); + + // Inner cancelled task + outer marker must each see exactly one Create and one Complete on their own TaskId. + AssertExactlyOneCreateAndComplete(stream, innerCallstacks[0].TaskId, nameof(RuntimeAsync_TaskCancellation_Inner_Marker)); + AssertExactlyOneCreateAndComplete(stream, markerCallstacks[0].TaskId, nameof(RuntimeAsync_TaskCancellation_Marker)); + } + + private static InlinePostSynchronizationContext? s_runtimeAsyncSyncContextCtx; + + [RuntimeAsyncMethodGeneration(true)] + [MethodImpl(MethodImplOptions.NoInlining)] + private static async Task RuntimeAsync_CustomSyncContext_EmitsContextEventsAndCallstack_Marker() + { + // Install a non-default SynchronizationContext on this thread so the await captures it. + // The await's continuation will be routed via SynchronizationContextAwaitTaskContinuation, + // which the V2 runtime-async dispatch loop should honor when resuming the chain. + int callerThreadId = Environment.CurrentManagedThreadId; + SynchronizationContext? prev = SynchronizationContext.Current; + SynchronizationContext.SetSynchronizationContext(s_runtimeAsyncSyncContextCtx); + try + { + await Task.Delay(100); + } + finally + { + if (Environment.CurrentManagedThreadId == callerThreadId) + { + SynchronizationContext.SetSynchronizationContext(prev); + } + } + } + + [ConditionalFact(typeof(AsyncProfilerTests), nameof(IsRuntimeAsyncSupported))] + public async Task RuntimeAsync_CustomSyncContext_EmitsContextEventsAndCallstack() + { + s_runtimeAsyncSyncContextCtx = new InlinePostSynchronizationContext(); + + var events = await CollectEventsAsync(CallstackKeywords, RuntimeAsync_CustomSyncContext_EmitsContextEventsAndCallstack_Marker); + + // DumpAllEvents(events); + + // The custom SyncContext should have received at least one Post for the await continuation. + Assert.True(s_runtimeAsyncSyncContextCtx.PostCount > 0, + $"Expected custom SynchronizationContext to receive at least one Post, got {s_runtimeAsyncSyncContextCtx.PostCount}"); + + var stream = ParseAllEvents(events); + + // The marker frame should appear in the Resume callstack. + var markerCallstacks = stream.CallstacksWithMarker(AsyncEventID.ResumeAsyncCallstack, nameof(RuntimeAsync_CustomSyncContext_EmitsContextEventsAndCallstack_Marker)); + Assert.NotEmpty(markerCallstacks); + + // The marker's chain should see exactly one Create and one Complete on its own TaskId. + AssertExactlyOneCreateAndComplete(stream, markerCallstacks[0].TaskId, nameof(RuntimeAsync_CustomSyncContext_EmitsContextEventsAndCallstack_Marker)); + + // Verify the standard Create -> Resume -> Complete sequence fired in order for our context. + ulong taskId = markerCallstacks[0].TaskId; + var ids = stream.ForTask(taskId).Select(e => e.EventId).ToList(); + + int createIdx = ids.IndexOf(AsyncEventID.CreateAsyncContext); + int resumeIdx = ids.IndexOf(AsyncEventID.ResumeAsyncContext, createIdx + 1); + Assert.True(resumeIdx > createIdx, "Expected ResumeAsyncContext after Create"); + + int completeIdx = ids.IndexOf(AsyncEventID.CompleteAsyncContext, resumeIdx + 1); + Assert.True(completeIdx > resumeIdx, "Expected CompleteAsyncContext after Resume"); + } + + [RuntimeAsyncMethodGeneration(true)] + [MethodImpl(MethodImplOptions.NoInlining)] + private static async Task RuntimeAsync_CustomTaskScheduler_EmitsContextEventsAndCallstack_Marker() + { + await Task.Delay(100); + } + + [ConditionalFact(typeof(AsyncProfilerTests), nameof(IsRuntimeAsyncSupported))] + public async Task RuntimeAsync_CustomTaskScheduler_EmitsContextEventsAndCallstack() + { + var scheduler = new InlineRunTaskScheduler(); + + var events = await CollectEventsAsync(CallstackKeywords, async () => + { + // Start the marker on the custom scheduler so the resulting Task is queued through it. + await Task.Factory.StartNew( + () => RuntimeAsync_CustomTaskScheduler_EmitsContextEventsAndCallstack_Marker(), + CancellationToken.None, + TaskCreationOptions.None, + scheduler).Unwrap(); + }); + + // DumpAllEvents(events); + + // The custom scheduler must have received at least one QueueTask call. + Assert.True(scheduler.QueuedCount >= 1, + $"Expected custom TaskScheduler to receive at least one QueueTask call, got {scheduler.QueuedCount}"); + + var stream = ParseAllEvents(events); + + var markerCallstacks = stream.CallstacksWithMarker(AsyncEventID.ResumeAsyncCallstack, nameof(RuntimeAsync_CustomTaskScheduler_EmitsContextEventsAndCallstack_Marker)); + Assert.NotEmpty(markerCallstacks); + + // The marker's chain should see exactly one Create and one Complete on its own TaskId. + AssertExactlyOneCreateAndComplete(stream, markerCallstacks[0].TaskId, nameof(RuntimeAsync_CustomTaskScheduler_EmitsContextEventsAndCallstack_Marker)); + + // Verify the standard Create -> Resume -> Complete sequence fired in order for our context. + ulong taskId = markerCallstacks[0].TaskId; + var ids = stream.ForTask(taskId).Select(e => e.EventId).ToList(); + + int createIdx = ids.IndexOf(AsyncEventID.CreateAsyncContext); + int resumeIdx = ids.IndexOf(AsyncEventID.ResumeAsyncContext, createIdx + 1); + Assert.True(resumeIdx > createIdx, "Expected ResumeAsyncContext after Create"); + + int completeIdx = ids.IndexOf(AsyncEventID.CompleteAsyncContext, resumeIdx + 1); + Assert.True(completeIdx > resumeIdx, "Expected CompleteAsyncContext after Resume"); + } + + [System.Runtime.CompilerServices.RuntimeAsyncMethodGeneration(true)] + [System.Runtime.CompilerServices.MethodImpl(System.Runtime.CompilerServices.MethodImplOptions.NoInlining)] + private static async ValueTask RuntimeAsync_ValueTask_EventSequenceOrder_Marker() + { + await RuntimeAsync_ValueTask_Level1(); + } + + [ConditionalFact(typeof(AsyncProfilerTests), nameof(IsRuntimeAsyncSupported))] + public async Task RuntimeAsync_ValueTask_EventSequenceOrder() + { + var events = await CollectEventsAsync(CallstackKeywords, async () => await RuntimeAsync_ValueTask_EventSequenceOrder_Marker()); + + // DumpAllEvents(events); + + var stream = ParseAllEvents(events); + + var markerCallstacks = stream.CallstacksWithMarker(AsyncEventID.ResumeAsyncCallstack, nameof(RuntimeAsync_ValueTask_EventSequenceOrder_Marker)); + Assert.NotEmpty(markerCallstacks); + + ulong taskId = markerCallstacks[0].TaskId; + var ids = stream.ForTask(taskId).Select(e => e.EventId).ToList(); + + int createIdx = ids.IndexOf(AsyncEventID.CreateAsyncContext); + Assert.True(createIdx >= 0, "Expected CreateAsyncContext"); + + int resumeIdx = ids.IndexOf(AsyncEventID.ResumeAsyncContext, createIdx + 1); + Assert.True(resumeIdx > createIdx, "Expected ResumeAsyncContext after Create"); + + int completeIdx = ids.IndexOf(AsyncEventID.CompleteAsyncContext, resumeIdx + 1); + Assert.True(completeIdx > resumeIdx, "Expected CompleteAsyncContext after Resume"); + } + + [System.Runtime.CompilerServices.RuntimeAsyncMethodGeneration(true)] + [System.Runtime.CompilerServices.MethodImpl(System.Runtime.CompilerServices.MethodImplOptions.NoInlining)] + private static async ValueTask RuntimeAsync_ValueTask_MethodEventsEmitted_Marker() + { + await RuntimeAsync_ValueTask_Level1(); + } + + [ConditionalFact(typeof(AsyncProfilerTests), nameof(IsRuntimeAsyncSupported))] + public async Task RuntimeAsync_ValueTask_MethodEventsEmitted() + { + var events = await CollectEventsAsync(MethodKeywords | CoreKeywords, async () => await RuntimeAsync_ValueTask_MethodEventsEmitted_Marker()); + + // DumpAllEvents(events); + + var stream = ParseAllEvents(events); + + var methodEvents = stream.All + .Where(e => e.EventId is AsyncEventID.ResumeAsyncMethod or AsyncEventID.CompleteAsyncMethod) + .Select(e => e.EventId) + .ToList(); + + int resumeCount = methodEvents.Count(id => id == AsyncEventID.ResumeAsyncMethod); + int completeCount = methodEvents.Count(id => id == AsyncEventID.CompleteAsyncMethod); + + // Marker -> Level1 -> Level2 -> Level3 + Assert.True(resumeCount >= 4, $"Expected at least 4 ResumeAsyncMethod events for ValueTask chain, got {resumeCount}"); + Assert.True(completeCount >= 4, $"Expected at least 4 CompleteAsyncMethod events for ValueTask chain, got {completeCount}"); + } + + [System.Runtime.CompilerServices.RuntimeAsyncMethodGeneration(true)] + [System.Runtime.CompilerServices.MethodImpl(System.Runtime.CompilerServices.MethodImplOptions.NoInlining)] + private static async ValueTask RuntimeAsync_ValueTask_CallstackDepthMatchesChainDepth_Marker() + { + await RuntimeAsync_ValueTask_Level1(); + } + + [ConditionalFact(typeof(AsyncProfilerTests), nameof(IsRuntimeAsyncSupported))] + public async Task RuntimeAsync_ValueTask_CallstackDepthMatchesChainDepth() + { + var events = await CollectEventsAsync(CallstackKeywords, async () => await RuntimeAsync_ValueTask_CallstackDepthMatchesChainDepth_Marker()); + + // DumpAllEvents(events); + + var stream = ParseAllEvents(events); + + var markerCallstacks = stream.CallstacksWithMarker(AsyncEventID.ResumeAsyncCallstack, nameof(RuntimeAsync_ValueTask_CallstackDepthMatchesChainDepth_Marker)); + Assert.NotEmpty(markerCallstacks); + + // Async lambda -> Marker -> Level1 -> Level2 -> Level3. + Assert.Equal(5, markerCallstacks[0].FrameCount); + } + + [System.Runtime.CompilerServices.RuntimeAsyncMethodGeneration(true)] + [System.Runtime.CompilerServices.MethodImpl(System.Runtime.CompilerServices.MethodImplOptions.NoInlining)] + private static async ValueTask RuntimeAsync_ValueTask_CallstackFramesHaveDistinctMethodIds_Marker() + { + await RuntimeAsync_ValueTask_Level1(); + } + + [ConditionalFact(typeof(AsyncProfilerTests), nameof(IsRuntimeAsyncSupported))] + public async Task RuntimeAsync_ValueTask_CallstackFramesHaveDistinctMethodIds() + { + var events = await CollectEventsAsync(CallstackKeywords, async () => await RuntimeAsync_ValueTask_CallstackFramesHaveDistinctMethodIds_Marker()); + + // DumpAllEvents(events); + + var stream = ParseAllEvents(events); + + var markerCallstacks = stream.CallstacksWithMarker(AsyncEventID.ResumeAsyncCallstack, nameof(RuntimeAsync_ValueTask_CallstackFramesHaveDistinctMethodIds_Marker)); + Assert.NotEmpty(markerCallstacks); + + var methodIds = markerCallstacks[0].Frames.Select(f => f.MethodId).ToList(); + Assert.Equal(methodIds.Count, methodIds.Distinct().Count()); + } + + [System.Runtime.CompilerServices.RuntimeAsyncMethodGeneration(true)] + [System.Runtime.CompilerServices.MethodImpl(System.Runtime.CompilerServices.MethodImplOptions.NoInlining)] + private static async ValueTask RuntimeAsync_ValueTask_HandledException_InnerThrows_Marker() + { + await Task.Yield(); + throw new InvalidOperationException("valuetask inner throw"); + } + + [System.Runtime.CompilerServices.RuntimeAsyncMethodGeneration(true)] + [System.Runtime.CompilerServices.MethodImpl(System.Runtime.CompilerServices.MethodImplOptions.NoInlining)] + private static async ValueTask RuntimeAsync_ValueTask_HandledException_Handled_Marker() + { + try + { + await RuntimeAsync_ValueTask_HandledException_InnerThrows_Marker(); + } + catch (InvalidOperationException) + { + } + } + + [System.Runtime.CompilerServices.RuntimeAsyncMethodGeneration(true)] + [System.Runtime.CompilerServices.MethodImpl(System.Runtime.CompilerServices.MethodImplOptions.NoInlining)] + private static async ValueTask RuntimeAsync_ValueTask_HandledException_EmitsUnwindAndComplete_Marker() + { + await RuntimeAsync_ValueTask_HandledException_Handled_Marker(); + } + + [ConditionalFact(typeof(AsyncProfilerTests), nameof(IsRuntimeAsyncSupported))] + public async Task RuntimeAsync_ValueTask_HandledException_EmitsUnwindAndComplete() + { + var events = await CollectEventsAsync(CallstackKeywords | UnwindAsyncExceptionKeyword, async () => await RuntimeAsync_ValueTask_HandledException_EmitsUnwindAndComplete_Marker()); + + // DumpAllEvents(events); + + var stream = ParseAllEvents(events); + + var markerCallstacks = stream.CallstacksWithMarker(AsyncEventID.ResumeAsyncCallstack, nameof(RuntimeAsync_ValueTask_HandledException_EmitsUnwindAndComplete_Marker)); + Assert.NotEmpty(markerCallstacks); + + ulong taskId = markerCallstacks[0].TaskId; + var ids = stream.ForTask(taskId).Select(e => e.EventId).ToList(); + + int createIdx = ids.IndexOf(AsyncEventID.CreateAsyncContext); + Assert.True(createIdx >= 0, "Expected CreateAsyncContext"); + + int resumeIdx = ids.IndexOf(AsyncEventID.ResumeAsyncContext, createIdx + 1); + Assert.True(resumeIdx > createIdx, "Expected ResumeAsyncContext after Create"); + + int unwindIdx = ids.IndexOf(AsyncEventID.UnwindAsyncException, resumeIdx + 1); + Assert.True(unwindIdx > resumeIdx, "Expected UnwindAsyncException after Resume"); + + int completeIdx = ids.IndexOf(AsyncEventID.CompleteAsyncContext, unwindIdx + 1); + Assert.True(completeIdx > unwindIdx, "Expected CompleteAsyncContext after Unwind"); + } + + [System.Runtime.CompilerServices.RuntimeAsyncMethodGeneration(true)] + [System.Runtime.CompilerServices.MethodImpl(System.Runtime.CompilerServices.MethodImplOptions.NoInlining)] + private static async ValueTask RuntimeAsync_ValueTask_UnhandledException_UnhandledOuter_Marker() + { + await RuntimeAsync_ValueTask_UnhandledException_UnhandledInner_Marker(); + } + + [System.Runtime.CompilerServices.RuntimeAsyncMethodGeneration(true)] + [System.Runtime.CompilerServices.MethodImpl(System.Runtime.CompilerServices.MethodImplOptions.NoInlining)] + private static async ValueTask RuntimeAsync_ValueTask_UnhandledException_UnhandledInner_Marker() + { + await Task.Yield(); + throw new InvalidOperationException("valuetask unhandled inner"); + } + + [System.Runtime.CompilerServices.RuntimeAsyncMethodGeneration(true)] + [System.Runtime.CompilerServices.MethodImpl(System.Runtime.CompilerServices.MethodImplOptions.NoInlining)] + private static async ValueTask RuntimeAsync_ValueTask_UnhandledException_EmitsUnwindAndComplete_Marker() + { + await RuntimeAsync_ValueTask_UnhandledException_UnhandledOuter_Marker(); + } + + [ConditionalFact(typeof(AsyncProfilerTests), nameof(IsRuntimeAsyncSupported))] + public async Task RuntimeAsync_ValueTask_UnhandledException_EmitsUnwindAndComplete() + { + var events = await CollectEventsAsync(CallstackKeywords | UnwindAsyncExceptionKeyword, async () => + { + try + { + await RuntimeAsync_ValueTask_UnhandledException_EmitsUnwindAndComplete_Marker(); + } + catch (InvalidOperationException) + { + } + }); + + // DumpAllEvents(events); + + var stream = ParseAllEvents(events); + + var markerCallstacks = stream.CallstacksWithMarker(AsyncEventID.ResumeAsyncCallstack, nameof(RuntimeAsync_ValueTask_UnhandledException_EmitsUnwindAndComplete_Marker)); + Assert.NotEmpty(markerCallstacks); + + ulong taskId = markerCallstacks[0].TaskId; + var ids = stream.ForTask(taskId).Select(e => e.EventId).ToList(); + + int createIdx = ids.IndexOf(AsyncEventID.CreateAsyncContext); + Assert.True(createIdx >= 0, "Expected CreateAsyncContext"); + + int resumeIdx = ids.IndexOf(AsyncEventID.ResumeAsyncContext, createIdx + 1); + Assert.True(resumeIdx > createIdx, "Expected ResumeAsyncContext after Create"); + + int unwindIdx = ids.IndexOf(AsyncEventID.UnwindAsyncException, resumeIdx + 1); + Assert.True(unwindIdx > resumeIdx, "Expected UnwindAsyncException after Resume"); + + int completeIdx = ids.IndexOf(AsyncEventID.CompleteAsyncContext, unwindIdx + 1); + Assert.True(completeIdx > unwindIdx, "Expected CompleteAsyncContext after Unwind"); + } + } +} 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 84894a3857d957..d1680d8f7b2a61 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 @@ -61,6 +61,8 @@ + +