From 3b38304ef51d900f07d338306a0e777ff66c9a4f Mon Sep 17 00:00:00 2001 From: lateralusX Date: Wed, 24 Jun 2026 16:25:27 +0200 Subject: [PATCH 1/2] Add async profiler v1 support to pooled async value tasks. --- .../Runtime/CompilerServices/AsyncProfiler.cs | 27 ++-------- .../AsyncStateMachineDispatcher.cs | 54 ++++++++++++------- .../AsyncTaskMethodBuilderT.cs | 11 +--- .../ConfiguredValueTaskAwaitable.cs | 4 +- .../PoolingAsyncValueTaskMethodBuilderT.cs | 43 +++++++++++++-- .../CompilerServices/ValueTaskAwaiter.cs | 4 +- .../Sources/ManualResetValueTaskSourceCore.cs | 3 ++ 7 files changed, 89 insertions(+), 57 deletions(-) diff --git a/src/libraries/System.Private.CoreLib/src/System/Runtime/CompilerServices/AsyncProfiler.cs b/src/libraries/System.Private.CoreLib/src/System/Runtime/CompilerServices/AsyncProfiler.cs index ee1b064836140a..3b508fbf90518d 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 @@ -155,6 +155,7 @@ internal ref struct Info { public object? Context; public object? CurrentContinuation; + public bool CurrentContinuationCompleted; public ref nint ContinuationTable; public uint ContinuationIndex; } @@ -163,6 +164,7 @@ internal static void InitInfo(ref Info info) { info.Context = null; info.CurrentContinuation = null; + info.CurrentContinuationCompleted = false; ContinuationWrapper.InitInfo(ref info); } @@ -1015,7 +1017,7 @@ public static void Append(AsyncStateMachineDispatcher dispatcher, AsyncThreadCon { if (IsEnabled.ResumeStateMachineAsyncCallstackEvent(context.ActiveEventKeywords) && dispatcher.ContinuationChainChanged) { - AsyncCallstack.EmitEvent(dispatcher, context, dispatcher.LastContinuation?.ContinuationForDiagnostics, currentTimestamp, AsyncEventID.AppendStateMachineAsyncCallstack, DispatcherIds.GetDispatcherId(dispatcher)); + AsyncCallstack.EmitEvent(dispatcher, context, dispatcher.NextContinuationForDiagnostics, currentTimestamp, AsyncEventID.AppendStateMachineAsyncCallstack, DispatcherIds.GetDispatcherId(dispatcher)); } } @@ -1607,17 +1609,7 @@ public static void EmitEvent(ref AsyncStateMachineDispatcherInfo info, AsyncThre EmitAsyncCallstack(context, currentTimestamp, currentTimestamp - context.LastEventTimestamp, AsyncEventID.ResumeStateMachineAsyncCallstack, 0, dispatcherId, ref state); - IAsyncStateMachineBox? last = IsTruncated(in state) ? null : ResolveAsyncStateMachineBox(state.LastContinuation); - if (last != null) - { - Debug.Assert(last is Task); - info.Dispatcher.LastContinuation = Unsafe.As(last); - } - else - { - info.Dispatcher.LastContinuation = null; - } - + info.Dispatcher.LastContinuation = IsTruncated(in state) ? null : ResolveAsyncStateMachineBox(state.LastContinuation); info.Dispatcher.ReachedLastContinuation = false; } @@ -1635,16 +1627,7 @@ public static void EmitEvent(AsyncStateMachineDispatcher dispatcher, AsyncThread EmitAsyncCallstack(context, currentTimestamp, currentTimestamp - context.LastEventTimestamp, eventID, 0, dispatcherId, ref state); - box = IsTruncated(in state) ? null : ResolveAsyncStateMachineBox(state.LastContinuation); - if (box != null) - { - Debug.Assert(box is Task); - dispatcher.LastContinuation = Unsafe.As(box); - } - else - { - dispatcher.LastContinuation = null; - } + dispatcher.LastContinuation = IsTruncated(in state) ? null : ResolveAsyncStateMachineBox(state.LastContinuation); } else { diff --git a/src/libraries/System.Private.CoreLib/src/System/Runtime/CompilerServices/AsyncStateMachineDispatcher.cs b/src/libraries/System.Private.CoreLib/src/System/Runtime/CompilerServices/AsyncStateMachineDispatcher.cs index f0d5762a73c293..67dc9e52962db0 100644 --- a/src/libraries/System.Private.CoreLib/src/System/Runtime/CompilerServices/AsyncStateMachineDispatcher.cs +++ b/src/libraries/System.Private.CoreLib/src/System/Runtime/CompilerServices/AsyncStateMachineDispatcher.cs @@ -93,12 +93,17 @@ internal static unsafe AsyncStateMachineDispatcher CreateDispatcher(IAsyncStateM return dispatcher; } - internal static unsafe void UnwindAsyncFrame() + internal static unsafe void UnwindAsyncFrame(AsyncInstrumentation.Flags flags) { AsyncStateMachineDispatcherInfo* info = t_current; if (info != null) { - AsyncProfiler.AsyncMethodException.UnwindFrames(ref *info, 1); + info->AsyncProfilerInfo.CurrentContinuationCompleted = true; + + if (AsyncInstrumentation.IsEnabled.UnwindAsyncException(flags)) + { + AsyncProfiler.AsyncMethodException.UnwindFrames(ref *info, 1); + } } } @@ -113,6 +118,7 @@ internal static unsafe void ResumeAsyncMethod(IAsyncStateMachineBox box, AsyncIn } info->AsyncProfilerInfo.CurrentContinuation = box; + info->AsyncProfilerInfo.CurrentContinuationCompleted = false; AsyncProfiler.SyncPoint.Check(ref info->AsyncProfilerInfo); @@ -142,12 +148,17 @@ private static unsafe void ResumeAsyncMethod(AsyncStateMachineDispatcher activeD } [MethodImpl(MethodImplOptions.AggressiveInlining)] - internal static unsafe void CompleteAsyncMethod() + internal static unsafe void CompleteAsyncMethod(AsyncInstrumentation.Flags flags) { AsyncStateMachineDispatcherInfo* info = t_current; if (info != null) { - AsyncProfiler.CompleteAsyncMethod.Complete(ref *info); + info->AsyncProfilerInfo.CurrentContinuationCompleted = true; + + if (AsyncInstrumentation.IsEnabled.CompleteAsyncMethod(flags)) + { + AsyncProfiler.CompleteAsyncMethod.Complete(ref *info); + } } } } @@ -157,11 +168,25 @@ internal sealed class AsyncStateMachineDispatcher : Task, IAsync private IAsyncStateMachineBox? _inner; private Action? _moveNextAction; - internal Task? LastContinuation; + internal IAsyncStateMachineBox? LastContinuation; internal bool ReachedLastContinuation; - internal bool ContinuationChainChanged => LastContinuation?.ContinuationForDiagnostics != null; + internal object? NextContinuationForDiagnostics + { + get + { + IAsyncStateMachineBox? last = LastContinuation; + if (last is Task task) + { + return task.ContinuationForDiagnostics; + } + + return last is not null && last.GetDiagnosticData(out _, out _, out object? next) ? next : null; + } + } + + internal bool ContinuationChainChanged => NextContinuationForDiagnostics != null; internal AsyncStateMachineDispatcher(IAsyncStateMachineBox inner) : base() { @@ -245,21 +270,14 @@ private void InstrumentedMoveNext(ref AsyncStateMachineDispatcherInfo info, IAsy } finally { - if (info.AsyncProfilerInfo.CurrentContinuation is Task curContinuation) + bool isCompleted = info.AsyncProfilerInfo.CurrentContinuationCompleted; + if (AsyncInstrumentation.IsEnabled.CompleteAsyncContext(flags) && isCompleted) { - bool isCompleted = curContinuation.IsCompleted; - if (AsyncInstrumentation.IsEnabled.CompleteAsyncContext(flags) && isCompleted) - { - AsyncProfiler.CompleteAsyncContext.Complete(this, ref info.AsyncProfilerInfo); - } - else if (AsyncInstrumentation.IsEnabled.SuspendAsyncContext(flags) && !isCompleted) - { - AsyncProfiler.SuspendAsyncContext.Suspend(this, ref info.AsyncProfilerInfo); - } + AsyncProfiler.CompleteAsyncContext.Complete(this, ref info.AsyncProfilerInfo); } - else if (AsyncInstrumentation.IsEnabled.CompleteAsyncContext(flags)) + else if (AsyncInstrumentation.IsEnabled.SuspendAsyncContext(flags) && !isCompleted) { - AsyncProfiler.CompleteAsyncContext.Complete(this, ref info.AsyncProfilerInfo); + AsyncProfiler.SuspendAsyncContext.Suspend(this, ref info.AsyncProfilerInfo); } } } 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 f9105795c66647..46c8eefcd475a3 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 @@ -442,7 +442,6 @@ bool IAsyncStateMachineBox.GetDiagnosticData(out ulong methodId, out int state, nextContinuation = null; return false; } - } /// Gets the for this builder. @@ -510,10 +509,7 @@ internal static void SetExistingTaskResult(Task task, TResult? result) if (AsyncStateMachineDispatcherInfo.AsyncProfilerInstrumentCheckPoint) { - if (AsyncInstrumentation.IsEnabled.CompleteAsyncMethod(AsyncInstrumentation.ActiveFlags)) - { - AsyncStateMachineDispatcherInfo.CompleteAsyncMethod(); - } + AsyncStateMachineDispatcherInfo.CompleteAsyncMethod(AsyncInstrumentation.ActiveFlags); } if (TplEventSource.Log.IsEnabled()) @@ -548,10 +544,7 @@ internal static void SetException(Exception exception, ref Task? taskFi if (AsyncStateMachineDispatcherInfo.AsyncProfilerInstrumentCheckPoint) { - if (AsyncInstrumentation.IsEnabled.UnwindAsyncException(AsyncInstrumentation.ActiveFlags)) - { - AsyncStateMachineDispatcherInfo.UnwindAsyncFrame(); - } + AsyncStateMachineDispatcherInfo.UnwindAsyncFrame(AsyncInstrumentation.ActiveFlags); } // If the exception represents cancellation, cancel the task. Otherwise, fault the task. 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 1f7b64b55c0d67..4a85092ec0c0be 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,7 +102,7 @@ void IStateMachineBoxAwareAwaiter.AwaitUnsafeOnCompleted(IAsyncStateMachineBox b } else if (obj != null) { - if (AsyncStateMachineDispatcherInfo.AsyncProfilerInstrumentCheckPoint) + if (AsyncStateMachineDispatcherInfo.AsyncProfilerInstrumentCheckPoint && obj is not IAsyncStateMachineBox) { box = AsyncStateMachineDispatcherInfo.CreateDispatcher(box); } @@ -212,7 +212,7 @@ void IStateMachineBoxAwareAwaiter.AwaitUnsafeOnCompleted(IAsyncStateMachineBox b } else if (obj != null) { - if (AsyncStateMachineDispatcherInfo.AsyncProfilerInstrumentCheckPoint) + if (AsyncStateMachineDispatcherInfo.AsyncProfilerInstrumentCheckPoint && obj is not IAsyncStateMachineBox) { box = AsyncStateMachineDispatcherInfo.CreateDispatcher(box); } 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 6cc757078054d5..af8eac56a1f7ef 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 @@ -245,13 +245,27 @@ internal abstract class StateMachineBox : IValueTaskSource, IValueTaskS /// Completes the box with a result. /// The result. - public void SetResult(TResult result) => + public void SetResult(TResult result) + { + if (AsyncStateMachineDispatcherInfo.AsyncProfilerInstrumentCheckPoint) + { + AsyncStateMachineDispatcherInfo.CompleteAsyncMethod(AsyncInstrumentation.ActiveFlags); + } + _valueTaskSource.SetResult(result); + } /// Completes the box with an error. /// The exception. - public void SetException(Exception error) => + public void SetException(Exception error) + { + if (AsyncStateMachineDispatcherInfo.AsyncProfilerInstrumentCheckPoint) + { + AsyncStateMachineDispatcherInfo.UnwindAsyncFrame(AsyncInstrumentation.ActiveFlags); + } + _valueTaskSource.SetException(error); + } /// Gets the status of the box. public ValueTaskSourceStatus GetStatus(short token) => _valueTaskSource.GetStatus(token); @@ -401,6 +415,11 @@ private static void ExecutionContextCallback(object? s) /// Calls MoveNext on public void MoveNext() { + if (AsyncStateMachineDispatcherInfo.AsyncProfilerInstrumentCheckPoint) + { + AsyncStateMachineDispatcherInfo.ResumeAsyncMethod(this, AsyncInstrumentation.ActiveFlags); + } + ExecutionContext? context = Context; if (context == ExecutionContext.DefaultFlowSuppressed) @@ -440,17 +459,33 @@ void IValueTaskSource.GetResult(short token) } } - /// Gets the state machine as a boxed object. This should only be used for debugging purposes. + /// 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. + if (AsyncStateMachineDispatcherInfo.InstrumentCheckPoint) + { + methodId = AsyncStateMachineDiagnostics.MethodId; + state = AsyncStateMachineDiagnostics.GetState(ref StateMachine); + nextContinuation = ContinuationForDiagnostics; + return true; + } + methodId = 0; state = -1; nextContinuation = null; return false; } + + private object? ContinuationForDiagnostics + { + get + { + object? continuation = _valueTaskSource.ContinuationForDiagnostics; + return ReferenceEquals(continuation, this) ? null : continuation; + } + } } } } 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 b2a5b9dd279da5..7d90d1379f0187 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,7 +94,7 @@ void IStateMachineBoxAwareAwaiter.AwaitUnsafeOnCompleted(IAsyncStateMachineBox b } else if (obj != null) { - if (AsyncStateMachineDispatcherInfo.AsyncProfilerInstrumentCheckPoint) + if (AsyncStateMachineDispatcherInfo.AsyncProfilerInstrumentCheckPoint && obj is not IAsyncStateMachineBox) { box = AsyncStateMachineDispatcherInfo.CreateDispatcher(box); } @@ -181,7 +181,7 @@ void IStateMachineBoxAwareAwaiter.AwaitUnsafeOnCompleted(IAsyncStateMachineBox b } else if (obj != null) { - if (AsyncStateMachineDispatcherInfo.AsyncProfilerInstrumentCheckPoint) + if (AsyncStateMachineDispatcherInfo.AsyncProfilerInstrumentCheckPoint && obj is not IAsyncStateMachineBox) { box = AsyncStateMachineDispatcherInfo.CreateDispatcher(box); } diff --git a/src/libraries/System.Private.CoreLib/src/System/Threading/Tasks/Sources/ManualResetValueTaskSourceCore.cs b/src/libraries/System.Private.CoreLib/src/System/Threading/Tasks/Sources/ManualResetValueTaskSourceCore.cs index d23cbb298731cc..f7434075c95d6b 100644 --- a/src/libraries/System.Private.CoreLib/src/System/Threading/Tasks/Sources/ManualResetValueTaskSourceCore.cs +++ b/src/libraries/System.Private.CoreLib/src/System/Threading/Tasks/Sources/ManualResetValueTaskSourceCore.cs @@ -82,6 +82,9 @@ public void SetException(Exception error) /// Gets whether the operation has completed. internal bool IsCompleted => ReferenceEquals(Volatile.Read(ref _continuation), ManualResetValueTaskSourceCoreShared.s_sentinel); + /// Gets the continuation object for diagnostic purposes only. + internal object? ContinuationForDiagnostics => _continuationState; + /// Gets the status of the operation. /// Opaque value that was provided to the 's constructor. public ValueTaskSourceStatus GetStatus(short token) From bf701cd670fdb5c39d639954538c00c231fabd86 Mon Sep 17 00:00:00 2001 From: lateralusX Date: Wed, 24 Jun 2026 16:54:18 +0200 Subject: [PATCH 2/2] New test covering pooled value task paths. --- .../AsyncProfilerV1Tests.cs | 1125 +++++++++++++---- 1 file changed, 897 insertions(+), 228 deletions(-) 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 index caecefc57696b3..78a3db393ccb0b 100644 --- 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 @@ -4,7 +4,9 @@ using System.Collections.Generic; using System.Diagnostics.Tracing; using System.Linq; +using System.Reflection; using System.Runtime.CompilerServices; +using System.Threading.Tasks.Sources; using Xunit; namespace System.Threading.Tasks.Tests @@ -114,21 +116,45 @@ private static async Task StateMachineAsync_RecursiveChainGated(int depth, Task [RuntimeAsyncMethodGeneration(false)] [MethodImpl(MethodImplOptions.NoInlining)] - private static async ValueTask ValueStateMachineAsync_Level1() + private static async ValueTask StateMachineAsync_ValueTask_Level1() { - await ValueStateMachineAsync_Level2(); + await StateMachineAsync_ValueTask_Level2(); } [RuntimeAsyncMethodGeneration(false)] [MethodImpl(MethodImplOptions.NoInlining)] - private static async ValueTask ValueStateMachineAsync_Level2() + private static async ValueTask StateMachineAsync_ValueTask_Level2() { - await ValueStateMachineAsync_Level3(); + await StateMachineAsync_ValueTask_Level3(); } [RuntimeAsyncMethodGeneration(false)] [MethodImpl(MethodImplOptions.NoInlining)] - private static async ValueTask ValueStateMachineAsync_Level3() + private static async ValueTask StateMachineAsync_ValueTask_Level3() + { + await Task.Delay(100); + } + + [RuntimeAsyncMethodGeneration(false)] + [MethodImpl(MethodImplOptions.NoInlining)] + [AsyncMethodBuilder(typeof(PoolingAsyncValueTaskMethodBuilder))] + private static async ValueTask StateMachineAsync_PoolingValueTask_Level1() + { + await StateMachineAsync_PoolingValueTask_Level2(); + } + + [RuntimeAsyncMethodGeneration(false)] + [MethodImpl(MethodImplOptions.NoInlining)] + [AsyncMethodBuilder(typeof(PoolingAsyncValueTaskMethodBuilder))] + private static async ValueTask StateMachineAsync_PoolingValueTask_Level2() + { + await StateMachineAsync_PoolingValueTask_Level3(); + } + + [RuntimeAsyncMethodGeneration(false)] + [MethodImpl(MethodImplOptions.NoInlining)] + [AsyncMethodBuilder(typeof(PoolingAsyncValueTaskMethodBuilder))] + private static async ValueTask StateMachineAsync_PoolingValueTask_Level3() { await Task.Delay(100); } @@ -1674,24 +1700,24 @@ public void StateMachineAsync_TaskCancellation() [RuntimeAsyncMethodGeneration(false)] [MethodImpl(MethodImplOptions.NoInlining)] - private static async ValueTask ValueStateMachineAsync_EventSequenceOrder_Marker() + private static async ValueTask StateMachineAsync_ValueTask_EventSequenceOrder_Marker() { - await ValueStateMachineAsync_Level1(); + await StateMachineAsync_ValueTask_Level1(); } [ConditionalFact(typeof(AsyncProfilerTests), nameof(IsStateMachineAsyncAndThreadingSupported))] - public void ValueStateMachineAsync_EventSequenceOrder() + public void StateMachineAsync_ValueTask_EventSequenceOrder() { var events = CollectEvents(ResumeStateMachineAsyncCallstackKeyword | StateMachineAsyncCoreKeywords, () => { - RunScenarioAndFlush(() => ValueStateMachineAsync_EventSequenceOrder_Marker().AsTask()); + RunScenarioAndFlush(() => StateMachineAsync_ValueTask_EventSequenceOrder_Marker().AsTask()); }); // DumpAllEvents(events); var stream = ParseAllEvents(events); - var markerCallstacks = stream.CallstacksWithMarker(AsyncEventID.ResumeStateMachineAsyncCallstack, nameof(ValueStateMachineAsync_EventSequenceOrder_Marker)); + var markerCallstacks = stream.CallstacksWithMarker(AsyncEventID.ResumeStateMachineAsyncCallstack, nameof(StateMachineAsync_ValueTask_EventSequenceOrder_Marker)); AssertNotEmpty(stream, markerCallstacks); ulong dispatcherId = markerCallstacks[0].DispatcherId; @@ -1709,17 +1735,17 @@ public void ValueStateMachineAsync_EventSequenceOrder() [RuntimeAsyncMethodGeneration(false)] [MethodImpl(MethodImplOptions.NoInlining)] - private static async ValueTask ValueStateMachineAsync_MethodEventsEmitted_Marker() + private static async ValueTask StateMachineAsync_ValueTask_MethodEventsEmitted_Marker() { - await ValueStateMachineAsync_Level1(); + await StateMachineAsync_ValueTask_Level1(); } [ConditionalFact(typeof(AsyncProfilerTests), nameof(IsStateMachineAsyncAndThreadingSupported))] - public void ValueStateMachineAsync_MethodEventsEmitted() + public void StateMachineAsync_ValueTask_MethodEventsEmitted() { var events = CollectEvents(StateMachineAsyncMethodKeywords | StateMachineAsyncCoreKeywords, () => { - RunScenarioAndFlush(() => ValueStateMachineAsync_MethodEventsEmitted_Marker().AsTask()); + RunScenarioAndFlush(() => StateMachineAsync_ValueTask_MethodEventsEmitted_Marker().AsTask()); }); // DumpAllEvents(events); @@ -1742,24 +1768,24 @@ public void ValueStateMachineAsync_MethodEventsEmitted() [RuntimeAsyncMethodGeneration(false)] [MethodImpl(MethodImplOptions.NoInlining)] - private static async ValueTask ValueStateMachineAsync_CallstackDepthMatchesChainDepth_Marker() + private static async ValueTask StateMachineAsync_ValueTask_CallstackDepthMatchesChainDepth_Marker() { - await ValueStateMachineAsync_Level1(); + await StateMachineAsync_ValueTask_Level1(); } [ConditionalFact(typeof(AsyncProfilerTests), nameof(IsStateMachineAsyncAndThreadingSupported))] - public void ValueStateMachineAsync_CallstackDepthMatchesChainDepth() + public void StateMachineAsync_ValueTask_CallstackDepthMatchesChainDepth() { var events = CollectEvents(ResumeStateMachineAsyncCallstackKeyword | StateMachineAsyncCoreKeywords, () => { - RunScenarioAndFlush(() => ValueStateMachineAsync_CallstackDepthMatchesChainDepth_Marker().AsTask()); + RunScenarioAndFlush(() => StateMachineAsync_ValueTask_CallstackDepthMatchesChainDepth_Marker().AsTask()); }); // DumpAllEvents(events); var stream = ParseAllEvents(events); - var markerCallstacks = stream.CallstacksWithMarker(AsyncEventID.ResumeStateMachineAsyncCallstack, nameof(ValueStateMachineAsync_CallstackDepthMatchesChainDepth_Marker)); + var markerCallstacks = stream.CallstacksWithMarker(AsyncEventID.ResumeStateMachineAsyncCallstack, nameof(StateMachineAsync_ValueTask_CallstackDepthMatchesChainDepth_Marker)); AssertNotEmpty(stream, markerCallstacks); // Marker -> Level1 -> Level2 -> Level3 @@ -1768,24 +1794,24 @@ public void ValueStateMachineAsync_CallstackDepthMatchesChainDepth() [RuntimeAsyncMethodGeneration(false)] [MethodImpl(MethodImplOptions.NoInlining)] - private static async ValueTask ValueStateMachineAsync_CallstackFramesHaveDistinctMethodIds_Marker() + private static async ValueTask StateMachineAsync_ValueTask_CallstackFramesHaveDistinctMethodIds_Marker() { - await ValueStateMachineAsync_Level1(); + await StateMachineAsync_ValueTask_Level1(); } [ConditionalFact(typeof(AsyncProfilerTests), nameof(IsStateMachineAsyncAndThreadingSupported))] - public void ValueStateMachineAsync_CallstackFramesHaveDistinctMethodIds() + public void StateMachineAsync_ValueTask_CallstackFramesHaveDistinctMethodIds() { var events = CollectEvents(ResumeStateMachineAsyncCallstackKeyword | StateMachineAsyncCoreKeywords, () => { - RunScenarioAndFlush(() => ValueStateMachineAsync_CallstackFramesHaveDistinctMethodIds_Marker().AsTask()); + RunScenarioAndFlush(() => StateMachineAsync_ValueTask_CallstackFramesHaveDistinctMethodIds_Marker().AsTask()); }); // DumpAllEvents(events); var stream = ParseAllEvents(events); - var markerCallstacks = stream.CallstacksWithMarker(AsyncEventID.ResumeStateMachineAsyncCallstack, nameof(ValueStateMachineAsync_CallstackFramesHaveDistinctMethodIds_Marker)); + var markerCallstacks = stream.CallstacksWithMarker(AsyncEventID.ResumeStateMachineAsyncCallstack, nameof(StateMachineAsync_ValueTask_CallstackFramesHaveDistinctMethodIds_Marker)); AssertNotEmpty(stream, markerCallstacks); var methodIds = markerCallstacks[0].Frames.Select(f => f.MethodId).ToList(); @@ -1794,7 +1820,7 @@ public void ValueStateMachineAsync_CallstackFramesHaveDistinctMethodIds() [RuntimeAsyncMethodGeneration(false)] [MethodImpl(MethodImplOptions.NoInlining)] - private static async ValueTask ValueStateMachineAsync_HandledException_EmitsUnwindAndComplete_InnerThrows_Marker() + private static async ValueTask StateMachineAsync_ValueTask_HandledException_EmitsUnwindAndComplete_InnerThrows_Marker() { await Task.Delay(100); throw new InvalidOperationException("valuetask inner throw"); @@ -1802,11 +1828,11 @@ private static async ValueTask ValueStateMachineAsync_HandledException_EmitsUnwi [RuntimeAsyncMethodGeneration(false)] [MethodImpl(MethodImplOptions.NoInlining)] - private static async ValueTask ValueStateMachineAsync_HandledException_EmitsUnwindAndComplete_Handled_Marker() + private static async ValueTask StateMachineAsync_ValueTask_HandledException_EmitsUnwindAndComplete_Handled_Marker() { try { - await ValueStateMachineAsync_HandledException_EmitsUnwindAndComplete_InnerThrows_Marker(); + await StateMachineAsync_ValueTask_HandledException_EmitsUnwindAndComplete_InnerThrows_Marker(); } catch (InvalidOperationException) { @@ -1815,24 +1841,24 @@ private static async ValueTask ValueStateMachineAsync_HandledException_EmitsUnwi [RuntimeAsyncMethodGeneration(false)] [MethodImpl(MethodImplOptions.NoInlining)] - private static async ValueTask ValueStateMachineAsync_HandledException_EmitsUnwindAndComplete_Marker() + private static async ValueTask StateMachineAsync_ValueTask_HandledException_EmitsUnwindAndComplete_Marker() { - await ValueStateMachineAsync_HandledException_EmitsUnwindAndComplete_Handled_Marker(); + await StateMachineAsync_ValueTask_HandledException_EmitsUnwindAndComplete_Handled_Marker(); } [ConditionalFact(typeof(AsyncProfilerTests), nameof(IsStateMachineAsyncAndThreadingSupported))] - public void ValueStateMachineAsync_HandledException_EmitsUnwindAndComplete() + public void StateMachineAsync_ValueTask_HandledException_EmitsUnwindAndComplete() { var events = CollectEvents(ResumeStateMachineAsyncCallstackKeyword | StateMachineAsyncCoreKeywords | UnwindStateMachineAsyncExceptionKeyword, () => { - RunScenarioAndFlush(() => ValueStateMachineAsync_HandledException_EmitsUnwindAndComplete_Marker().AsTask()); + RunScenarioAndFlush(() => StateMachineAsync_ValueTask_HandledException_EmitsUnwindAndComplete_Marker().AsTask()); }); // DumpAllEvents(events); var stream = ParseAllEvents(events); - var markerCallstacks = stream.CallstacksWithMarker(AsyncEventID.ResumeStateMachineAsyncCallstack, nameof(ValueStateMachineAsync_HandledException_EmitsUnwindAndComplete_Marker)); + var markerCallstacks = stream.CallstacksWithMarker(AsyncEventID.ResumeStateMachineAsyncCallstack, nameof(StateMachineAsync_ValueTask_HandledException_EmitsUnwindAndComplete_Marker)); AssertNotEmpty(stream, markerCallstacks); ulong dispatcherId = markerCallstacks[0].DispatcherId; @@ -1854,14 +1880,14 @@ public void ValueStateMachineAsync_HandledException_EmitsUnwindAndComplete() [RuntimeAsyncMethodGeneration(false)] [MethodImpl(MethodImplOptions.NoInlining)] - private static async ValueTask ValueStateMachineAsync_UnhandledException_EmitsUnwindAndComplete_UnhandledOuter_Marker() + private static async ValueTask StateMachineAsync_ValueTask_UnhandledException_EmitsUnwindAndComplete_UnhandledOuter_Marker() { - await ValueStateMachineAsync_UnhandledException_EmitsUnwindAndComplete_UnhandledInner_Marker(); + await StateMachineAsync_ValueTask_UnhandledException_EmitsUnwindAndComplete_UnhandledInner_Marker(); } [RuntimeAsyncMethodGeneration(false)] [MethodImpl(MethodImplOptions.NoInlining)] - private static async ValueTask ValueStateMachineAsync_UnhandledException_EmitsUnwindAndComplete_UnhandledInner_Marker() + private static async ValueTask StateMachineAsync_ValueTask_UnhandledException_EmitsUnwindAndComplete_UnhandledInner_Marker() { await Task.Delay(100); throw new InvalidOperationException("valuetask unhandled inner"); @@ -1869,19 +1895,19 @@ private static async ValueTask ValueStateMachineAsync_UnhandledException_EmitsUn [RuntimeAsyncMethodGeneration(false)] [MethodImpl(MethodImplOptions.NoInlining)] - private static async ValueTask ValueStateMachineAsync_UnhandledException_EmitsUnwindAndComplete_Marker() + private static async ValueTask StateMachineAsync_ValueTask_UnhandledException_EmitsUnwindAndComplete_Marker() { - await ValueStateMachineAsync_UnhandledException_EmitsUnwindAndComplete_UnhandledOuter_Marker(); + await StateMachineAsync_ValueTask_UnhandledException_EmitsUnwindAndComplete_UnhandledOuter_Marker(); } [ConditionalFact(typeof(AsyncProfilerTests), nameof(IsStateMachineAsyncAndThreadingSupported))] - public void ValueStateMachineAsync_UnhandledException_EmitsUnwindAndComplete() + public void StateMachineAsync_ValueTask_UnhandledException_EmitsUnwindAndComplete() { var events = CollectEvents(ResumeStateMachineAsyncCallstackKeyword | StateMachineAsyncCoreKeywords | UnwindStateMachineAsyncExceptionKeyword, () => { try { - RunScenarioAndFlush(() => ValueStateMachineAsync_UnhandledException_EmitsUnwindAndComplete_Marker().AsTask()); + RunScenarioAndFlush(() => StateMachineAsync_ValueTask_UnhandledException_EmitsUnwindAndComplete_Marker().AsTask()); } catch (InvalidOperationException) { @@ -1892,7 +1918,7 @@ public void ValueStateMachineAsync_UnhandledException_EmitsUnwindAndComplete() var stream = ParseAllEvents(events); - var markerCallstacks = stream.CallstacksWithMarker(AsyncEventID.ResumeStateMachineAsyncCallstack, nameof(ValueStateMachineAsync_UnhandledException_EmitsUnwindAndComplete_Marker)); + var markerCallstacks = stream.CallstacksWithMarker(AsyncEventID.ResumeStateMachineAsyncCallstack, nameof(StateMachineAsync_ValueTask_UnhandledException_EmitsUnwindAndComplete_Marker)); AssertNotEmpty(stream, markerCallstacks); ulong dispatcherId = markerCallstacks[0].DispatcherId; @@ -1914,258 +1940,764 @@ public void ValueStateMachineAsync_UnhandledException_EmitsUnwindAndComplete() AssertTrue(stream, completeIdx > unwindIdx2, "Expected CompleteStateMachineAsyncContext after second Unwind"); } - [RuntimeAsyncMethodGeneration(false)] - [MethodImpl(MethodImplOptions.NoInlining)] - private static async Task StateMachineAsync_ResetContext_ReplaysPendingV1Chain_Inner_Marker() + [ConditionalFact(typeof(AsyncProfilerTests), nameof(IsStateMachineAsyncAndThreadingSupported))] + public void StateMachineAsync_PoolingValueTask_UsesPoolingBox() { - using var dummy = new TestEventListener(); - dummy.AddSource(AsyncProfilerEventSourceName, EventLevel.Informational, EventKeywords.None); - await Task.Yield(); - } + ValueTask pending = StateMachineAsync_PoolingValueTask_Level3(); - [RuntimeAsyncMethodGeneration(false)] - [MethodImpl(MethodImplOptions.NoInlining)] - private static async Task StateMachineAsync_ResetContext_ReplaysPendingV1Chain_Mid_Marker() - { - await StateMachineAsync_ResetContext_ReplaysPendingV1Chain_Inner_Marker(); + try + { + Assert.False(pending.IsCompleted, "Expected a pending ValueTask so a pooling box was rented"); + + object? backing = typeof(ValueTask).GetField("_obj", BindingFlags.Instance | BindingFlags.NonPublic)!.GetValue(pending); + Assert.True(backing is IValueTaskSource, $"Expected the pending ValueTask to be backed by an IValueTaskSource pooling box, got {backing?.GetType().Name ?? "null"}"); + Assert.False(backing is Task, "A pending ValueTask backed by a Task means the default (non-pooling) builder was used"); + } + finally + { + pending.AsTask().GetAwaiter().GetResult(); + } } [RuntimeAsyncMethodGeneration(false)] [MethodImpl(MethodImplOptions.NoInlining)] - private static async Task StateMachineAsync_ResetContext_ReplaysPendingV1Chain_Outer_Marker() + [AsyncMethodBuilder(typeof(PoolingAsyncValueTaskMethodBuilder))] + private static async ValueTask StateMachineAsync_PoolingValueTask_EventSequenceOrder_Marker() { - await StateMachineAsync_ResetContext_ReplaysPendingV1Chain_Mid_Marker(); + await StateMachineAsync_PoolingValueTask_Level1(); } [ConditionalFact(typeof(AsyncProfilerTests), nameof(IsStateMachineAsyncAndThreadingSupported))] - public void StateMachineAsync_ResetContext_ReplaysPendingV1Chain() + public void StateMachineAsync_PoolingValueTask_EventSequenceOrder() { - var events = CollectEvents(AllStateMachineAsyncKeywords, () => + var events = CollectEvents(ResumeStateMachineAsyncCallstackKeyword | StateMachineAsyncCoreKeywords, () => { - RunScenarioAndFlush(() => StateMachineAsync_ResetContext_ReplaysPendingV1Chain_Outer_Marker()); + RunScenarioAndFlush(() => StateMachineAsync_PoolingValueTask_EventSequenceOrder_Marker().AsTask()); }); // DumpAllEvents(events); var stream = ParseAllEvents(events); - // Locate the StateMachine dispatcher driving the marker chain via its ResumeStateMachineAsyncCallstack - // events (which carry the marker frames in their callstack). - var markerCallstacks = stream.CallstacksWithMarker(AsyncEventID.ResumeStateMachineAsyncCallstack, "StateMachineAsync_ResetContext_ReplaysPendingV1Chain"); + var markerCallstacks = stream.CallstacksWithMarker(AsyncEventID.ResumeStateMachineAsyncCallstack, nameof(StateMachineAsync_PoolingValueTask_EventSequenceOrder_Marker)); AssertNotEmpty(stream, markerCallstacks); - // Thread-scoped replay assertion: at least one OS thread must have seen two - // ResetAsyncThreadContext events AND show a marker ResumeStateMachineAsyncCallstack plus - // matching ResumeStateMachineAsyncContext after its most recent reset. Multiple threads - // in the trace can accumulate two resets (e.g. the test thread re-enables on - // its initial event then again later), so we have to look at each candidate - // thread, not just the first one. The thread that actually drove the replay - // is the one the StateMachine continuation resumed onto. - var resetsByThread = stream.All - .Where(e => e.EventId == AsyncEventID.ResetAsyncThreadContext && e.OsThreadId != 0) - .GroupBy(e => e.OsThreadId) - .Where(g => g.Count() >= 2) - .ToList(); - AssertNotEmpty(stream, resetsByThread); - - bool found = false; - foreach (var threadResets in resetsByThread) - { - ulong threadId = threadResets.Key; - long lastResetTimestamp = threadResets.Max(e => e.Timestamp); - - var postResetMarkerCallstacks = markerCallstacks - .Where(c => c.OsThreadId == threadId && c.Timestamp >= lastResetTimestamp) - .ToList(); - if (postResetMarkerCallstacks.Count == 0) - { - continue; - } - - var replayDispatcherIds = postResetMarkerCallstacks.Select(c => c.DispatcherId).ToHashSet(); - var postResetResumeContext = stream.All - .Where(e => e.EventId == AsyncEventID.ResumeStateMachineAsyncContext - && e.OsThreadId == threadId - && e.Timestamp >= lastResetTimestamp - && replayDispatcherIds.Contains(e.DispatcherId)) - .ToList(); - if (postResetResumeContext.Count == 0) - { - continue; - } + ulong dispatcherId = markerCallstacks[0].DispatcherId; + var ids = stream.ChainEventsFromDispatcher(dispatcherId).Select(e => e.EventId).ToList(); - found = true; - break; - } + int createIdx = ids.IndexOf(AsyncEventID.CreateStateMachineAsyncContext); + AssertTrue(stream, createIdx >= 0, "Expected CreateStateMachineAsyncContext"); - AssertTrue(stream, found, - "Expected at least one OS thread with >= 2 ResetAsyncThreadContext events followed by a marker ResumeAsyncCallstack and matching ResumeAsyncContext."); - } + int resumeIdx = ids.IndexOf(AsyncEventID.ResumeStateMachineAsyncContext, createIdx + 1); + AssertTrue(stream, resumeIdx > createIdx, "Expected ResumeStateMachineAsyncContext after Create"); - [RuntimeAsyncMethodGeneration(false)] - [MethodImpl(MethodImplOptions.NoInlining)] - private static async Task StateMachineAsync_ResetContext_ReplaysMultipleDispatchers_Inner_Marker( - Task gate, - ManualResetEventSlim block, - Action onBlocked, - TaskCompletionSource innerDone) - { - await gate; - onBlocked(); - block.Wait(); - innerDone.SetResult(); + int completeIdx = ids.IndexOf(AsyncEventID.CompleteStateMachineAsyncContext, resumeIdx + 1); + AssertTrue(stream, completeIdx > resumeIdx, "Expected CompleteStateMachineAsyncContext after Resume"); } [RuntimeAsyncMethodGeneration(false)] [MethodImpl(MethodImplOptions.NoInlining)] - private static async Task StateMachineAsync_ResetContext_ReplaysMultipleDispatchers_Outer_Marker(TaskCompletionSource innerDone) + [AsyncMethodBuilder(typeof(PoolingAsyncValueTaskMethodBuilder))] + private static async ValueTask StateMachineAsync_PoolingValueTask_MethodEventsEmitted_Marker() { - await innerDone.Task; + await StateMachineAsync_PoolingValueTask_Level1(); } [ConditionalFact(typeof(AsyncProfilerTests), nameof(IsStateMachineAsyncAndThreadingSupported))] - public void StateMachineAsync_ResetContext_ReplaysMultipleDispatchers() + public void StateMachineAsync_PoolingValueTask_MethodEventsEmitted() { - var events = CollectEvents(AllStateMachineAsyncKeywords, () => + var events = CollectEvents(StateMachineAsyncMethodKeywords | StateMachineAsyncCoreKeywords, () => { - RunScenarioAndFlush(async () => - { - var gate = new TaskCompletionSource(TaskCreationOptions.RunContinuationsAsynchronously); - var block = new ManualResetEventSlim(false); - var blocked = new TaskCompletionSource(TaskCreationOptions.RunContinuationsAsynchronously); - var innerDone = new TaskCompletionSource(); + RunScenarioAndFlush(() => StateMachineAsync_PoolingValueTask_MethodEventsEmitted_Marker().AsTask()); + }); - Task inner = StateMachineAsync_ResetContext_ReplaysMultipleDispatchers_Inner_Marker(gate.Task, block, () => blocked.SetResult(), innerDone); - Task outer = StateMachineAsync_ResetContext_ReplaysMultipleDispatchers_Outer_Marker(innerDone); + // DumpAllEvents(events); - _ = Task.Run(() => gate.SetResult()); + var stream = ParseAllEvents(events); - await blocked.Task; + var methodEvents = stream.All + .Where(e => e.EventId is AsyncEventID.ResumeStateMachineAsyncMethod or AsyncEventID.CompleteStateMachineAsyncMethod) + .Select(e => e.EventId) + .ToList(); - var dummy = new TestEventListener(); - try - { - dummy.AddSource(AsyncProfilerEventSourceName, EventLevel.Informational, EventKeywords.None); + int resumeCount = methodEvents.Count(id => id == AsyncEventID.ResumeStateMachineAsyncMethod); + int completeCount = methodEvents.Count(id => id == AsyncEventID.CompleteStateMachineAsyncMethod); - block.Set(); + // Marker -> Level1 -> Level2 -> Level3 + AssertTrue(stream, resumeCount >= 4, $"Expected at least 4 ResumeStateMachineAsyncMethod events for pooling ValueTask chain, got {resumeCount}"); + AssertTrue(stream, completeCount >= 4, $"Expected at least 4 CompleteStateMachineAsyncMethod events for pooling ValueTask chain, got {completeCount}"); + } - await outer; - await inner; - } - finally - { - dummy.Dispose(); - } - }); + [RuntimeAsyncMethodGeneration(false)] + [MethodImpl(MethodImplOptions.NoInlining)] + [AsyncMethodBuilder(typeof(PoolingAsyncValueTaskMethodBuilder))] + private static async ValueTask StateMachineAsync_PoolingValueTask_SuspendResumeCompleteEvents_Marker() + { + await Task.Delay(100); + await Task.Delay(100); + await Task.Delay(100); + } + + [ConditionalFact(typeof(AsyncProfilerTests), nameof(IsStateMachineAsyncAndThreadingSupported))] + public void StateMachineAsync_PoolingValueTask_SuspendResumeCompleteEvents() + { + var events = CollectEvents(ResumeStateMachineAsyncCallstackKeyword | StateMachineAsyncCoreKeywords, () => + { + RunScenarioAndFlush(() => StateMachineAsync_PoolingValueTask_SuspendResumeCompleteEvents_Marker().AsTask()); }); // DumpAllEvents(events); var stream = ParseAllEvents(events); - // The replay must emit at least one ResumeAsyncCallstack carrying the StateMachine marker chain. - var markerCallstacks = stream.CallstacksWithMarker(AsyncEventID.ResumeStateMachineAsyncCallstack, "StateMachineAsync_ResetContext_ReplaysMultipleDispatchers"); + var markerCallstacks = stream.CallstacksWithMarker(AsyncEventID.ResumeStateMachineAsyncCallstack, nameof(StateMachineAsync_PoolingValueTask_SuspendResumeCompleteEvents_Marker)); AssertNotEmpty(stream, markerCallstacks); - // The decisive proof: find an OS thread where a single ResetAsyncThreadContext - // event is followed by >= 2 ResumeAsyncContext events before the next reset - // (or end of trace). A normal dispatcher resume can only emit one Resume - // per MoveNext invocation; observing >= 2 in a single reset window proves - // the walker traversed multiple nested dispatchers from a single ResetContext. - var resetsByThread = stream.All - .Where(e => e.EventId == AsyncEventID.ResetAsyncThreadContext && e.OsThreadId != 0) - .GroupBy(e => e.OsThreadId) - .ToList(); - AssertNotEmpty(stream, resetsByThread); - - bool found = false; - foreach (var threadResets in resetsByThread) - { - ulong threadId = threadResets.Key; - var orderedResets = threadResets.OrderBy(e => e.Timestamp).ToList(); - - for (int i = 0; i < orderedResets.Count; i++) - { - long windowStart = orderedResets[i].Timestamp; - long windowEnd = (i + 1 < orderedResets.Count) - ? orderedResets[i + 1].Timestamp - : long.MaxValue; - - int resumeContextCount = stream.All - .Count(e => e.EventId == AsyncEventID.ResumeStateMachineAsyncCallstack - && e.OsThreadId == threadId - && e.Timestamp >= windowStart - && e.Timestamp < windowEnd); + ulong dispatcherId = markerCallstacks[0].DispatcherId; + var ids = stream.ChainEventsFromDispatcher(dispatcherId).Select(e => e.EventId).ToList(); - if (resumeContextCount >= 2) - { - found = true; - break; - } - } + int resumeCount = ids.Count(id => id == AsyncEventID.ResumeStateMachineAsyncContext); + int suspendCount = ids.Count(id => id == AsyncEventID.SuspendStateMachineAsyncContext); + int completeCount = ids.Count(id => id == AsyncEventID.CompleteStateMachineAsyncContext); - if (found) - { - break; - } - } + AssertTrue(stream, suspendCount >= 1, "Expected at least one SuspendStateMachineAsyncContext for the re-suspending pooling method"); - AssertTrue(stream, found, - "Expected at least one OS thread with a ResetAsyncThreadContext event " + - "followed by >= 2 ResumeAsyncContext events in the same reset window, " + - "proving the reset-replay walker traversed multiple nested dispatchers."); + // Each resume ends in exactly one suspend (yielded) or one complete (finished). + AssertEqual(stream, resumeCount, completeCount + suspendCount); } [RuntimeAsyncMethodGeneration(false)] [MethodImpl(MethodImplOptions.NoInlining)] - private static async Task StateMachineAsync_ResetContext_ReplayResumeCompleteBalance_Inner_Marker() + [AsyncMethodBuilder(typeof(PoolingAsyncValueTaskMethodBuilder))] + private static async ValueTask StateMachineAsync_PoolingValueTask_CallstackDepthMatchesChainDepth_Marker() { - using var dummy = new TestEventListener(); - dummy.AddSource(AsyncProfilerEventSourceName, EventLevel.Informational, EventKeywords.None); - await Task.Yield(); + await StateMachineAsync_PoolingValueTask_Level1(); } - [RuntimeAsyncMethodGeneration(false)] - [MethodImpl(MethodImplOptions.NoInlining)] - private static async Task StateMachineAsync_ResetContext_ReplayResumeCompleteBalance_Mid_Marker() + [ConditionalFact(typeof(AsyncProfilerTests), nameof(IsStateMachineAsyncAndThreadingSupported))] + public void StateMachineAsync_PoolingValueTask_CallstackDepthMatchesChainDepth() { - await StateMachineAsync_ResetContext_ReplayResumeCompleteBalance_Inner_Marker(); + var events = CollectEvents(ResumeStateMachineAsyncCallstackKeyword | StateMachineAsyncCoreKeywords, () => + { + RunScenarioAndFlush(() => StateMachineAsync_PoolingValueTask_CallstackDepthMatchesChainDepth_Marker().AsTask()); + }); + + // DumpAllEvents(events); + + var stream = ParseAllEvents(events); + + var markerCallstacks = stream.CallstacksWithMarker(AsyncEventID.ResumeStateMachineAsyncCallstack, nameof(StateMachineAsync_PoolingValueTask_CallstackDepthMatchesChainDepth_Marker)); + AssertNotEmpty(stream, markerCallstacks); + + // Marker -> Level1 -> Level2 -> Level3. + AssertEqual(stream, 4, markerCallstacks[0].FrameCount); } [RuntimeAsyncMethodGeneration(false)] [MethodImpl(MethodImplOptions.NoInlining)] - private static async Task StateMachineAsync_ResetContext_ReplayResumeCompleteBalance_Outer_Marker() + [AsyncMethodBuilder(typeof(PoolingAsyncValueTaskMethodBuilder))] + private static async ValueTask StateMachineAsync_PoolingValueTask_CallstackFramesHaveDistinctMethodIds_Marker() { - await StateMachineAsync_ResetContext_ReplayResumeCompleteBalance_Mid_Marker(); + await StateMachineAsync_PoolingValueTask_Level1(); } [ConditionalFact(typeof(AsyncProfilerTests), nameof(IsStateMachineAsyncAndThreadingSupported))] - public void StateMachineAsync_ResetContext_ReplayResumeCompleteBalance() + public void StateMachineAsync_PoolingValueTask_CallstackFramesHaveDistinctMethodIds() { - var events = CollectEvents(AllStateMachineAsyncKeywords, () => + var events = CollectEvents(ResumeStateMachineAsyncCallstackKeyword | StateMachineAsyncCoreKeywords, () => { - RunScenarioAndFlush(() => StateMachineAsync_ResetContext_ReplayResumeCompleteBalance_Outer_Marker()); + RunScenarioAndFlush(() => StateMachineAsync_PoolingValueTask_CallstackFramesHaveDistinctMethodIds_Marker().AsTask()); }); // DumpAllEvents(events); var stream = ParseAllEvents(events); - // Locate the replayed marker ResumeAsyncCallstack: the reset (triggered by the dummy - // listener inside the inner marker) replays the suspended StateMachine chain, producing a - // callstack carrying the marker frames. - var markerCallstacks = stream.CallstacksWithMarker(AsyncEventID.ResumeStateMachineAsyncCallstack, "StateMachineAsync_ResetContext_ReplayResumeCompleteBalance"); + var markerCallstacks = stream.CallstacksWithMarker(AsyncEventID.ResumeStateMachineAsyncCallstack, nameof(StateMachineAsync_PoolingValueTask_CallstackFramesHaveDistinctMethodIds_Marker)); AssertNotEmpty(stream, markerCallstacks); - // Resume/Complete balance across the replay boundary. StateMachine uses a per-MoveNext dispatcher - // model, so the marker chain spans a dispatcher tree whose deepest replayed callstack - // has N frames; each of those N resumed async methods must eventually complete. Balance - // is NOT preserved per reset epoch by design (a Resume can land in one epoch and its - // Complete in a later epoch once a config change bumps the revision mid-chain), so the - // count is reconstructed over the whole trace, scoped to the marker dispatcher's chain. - // Using >= keeps the assertion robust against additional method events from the harness. - var deepest = markerCallstacks.OrderByDescending(c => c.FrameCount).First(); - ulong leafDispatcherId = deepest.DispatcherId; + var methodIds = markerCallstacks[0].Frames.Select(f => f.MethodId).ToList(); + AssertEqual(stream, methodIds.Count, methodIds.Distinct().Count()); + } + + [RuntimeAsyncMethodGeneration(false)] + [MethodImpl(MethodImplOptions.NoInlining)] + [AsyncMethodBuilder(typeof(PoolingAsyncValueTaskMethodBuilder))] + private static async ValueTask StateMachineAsync_PoolingValueTask_HandledException_EmitsUnwindAndComplete_InnerThrows_Marker() + { + await Task.Delay(100); + throw new InvalidOperationException("pooling valuetask inner throw"); + } + + [RuntimeAsyncMethodGeneration(false)] + [MethodImpl(MethodImplOptions.NoInlining)] + [AsyncMethodBuilder(typeof(PoolingAsyncValueTaskMethodBuilder))] + private static async ValueTask StateMachineAsync_PoolingValueTask_HandledException_EmitsUnwindAndComplete_Handled_Marker() + { + try + { + await StateMachineAsync_PoolingValueTask_HandledException_EmitsUnwindAndComplete_InnerThrows_Marker(); + } + catch (InvalidOperationException) + { + } + } + + [RuntimeAsyncMethodGeneration(false)] + [MethodImpl(MethodImplOptions.NoInlining)] + [AsyncMethodBuilder(typeof(PoolingAsyncValueTaskMethodBuilder))] + private static async ValueTask StateMachineAsync_PoolingValueTask_HandledException_EmitsUnwindAndComplete_Marker() + { + await StateMachineAsync_PoolingValueTask_HandledException_EmitsUnwindAndComplete_Handled_Marker(); + } + + [ConditionalFact(typeof(AsyncProfilerTests), nameof(IsStateMachineAsyncAndThreadingSupported))] + public void StateMachineAsync_PoolingValueTask_HandledException_EmitsUnwindAndComplete() + { + var events = CollectEvents(ResumeStateMachineAsyncCallstackKeyword | StateMachineAsyncCoreKeywords | UnwindStateMachineAsyncExceptionKeyword, () => + { + RunScenarioAndFlush(() => StateMachineAsync_PoolingValueTask_HandledException_EmitsUnwindAndComplete_Marker().AsTask()); + }); + + // DumpAllEvents(events); + + var stream = ParseAllEvents(events); + + var markerCallstacks = stream.CallstacksWithMarker(AsyncEventID.ResumeStateMachineAsyncCallstack, nameof(StateMachineAsync_PoolingValueTask_HandledException_EmitsUnwindAndComplete_Marker)); + AssertNotEmpty(stream, markerCallstacks); + + ulong dispatcherId = markerCallstacks[0].DispatcherId; + var ids = stream.ChainEventsFromDispatcher(dispatcherId).Select(e => e.EventId).ToList(); + + int createIdx = ids.IndexOf(AsyncEventID.CreateStateMachineAsyncContext); + AssertTrue(stream, createIdx >= 0, "Expected CreateStateMachineAsyncContext"); + + int resumeIdx = ids.IndexOf(AsyncEventID.ResumeStateMachineAsyncContext, createIdx + 1); + AssertTrue(stream, resumeIdx > createIdx, "Expected ResumeStateMachineAsyncContext after Create"); + + int unwindIdx = ids.IndexOf(AsyncEventID.UnwindStateMachineAsyncException, resumeIdx + 1); + AssertTrue(stream, unwindIdx > resumeIdx, "Expected UnwindStateMachineAsyncException after Resume"); + + int completeIdx = ids.IndexOf(AsyncEventID.CompleteStateMachineAsyncContext, unwindIdx + 1); + AssertTrue(stream, completeIdx > unwindIdx, "Expected CompleteStateMachineAsyncContext after Unwind"); + } + + [RuntimeAsyncMethodGeneration(false)] + [MethodImpl(MethodImplOptions.NoInlining)] + [AsyncMethodBuilder(typeof(PoolingAsyncValueTaskMethodBuilder))] + private static async ValueTask StateMachineAsync_PoolingValueTask_UnhandledException_EmitsUnwindAndComplete_UnhandledOuter_Marker() + { + await StateMachineAsync_PoolingValueTask_UnhandledException_EmitsUnwindAndComplete_UnhandledInner_Marker(); + } + + [RuntimeAsyncMethodGeneration(false)] + [MethodImpl(MethodImplOptions.NoInlining)] + [AsyncMethodBuilder(typeof(PoolingAsyncValueTaskMethodBuilder))] + private static async ValueTask StateMachineAsync_PoolingValueTask_UnhandledException_EmitsUnwindAndComplete_UnhandledInner_Marker() + { + await Task.Delay(100); + throw new InvalidOperationException("pooling valuetask unhandled inner"); + } + + [RuntimeAsyncMethodGeneration(false)] + [MethodImpl(MethodImplOptions.NoInlining)] + [AsyncMethodBuilder(typeof(PoolingAsyncValueTaskMethodBuilder))] + private static async ValueTask StateMachineAsync_PoolingValueTask_UnhandledException_EmitsUnwindAndComplete_Marker() + { + await StateMachineAsync_PoolingValueTask_UnhandledException_EmitsUnwindAndComplete_UnhandledOuter_Marker(); + } + + [ConditionalFact(typeof(AsyncProfilerTests), nameof(IsStateMachineAsyncAndThreadingSupported))] + public void StateMachineAsync_PoolingValueTask_UnhandledException_EmitsUnwindAndComplete() + { + var events = CollectEvents(ResumeStateMachineAsyncCallstackKeyword | StateMachineAsyncCoreKeywords | UnwindStateMachineAsyncExceptionKeyword, () => + { + try + { + RunScenarioAndFlush(() => StateMachineAsync_PoolingValueTask_UnhandledException_EmitsUnwindAndComplete_Marker().AsTask()); + } + catch (InvalidOperationException) + { + } + }); + + // DumpAllEvents(events); + + var stream = ParseAllEvents(events); + + var markerCallstacks = stream.CallstacksWithMarker(AsyncEventID.ResumeStateMachineAsyncCallstack, nameof(StateMachineAsync_PoolingValueTask_UnhandledException_EmitsUnwindAndComplete_Marker)); + AssertNotEmpty(stream, markerCallstacks); + + ulong dispatcherId = markerCallstacks[0].DispatcherId; + var ids = stream.ChainEventsFromDispatcher(dispatcherId).Select(e => e.EventId).ToList(); + + int createIdx = ids.IndexOf(AsyncEventID.CreateStateMachineAsyncContext); + AssertTrue(stream, createIdx >= 0, "Expected CreateStateMachineAsyncContext"); + + int resumeIdx = ids.IndexOf(AsyncEventID.ResumeStateMachineAsyncContext, createIdx + 1); + AssertTrue(stream, resumeIdx > createIdx, "Expected ResumeStateMachineAsyncContext after Create"); + + int unwindIdx1 = ids.IndexOf(AsyncEventID.UnwindStateMachineAsyncException, resumeIdx + 1); + AssertTrue(stream, unwindIdx1 > resumeIdx, "Expected first UnwindStateMachineAsyncException after Resume"); + + int unwindIdx2 = ids.IndexOf(AsyncEventID.UnwindStateMachineAsyncException, unwindIdx1 + 1); + AssertTrue(stream, unwindIdx2 > unwindIdx1, "Expected second UnwindStateMachineAsyncException after first Unwind"); + + int completeIdx = ids.IndexOf(AsyncEventID.CompleteStateMachineAsyncContext, unwindIdx2 + 1); + AssertTrue(stream, completeIdx > unwindIdx2, "Expected CompleteStateMachineAsyncContext after second Unwind"); + } + + [RuntimeAsyncMethodGeneration(false)] + [MethodImpl(MethodImplOptions.NoInlining)] + [AsyncMethodBuilder(typeof(PoolingAsyncValueTaskMethodBuilder))] + private static async ValueTask StateMachineAsync_PoolingValueTask_ConfigureAwaitFalse_Leaf_Marker() + { + await Task.Delay(100).ConfigureAwait(false); + } + + [RuntimeAsyncMethodGeneration(false)] + [MethodImpl(MethodImplOptions.NoInlining)] + [AsyncMethodBuilder(typeof(PoolingAsyncValueTaskMethodBuilder))] + private static async ValueTask StateMachineAsync_PoolingValueTask_ConfigureAwaitFalse_Mid_Marker() + { + await StateMachineAsync_PoolingValueTask_ConfigureAwaitFalse_Leaf_Marker().ConfigureAwait(false); + } + + [RuntimeAsyncMethodGeneration(false)] + [MethodImpl(MethodImplOptions.NoInlining)] + [AsyncMethodBuilder(typeof(PoolingAsyncValueTaskMethodBuilder))] + private static async ValueTask StateMachineAsync_PoolingValueTask_ConfigureAwaitFalse_Marker() + { + await StateMachineAsync_PoolingValueTask_ConfigureAwaitFalse_Mid_Marker().ConfigureAwait(false); + } + + [ConditionalFact(typeof(AsyncProfilerTests), nameof(IsStateMachineAsyncAndThreadingSupported))] + public void StateMachineAsync_PoolingValueTask_ConfigureAwaitFalse() + { + var events = CollectEvents(ResumeStateMachineAsyncCallstackKeyword | StateMachineAsyncCoreKeywords, () => + { + RunScenarioAndFlush(() => StateMachineAsync_PoolingValueTask_ConfigureAwaitFalse_Marker().AsTask()); + }); + + // DumpAllEvents(events); + + var stream = ParseAllEvents(events); + + var markerCallstacks = stream.CallstacksWithMarker(AsyncEventID.ResumeStateMachineAsyncCallstack, nameof(StateMachineAsync_PoolingValueTask_ConfigureAwaitFalse_Marker)); + AssertNotEmpty(stream, markerCallstacks); + + var frameNames = markerCallstacks[0].Frames + .Select(f => GetMethodNameFromMethodId(markerCallstacks[0].CallstackType, f.MethodId)) + .Where(n => n is not null) + .ToList(); + + AssertContains(stream, nameof(StateMachineAsync_PoolingValueTask_ConfigureAwaitFalse_Leaf_Marker), frameNames); + AssertContains(stream, nameof(StateMachineAsync_PoolingValueTask_ConfigureAwaitFalse_Mid_Marker), frameNames); + AssertContains(stream, nameof(StateMachineAsync_PoolingValueTask_ConfigureAwaitFalse_Marker), frameNames); + + AssertExactlyOneCreateAndComplete(stream, markerCallstacks[0].DispatcherId, nameof(StateMachineAsync_PoolingValueTask_ConfigureAwaitFalse_Marker)); + } + + [RuntimeAsyncMethodGeneration(false)] + [MethodImpl(MethodImplOptions.NoInlining)] + [AsyncMethodBuilder(typeof(PoolingAsyncValueTaskMethodBuilder<>))] + private static async ValueTask StateMachineAsync_PoolingValueTaskOfT_ConfigureAwaitFalse_Leaf_Marker() + { + await Task.Delay(100).ConfigureAwait(false); + return 1; + } + + [RuntimeAsyncMethodGeneration(false)] + [MethodImpl(MethodImplOptions.NoInlining)] + [AsyncMethodBuilder(typeof(PoolingAsyncValueTaskMethodBuilder<>))] + private static async ValueTask StateMachineAsync_PoolingValueTaskOfT_ConfigureAwaitFalse_Mid_Marker() + { + return await StateMachineAsync_PoolingValueTaskOfT_ConfigureAwaitFalse_Leaf_Marker().ConfigureAwait(false) + 1; + } + + [RuntimeAsyncMethodGeneration(false)] + [MethodImpl(MethodImplOptions.NoInlining)] + [AsyncMethodBuilder(typeof(PoolingAsyncValueTaskMethodBuilder<>))] + private static async ValueTask StateMachineAsync_PoolingValueTaskOfT_ConfigureAwaitFalse_Marker() + { + return await StateMachineAsync_PoolingValueTaskOfT_ConfigureAwaitFalse_Mid_Marker().ConfigureAwait(false) + 1; + } + + [ConditionalFact(typeof(AsyncProfilerTests), nameof(IsStateMachineAsyncAndThreadingSupported))] + public void StateMachineAsync_PoolingValueTaskOfT_ConfigureAwaitFalse() + { + var events = CollectEvents(ResumeStateMachineAsyncCallstackKeyword | StateMachineAsyncCoreKeywords, () => + { + RunScenarioAndFlush(() => StateMachineAsync_PoolingValueTaskOfT_ConfigureAwaitFalse_Marker().AsTask()); + }); + + // DumpAllEvents(events); + + var stream = ParseAllEvents(events); + + var markerCallstacks = stream.CallstacksWithMarker(AsyncEventID.ResumeStateMachineAsyncCallstack, nameof(StateMachineAsync_PoolingValueTaskOfT_ConfigureAwaitFalse_Marker)); + AssertNotEmpty(stream, markerCallstacks); + + var frameNames = markerCallstacks[0].Frames + .Select(f => GetMethodNameFromMethodId(markerCallstacks[0].CallstackType, f.MethodId)) + .Where(n => n is not null) + .ToList(); + + AssertContains(stream, nameof(StateMachineAsync_PoolingValueTaskOfT_ConfigureAwaitFalse_Leaf_Marker), frameNames); + AssertContains(stream, nameof(StateMachineAsync_PoolingValueTaskOfT_ConfigureAwaitFalse_Mid_Marker), frameNames); + AssertContains(stream, nameof(StateMachineAsync_PoolingValueTaskOfT_ConfigureAwaitFalse_Marker), frameNames); + + AssertExactlyOneCreateAndComplete(stream, markerCallstacks[0].DispatcherId, nameof(StateMachineAsync_PoolingValueTaskOfT_ConfigureAwaitFalse_Marker)); + } + + private static SemaphoreSlim s_poolingAppendRace_proceed; + + [RuntimeAsyncMethodGeneration(false)] + [MethodImpl(MethodImplOptions.NoInlining)] + [AsyncMethodBuilder(typeof(PoolingAsyncValueTaskMethodBuilder))] + private static async ValueTask StateMachineAsync_PoolingValueTask_AppendCallstack_FiresOnLateParentRegistration_Child_Marker() + { + await Task.Yield(); + s_poolingAppendRace_proceed.Release(); + Thread.Sleep(200); + await Task.Yield(); + } + + [RuntimeAsyncMethodGeneration(false)] + [MethodImpl(MethodImplOptions.NoInlining)] + [AsyncMethodBuilder(typeof(PoolingAsyncValueTaskMethodBuilder))] + private static async ValueTask StateMachineAsync_PoolingValueTask_AppendCallstack_FiresOnLateParentRegistration_Parent_Marker() + { + ValueTask t = StateMachineAsync_PoolingValueTask_AppendCallstack_FiresOnLateParentRegistration_Child_Marker(); + Assert.True(s_poolingAppendRace_proceed.Wait(TimeSpan.FromSeconds(20)), "Timed out waiting for child to reach append-race checkpoint"); + await t; + } + + [ConditionalFact(typeof(AsyncProfilerTests), nameof(IsStateMachineAsyncAndThreadingSupported))] + public void StateMachineAsync_PoolingValueTask_AppendCallstack_FiresOnLateParentRegistration() + { + s_poolingAppendRace_proceed = new SemaphoreSlim(0, 1); + + var events = CollectEvents(ResumeStateMachineAsyncCallstackKeyword | StateMachineAsyncCoreKeywords, () => + { + RunScenarioAndFlush(() => StateMachineAsync_PoolingValueTask_AppendCallstack_FiresOnLateParentRegistration_Parent_Marker().AsTask()); + }); + + // DumpAllEvents(events); + + var stream = ParseAllEvents(events); + + var childOnlyResumes = stream.CallstacksWithMarker(AsyncEventID.ResumeStateMachineAsyncCallstack, nameof(StateMachineAsync_PoolingValueTask_AppendCallstack_FiresOnLateParentRegistration_Child_Marker)); + AssertNotEmpty(stream, childOnlyResumes); + + var appendsWithParent = stream.CallstacksWithMarker(AsyncEventID.AppendStateMachineAsyncCallstack, nameof(StateMachineAsync_PoolingValueTask_AppendCallstack_FiresOnLateParentRegistration_Parent_Marker)); + AssertNotEmpty(stream, appendsWithParent); + } + + [RuntimeAsyncMethodGeneration(false)] + [MethodImpl(MethodImplOptions.NoInlining)] + [AsyncMethodBuilder(typeof(PoolingAsyncValueTaskMethodBuilder<>))] + private static async ValueTask StateMachineAsync_PoolingValueTaskOfT_Level3() + { + await Task.Delay(100); + return 1; + } + + [RuntimeAsyncMethodGeneration(false)] + [MethodImpl(MethodImplOptions.NoInlining)] + [AsyncMethodBuilder(typeof(PoolingAsyncValueTaskMethodBuilder<>))] + private static async ValueTask StateMachineAsync_PoolingValueTaskOfT_Level2() + { + return await StateMachineAsync_PoolingValueTaskOfT_Level3() + 1; + } + + [RuntimeAsyncMethodGeneration(false)] + [MethodImpl(MethodImplOptions.NoInlining)] + [AsyncMethodBuilder(typeof(PoolingAsyncValueTaskMethodBuilder<>))] + private static async ValueTask StateMachineAsync_PoolingValueTaskOfT_Level1() + { + return await StateMachineAsync_PoolingValueTaskOfT_Level2() + 1; + } + + [RuntimeAsyncMethodGeneration(false)] + [MethodImpl(MethodImplOptions.NoInlining)] + [AsyncMethodBuilder(typeof(PoolingAsyncValueTaskMethodBuilder<>))] + private static async ValueTask StateMachineAsync_PoolingValueTaskOfT_CallstackDepthMatchesChainDepth_Marker() + { + return await StateMachineAsync_PoolingValueTaskOfT_Level1(); + } + + [ConditionalFact(typeof(AsyncProfilerTests), nameof(IsStateMachineAsyncAndThreadingSupported))] + public void StateMachineAsync_PoolingValueTaskOfT_CallstackDepthMatchesChainDepth() + { + var events = CollectEvents(ResumeStateMachineAsyncCallstackKeyword | StateMachineAsyncCoreKeywords, () => + { + RunScenarioAndFlush(() => StateMachineAsync_PoolingValueTaskOfT_CallstackDepthMatchesChainDepth_Marker().AsTask()); + }); + + // DumpAllEvents(events); + + var stream = ParseAllEvents(events); + + var markerCallstacks = stream.CallstacksWithMarker(AsyncEventID.ResumeStateMachineAsyncCallstack, nameof(StateMachineAsync_PoolingValueTaskOfT_CallstackDepthMatchesChainDepth_Marker)); + AssertNotEmpty(stream, markerCallstacks); + + // Marker -> Level1 -> Level2 -> Level3. + AssertEqual(stream, 4, markerCallstacks[0].FrameCount); + } + + [RuntimeAsyncMethodGeneration(false)] + [MethodImpl(MethodImplOptions.NoInlining)] + private static async Task StateMachineAsync_ResetContext_ReplaysPendingV1Chain_Inner_Marker() + { + using var dummy = new TestEventListener(); + dummy.AddSource(AsyncProfilerEventSourceName, EventLevel.Informational, EventKeywords.None); + await Task.Yield(); + } + + [RuntimeAsyncMethodGeneration(false)] + [MethodImpl(MethodImplOptions.NoInlining)] + private static async Task StateMachineAsync_ResetContext_ReplaysPendingV1Chain_Mid_Marker() + { + await StateMachineAsync_ResetContext_ReplaysPendingV1Chain_Inner_Marker(); + } + + [RuntimeAsyncMethodGeneration(false)] + [MethodImpl(MethodImplOptions.NoInlining)] + private static async Task StateMachineAsync_ResetContext_ReplaysPendingV1Chain_Outer_Marker() + { + await StateMachineAsync_ResetContext_ReplaysPendingV1Chain_Mid_Marker(); + } + + [ConditionalFact(typeof(AsyncProfilerTests), nameof(IsStateMachineAsyncAndThreadingSupported))] + public void StateMachineAsync_ResetContext_ReplaysPendingV1Chain() + { + var events = CollectEvents(AllStateMachineAsyncKeywords, () => + { + RunScenarioAndFlush(() => StateMachineAsync_ResetContext_ReplaysPendingV1Chain_Outer_Marker()); + }); + + // DumpAllEvents(events); + + var stream = ParseAllEvents(events); + + // Locate the StateMachine dispatcher driving the marker chain via its ResumeStateMachineAsyncCallstack + // events (which carry the marker frames in their callstack). + var markerCallstacks = stream.CallstacksWithMarker(AsyncEventID.ResumeStateMachineAsyncCallstack, "StateMachineAsync_ResetContext_ReplaysPendingV1Chain"); + AssertNotEmpty(stream, markerCallstacks); + + // Thread-scoped replay assertion: at least one OS thread must have seen two + // ResetAsyncThreadContext events AND show a marker ResumeStateMachineAsyncCallstack plus + // matching ResumeStateMachineAsyncContext after its most recent reset. Multiple threads + // in the trace can accumulate two resets (e.g. the test thread re-enables on + // its initial event then again later), so we have to look at each candidate + // thread, not just the first one. The thread that actually drove the replay + // is the one the StateMachine continuation resumed onto. + var resetsByThread = stream.All + .Where(e => e.EventId == AsyncEventID.ResetAsyncThreadContext && e.OsThreadId != 0) + .GroupBy(e => e.OsThreadId) + .Where(g => g.Count() >= 2) + .ToList(); + AssertNotEmpty(stream, resetsByThread); + + bool found = false; + foreach (var threadResets in resetsByThread) + { + ulong threadId = threadResets.Key; + long lastResetTimestamp = threadResets.Max(e => e.Timestamp); + + var postResetMarkerCallstacks = markerCallstacks + .Where(c => c.OsThreadId == threadId && c.Timestamp >= lastResetTimestamp) + .ToList(); + if (postResetMarkerCallstacks.Count == 0) + { + continue; + } + + var replayDispatcherIds = postResetMarkerCallstacks.Select(c => c.DispatcherId).ToHashSet(); + var postResetResumeContext = stream.All + .Where(e => e.EventId == AsyncEventID.ResumeStateMachineAsyncContext + && e.OsThreadId == threadId + && e.Timestamp >= lastResetTimestamp + && replayDispatcherIds.Contains(e.DispatcherId)) + .ToList(); + if (postResetResumeContext.Count == 0) + { + continue; + } + + found = true; + break; + } + + AssertTrue(stream, found, + "Expected at least one OS thread with >= 2 ResetAsyncThreadContext events followed by a marker ResumeAsyncCallstack and matching ResumeAsyncContext."); + } + + [RuntimeAsyncMethodGeneration(false)] + [MethodImpl(MethodImplOptions.NoInlining)] + private static async Task StateMachineAsync_ResetContext_ReplaysMultipleDispatchers_Inner_Marker( + Task gate, + ManualResetEventSlim block, + Action onBlocked, + TaskCompletionSource innerDone) + { + await gate; + onBlocked(); + block.Wait(); + innerDone.SetResult(); + } + + [RuntimeAsyncMethodGeneration(false)] + [MethodImpl(MethodImplOptions.NoInlining)] + private static async Task StateMachineAsync_ResetContext_ReplaysMultipleDispatchers_Outer_Marker(TaskCompletionSource innerDone) + { + await innerDone.Task; + } + + [ConditionalFact(typeof(AsyncProfilerTests), nameof(IsStateMachineAsyncAndThreadingSupported))] + public void StateMachineAsync_ResetContext_ReplaysMultipleDispatchers() + { + var events = CollectEvents(AllStateMachineAsyncKeywords, () => + { + RunScenarioAndFlush(async () => + { + var gate = new TaskCompletionSource(TaskCreationOptions.RunContinuationsAsynchronously); + var block = new ManualResetEventSlim(false); + var blocked = new TaskCompletionSource(TaskCreationOptions.RunContinuationsAsynchronously); + var innerDone = new TaskCompletionSource(); + + Task inner = StateMachineAsync_ResetContext_ReplaysMultipleDispatchers_Inner_Marker(gate.Task, block, () => blocked.SetResult(), innerDone); + Task outer = StateMachineAsync_ResetContext_ReplaysMultipleDispatchers_Outer_Marker(innerDone); + + _ = Task.Run(() => gate.SetResult()); + + await blocked.Task; + + var dummy = new TestEventListener(); + try + { + dummy.AddSource(AsyncProfilerEventSourceName, EventLevel.Informational, EventKeywords.None); + + block.Set(); + + await outer; + await inner; + } + finally + { + dummy.Dispose(); + } + }); + }); + + // DumpAllEvents(events); + + var stream = ParseAllEvents(events); + + // The replay must emit at least one ResumeAsyncCallstack carrying the StateMachine marker chain. + var markerCallstacks = stream.CallstacksWithMarker(AsyncEventID.ResumeStateMachineAsyncCallstack, "StateMachineAsync_ResetContext_ReplaysMultipleDispatchers"); + AssertNotEmpty(stream, markerCallstacks); + + // The decisive proof: find an OS thread where a single ResetAsyncThreadContext + // event is followed by >= 2 ResumeAsyncContext events before the next reset + // (or end of trace). A normal dispatcher resume can only emit one Resume + // per MoveNext invocation; observing >= 2 in a single reset window proves + // the walker traversed multiple nested dispatchers from a single ResetContext. + var resetsByThread = stream.All + .Where(e => e.EventId == AsyncEventID.ResetAsyncThreadContext && e.OsThreadId != 0) + .GroupBy(e => e.OsThreadId) + .ToList(); + AssertNotEmpty(stream, resetsByThread); + + bool found = false; + foreach (var threadResets in resetsByThread) + { + ulong threadId = threadResets.Key; + var orderedResets = threadResets.OrderBy(e => e.Timestamp).ToList(); + + for (int i = 0; i < orderedResets.Count; i++) + { + long windowStart = orderedResets[i].Timestamp; + long windowEnd = (i + 1 < orderedResets.Count) + ? orderedResets[i + 1].Timestamp + : long.MaxValue; + + int resumeContextCount = stream.All + .Count(e => e.EventId == AsyncEventID.ResumeStateMachineAsyncCallstack + && e.OsThreadId == threadId + && e.Timestamp >= windowStart + && e.Timestamp < windowEnd); + + if (resumeContextCount >= 2) + { + found = true; + break; + } + } + + if (found) + { + break; + } + } + + AssertTrue(stream, found, + "Expected at least one OS thread with a ResetAsyncThreadContext event " + + "followed by >= 2 ResumeAsyncContext events in the same reset window, " + + "proving the reset-replay walker traversed multiple nested dispatchers."); + } + + [RuntimeAsyncMethodGeneration(false)] + [MethodImpl(MethodImplOptions.NoInlining)] + private static async Task StateMachineAsync_ResetContext_ReplayResumeCompleteBalance_Inner_Marker() + { + using var dummy = new TestEventListener(); + dummy.AddSource(AsyncProfilerEventSourceName, EventLevel.Informational, EventKeywords.None); + await Task.Yield(); + } + + [RuntimeAsyncMethodGeneration(false)] + [MethodImpl(MethodImplOptions.NoInlining)] + private static async Task StateMachineAsync_ResetContext_ReplayResumeCompleteBalance_Mid_Marker() + { + await StateMachineAsync_ResetContext_ReplayResumeCompleteBalance_Inner_Marker(); + } + + [RuntimeAsyncMethodGeneration(false)] + [MethodImpl(MethodImplOptions.NoInlining)] + private static async Task StateMachineAsync_ResetContext_ReplayResumeCompleteBalance_Outer_Marker() + { + await StateMachineAsync_ResetContext_ReplayResumeCompleteBalance_Mid_Marker(); + } + + [ConditionalFact(typeof(AsyncProfilerTests), nameof(IsStateMachineAsyncAndThreadingSupported))] + public void StateMachineAsync_ResetContext_ReplayResumeCompleteBalance() + { + var events = CollectEvents(AllStateMachineAsyncKeywords, () => + { + RunScenarioAndFlush(() => StateMachineAsync_ResetContext_ReplayResumeCompleteBalance_Outer_Marker()); + }); + + // DumpAllEvents(events); + + var stream = ParseAllEvents(events); + + // Locate the replayed marker ResumeAsyncCallstack: the reset (triggered by the dummy + // listener inside the inner marker) replays the suspended StateMachine chain, producing a + // callstack carrying the marker frames. + var markerCallstacks = stream.CallstacksWithMarker(AsyncEventID.ResumeStateMachineAsyncCallstack, "StateMachineAsync_ResetContext_ReplayResumeCompleteBalance"); + AssertNotEmpty(stream, markerCallstacks); + + // Resume/Complete balance across the replay boundary. StateMachine uses a per-MoveNext dispatcher + // model, so the marker chain spans a dispatcher tree whose deepest replayed callstack + // has N frames; each of those N resumed async methods must eventually complete. Balance + // is NOT preserved per reset epoch by design (a Resume can land in one epoch and its + // Complete in a later epoch once a config change bumps the revision mid-chain), so the + // count is reconstructed over the whole trace, scoped to the marker dispatcher's chain. + // Using >= keeps the assertion robust against additional method events from the harness. + var deepest = markerCallstacks.OrderByDescending(c => c.FrameCount).First(); + ulong leafDispatcherId = deepest.DispatcherId; int completeMethodCount = stream.ChainEventsFromDispatcher(leafDispatcherId) .Count(e => e.EventId == AsyncEventID.CompleteStateMachineAsyncMethod); @@ -2212,10 +2744,6 @@ public async Task StateMachineAsync_SingleThread_ChainEventsAndCallstack() var stream = ParseAllEvents(events); - // The marker frame must appear in a Resume callstack -- proves the chain was walkable. - // This is the deterministic, scheduling-independent core of the test: when the chain - // first resumes (gate set), StateMachine emits a ResumeAsyncCallstack carrying the remaining chain, - // which at first resume is all three frames. var markerCallstacks = stream.CallstacksWithMarker(AsyncEventID.ResumeStateMachineAsyncCallstack, nameof(StateMachineAsync_SingleThread_ChainEventsAndCallstack_Marker)); AssertNotEmpty(stream, markerCallstacks); @@ -2228,14 +2756,155 @@ public async Task StateMachineAsync_SingleThread_ChainEventsAndCallstack() AssertContains(stream, nameof(StateMachineAsync_SingleThread_ChainEventsAndCallstack_Mid_Marker), frameNames); AssertContains(stream, nameof(StateMachineAsync_SingleThread_ChainEventsAndCallstack_Marker), frameNames); - // Lifecycle ORDERING (not counting). We deliberately do not assert global Create/Complete - // balance or an exact dispatcher count: depending on whether the resume cascade inlines, - // the chain may collapse into one dispatcher or split into several across scheduling - // boundaries, and a scheduled/nested resume can push a dispatcher's CompleteAsyncContext - // (or its method-complete events) outside the captured trace window. Those counts are - // therefore not invariants. What IS invariant is the per-dispatcher ordering of whatever - // events were captured: a dispatcher is created before it resumes, and completes (if its - // Complete is in-window) after it resumes. + foreach (ulong dispatcherId in markerCallstacks.Select(c => c.DispatcherId).Distinct()) + { + var dispatcherEvents = stream.All + .Where(e => e.DispatcherId == dispatcherId) + .OrderBy(e => e.Timestamp) + .ToList(); + + ParsedEvent? createEvt = dispatcherEvents.FirstOrDefault(e => e.EventId == AsyncEventID.CreateStateMachineAsyncContext); + ParsedEvent? resumeEvt = dispatcherEvents.FirstOrDefault(e => e.EventId == AsyncEventID.ResumeStateMachineAsyncContext); + ParsedEvent? completeEvt = dispatcherEvents.FirstOrDefault(e => e.EventId == AsyncEventID.CompleteStateMachineAsyncContext); + + if (createEvt is not null && resumeEvt is not null) + { + AssertTrue(stream, createEvt.Timestamp <= resumeEvt.Timestamp, + $"Dispatcher {dispatcherId}: CreateStateMachineAsyncContext must precede ResumeStateMachineAsyncContext."); + } + + if (resumeEvt is not null && completeEvt is not null) + { + AssertTrue(stream, resumeEvt.Timestamp <= completeEvt.Timestamp, + $"Dispatcher {dispatcherId}: CompleteStateMachineAsyncContext must follow ResumeStateMachineAsyncContext."); + } + } + } + + [RuntimeAsyncMethodGeneration(false)] + [MethodImpl(MethodImplOptions.NoInlining)] + private static async ValueTask StateMachineAsync_ValueTask_SingleThread_ChainEventsAndCallstack_Inner_Marker(Task gate) + { + await gate; + } + + [RuntimeAsyncMethodGeneration(false)] + [MethodImpl(MethodImplOptions.NoInlining)] + private static async ValueTask StateMachineAsync_ValueTask_SingleThread_ChainEventsAndCallstack_Mid_Marker(Task gate) + { + await StateMachineAsync_ValueTask_SingleThread_ChainEventsAndCallstack_Inner_Marker(gate); + } + + [RuntimeAsyncMethodGeneration(false)] + [MethodImpl(MethodImplOptions.NoInlining)] + private static async ValueTask StateMachineAsync_ValueTask_SingleThread_ChainEventsAndCallstack_Marker(Task gate) + { + await StateMachineAsync_ValueTask_SingleThread_ChainEventsAndCallstack_Mid_Marker(gate); + } + + [ConditionalFact(typeof(AsyncProfilerTests), nameof(IsStateMachineAsyncSupported))] + public async Task StateMachineAsync_ValueTask_SingleThread_ChainEventsAndCallstack() + { + var events = await CollectEventsAsync(ResumeStateMachineAsyncCallstackKeyword | StateMachineAsyncCoreKeywords | StateMachineAsyncMethodKeywords, async () => + { + var tcs = new TaskCompletionSource(); + ValueTask chain = StateMachineAsync_ValueTask_SingleThread_ChainEventsAndCallstack_Marker(tcs.Task); + tcs.SetResult(); + await chain; + }); + + // DumpAllEvents(events); + + var stream = ParseAllEvents(events); + + var markerCallstacks = stream.CallstacksWithMarker(AsyncEventID.ResumeStateMachineAsyncCallstack, nameof(StateMachineAsync_ValueTask_SingleThread_ChainEventsAndCallstack_Marker)); + AssertNotEmpty(stream, 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(); + AssertContains(stream, nameof(StateMachineAsync_ValueTask_SingleThread_ChainEventsAndCallstack_Inner_Marker), frameNames); + AssertContains(stream, nameof(StateMachineAsync_ValueTask_SingleThread_ChainEventsAndCallstack_Mid_Marker), frameNames); + AssertContains(stream, nameof(StateMachineAsync_ValueTask_SingleThread_ChainEventsAndCallstack_Marker), frameNames); + + foreach (ulong dispatcherId in markerCallstacks.Select(c => c.DispatcherId).Distinct()) + { + var dispatcherEvents = stream.All + .Where(e => e.DispatcherId == dispatcherId) + .OrderBy(e => e.Timestamp) + .ToList(); + + ParsedEvent? createEvt = dispatcherEvents.FirstOrDefault(e => e.EventId == AsyncEventID.CreateStateMachineAsyncContext); + ParsedEvent? resumeEvt = dispatcherEvents.FirstOrDefault(e => e.EventId == AsyncEventID.ResumeStateMachineAsyncContext); + ParsedEvent? completeEvt = dispatcherEvents.FirstOrDefault(e => e.EventId == AsyncEventID.CompleteStateMachineAsyncContext); + + if (createEvt is not null && resumeEvt is not null) + { + AssertTrue(stream, createEvt.Timestamp <= resumeEvt.Timestamp, + $"Dispatcher {dispatcherId}: CreateStateMachineAsyncContext must precede ResumeStateMachineAsyncContext."); + } + + if (resumeEvt is not null && completeEvt is not null) + { + AssertTrue(stream, resumeEvt.Timestamp <= completeEvt.Timestamp, + $"Dispatcher {dispatcherId}: CompleteStateMachineAsyncContext must follow ResumeStateMachineAsyncContext."); + } + } + } + + [RuntimeAsyncMethodGeneration(false)] + [MethodImpl(MethodImplOptions.NoInlining)] + [AsyncMethodBuilder(typeof(PoolingAsyncValueTaskMethodBuilder))] + private static async ValueTask StateMachineAsync_PoolingValueTask_SingleThread_ChainEventsAndCallstack_Inner_Marker(Task gate) + { + await gate; + } + + [RuntimeAsyncMethodGeneration(false)] + [MethodImpl(MethodImplOptions.NoInlining)] + [AsyncMethodBuilder(typeof(PoolingAsyncValueTaskMethodBuilder))] + private static async ValueTask StateMachineAsync_PoolingValueTask_SingleThread_ChainEventsAndCallstack_Mid_Marker(Task gate) + { + await StateMachineAsync_PoolingValueTask_SingleThread_ChainEventsAndCallstack_Inner_Marker(gate); + } + + [RuntimeAsyncMethodGeneration(false)] + [MethodImpl(MethodImplOptions.NoInlining)] + [AsyncMethodBuilder(typeof(PoolingAsyncValueTaskMethodBuilder))] + private static async ValueTask StateMachineAsync_PoolingValueTask_SingleThread_ChainEventsAndCallstack_Marker(Task gate) + { + await StateMachineAsync_PoolingValueTask_SingleThread_ChainEventsAndCallstack_Mid_Marker(gate); + } + + [ConditionalFact(typeof(AsyncProfilerTests), nameof(IsStateMachineAsyncSupported))] + public async Task StateMachineAsync_PoolingValueTask_SingleThread_ChainEventsAndCallstack() + { + var events = await CollectEventsAsync(ResumeStateMachineAsyncCallstackKeyword | StateMachineAsyncCoreKeywords | StateMachineAsyncMethodKeywords, async () => + { + var tcs = new TaskCompletionSource(); + ValueTask chain = StateMachineAsync_PoolingValueTask_SingleThread_ChainEventsAndCallstack_Marker(tcs.Task); + tcs.SetResult(); + await chain; + }); + + // DumpAllEvents(events); + + var stream = ParseAllEvents(events); + + var markerCallstacks = stream.CallstacksWithMarker(AsyncEventID.ResumeStateMachineAsyncCallstack, nameof(StateMachineAsync_PoolingValueTask_SingleThread_ChainEventsAndCallstack_Marker)); + AssertNotEmpty(stream, 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(); + AssertContains(stream, nameof(StateMachineAsync_PoolingValueTask_SingleThread_ChainEventsAndCallstack_Inner_Marker), frameNames); + AssertContains(stream, nameof(StateMachineAsync_PoolingValueTask_SingleThread_ChainEventsAndCallstack_Mid_Marker), frameNames); + AssertContains(stream, nameof(StateMachineAsync_PoolingValueTask_SingleThread_ChainEventsAndCallstack_Marker), frameNames); + foreach (ulong dispatcherId in markerCallstacks.Select(c => c.DispatcherId).Distinct()) { var dispatcherEvents = stream.All