Skip to content
Merged
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 @@ -337,6 +337,7 @@ private protected override void InstrumentedMoveNext(Thread? threadPoolThread, A
base.InstrumentedMoveNext(threadPoolThread, flags);
}

[MethodImpl(MethodImplOptions.NoInlining)]
[StackTraceHidden]
// Diagnostic tooling depends on this name when classifying async callstack frames.
private unsafe void MoveNextAsDispatcher(Thread? threadPoolThread, AsyncInstrumentation.Flags flags)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -163,7 +163,7 @@ private static Type GetCoreLibNestedType(string declaringTypeFullName, string ne

// Asserts the async dispatch method the stitcher recognizes by name still exists under that name.
// Keys mirror the string constants in the stitcher's AsyncStitchBoundary classifier.
private static void AssertAsyncDispatchMethodExists(string key)
private static MethodInfo AssertAsyncDispatchMethodExists(string key)
{
MethodInfo? method = key switch
{
Expand All @@ -184,6 +184,16 @@ private static void AssertAsyncDispatchMethodExists(string key)

Assert.True(method is not null,
$"Async dispatch method for contract key '{key}' was not found; the CPU stitcher recognizes it by name.");
return method!;
}

private static void AssertMethodImplementationFlags(
MethodInfo method, MethodImplAttributes requiredFlags, string contractKey)
{
MethodImplAttributes actualFlags = method.GetMethodImplementationFlags();
Assert.True((actualFlags & requiredFlags) == requiredFlags,
$"Async dispatch method for contract key '{contractKey}' must have implementation flags " +
$"'{requiredFlags}' but had '{actualFlags}'.");
}

// ------------------------------------------------------------------------------------------------
Expand Down Expand Up @@ -223,20 +233,29 @@ _ when name.StartsWith("Continuation_Wrapper_", StringComparison.Ordinal) => "Wr
// Captures the current thread's physical managed stack UNFILTERED and returns the stitcher-recognized
// async boundary labels in leaf->root order. Must be called from inside a resumed async continuation
// (while the async profiler is active) so the dispatch machinery is on the stack.
private static List<string> CaptureAsyncBoundarySequence() => CaptureAsyncStack().Boundaries;
private static (List<string> Boundaries, List<string> DiagnosticFrames) CaptureAsyncBoundarySequence()
{
var capture = CaptureAsyncStack();
return (capture.Boundaries, capture.DiagnosticFrames);
}

// Captures the physical stack once and returns, both leaf->root: the stitcher boundary labels, and the
// per-frame declaring type names. The declaring type names let inline-completion tests locate a
// still-completing child's state-machine frame (its method name is the compiler-generated MoveNext, so
// it is identified by its declaring state-machine type name) relative to the resuming parent frame.
private static (List<string> Boundaries, List<string> DeclaringTypeNames) CaptureAsyncStack()
// Captures the physical stack once and returns, all leaf->root: the stitcher boundary labels, the
// per-frame declaring type names, and diagnostic frame names. The declaring type names let
// inline-completion tests locate a still-completing child's state-machine frame (its method name is the
// compiler-generated MoveNext, so it is identified by its declaring state-machine type name) relative
// to the resuming parent frame. The diagnostic names preserve every captured frame for failure output.
private static (List<string> Boundaries, List<string> DeclaringTypeNames, List<string> DiagnosticFrames) CaptureAsyncStack()
{
var boundaries = new List<string>();
var typeNames = new List<string>();
var diagnosticFrames = new List<string>();
foreach (StackFrame frame in new StackTrace(fNeedFileInfo: false).GetFrames())
{
DiagnosticMethodInfo? info = DiagnosticMethodInfo.Create(frame);
typeNames.Add(info?.DeclaringTypeName ?? string.Empty);
string typeName = info?.DeclaringTypeName ?? "<unknown-type>";
string methodName = info?.Name ?? "<unknown-method>";
typeNames.Add(typeName);
diagnosticFrames.Add($"{typeName}::{methodName}");

string? label = ClassifyAsyncBoundaryFrame(info);
if (label is not null)
Expand All @@ -245,13 +264,16 @@ private static (List<string> Boundaries, List<string> DeclaringTypeNames) Captur
}
}

return (boundaries, typeNames);
return (boundaries, typeNames, diagnosticFrames);
}

// Asserts that expectedLeafToRoot appears as an ordered subsequence of the captured boundary labels
// (leaf->root). Subsequence (not contiguous) matching tolerates unrelated frames between boundaries
// and repeated boundaries from nested resumes, while still enforcing the required relative order.
private static void AssertBoundarySubsequence(List<string> capturedLeafToRoot, params string[] expectedLeafToRoot)
private static void AssertBoundarySubsequence(
List<string> capturedLeafToRoot,
List<string> diagnosticFramesLeafToRoot,
params string[] expectedLeafToRoot)
{
int matched = 0;
foreach (string label in capturedLeafToRoot)
Expand All @@ -264,13 +286,17 @@ private static void AssertBoundarySubsequence(List<string> capturedLeafToRoot, p

Assert.True(matched == expectedLeafToRoot.Length,
$"Expected async boundary frames [{string.Join(" -> ", expectedLeafToRoot)}] (leaf->root) were not all " +
$"present in order. Captured boundaries (leaf->root): [{string.Join(" -> ", capturedLeafToRoot)}].");
$"present in order. Captured boundaries (leaf->root): [{string.Join(" -> ", capturedLeafToRoot)}]. " +
$"Captured diagnostic frames (leaf->root): [{string.Join(" -> ", diagnosticFramesLeafToRoot)}].");
}

// Asserts each expected fragment appears (as a substring of a declaring type name) as an ordered
// subsequence of the captured declaring type names (leaf->root), tolerating unrelated frames between.
// Used to locate specific user state-machine frames by the method name embedded in their generated type.
private static void AssertFrameOrder(List<string> capturedLeafToRoot, params string[] expectedFragmentsLeafToRoot)
private static void AssertFrameOrder(
List<string> capturedLeafToRoot,
List<string> diagnosticFramesLeafToRoot,
params string[] expectedFragmentsLeafToRoot)
{
int matched = 0;
foreach (string typeName in capturedLeafToRoot)
Expand All @@ -284,7 +310,8 @@ private static void AssertFrameOrder(List<string> capturedLeafToRoot, params str

Assert.True(matched == expectedFragmentsLeafToRoot.Length,
$"Expected frames [{string.Join(" -> ", expectedFragmentsLeafToRoot)}] (leaf->root) were not all present " +
$"in order. Captured declaring types (leaf->root): [{string.Join(" -> ", capturedLeafToRoot)}].");
$"in order. Captured declaring types (leaf->root): [{string.Join(" -> ", capturedLeafToRoot)}]. " +
$"Captured diagnostic frames (leaf->root): [{string.Join(" -> ", diagnosticFramesLeafToRoot)}].");
}

private const string AsyncProfilerEventSourceName = "System.Runtime.CompilerServices.AsyncProfilerEventSource";
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -3384,7 +3384,8 @@ public async Task StateMachineAsync_PoolingValueTask_SingleThread_ChainEventsAnd

[RuntimeAsyncMethodGeneration(false)]
[MethodImpl(MethodImplOptions.NoInlining)]
private static async Task StateMachineAsync_ResumeDispatchStack_ContainsExpectedBoundaryFrames(StrongBox<List<string>> capture)
private static async Task StateMachineAsync_ResumeDispatchStack_ContainsExpectedBoundaryFrames(
StrongBox<(List<string> Boundaries, List<string> DiagnosticFrames)> capture)
{
await Task.Yield();
capture.Value = CaptureAsyncBoundarySequence();
Expand All @@ -3393,7 +3394,7 @@ private static async Task StateMachineAsync_ResumeDispatchStack_ContainsExpected
[ConditionalFact(typeof(AsyncProfilerTests), nameof(IsStateMachineAsyncAndThreadingSupported))]
public void StateMachineAsync_ResumeDispatchStack()
{
var capture = new StrongBox<List<string>>();
var capture = new StrongBox<(List<string> Boundaries, List<string> DiagnosticFrames)>();

var events = CollectEvents(ResumeStateMachineAsyncCallstackKeyword | StateMachineAsyncCoreKeywords, () =>
{
Expand All @@ -3402,17 +3403,21 @@ public void StateMachineAsync_ResumeDispatchStack()

// DumpAllEvents(events);

Assert.True(capture.Value is not null,
Assert.True(capture.Value.Boundaries is not null,
"The state-machine continuation did not run (no boundary sequence was captured).");

// A V1 leaf resume runs through MoveNextAsDispatcher.
AssertBoundarySubsequence(capture.Value!, "V1.MoveNextAsDispatcher");
AssertBoundarySubsequence(
capture.Value.Boundaries,
capture.Value.DiagnosticFrames,
"V1.MoveNextAsDispatcher");
}

[RuntimeAsyncMethodGeneration(false)]
[MethodImpl(MethodImplOptions.NoInlining)]
[AsyncMethodBuilder(typeof(PoolingAsyncValueTaskMethodBuilder))]
private static async ValueTask StateMachineAsync_PoolingResumeDispatchStack_ContainsExpectedBoundaryFrames(StrongBox<List<string>> capture)
private static async ValueTask StateMachineAsync_PoolingResumeDispatchStack_ContainsExpectedBoundaryFrames(
StrongBox<(List<string> Boundaries, List<string> DiagnosticFrames)> capture)
{
await Task.Yield();
capture.Value = CaptureAsyncBoundarySequence();
Expand All @@ -3421,7 +3426,7 @@ private static async ValueTask StateMachineAsync_PoolingResumeDispatchStack_Cont
[ConditionalFact(typeof(AsyncProfilerTests), nameof(IsStateMachineAsyncAndThreadingSupported))]
public void StateMachineAsync_PoolingResumeDispatchStack()
{
var capture = new StrongBox<List<string>>();
var capture = new StrongBox<(List<string> Boundaries, List<string> DiagnosticFrames)>();

var events = CollectEvents(ResumeStateMachineAsyncCallstackKeyword | StateMachineAsyncCoreKeywords, () =>
{
Expand All @@ -3430,10 +3435,13 @@ public void StateMachineAsync_PoolingResumeDispatchStack()

// DumpAllEvents(events);

Assert.True(capture.Value is not null,
Assert.True(capture.Value.Boundaries is not null,
"The pooling state-machine continuation did not run (no boundary sequence was captured).");

AssertBoundarySubsequence(capture.Value!, "V1.AsyncStateMachineDispatcher.MoveNext");
AssertBoundarySubsequence(
capture.Value.Boundaries,
capture.Value.DiagnosticFrames,
"V1.AsyncStateMachineDispatcher.MoveNext");
}

[RuntimeAsyncMethodGeneration(false)]
Expand All @@ -3446,7 +3454,7 @@ private static async Task StateMachineAsync_InlineCompletionClimb_Child(Task chi
[RuntimeAsyncMethodGeneration(false)]
[MethodImpl(MethodImplOptions.NoInlining)]
private static async Task StateMachineAsync_InlineCompletionClimb_ContainsExpectedBoundaryFrames(
Task childTask, StrongBox<(List<string> Boundaries, List<string> Types)> capture)
Task childTask, StrongBox<(List<string> Boundaries, List<string> Types, List<string> DiagnosticFrames)> capture)
{
// This continuation is resumed inline as the child's continuation while the child is still
// completing, so the child's frames (its dispatch boundary + state-machine MoveNext) remain on the
Expand All @@ -3459,7 +3467,7 @@ private static async Task StateMachineAsync_InlineCompletionClimb_ContainsExpect
[ConditionalFact(typeof(AsyncProfilerTests), nameof(IsStateMachineAsyncAndThreadingSupported))]
public void StateMachineAsync_InlineCompletionClimb()
{
var capture = new StrongBox<(List<string> Boundaries, List<string> Types)>();
var capture = new StrongBox<(List<string> Boundaries, List<string> Types, List<string> DiagnosticFrames)>();

var events = CollectEvents(ResumeStateMachineAsyncCallstackKeyword | StateMachineAsyncCoreKeywords, () =>
{
Expand Down Expand Up @@ -3487,19 +3495,30 @@ public void StateMachineAsync_InlineCompletionClimb()

// The still-completing child's state-machine frame remains on the stack below the resuming parent
// (leaf->root: parent above child), so a CPU sample here sees the completed child leaf frame.
AssertFrameOrder(capture.Value.Types,
AssertFrameOrder(
capture.Value.Types,
capture.Value.DiagnosticFrames,
nameof(StateMachineAsync_InlineCompletionClimb_ContainsExpectedBoundaryFrames),
nameof(StateMachineAsync_InlineCompletionClimb_Child));

// The child was resumed as a leaf through MoveNextAsDispatcher, and that boundary is still on the
// stack beneath the parent, so the completed leaf frame is paired with its dispatch boundary.
Assert.Contains("V1.MoveNextAsDispatcher", capture.Value.Boundaries);
Assert.True(capture.Value.Boundaries.Contains("V1.MoveNextAsDispatcher"),
$"Expected boundary 'V1.MoveNextAsDispatcher' was not present. Captured boundaries (leaf->root): " +
$"[{string.Join(" -> ", capture.Value.Boundaries)}]. Captured diagnostic frames (leaf->root): " +
$"[{string.Join(" -> ", capture.Value.DiagnosticFrames)}].");
}

[ConditionalTheory(nameof(IsStateMachineAsyncSupported))]
[InlineData("V1.MoveNextAsDispatcher")]
[InlineData("V1.AsyncStateMachineDispatcher.MoveNext")]
public void V1AsyncDispatchMethod_NameContract_IsStable(string key) =>
AssertAsyncDispatchMethodExists(key);
[InlineData("V1.MoveNextAsDispatcher", true)]
[InlineData("V1.AsyncStateMachineDispatcher.MoveNext", false)]
public void V1AsyncDispatchMethod_NameContract_IsStable(string key, bool requiresNoInlining)
{
MethodInfo method = AssertAsyncDispatchMethodExists(key);
if (requiresNoInlining)
{
AssertMethodImplementationFlags(method, MethodImplAttributes.NoInlining, key);
}
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -2662,7 +2662,8 @@ public void V2AsyncDispatchMethod_NameContract_IsStable(string key) =>

[RuntimeAsyncMethodGeneration(true)]
[MethodImpl(MethodImplOptions.NoInlining)]
private static async Task RuntimeAsync_ResumeDispatchStack_ContainsExpectedBoundaryFrames(StrongBox<List<string>> capture)
private static async Task RuntimeAsync_ResumeDispatchStack_ContainsExpectedBoundaryFrames(
StrongBox<(List<string> Boundaries, List<string> DiagnosticFrames)> capture)
{
await Task.Yield();
capture.Value = CaptureAsyncBoundarySequence();
Expand All @@ -2671,7 +2672,7 @@ private static async Task RuntimeAsync_ResumeDispatchStack_ContainsExpectedBound
[ConditionalFact(nameof(IsRuntimeAsyncAndThreadingSupported))]
public void RuntimeAsync_ResumeDispatchStack()
{
var capture = new StrongBox<List<string>>();
var capture = new StrongBox<(List<string> Boundaries, List<string> DiagnosticFrames)>();

var events = CollectEvents(AllRuntimeAsyncKeywords, () =>
{
Expand All @@ -2680,10 +2681,12 @@ public void RuntimeAsync_ResumeDispatchStack()

// DumpAllEvents(events);

Assert.True(capture.Value is not null,
Assert.True(capture.Value.Boundaries is not null,
"The runtime-async continuation did not run (no boundary sequence was captured).");

AssertBoundarySubsequence(capture.Value!,
AssertBoundarySubsequence(
capture.Value.Boundaries,
capture.Value.DiagnosticFrames,
"Wrapper",
"V2.InstrumentedDispatchContinuations",
"V2.DispatchContinuations");
Expand Down
Loading