From 444a9adc4dad31cb69c866ccd870d16b1533c223 Mon Sep 17 00:00:00 2001 From: lateralusX Date: Wed, 1 Jul 2026 12:16:21 +0200 Subject: [PATCH 1/2] Reuse v1 dispatcher on suspend. --- .../Runtime/CompilerServices/AsyncProfiler.cs | 15 +++++++++++++++ .../AsyncStateMachineDispatcher.cs | 15 +++++++++++++++ .../AsyncProfilerTests.cs | 12 ++++++------ .../AsyncProfilerV1Tests.cs | 19 ++++++++++++------- 4 files changed, 48 insertions(+), 13 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 3b508fbf90518d..c646443c910719 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 @@ -961,6 +961,21 @@ public static void Create(ulong parentDispatcherId, ulong dispatcherId) AsyncThreadContext.Release(context); } + public static void Append(AsyncStateMachineDispatcher dispatcher, ref Info info) + { + AsyncThreadContext context = AsyncThreadContext.Acquire(ref info); + + SyncPoint.Check(context); + + EventKeywords activeEventKeywords = context.ActiveEventKeywords; + if (IsEnabled.AnyAsyncEvents(activeEventKeywords) && IsEnabled.ResumeStateMachineAsyncCallstackEvent(activeEventKeywords)) + { + ResumeAsyncContext.Append(dispatcher, context, Stopwatch.GetTimestamp()); + } + + AsyncThreadContext.Release(context); + } + public static void EmitEvent(AsyncThreadContext context, long currentTimestamp, ulong parentDispatcherId, ulong dispatcherId, AsyncEventID eventID) { Debug.Assert(eventID == AsyncEventID.CreateRuntimeAsyncContext || eventID == AsyncEventID.CreateStateMachineAsyncContext); 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 5a1f6ef5179e02..b79d00965c213e 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 @@ -71,6 +71,16 @@ internal static unsafe IAsyncStateMachineBox CreateDispatcher(IAsyncStateMachine AsyncStateMachineDispatcherInfo* info = AsyncStateMachineDispatcherInfo.t_current; AsyncStateMachineDispatcher? activeDispatcher = info != null ? info->Dispatcher : null; + if (activeDispatcher != null && ReferenceEquals(activeDispatcher.InnerBox, box)) + { + if (AsyncInstrumentation.IsEnabled.ResumeAsyncContext(flags)) + { + AsyncProfiler.CreateAsyncContext.Append(activeDispatcher, ref info->AsyncProfilerInfo); + } + + return activeDispatcher; + } + AsyncStateMachineDispatcher dispatcher = new AsyncStateMachineDispatcher(box); if (AsyncInstrumentation.IsEnabled.CreateAsyncContext(flags) || AsyncInstrumentation.IsEnabled.ResumeAsyncContext(flags)) @@ -179,6 +189,8 @@ internal sealed class AsyncStateMachineDispatcher : Task, IAsync private IAsyncStateMachineBox? _inner; private Action? _moveNextAction; + internal IAsyncStateMachineBox? InnerBox => _inner; + internal IAsyncStateMachineBox? LastContinuation; internal bool ReachedLastContinuation; @@ -225,6 +237,9 @@ public unsafe void MoveNext() info.Dispatcher = this; info.AsyncProfilerInfo.CurrentContinuation = inner; + LastContinuation = null; + ReachedLastContinuation = false; + try { InstrumentedMoveNext(ref info, inner); diff --git a/src/libraries/System.Runtime/tests/System.Threading.Tasks.Tests/System.Runtime.CompilerServices/AsyncProfilerTests.cs b/src/libraries/System.Runtime/tests/System.Threading.Tasks.Tests/System.Runtime.CompilerServices/AsyncProfilerTests.cs index 821881db101f68..1ebe969e4ca011 100644 --- a/src/libraries/System.Runtime/tests/System.Threading.Tasks.Tests/System.Runtime.CompilerServices/AsyncProfilerTests.cs +++ b/src/libraries/System.Runtime/tests/System.Threading.Tasks.Tests/System.Runtime.CompilerServices/AsyncProfilerTests.cs @@ -1693,19 +1693,19 @@ private static void AssertExactlyOneCreateAndComplete(ParsedEventStream stream, AssertTrue(stream, completes == 1, $"Expected exactly 1 CompleteAsyncContext for {chainName} (DispatcherId {dispatcherId}), got {completes}"); } - // StateMachine-friendly variant: StateMachine's per-MoveNext dispatcher model emits one Create per await - // suspension, so a method with N awaits produces N dispatchers / N Creates within the - // same dispatcher tree. Each dispatcher ends in exactly one Suspend (it yielded) or one - // Complete (it finished), so every Create is balanced by a Suspend or a Complete: - // creates == completes + suspends (creates >= 1). + // StateMachine dispatcher model with reuse: a single dispatcher spans all of a method's yields + // (it resumes/suspends multiple times) and is created + completed exactly once. So within a + // dispatcher tree every Create is balanced by exactly one Complete; Suspends are interior events. private static void AssertCreateBalancesSuspendAndCompleteInChain(ParsedEventStream stream, ulong dispatcherId, string chainName) { var ids = stream.ChainEventsFromDispatcher(dispatcherId).Select(e => e.EventId).ToList(); int creates = ids.Count(id => id == AsyncEventID.CreateStateMachineAsyncContext); + int resumes = ids.Count(id => id == AsyncEventID.ResumeStateMachineAsyncContext); int suspends = ids.Count(id => id == AsyncEventID.SuspendStateMachineAsyncContext); int completes = ids.Count(id => id == AsyncEventID.CompleteStateMachineAsyncContext); AssertTrue(stream, creates >= 1, $"Expected at least 1 CreateStateMachineAsyncContext for {chainName} (DispatcherId {dispatcherId}), got {creates}"); - AssertTrue(stream, creates == completes + suspends, $"Expected CreateStateMachineAsyncContext count == CompleteStateMachineAsyncContext + SuspendStateMachineAsyncContext count for {chainName} (DispatcherId {dispatcherId}), got {creates} creates, {completes} completes, {suspends} suspends"); + AssertTrue(stream, creates == completes, $"Expected CreateStateMachineAsyncContext count == CompleteStateMachineAsyncContext count for {chainName} (DispatcherId {dispatcherId}), got {creates} creates, {completes} completes"); + AssertTrue(stream, resumes == completes + suspends, $"Expected ResumeStateMachineAsyncContext count == Complete + Suspend count for {chainName} (DispatcherId {dispatcherId}), got {resumes} resumes, {completes} completes, {suspends} suspends"); } private sealed class InlinePostSynchronizationContext : SynchronizationContext 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 31a5eb5a8dfee4..24d5543da70189 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 @@ -1541,10 +1541,13 @@ public void StateMachineAsync_WaitThenYield_BalancesResumeAndComplete() AssertTrue(stream, createCount >= 1, $"Expected at least one CreateStateMachineAsyncContext event, got {createCount}"); - // Each created dispatcher and each resume ends in exactly one suspend or one complete. - AssertEqual(stream, createCount, completeCount + suspendCount); + // Each resume cycle ends in exactly one suspend or one complete (model-agnostic). AssertEqual(stream, resumeCount, completeCount + suspendCount); + // With dispatcher reuse a single dispatcher spans all of a method's yields: it may suspend + // multiple times (interior) but is created and completed exactly once, so creates == completes. + AssertEqual(stream, createCount, completeCount); + AssertTrue(stream, createCount >= 3, $"Expected fan-out chain to produce at least 3 CreateStateMachineAsyncContext events (root + 2 child wraps), got {createCount}"); } @@ -2507,9 +2510,11 @@ public void StateMachineAsync_PoolingValueTask_InlineReentrantCompletion() AsyncEventID.ResumeStateMachineAsyncCallstack, nameof(StateMachineAsync_PoolingValueTask_InlineReentrantCompletion_Outer_Marker)); AssertNotEmpty(stream, markerCallstacks); - // The Outer frame resumes after resumeGate and then RE-SUSPENDS on finalGate, so its dispatcher must - // emit a Suspend and must NOT emit a Complete at that point. A mis-attributed inline completion of - // Inner flips CurrentContinuationCompleted on this frame and produces a (wrong) Complete instead. + // The Outer frame resumes after resumeGate, fires Inner's inline completion, then RE-SUSPENDS on + // finalGate before finally completing. With dispatcher reuse this is one dispatcher that Suspends + // (interior) and later Completes. A mis-attributed inline completion of Inner would flip + // CurrentContinuationCompleted on the Outer frame during its first resume, making it Complete + // before (or instead of) that Suspend. So: Outer must re-suspend, and its Complete must come after. var markerDispatcherIds = markerCallstacks.Select(c => c.DispatcherId).Distinct().ToList(); bool foundReSuspend = false; @@ -2527,8 +2532,8 @@ public void StateMachineAsync_PoolingValueTask_InlineReentrantCompletion() { foundReSuspend = true; int completeIdx = ids.IndexOf(AsyncEventID.CompleteStateMachineAsyncContext, resumeIdx + 1); - AssertTrue(stream, completeIdx < 0, - "Outer re-suspending frame emitted a Complete; inline inner completion was mis-attributed to it"); + AssertTrue(stream, completeIdx < 0 || suspendIdx < completeIdx, + "Outer completed before it re-suspended; inline inner completion was mis-attributed to it"); } } From eb135c6bcf97753efc55cef0595b54ed9cb5682b Mon Sep 17 00:00:00 2001 From: Johan Lorensson Date: Tue, 7 Jul 2026 15:14:17 +0200 Subject: [PATCH 2/2] Strength test assertion. Co-authored-by: Copilot Autofix powered by AI <175728472+Copilot@users.noreply.github.com> --- .../System.Runtime.CompilerServices/AsyncProfilerV1Tests.cs | 4 ++-- 1 file changed, 2 insertions(+), 2 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 24d5543da70189..340c77d6e7cc30 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 @@ -2532,8 +2532,8 @@ public void StateMachineAsync_PoolingValueTask_InlineReentrantCompletion() { foundReSuspend = true; int completeIdx = ids.IndexOf(AsyncEventID.CompleteStateMachineAsyncContext, resumeIdx + 1); - AssertTrue(stream, completeIdx < 0 || suspendIdx < completeIdx, - "Outer completed before it re-suspended; inline inner completion was mis-attributed to it"); + AssertTrue(stream, completeIdx > suspendIdx, + "Outer did not complete after it re-suspended; inline inner completion was mis-attributed to it"); } }