Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -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);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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))
Expand Down Expand Up @@ -179,6 +189,8 @@ internal sealed class AsyncStateMachineDispatcher : Task<VoidTaskResult>, IAsync
private IAsyncStateMachineBox? _inner;
private Action? _moveNextAction;

internal IAsyncStateMachineBox? InnerBox => _inner;

internal IAsyncStateMachineBox? LastContinuation;

internal bool ReachedLastContinuation;
Expand Down Expand Up @@ -225,6 +237,9 @@ public unsafe void MoveNext()
info.Dispatcher = this;
info.AsyncProfilerInfo.CurrentContinuation = inner;

LastContinuation = null;
ReachedLastContinuation = false;

try
{
InstrumentedMoveNext(ref info, inner);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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)
{
Comment on lines +1696 to 1700
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
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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}");
}
Expand Down Expand Up @@ -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;
Expand All @@ -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 > suspendIdx,
"Outer did not complete after it re-suspended; inline inner completion was mis-attributed to it");
}
}

Expand Down
Loading