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
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