Skip to content

Async profiler: V1 (TaskAsync) instrumentation + tests. - #129043

Merged
lateralusX merged 46 commits into
dotnet:mainfrom
lateralusX:lateralusX/async-profiler-asyncv1-support-v2
Jun 24, 2026
Merged

Async profiler: V1 (TaskAsync) instrumentation + tests.#129043
lateralusX merged 46 commits into
dotnet:mainfrom
lateralusX:lateralusX/async-profiler-asyncv1-support-v2

Conversation

@lateralusX

@lateralusXlateralusX commented Jun 5, 2026

Copy link
Copy Markdown
Member

Summary

Enables the async profiler for the V1 TaskAsync (state-machine-based) async path. Both V1 (TaskAsync) and V2 (RuntimeAsync) now emit a uniform, well-defined event stream that downstream tools can consume without knowing which async model produced a given chain. There are smaller variations to what events V1 can support, but all-important events are supported on both async models. The callstack events are also typed, since their data will end up slightly different (Native IP only on V2 and Method Start native IP and state on V1).

PR includes extensive test suite covering both paths as well as refactoring.

Motivation

V1 async (the C#-compiler-generated state-machine IAsyncStateMachineBox model) is the dominant async path in the wild. Until now the async profiler only instrumented the V2 (RuntimeAsync) path, so V1 chains were invisible to consumers. This PR extends the instrumentation to V1 while keeping the event stream identical in shape between the two models.

What's in this PR

Runtime V1 instrumentation

  • AsyncStateMachineDispatcher — class deriving from Task that wraps the actual state-machine box, with per-invocation TLS-pushed state held in AsyncStateMachineDispatcherInfo (ref struct).
  • Per-dispatcher fields (LastContinuation, ReachedLastContinuation, InnerBox) track the cascade so we can emit accurate Resume, Suspend, Complete, and append events as chains grow.
  • Cooperative append mechanism: when a parent registers after a child has already started walking, the runtime emits AppendAsyncCallstack to backfill the visible chain. Three race outcomes are handled (parent-registers-before-child-completes, parent-registers-during-suspend, and the unrecoverable late-parent case which is a design limit).
  • StateMachineDiagnosticData / GetDiagnosticData plumbed through AsyncTaskMethodBuilderT, AsyncMethodBuilderCore, and IAsyncStateMachineBox to expose the walkable chain to the profiler. NativeAOT returns false from GetDiagnosticData due to lack of native method IP and state field access.
  • InstrumentCheckPoint guards at all V1 builder await-completion sites (TaskAwaiter, ValueTaskAwaiter, ConfiguredValueTaskAwaitable, YieldAwaitable, PoolingAsyncValueTaskMethodBuilderT), linked out when no event source support.

Async profiler V1 event model

The runtime emits Create + Resume + Suspend + Complete per dispatcher MoveNext.

Create/suspend callstacks is not emitted on V1. The continuation chain is built and finalized after emitting a create/suspend event. Create/Suspend callstacks can be calculated when parsing the whole trace since the next resume for that context will carry the callstack.

On V2, continuation chains are build and finalized before scheduled for execution. On V1 this happens in parallel and chains can end up truncated in case completion race between the thread yielding and the thread executing the resumed continuation chain. A continuation chain might also continue to build after a thread started to execute a continuation chain. To handle this a new event was added to async profiler, AppendAsyncCallstack, it can fire several times between a resume async context and its completion. This gives a parser the ability to recreate the full resumed async callstack at resume point, even if it didn't exist at that point during runtime.

Tests

Test files split by async model to keep each focused:

  • AsyncProfilerTests.cs — shared partial-class infrastructure: parsers, event listeners, scenario runners.
  • AsyncProfilerV1Tests.cs — tests under the TaskAsync_* prefix covering V1 scenarios.
  • AsyncProfilerV2Tests.cs — tests under the RuntimeAsync_* prefix covering V2 scenarios.

Total of 116 tests covering both V1 and V2 scenarios.

Out of scope (follow-ups)

  • Using the AsyncInstrumentation opens up for folding existing TPL and debugger checks into the same guard. This will be handled in a follow-up PR and could offer performance improvements on existing TPL and debugger paths where async profiler co-exists.
  • Code currently uses a dispatcher box put at the head of continuation chain to push/pop needed async dispatcher info on tls. There are some code paths that are known (thread pool, default sync context and scheduler), we could optimize those paths if we knew they are always taken at create location, removing the allocation. Having that said, every continuation in the chain is allocated, so might not be a big deal in the end anyways.
  • AsyncV1 have a late attach issue, same applies to TPL. Unless we always create/enable the async dispatcher info for AsyncV1, there will be potential blind spots in the stack when doing late attach and profiler have not been enabled at startup.
  • PoolingAsyncValueTaskMethodBuilderT instrumentation, can be added later, if needed.
  • NativeAOT V1 callstack support. NativeAOT async support is already limited in tooling. Since lack of async callstacks will void the majority of scenarios, NAOT is currently not supported on V1. Could be added in future if needed
  • Run V1 tests on Mono. AsyncProfiler + V1 instrumentation builds on Mono, but currently no tests are executed. Can be revisited later.

CopilotAI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Extends the async-profiler runtime instrumentation to cover the V1 Task/state-machine (compiler-generated) async path, aiming to make V1 and V2 produce a uniform async-profiler event stream (including callstacks and V1 append/backfill behavior), and adds a large test suite split by async model.

Changes:

  • Adds V1 instrumentation via an AsyncTaskDispatcher wrapper (plus TLS-pushed dispatcher state) and inserts instrumentation checkpoints across key await/builder scheduling sites.
  • Plumbs continuation-walk diagnostics through IAsyncStateMachineBox.GetDiagnosticData and adds AsyncStateMachineDiagnostics<TStateMachine> to support V1 callstack capture.
  • Adds/organizes async-profiler tests into V1/V2-specific files and updates the test project to compile them.

Reviewed changes

Copilot reviewed 17 out of 19 changed files in this pull request and generated 4 comments.

Show a summary per file
FileDescription
src/libraries/System.Runtime/tests/System.Threading.Tasks.Tests/System.Threading.Tasks.Tests.csprojIncludes new V1/V2 async-profiler test files in the test project.
src/libraries/System.Runtime/tests/System.Threading.Tasks.Tests/System.Runtime.CompilerServices/AsyncProfilerV1Tests.csAdds V1 (TaskAsync) async-profiler scenario coverage and validations.
src/libraries/System.Runtime/tests/System.Threading.Tasks.Tests/System.Runtime.CompilerServices/AsyncProfilerV2Tests.csAdds V2 (runtime-async) async-profiler scenario coverage and validations.
src/libraries/System.Private.CoreLib/src/System/Threading/Tasks/TaskContinuation.csWraps scheduled async state-machine boxes with dispatcher when async-profiler instrumentation is enabled.
src/libraries/System.Private.CoreLib/src/System/Threading/Tasks/Task.csExposes continuation object for diagnostics to support continuation walking.
src/libraries/System.Private.CoreLib/src/System/Runtime/CompilerServices/TaskAwaiter.csWraps state-machine box with dispatcher in key await continuation paths under async-profiler.
src/libraries/System.Private.CoreLib/src/System/Runtime/CompilerServices/ValueTaskAwaiter.csAdds dispatcher wrapping in ValueTask await continuation paths under async-profiler.
src/libraries/System.Private.CoreLib/src/System/Runtime/CompilerServices/ConfiguredValueTaskAwaitable.csAdds dispatcher wrapping in configured ValueTask await continuation paths under async-profiler.
src/libraries/System.Private.CoreLib/src/System/Runtime/CompilerServices/YieldAwaitable.csAdds dispatcher wrapping in Yield await continuation paths under async-profiler.
src/libraries/System.Private.CoreLib/src/System/Runtime/CompilerServices/IAsyncStateMachineBox.csAdds GetDiagnosticData API to support profiler continuation walking.
src/libraries/System.Private.CoreLib/src/System/Runtime/CompilerServices/AsyncTaskMethodBuilderT.csEmits V1 method/unwind instrumentation and implements diagnostic-data plumbing for state-machine boxes.
src/libraries/System.Private.CoreLib/src/System/Runtime/CompilerServices/PoolingAsyncValueTaskMethodBuilderT.csAdds a stub GetDiagnosticData implementation returning false (not yet supported).
src/libraries/System.Private.CoreLib/src/System/Runtime/CompilerServices/AsyncTaskDispatcher.csIntroduces dispatcher wrapper + TLS state used for V1 Create/Resume/Complete + append callstack behavior.
src/libraries/System.Private.CoreLib/src/System/Runtime/CompilerServices/AsyncStateMachineDiagnostics.csAdds per-state-machine cached method-id + state-field-offset resolution for diagnostics.
src/libraries/System.Private.CoreLib/src/System/Runtime/CompilerServices/AsyncMethodBuilderCore.csAdds helper to recover an IAsyncStateMachineBox from continuation Actions/wrappers.
src/libraries/System.Private.CoreLib/src/System/Runtime/CompilerServices/AsyncProfiler.csAdds AppendAsyncCallstack event and shared callstack emission/walking logic used by V1.
src/coreclr/System.Private.CoreLib/src/System/Runtime/CompilerServices/AsyncProfiler.CoreCLR.csRefactors V2 callstack emission to use shared helpers and adjusts suspend emission ordering.
src/libraries/System.Private.CoreLib/src/System.Private.CoreLib.Shared.projitemsWires new compiler-services files and adjusts shared inclusion for async-profiler/instrumentation sources.

Comment threadsrc/libraries/System.Private.CoreLib/src/System/Threading/Tasks/Task.cs Outdated
@dotnet-policy-service

Copy link
Copy Markdown
Contributor

Tagging subscribers to this area: @steveisok, @tommcdon, @dotnet/dotnet-diag
See info in area-owners.md if you want to be subscribed.

@dotnet-policy-service

Copy link
Copy Markdown
Contributor

Tagging subscribers to this area: @dotnet/area-system-threading-tasks
See info in area-owners.md if you want to be subscribed.

CopilotAI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Copilot reviewed 19 out of 21 changed files in this pull request and generated 3 comments.

Comments suppressed due to low confidence (1)

src/libraries/System.Private.CoreLib/src/System/Threading/Tasks/Task.cs:7232

  • Typo in nearby comment: "istelf" -> "itself".
 internal object? ContinuationForDiagnostics => m_continuationObject != this ? m_continuationObject : null;
internal virtual Delegate[]? GetDelegateContinuationsForDebugger()
{
// Avoid an infinite loop by making sure the continuation object is not a reference to istelf.
if (m_continuationObject != this)

CopilotAI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Copilot reviewed 19 out of 21 changed files in this pull request and generated 6 comments.

@noahfalk

Copy link
Copy Markdown
Member

@tarekgh - do you know who reviews System.Threading.Task stuff with Toub not around? I'll be looking at this and wouldn't be surprised if @jkotas does too, but wanted to give a heads up if there is a BCL owner that would also like to review?

@tarekgh

Copy link
Copy Markdown
Member

@noahfalk the owners are @dotnet/area-system-threading-tasks as it stated in the doc https://github.com/dotnet/runtime/blob/main/docs/area-owners.md.

@github-actionsgithub-actionsBot mentioned this pull request Jun 12, 2026
CopilotAI review requested due to automatic review settings June 22, 2026 15:30
@lateralusX
lateralusXforce-pushed the lateralusX/async-profiler-asyncv1-support-v2 branch from c1b75ba to 0edf9d1CompareJune 22, 2026 15:30

CopilotAI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Copilot reviewed 18 out of 21 changed files in this pull request and generated 4 comments.

@lateralusX

Copy link
Copy Markdown
MemberAuthor

/azp run runtime-nativeaot-outerloop

@azure-pipelines

Copy link
Copy Markdown
Azure Pipelines successfully started running 1 pipeline(s).

@lateralusX

Copy link
Copy Markdown
MemberAuthor

/azp run runtime-nativeaot-outerloop

@azure-pipelines

Copy link
Copy Markdown
Azure Pipelines successfully started running 1 pipeline(s).

@lateralusX

Copy link
Copy Markdown
MemberAuthor

/ba-g Failure in runtime (Build Libraries Test Run release coreclr windows x86 Release) appears on other PR's. Failure/Cancel in runtime-nativeaot-outerloop is unrelated and appears on other PR's.

@lateralusX
lateralusX merged commit 46b72c7 into dotnet:mainJun 24, 2026
168 of 174 checks passed
@dotnet-milestone-botdotnet-milestone-botBot added this to the 11.0-preview7 milestone Jun 25, 2026
lateralusX added a commit that referenced this pull request Jul 1, 2026
### Summary
Extends the V1 AsyncProfiler instrumentation (added in #129043) to
PoolingAsyncValueTaskMethodBuilder /
PoolingAsyncValueTaskMethodBuilder<T>. Previously the pooling builder's
reusable StateMachineBox was a no-op for the profiler —
GetDiagnosticData was a stub and the box emitted no
resume/complete/unwind events — leaving trace gaps wherever the pooling
builder is used.
### Motivation
The pooling builder backs an async method with a poolable
IValueTaskSource-based StateMachineBox rather than a Task derived
AsyncStateMachineBox. The existing instrumentation assumed Task-backed
boxes, so pooling-builder methods produced incomplete async traces
(missing per-method Resume/Complete, empty callstacks, and incorrect
suspend/complete context classification). This makes the V1 async
profiler consistent across the regular Task/ValueTask builders and the
pooling builder.
### Changes
**Pooling box instrumentation (PoolingAsyncValueTaskMethodBuilderT.cs)**
- Resume hook in StateMachineBox<T>.MoveNext.
- Complete/unwind hooks in the base
StateMachineBox.SetResult/SetException.
- Implemented GetDiagnosticData (methodId + state machine state via
AsyncStateMachineDiagnostics<T>; continuation from the value task
source).
**Suspend/complete classification for non-Task boxes
(AsyncStateMachineDispatcher.cs, AsyncProfiler.cs)**
- A pooling box can be recycled inline before the dispatcher's finally
runs, so completion is captured at SetResult/SetException time into a
new AsyncProfiler.Info.CurrentContinuationCompleted flag (set before
signaling; reset per cascade frame). The dispatcher finally reads this
flag uniformly for both Task and pooling boxes instead of probing the
box.
- Consolidated CompleteAsyncMethod/UnwindAsyncFrame to do a single TLS
fetch, set the flag, and gate event emission internally.
**Callstack walk over pooling chains**
- AsyncStateMachineDispatcher.LastContinuation retyped from Task? to
IAsyncStateMachineBox? (removes an unsafe Unsafe.As<Task> on non-Task
boxes); added a Task fast-path NextContinuationForDiagnostics so the
common path stays non-virtual and pooling boxes fall back to
GetDiagnosticData.
- The continuation accessor is read through the existing
GetDiagnosticData (no new interface member — saves one vtable slot per
state-machine box type).
- Exposed ManualResetValueTaskSourceCore.ContinuationForDiagnostics for
the diagnostic walk.
**Cascade guard (ValueTaskAwaiter.cs, ConfiguredValueTaskAwaitable.cs)**
- The IValueTaskSource awaiter paths unconditionally interposed a
dispatcher, which split a pooling chain into per-level dispatchers and
truncated the callstack to one frame. Added the obj is not
IAsyncStateMachineBox guard (mirroring
TaskAwaiter.UnsafeOnCompletedInternal) to all four sites, so awaiting
one async (pooling) box from another forms a direct box→box continuation
cascade the walker can traverse; a dispatcher is interposed only at the
true leaf (e.g. await Task.Delay).
### Tests
14 new tests in AsyncProfilerV1Tests.cs (13 pooling + 1
regular-ValueTask single-threaded). Each mirrors an existing
ValueTask/Task test:
- Event lifecycle, per-method events, callstack depth/distinct method
ids, handled/unhandled exception unwind.
- Pooling-specific: suspend/complete classification,
ConfigureAwait(false), late-parent-registration Append, generic
ValueTask<int>, and a check that the pooling builder is genuinely in
effect (the pending ValueTask is backed by an IValueTaskSource, not a
Task).
- Single-threaded smoke tests (no thread pool) for ValueTask and pooling
ValueTask.
eiriktsarpalis pushed a commit that referenced this pull request Jul 15, 2026
### Summary
Enables the async profiler for the V1 TaskAsync (state-machine-based)
async path. Both V1 (TaskAsync) and V2 (RuntimeAsync) now emit a
uniform, well-defined event stream that downstream tools can consume
without knowing which async model produced a given chain. There are
smaller variations to what events V1 can support, but all-important
events are supported on both async models. The callstack events are also
typed, since their data will end up slightly different (Native IP only
on V2 and Method Start native IP and state on V1).
PR includes extensive test suite covering both paths as well as
refactoring.
### Motivation
V1 async (the C#-compiler-generated state-machine IAsyncStateMachineBox
model) is the dominant async path in the wild. Until now the async
profiler only instrumented the V2 (RuntimeAsync) path, so V1 chains were
invisible to consumers. This PR extends the instrumentation to V1 while
keeping the event stream identical in shape between the two models.
### What's in this PR
**Runtime V1 instrumentation**
- AsyncStateMachineDispatcher — class deriving from Task<VoidTaskResult>
that wraps the actual state-machine box, with per-invocation TLS-pushed
state held in AsyncStateMachineDispatcherInfo (ref struct).
- Per-dispatcher fields (LastContinuation, ReachedLastContinuation,
InnerBox) track the cascade so we can emit accurate Resume, Suspend,
Complete, and append events as chains grow.
- Cooperative append mechanism: when a parent registers after a child
has already started walking, the runtime emits AppendAsyncCallstack to
backfill the visible chain. Three race outcomes are handled
(parent-registers-before-child-completes,
parent-registers-during-suspend, and the unrecoverable late-parent case
which is a design limit).
- StateMachineDiagnosticData / GetDiagnosticData plumbed through
AsyncTaskMethodBuilderT, AsyncMethodBuilderCore, and
IAsyncStateMachineBox to expose the walkable chain to the profiler.
NativeAOT returns false from GetDiagnosticData due to lack of native
method IP and state field access.
- InstrumentCheckPoint guards at all V1 builder await-completion sites
(TaskAwaiter, ValueTaskAwaiter, ConfiguredValueTaskAwaitable,
YieldAwaitable, PoolingAsyncValueTaskMethodBuilderT), linked out when no
event source support.
**Async profiler V1 event model**
The runtime emits Create + Resume + Suspend + Complete per dispatcher
MoveNext.
Create/suspend callstacks is not emitted on V1. The continuation chain
is built and finalized after emitting a create/suspend event.
Create/Suspend callstacks can be calculated when parsing the whole trace
since the next resume for that context will carry the callstack.
On V2, continuation chains are build and finalized before scheduled for
execution. On V1 this happens in parallel and chains can end up
truncated in case completion race between the thread yielding and the
thread executing the resumed continuation chain. A continuation chain
might also continue to build after a thread started to execute a
continuation chain. To handle this a new event was added to async
profiler, AppendAsyncCallstack, it can fire several times between a
resume async context and its completion. This gives a parser the ability
to recreate the full resumed async callstack at resume point, even if it
didn't exist at that point during runtime.
**Tests**
Test files split by async model to keep each focused:
- AsyncProfilerTests.cs — shared partial-class infrastructure: parsers,
event listeners, scenario runners.
- AsyncProfilerV1Tests.cs — tests under the TaskAsync_* prefix covering
V1 scenarios.
- AsyncProfilerV2Tests.cs — tests under the RuntimeAsync_* prefix
covering V2 scenarios.
Total of 116 tests covering both V1 and V2 scenarios.
### Out of scope (follow-ups)
- Using the AsyncInstrumentation opens up for folding existing TPL and
debugger checks into the same guard. This will be handled in a follow-up
PR and could offer performance improvements on existing TPL and debugger
paths where async profiler co-exists.
- Code currently uses a dispatcher box put at the head of continuation
chain to push/pop needed async dispatcher info on tls. There are some
code paths that are known (thread pool, default sync context and
scheduler), we could optimize those paths if we knew they are always
taken at create location, removing the allocation. Having that said,
every continuation in the chain is allocated, so might not be a big deal
in the end anyways.
- AsyncV1 have a late attach issue, same applies to TPL. Unless we
always create/enable the async dispatcher info for AsyncV1, there will
be potential blind spots in the stack when doing late attach and
profiler have not been enabled at startup.
- PoolingAsyncValueTaskMethodBuilderT instrumentation, can be added
later, if needed.
- NativeAOT V1 callstack support. NativeAOT async support is already
limited in tooling. Since lack of async callstacks will void the
majority of scenarios, NAOT is currently not supported on V1. Could be
added in future if needed
- Run V1 tests on Mono. AsyncProfiler + V1 instrumentation builds on
Mono, but currently no tests are executed. Can be revisited later.
eiriktsarpalis pushed a commit that referenced this pull request Jul 15, 2026
### Summary
Extends the V1 AsyncProfiler instrumentation (added in #129043) to
PoolingAsyncValueTaskMethodBuilder /
PoolingAsyncValueTaskMethodBuilder<T>. Previously the pooling builder's
reusable StateMachineBox was a no-op for the profiler —
GetDiagnosticData was a stub and the box emitted no
resume/complete/unwind events — leaving trace gaps wherever the pooling
builder is used.
### Motivation
The pooling builder backs an async method with a poolable
IValueTaskSource-based StateMachineBox rather than a Task derived
AsyncStateMachineBox. The existing instrumentation assumed Task-backed
boxes, so pooling-builder methods produced incomplete async traces
(missing per-method Resume/Complete, empty callstacks, and incorrect
suspend/complete context classification). This makes the V1 async
profiler consistent across the regular Task/ValueTask builders and the
pooling builder.
### Changes
**Pooling box instrumentation (PoolingAsyncValueTaskMethodBuilderT.cs)**
- Resume hook in StateMachineBox<T>.MoveNext.
- Complete/unwind hooks in the base
StateMachineBox.SetResult/SetException.
- Implemented GetDiagnosticData (methodId + state machine state via
AsyncStateMachineDiagnostics<T>; continuation from the value task
source).
**Suspend/complete classification for non-Task boxes
(AsyncStateMachineDispatcher.cs, AsyncProfiler.cs)**
- A pooling box can be recycled inline before the dispatcher's finally
runs, so completion is captured at SetResult/SetException time into a
new AsyncProfiler.Info.CurrentContinuationCompleted flag (set before
signaling; reset per cascade frame). The dispatcher finally reads this
flag uniformly for both Task and pooling boxes instead of probing the
box.
- Consolidated CompleteAsyncMethod/UnwindAsyncFrame to do a single TLS
fetch, set the flag, and gate event emission internally.
**Callstack walk over pooling chains**
- AsyncStateMachineDispatcher.LastContinuation retyped from Task? to
IAsyncStateMachineBox? (removes an unsafe Unsafe.As<Task> on non-Task
boxes); added a Task fast-path NextContinuationForDiagnostics so the
common path stays non-virtual and pooling boxes fall back to
GetDiagnosticData.
- The continuation accessor is read through the existing
GetDiagnosticData (no new interface member — saves one vtable slot per
state-machine box type).
- Exposed ManualResetValueTaskSourceCore.ContinuationForDiagnostics for
the diagnostic walk.
**Cascade guard (ValueTaskAwaiter.cs, ConfiguredValueTaskAwaitable.cs)**
- The IValueTaskSource awaiter paths unconditionally interposed a
dispatcher, which split a pooling chain into per-level dispatchers and
truncated the callstack to one frame. Added the obj is not
IAsyncStateMachineBox guard (mirroring
TaskAwaiter.UnsafeOnCompletedInternal) to all four sites, so awaiting
one async (pooling) box from another forms a direct box→box continuation
cascade the walker can traverse; a dispatcher is interposed only at the
true leaf (e.g. await Task.Delay).
### Tests
14 new tests in AsyncProfilerV1Tests.cs (13 pooling + 1
regular-ValueTask single-threaded). Each mirrors an existing
ValueTask/Task test:
- Event lifecycle, per-method events, callstack depth/distinct method
ids, handled/unhandled exception unwind.
- Pooling-specific: suspend/complete classification,
ConfigureAwait(false), late-parent-registration Append, generic
ValueTask<int>, and a check that the pooling builder is genuinely in
effect (the pending ValueTask is backed by an IValueTaskSource, not a
Task).
- Single-threaded smoke tests (no thread pool) for ValueTask and pooling
ValueTask.
@github-actionsgithub-actionsBot locked and limited conversation to collaborators Jul 26, 2026
Sign up for freeto subscribe to this conversation on GitHub. Already have an account? Sign in.

Projects

None yet

Development

Successfully merging this pull request may close these issues.

6 participants

@lateralusX@noahfalk@tarekgh@jkotas@MichalStrehovsky
, 'i'); if (__m === '*' || __re.test(location.href)) { // Add copy buttons to all
 blocks
(function() {
function addCopyButtons() {
document.querySelectorAll('pre code').forEach(function(codeBlock) {
if (codeBlock.parentElement.hasAttribute('data-copy-added')) return;
codeBlock.parentElement.setAttribute('data-copy-added', 'true');
var btn = document.createElement('button');
btn.textContent = 'Copy';
btn.style.cssText = 'position:absolute;top:4px;right:4px;padding:2px 8px;font-size:11px;background:#4ecdc4;border:none;border-radius:4px;color:#1a1a2e;cursor:pointer;opacity:0.7;transition:opacity 0.2s;';
btn.onmouseover = function() { this.style.opacity = '1'; };
btn.onmouseout = function() { this.style.opacity = '0.7'; };
btn.onclick = function() {
navigator.clipboard.writeText(codeBlock.textContent).then(function() {
btn.textContent = 'Copied!';
setTimeout(function() { btn.textContent = 'Copy'; }, 1500);
});
};
codeBlock.parentElement.style.position = 'relative';
codeBlock.parentElement.appendChild(btn);
});
}
addCopyButtons();
// Re-run on dynamic content
var observer = new MutationObserver(addCopyButtons);
observer.observe(document.body, { childList: true, subtree: true });
})();
}
} catch(__e) { console.warn('[Userscript:Add Copy Buttons to Code Blocks]', __e); }
})();
(function(){
try {
var __m = "github.com";
var __re = new RegExp('^' + "github\\.com" + '
 Async profiler: V1 (TaskAsync) instrumentation + tests. by lateralusX · Pull Request #129043 · dotnet/runtime · GitHub
Skip to content

Async profiler: V1 (TaskAsync) instrumentation + tests. - #129043

Merged
lateralusX merged 46 commits into
dotnet:mainfrom
lateralusX:lateralusX/async-profiler-asyncv1-support-v2
Jun 24, 2026
Merged

Async profiler: V1 (TaskAsync) instrumentation + tests.#129043
lateralusX merged 46 commits into
dotnet:mainfrom
lateralusX:lateralusX/async-profiler-asyncv1-support-v2

Conversation

@lateralusX

@lateralusXlateralusX commented Jun 5, 2026

Copy link
Copy Markdown
Member

Summary

Enables the async profiler for the V1 TaskAsync (state-machine-based) async path. Both V1 (TaskAsync) and V2 (RuntimeAsync) now emit a uniform, well-defined event stream that downstream tools can consume without knowing which async model produced a given chain. There are smaller variations to what events V1 can support, but all-important events are supported on both async models. The callstack events are also typed, since their data will end up slightly different (Native IP only on V2 and Method Start native IP and state on V1).

PR includes extensive test suite covering both paths as well as refactoring.

Motivation

V1 async (the C#-compiler-generated state-machine IAsyncStateMachineBox model) is the dominant async path in the wild. Until now the async profiler only instrumented the V2 (RuntimeAsync) path, so V1 chains were invisible to consumers. This PR extends the instrumentation to V1 while keeping the event stream identical in shape between the two models.

What's in this PR

Runtime V1 instrumentation

  • AsyncStateMachineDispatcher — class deriving from Task that wraps the actual state-machine box, with per-invocation TLS-pushed state held in AsyncStateMachineDispatcherInfo (ref struct).
  • Per-dispatcher fields (LastContinuation, ReachedLastContinuation, InnerBox) track the cascade so we can emit accurate Resume, Suspend, Complete, and append events as chains grow.
  • Cooperative append mechanism: when a parent registers after a child has already started walking, the runtime emits AppendAsyncCallstack to backfill the visible chain. Three race outcomes are handled (parent-registers-before-child-completes, parent-registers-during-suspend, and the unrecoverable late-parent case which is a design limit).
  • StateMachineDiagnosticData / GetDiagnosticData plumbed through AsyncTaskMethodBuilderT, AsyncMethodBuilderCore, and IAsyncStateMachineBox to expose the walkable chain to the profiler. NativeAOT returns false from GetDiagnosticData due to lack of native method IP and state field access.
  • InstrumentCheckPoint guards at all V1 builder await-completion sites (TaskAwaiter, ValueTaskAwaiter, ConfiguredValueTaskAwaitable, YieldAwaitable, PoolingAsyncValueTaskMethodBuilderT), linked out when no event source support.

Async profiler V1 event model

The runtime emits Create + Resume + Suspend + Complete per dispatcher MoveNext.

Create/suspend callstacks is not emitted on V1. The continuation chain is built and finalized after emitting a create/suspend event. Create/Suspend callstacks can be calculated when parsing the whole trace since the next resume for that context will carry the callstack.

On V2, continuation chains are build and finalized before scheduled for execution. On V1 this happens in parallel and chains can end up truncated in case completion race between the thread yielding and the thread executing the resumed continuation chain. A continuation chain might also continue to build after a thread started to execute a continuation chain. To handle this a new event was added to async profiler, AppendAsyncCallstack, it can fire several times between a resume async context and its completion. This gives a parser the ability to recreate the full resumed async callstack at resume point, even if it didn't exist at that point during runtime.

Tests

Test files split by async model to keep each focused:

  • AsyncProfilerTests.cs — shared partial-class infrastructure: parsers, event listeners, scenario runners.
  • AsyncProfilerV1Tests.cs — tests under the TaskAsync_* prefix covering V1 scenarios.
  • AsyncProfilerV2Tests.cs — tests under the RuntimeAsync_* prefix covering V2 scenarios.

Total of 116 tests covering both V1 and V2 scenarios.

Out of scope (follow-ups)

  • Using the AsyncInstrumentation opens up for folding existing TPL and debugger checks into the same guard. This will be handled in a follow-up PR and could offer performance improvements on existing TPL and debugger paths where async profiler co-exists.
  • Code currently uses a dispatcher box put at the head of continuation chain to push/pop needed async dispatcher info on tls. There are some code paths that are known (thread pool, default sync context and scheduler), we could optimize those paths if we knew they are always taken at create location, removing the allocation. Having that said, every continuation in the chain is allocated, so might not be a big deal in the end anyways.
  • AsyncV1 have a late attach issue, same applies to TPL. Unless we always create/enable the async dispatcher info for AsyncV1, there will be potential blind spots in the stack when doing late attach and profiler have not been enabled at startup.
  • PoolingAsyncValueTaskMethodBuilderT instrumentation, can be added later, if needed.
  • NativeAOT V1 callstack support. NativeAOT async support is already limited in tooling. Since lack of async callstacks will void the majority of scenarios, NAOT is currently not supported on V1. Could be added in future if needed
  • Run V1 tests on Mono. AsyncProfiler + V1 instrumentation builds on Mono, but currently no tests are executed. Can be revisited later.

CopilotAI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Extends the async-profiler runtime instrumentation to cover the V1 Task/state-machine (compiler-generated) async path, aiming to make V1 and V2 produce a uniform async-profiler event stream (including callstacks and V1 append/backfill behavior), and adds a large test suite split by async model.

Changes:

  • Adds V1 instrumentation via an AsyncTaskDispatcher wrapper (plus TLS-pushed dispatcher state) and inserts instrumentation checkpoints across key await/builder scheduling sites.
  • Plumbs continuation-walk diagnostics through IAsyncStateMachineBox.GetDiagnosticData and adds AsyncStateMachineDiagnostics<TStateMachine> to support V1 callstack capture.
  • Adds/organizes async-profiler tests into V1/V2-specific files and updates the test project to compile them.

Reviewed changes

Copilot reviewed 17 out of 19 changed files in this pull request and generated 4 comments.

Show a summary per file
FileDescription
src/libraries/System.Runtime/tests/System.Threading.Tasks.Tests/System.Threading.Tasks.Tests.csprojIncludes new V1/V2 async-profiler test files in the test project.
src/libraries/System.Runtime/tests/System.Threading.Tasks.Tests/System.Runtime.CompilerServices/AsyncProfilerV1Tests.csAdds V1 (TaskAsync) async-profiler scenario coverage and validations.
src/libraries/System.Runtime/tests/System.Threading.Tasks.Tests/System.Runtime.CompilerServices/AsyncProfilerV2Tests.csAdds V2 (runtime-async) async-profiler scenario coverage and validations.
src/libraries/System.Private.CoreLib/src/System/Threading/Tasks/TaskContinuation.csWraps scheduled async state-machine boxes with dispatcher when async-profiler instrumentation is enabled.
src/libraries/System.Private.CoreLib/src/System/Threading/Tasks/Task.csExposes continuation object for diagnostics to support continuation walking.
src/libraries/System.Private.CoreLib/src/System/Runtime/CompilerServices/TaskAwaiter.csWraps state-machine box with dispatcher in key await continuation paths under async-profiler.
src/libraries/System.Private.CoreLib/src/System/Runtime/CompilerServices/ValueTaskAwaiter.csAdds dispatcher wrapping in ValueTask await continuation paths under async-profiler.
src/libraries/System.Private.CoreLib/src/System/Runtime/CompilerServices/ConfiguredValueTaskAwaitable.csAdds dispatcher wrapping in configured ValueTask await continuation paths under async-profiler.
src/libraries/System.Private.CoreLib/src/System/Runtime/CompilerServices/YieldAwaitable.csAdds dispatcher wrapping in Yield await continuation paths under async-profiler.
src/libraries/System.Private.CoreLib/src/System/Runtime/CompilerServices/IAsyncStateMachineBox.csAdds GetDiagnosticData API to support profiler continuation walking.
src/libraries/System.Private.CoreLib/src/System/Runtime/CompilerServices/AsyncTaskMethodBuilderT.csEmits V1 method/unwind instrumentation and implements diagnostic-data plumbing for state-machine boxes.
src/libraries/System.Private.CoreLib/src/System/Runtime/CompilerServices/PoolingAsyncValueTaskMethodBuilderT.csAdds a stub GetDiagnosticData implementation returning false (not yet supported).
src/libraries/System.Private.CoreLib/src/System/Runtime/CompilerServices/AsyncTaskDispatcher.csIntroduces dispatcher wrapper + TLS state used for V1 Create/Resume/Complete + append callstack behavior.
src/libraries/System.Private.CoreLib/src/System/Runtime/CompilerServices/AsyncStateMachineDiagnostics.csAdds per-state-machine cached method-id + state-field-offset resolution for diagnostics.
src/libraries/System.Private.CoreLib/src/System/Runtime/CompilerServices/AsyncMethodBuilderCore.csAdds helper to recover an IAsyncStateMachineBox from continuation Actions/wrappers.
src/libraries/System.Private.CoreLib/src/System/Runtime/CompilerServices/AsyncProfiler.csAdds AppendAsyncCallstack event and shared callstack emission/walking logic used by V1.
src/coreclr/System.Private.CoreLib/src/System/Runtime/CompilerServices/AsyncProfiler.CoreCLR.csRefactors V2 callstack emission to use shared helpers and adjusts suspend emission ordering.
src/libraries/System.Private.CoreLib/src/System.Private.CoreLib.Shared.projitemsWires new compiler-services files and adjusts shared inclusion for async-profiler/instrumentation sources.

Comment threadsrc/libraries/System.Private.CoreLib/src/System/Threading/Tasks/Task.cs Outdated
@dotnet-policy-service

Copy link
Copy Markdown
Contributor

Tagging subscribers to this area: @steveisok, @tommcdon, @dotnet/dotnet-diag
See info in area-owners.md if you want to be subscribed.

@dotnet-policy-service

Copy link
Copy Markdown
Contributor

Tagging subscribers to this area: @dotnet/area-system-threading-tasks
See info in area-owners.md if you want to be subscribed.

CopilotAI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Copilot reviewed 19 out of 21 changed files in this pull request and generated 3 comments.

Comments suppressed due to low confidence (1)

src/libraries/System.Private.CoreLib/src/System/Threading/Tasks/Task.cs:7232

  • Typo in nearby comment: "istelf" -> "itself".
 internal object? ContinuationForDiagnostics => m_continuationObject != this ? m_continuationObject : null;
internal virtual Delegate[]? GetDelegateContinuationsForDebugger()
{
// Avoid an infinite loop by making sure the continuation object is not a reference to istelf.
if (m_continuationObject != this)

CopilotAI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Copilot reviewed 19 out of 21 changed files in this pull request and generated 6 comments.

@noahfalk

Copy link
Copy Markdown
Member

@tarekgh - do you know who reviews System.Threading.Task stuff with Toub not around? I'll be looking at this and wouldn't be surprised if @jkotas does too, but wanted to give a heads up if there is a BCL owner that would also like to review?

@tarekgh

Copy link
Copy Markdown
Member

@noahfalk the owners are @dotnet/area-system-threading-tasks as it stated in the doc https://github.com/dotnet/runtime/blob/main/docs/area-owners.md.

@github-actionsgithub-actionsBot mentioned this pull request Jun 12, 2026
CopilotAI review requested due to automatic review settings June 22, 2026 15:30
@lateralusX
lateralusXforce-pushed the lateralusX/async-profiler-asyncv1-support-v2 branch from c1b75ba to 0edf9d1CompareJune 22, 2026 15:30

CopilotAI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Copilot reviewed 18 out of 21 changed files in this pull request and generated 4 comments.

@lateralusX

Copy link
Copy Markdown
MemberAuthor

/azp run runtime-nativeaot-outerloop

@azure-pipelines

Copy link
Copy Markdown
Azure Pipelines successfully started running 1 pipeline(s).

@lateralusX

Copy link
Copy Markdown
MemberAuthor

/azp run runtime-nativeaot-outerloop

@azure-pipelines

Copy link
Copy Markdown
Azure Pipelines successfully started running 1 pipeline(s).

@lateralusX

Copy link
Copy Markdown
MemberAuthor

/ba-g Failure in runtime (Build Libraries Test Run release coreclr windows x86 Release) appears on other PR's. Failure/Cancel in runtime-nativeaot-outerloop is unrelated and appears on other PR's.

@lateralusX
lateralusX merged commit 46b72c7 into dotnet:mainJun 24, 2026
168 of 174 checks passed
@dotnet-milestone-botdotnet-milestone-botBot added this to the 11.0-preview7 milestone Jun 25, 2026
lateralusX added a commit that referenced this pull request Jul 1, 2026
### Summary
Extends the V1 AsyncProfiler instrumentation (added in #129043) to
PoolingAsyncValueTaskMethodBuilder /
PoolingAsyncValueTaskMethodBuilder<T>. Previously the pooling builder's
reusable StateMachineBox was a no-op for the profiler —
GetDiagnosticData was a stub and the box emitted no
resume/complete/unwind events — leaving trace gaps wherever the pooling
builder is used.
### Motivation
The pooling builder backs an async method with a poolable
IValueTaskSource-based StateMachineBox rather than a Task derived
AsyncStateMachineBox. The existing instrumentation assumed Task-backed
boxes, so pooling-builder methods produced incomplete async traces
(missing per-method Resume/Complete, empty callstacks, and incorrect
suspend/complete context classification). This makes the V1 async
profiler consistent across the regular Task/ValueTask builders and the
pooling builder.
### Changes
**Pooling box instrumentation (PoolingAsyncValueTaskMethodBuilderT.cs)**
- Resume hook in StateMachineBox<T>.MoveNext.
- Complete/unwind hooks in the base
StateMachineBox.SetResult/SetException.
- Implemented GetDiagnosticData (methodId + state machine state via
AsyncStateMachineDiagnostics<T>; continuation from the value task
source).
**Suspend/complete classification for non-Task boxes
(AsyncStateMachineDispatcher.cs, AsyncProfiler.cs)**
- A pooling box can be recycled inline before the dispatcher's finally
runs, so completion is captured at SetResult/SetException time into a
new AsyncProfiler.Info.CurrentContinuationCompleted flag (set before
signaling; reset per cascade frame). The dispatcher finally reads this
flag uniformly for both Task and pooling boxes instead of probing the
box.
- Consolidated CompleteAsyncMethod/UnwindAsyncFrame to do a single TLS
fetch, set the flag, and gate event emission internally.
**Callstack walk over pooling chains**
- AsyncStateMachineDispatcher.LastContinuation retyped from Task? to
IAsyncStateMachineBox? (removes an unsafe Unsafe.As<Task> on non-Task
boxes); added a Task fast-path NextContinuationForDiagnostics so the
common path stays non-virtual and pooling boxes fall back to
GetDiagnosticData.
- The continuation accessor is read through the existing
GetDiagnosticData (no new interface member — saves one vtable slot per
state-machine box type).
- Exposed ManualResetValueTaskSourceCore.ContinuationForDiagnostics for
the diagnostic walk.
**Cascade guard (ValueTaskAwaiter.cs, ConfiguredValueTaskAwaitable.cs)**
- The IValueTaskSource awaiter paths unconditionally interposed a
dispatcher, which split a pooling chain into per-level dispatchers and
truncated the callstack to one frame. Added the obj is not
IAsyncStateMachineBox guard (mirroring
TaskAwaiter.UnsafeOnCompletedInternal) to all four sites, so awaiting
one async (pooling) box from another forms a direct box→box continuation
cascade the walker can traverse; a dispatcher is interposed only at the
true leaf (e.g. await Task.Delay).
### Tests
14 new tests in AsyncProfilerV1Tests.cs (13 pooling + 1
regular-ValueTask single-threaded). Each mirrors an existing
ValueTask/Task test:
- Event lifecycle, per-method events, callstack depth/distinct method
ids, handled/unhandled exception unwind.
- Pooling-specific: suspend/complete classification,
ConfigureAwait(false), late-parent-registration Append, generic
ValueTask<int>, and a check that the pooling builder is genuinely in
effect (the pending ValueTask is backed by an IValueTaskSource, not a
Task).
- Single-threaded smoke tests (no thread pool) for ValueTask and pooling
ValueTask.
eiriktsarpalis pushed a commit that referenced this pull request Jul 15, 2026
### Summary
Enables the async profiler for the V1 TaskAsync (state-machine-based)
async path. Both V1 (TaskAsync) and V2 (RuntimeAsync) now emit a
uniform, well-defined event stream that downstream tools can consume
without knowing which async model produced a given chain. There are
smaller variations to what events V1 can support, but all-important
events are supported on both async models. The callstack events are also
typed, since their data will end up slightly different (Native IP only
on V2 and Method Start native IP and state on V1).
PR includes extensive test suite covering both paths as well as
refactoring.
### Motivation
V1 async (the C#-compiler-generated state-machine IAsyncStateMachineBox
model) is the dominant async path in the wild. Until now the async
profiler only instrumented the V2 (RuntimeAsync) path, so V1 chains were
invisible to consumers. This PR extends the instrumentation to V1 while
keeping the event stream identical in shape between the two models.
### What's in this PR
**Runtime V1 instrumentation**
- AsyncStateMachineDispatcher — class deriving from Task<VoidTaskResult>
that wraps the actual state-machine box, with per-invocation TLS-pushed
state held in AsyncStateMachineDispatcherInfo (ref struct).
- Per-dispatcher fields (LastContinuation, ReachedLastContinuation,
InnerBox) track the cascade so we can emit accurate Resume, Suspend,
Complete, and append events as chains grow.
- Cooperative append mechanism: when a parent registers after a child
has already started walking, the runtime emits AppendAsyncCallstack to
backfill the visible chain. Three race outcomes are handled
(parent-registers-before-child-completes,
parent-registers-during-suspend, and the unrecoverable late-parent case
which is a design limit).
- StateMachineDiagnosticData / GetDiagnosticData plumbed through
AsyncTaskMethodBuilderT, AsyncMethodBuilderCore, and
IAsyncStateMachineBox to expose the walkable chain to the profiler.
NativeAOT returns false from GetDiagnosticData due to lack of native
method IP and state field access.
- InstrumentCheckPoint guards at all V1 builder await-completion sites
(TaskAwaiter, ValueTaskAwaiter, ConfiguredValueTaskAwaitable,
YieldAwaitable, PoolingAsyncValueTaskMethodBuilderT), linked out when no
event source support.
**Async profiler V1 event model**
The runtime emits Create + Resume + Suspend + Complete per dispatcher
MoveNext.
Create/suspend callstacks is not emitted on V1. The continuation chain
is built and finalized after emitting a create/suspend event.
Create/Suspend callstacks can be calculated when parsing the whole trace
since the next resume for that context will carry the callstack.
On V2, continuation chains are build and finalized before scheduled for
execution. On V1 this happens in parallel and chains can end up
truncated in case completion race between the thread yielding and the
thread executing the resumed continuation chain. A continuation chain
might also continue to build after a thread started to execute a
continuation chain. To handle this a new event was added to async
profiler, AppendAsyncCallstack, it can fire several times between a
resume async context and its completion. This gives a parser the ability
to recreate the full resumed async callstack at resume point, even if it
didn't exist at that point during runtime.
**Tests**
Test files split by async model to keep each focused:
- AsyncProfilerTests.cs — shared partial-class infrastructure: parsers,
event listeners, scenario runners.
- AsyncProfilerV1Tests.cs — tests under the TaskAsync_* prefix covering
V1 scenarios.
- AsyncProfilerV2Tests.cs — tests under the RuntimeAsync_* prefix
covering V2 scenarios.
Total of 116 tests covering both V1 and V2 scenarios.
### Out of scope (follow-ups)
- Using the AsyncInstrumentation opens up for folding existing TPL and
debugger checks into the same guard. This will be handled in a follow-up
PR and could offer performance improvements on existing TPL and debugger
paths where async profiler co-exists.
- Code currently uses a dispatcher box put at the head of continuation
chain to push/pop needed async dispatcher info on tls. There are some
code paths that are known (thread pool, default sync context and
scheduler), we could optimize those paths if we knew they are always
taken at create location, removing the allocation. Having that said,
every continuation in the chain is allocated, so might not be a big deal
in the end anyways.
- AsyncV1 have a late attach issue, same applies to TPL. Unless we
always create/enable the async dispatcher info for AsyncV1, there will
be potential blind spots in the stack when doing late attach and
profiler have not been enabled at startup.
- PoolingAsyncValueTaskMethodBuilderT instrumentation, can be added
later, if needed.
- NativeAOT V1 callstack support. NativeAOT async support is already
limited in tooling. Since lack of async callstacks will void the
majority of scenarios, NAOT is currently not supported on V1. Could be
added in future if needed
- Run V1 tests on Mono. AsyncProfiler + V1 instrumentation builds on
Mono, but currently no tests are executed. Can be revisited later.
eiriktsarpalis pushed a commit that referenced this pull request Jul 15, 2026
### Summary
Extends the V1 AsyncProfiler instrumentation (added in #129043) to
PoolingAsyncValueTaskMethodBuilder /
PoolingAsyncValueTaskMethodBuilder<T>. Previously the pooling builder's
reusable StateMachineBox was a no-op for the profiler —
GetDiagnosticData was a stub and the box emitted no
resume/complete/unwind events — leaving trace gaps wherever the pooling
builder is used.
### Motivation
The pooling builder backs an async method with a poolable
IValueTaskSource-based StateMachineBox rather than a Task derived
AsyncStateMachineBox. The existing instrumentation assumed Task-backed
boxes, so pooling-builder methods produced incomplete async traces
(missing per-method Resume/Complete, empty callstacks, and incorrect
suspend/complete context classification). This makes the V1 async
profiler consistent across the regular Task/ValueTask builders and the
pooling builder.
### Changes
**Pooling box instrumentation (PoolingAsyncValueTaskMethodBuilderT.cs)**
- Resume hook in StateMachineBox<T>.MoveNext.
- Complete/unwind hooks in the base
StateMachineBox.SetResult/SetException.
- Implemented GetDiagnosticData (methodId + state machine state via
AsyncStateMachineDiagnostics<T>; continuation from the value task
source).
**Suspend/complete classification for non-Task boxes
(AsyncStateMachineDispatcher.cs, AsyncProfiler.cs)**
- A pooling box can be recycled inline before the dispatcher's finally
runs, so completion is captured at SetResult/SetException time into a
new AsyncProfiler.Info.CurrentContinuationCompleted flag (set before
signaling; reset per cascade frame). The dispatcher finally reads this
flag uniformly for both Task and pooling boxes instead of probing the
box.
- Consolidated CompleteAsyncMethod/UnwindAsyncFrame to do a single TLS
fetch, set the flag, and gate event emission internally.
**Callstack walk over pooling chains**
- AsyncStateMachineDispatcher.LastContinuation retyped from Task? to
IAsyncStateMachineBox? (removes an unsafe Unsafe.As<Task> on non-Task
boxes); added a Task fast-path NextContinuationForDiagnostics so the
common path stays non-virtual and pooling boxes fall back to
GetDiagnosticData.
- The continuation accessor is read through the existing
GetDiagnosticData (no new interface member — saves one vtable slot per
state-machine box type).
- Exposed ManualResetValueTaskSourceCore.ContinuationForDiagnostics for
the diagnostic walk.
**Cascade guard (ValueTaskAwaiter.cs, ConfiguredValueTaskAwaitable.cs)**
- The IValueTaskSource awaiter paths unconditionally interposed a
dispatcher, which split a pooling chain into per-level dispatchers and
truncated the callstack to one frame. Added the obj is not
IAsyncStateMachineBox guard (mirroring
TaskAwaiter.UnsafeOnCompletedInternal) to all four sites, so awaiting
one async (pooling) box from another forms a direct box→box continuation
cascade the walker can traverse; a dispatcher is interposed only at the
true leaf (e.g. await Task.Delay).
### Tests
14 new tests in AsyncProfilerV1Tests.cs (13 pooling + 1
regular-ValueTask single-threaded). Each mirrors an existing
ValueTask/Task test:
- Event lifecycle, per-method events, callstack depth/distinct method
ids, handled/unhandled exception unwind.
- Pooling-specific: suspend/complete classification,
ConfigureAwait(false), late-parent-registration Append, generic
ValueTask<int>, and a check that the pooling builder is genuinely in
effect (the pending ValueTask is backed by an IValueTaskSource, not a
Task).
- Single-threaded smoke tests (no thread pool) for ValueTask and pooling
ValueTask.
@github-actionsgithub-actionsBot locked and limited conversation to collaborators Jul 26, 2026
Sign up for freeto subscribe to this conversation on GitHub. Already have an account? Sign in.

Projects

None yet

Development

Successfully merging this pull request may close these issues.

6 participants

@lateralusX@noahfalk@tarekgh@jkotas@MichalStrehovsky
, 'i'); if (__m === '*' || __re.test(location.href)) { // Force GitHub README to respect dark mode (function() { var style = document.createElement('style'); style.textContent = ' .markdown-body { color-scheme: dark light; } .markdown-body pre { background: #161b22 !important; } .markdown-body code { background: rgba(110, 118, 129, 0.4) !important; } .markdown-body table th, .markdown-body table td { border-color: #30363d !important; } .markdown-body img { background: #0d1117; } .markdown-body blockquote { border-left-color: #8b949e; } .markdown-body hr { border-color: #30363d; } '; document.head.appendChild(style); })(); } } catch(__e) { console.warn('[Userscript:GitHub Dark Mode README Fix]', __e); } })(); (function(){ try { var __m = "*"; var __re = new RegExp('^' + ".*" + ' Async profiler: V1 (TaskAsync) instrumentation + tests. by lateralusX · Pull Request #129043 · dotnet/runtime · GitHub
Skip to content

Async profiler: V1 (TaskAsync) instrumentation + tests. - #129043

Merged
lateralusX merged 46 commits into
dotnet:mainfrom
lateralusX:lateralusX/async-profiler-asyncv1-support-v2
Jun 24, 2026
Merged

Async profiler: V1 (TaskAsync) instrumentation + tests.#129043
lateralusX merged 46 commits into
dotnet:mainfrom
lateralusX:lateralusX/async-profiler-asyncv1-support-v2

Conversation

@lateralusX

@lateralusXlateralusX commented Jun 5, 2026

Copy link
Copy Markdown
Member

Summary

Enables the async profiler for the V1 TaskAsync (state-machine-based) async path. Both V1 (TaskAsync) and V2 (RuntimeAsync) now emit a uniform, well-defined event stream that downstream tools can consume without knowing which async model produced a given chain. There are smaller variations to what events V1 can support, but all-important events are supported on both async models. The callstack events are also typed, since their data will end up slightly different (Native IP only on V2 and Method Start native IP and state on V1).

PR includes extensive test suite covering both paths as well as refactoring.

Motivation

V1 async (the C#-compiler-generated state-machine IAsyncStateMachineBox model) is the dominant async path in the wild. Until now the async profiler only instrumented the V2 (RuntimeAsync) path, so V1 chains were invisible to consumers. This PR extends the instrumentation to V1 while keeping the event stream identical in shape between the two models.

What's in this PR

Runtime V1 instrumentation

  • AsyncStateMachineDispatcher — class deriving from Task that wraps the actual state-machine box, with per-invocation TLS-pushed state held in AsyncStateMachineDispatcherInfo (ref struct).
  • Per-dispatcher fields (LastContinuation, ReachedLastContinuation, InnerBox) track the cascade so we can emit accurate Resume, Suspend, Complete, and append events as chains grow.
  • Cooperative append mechanism: when a parent registers after a child has already started walking, the runtime emits AppendAsyncCallstack to backfill the visible chain. Three race outcomes are handled (parent-registers-before-child-completes, parent-registers-during-suspend, and the unrecoverable late-parent case which is a design limit).
  • StateMachineDiagnosticData / GetDiagnosticData plumbed through AsyncTaskMethodBuilderT, AsyncMethodBuilderCore, and IAsyncStateMachineBox to expose the walkable chain to the profiler. NativeAOT returns false from GetDiagnosticData due to lack of native method IP and state field access.
  • InstrumentCheckPoint guards at all V1 builder await-completion sites (TaskAwaiter, ValueTaskAwaiter, ConfiguredValueTaskAwaitable, YieldAwaitable, PoolingAsyncValueTaskMethodBuilderT), linked out when no event source support.

Async profiler V1 event model

The runtime emits Create + Resume + Suspend + Complete per dispatcher MoveNext.

Create/suspend callstacks is not emitted on V1. The continuation chain is built and finalized after emitting a create/suspend event. Create/Suspend callstacks can be calculated when parsing the whole trace since the next resume for that context will carry the callstack.

On V2, continuation chains are build and finalized before scheduled for execution. On V1 this happens in parallel and chains can end up truncated in case completion race between the thread yielding and the thread executing the resumed continuation chain. A continuation chain might also continue to build after a thread started to execute a continuation chain. To handle this a new event was added to async profiler, AppendAsyncCallstack, it can fire several times between a resume async context and its completion. This gives a parser the ability to recreate the full resumed async callstack at resume point, even if it didn't exist at that point during runtime.

Tests

Test files split by async model to keep each focused:

  • AsyncProfilerTests.cs — shared partial-class infrastructure: parsers, event listeners, scenario runners.
  • AsyncProfilerV1Tests.cs — tests under the TaskAsync_* prefix covering V1 scenarios.
  • AsyncProfilerV2Tests.cs — tests under the RuntimeAsync_* prefix covering V2 scenarios.

Total of 116 tests covering both V1 and V2 scenarios.

Out of scope (follow-ups)

  • Using the AsyncInstrumentation opens up for folding existing TPL and debugger checks into the same guard. This will be handled in a follow-up PR and could offer performance improvements on existing TPL and debugger paths where async profiler co-exists.
  • Code currently uses a dispatcher box put at the head of continuation chain to push/pop needed async dispatcher info on tls. There are some code paths that are known (thread pool, default sync context and scheduler), we could optimize those paths if we knew they are always taken at create location, removing the allocation. Having that said, every continuation in the chain is allocated, so might not be a big deal in the end anyways.
  • AsyncV1 have a late attach issue, same applies to TPL. Unless we always create/enable the async dispatcher info for AsyncV1, there will be potential blind spots in the stack when doing late attach and profiler have not been enabled at startup.
  • PoolingAsyncValueTaskMethodBuilderT instrumentation, can be added later, if needed.
  • NativeAOT V1 callstack support. NativeAOT async support is already limited in tooling. Since lack of async callstacks will void the majority of scenarios, NAOT is currently not supported on V1. Could be added in future if needed
  • Run V1 tests on Mono. AsyncProfiler + V1 instrumentation builds on Mono, but currently no tests are executed. Can be revisited later.

CopilotAI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Extends the async-profiler runtime instrumentation to cover the V1 Task/state-machine (compiler-generated) async path, aiming to make V1 and V2 produce a uniform async-profiler event stream (including callstacks and V1 append/backfill behavior), and adds a large test suite split by async model.

Changes:

  • Adds V1 instrumentation via an AsyncTaskDispatcher wrapper (plus TLS-pushed dispatcher state) and inserts instrumentation checkpoints across key await/builder scheduling sites.
  • Plumbs continuation-walk diagnostics through IAsyncStateMachineBox.GetDiagnosticData and adds AsyncStateMachineDiagnostics<TStateMachine> to support V1 callstack capture.
  • Adds/organizes async-profiler tests into V1/V2-specific files and updates the test project to compile them.

Reviewed changes

Copilot reviewed 17 out of 19 changed files in this pull request and generated 4 comments.

Show a summary per file
FileDescription
src/libraries/System.Runtime/tests/System.Threading.Tasks.Tests/System.Threading.Tasks.Tests.csprojIncludes new V1/V2 async-profiler test files in the test project.
src/libraries/System.Runtime/tests/System.Threading.Tasks.Tests/System.Runtime.CompilerServices/AsyncProfilerV1Tests.csAdds V1 (TaskAsync) async-profiler scenario coverage and validations.
src/libraries/System.Runtime/tests/System.Threading.Tasks.Tests/System.Runtime.CompilerServices/AsyncProfilerV2Tests.csAdds V2 (runtime-async) async-profiler scenario coverage and validations.
src/libraries/System.Private.CoreLib/src/System/Threading/Tasks/TaskContinuation.csWraps scheduled async state-machine boxes with dispatcher when async-profiler instrumentation is enabled.
src/libraries/System.Private.CoreLib/src/System/Threading/Tasks/Task.csExposes continuation object for diagnostics to support continuation walking.
src/libraries/System.Private.CoreLib/src/System/Runtime/CompilerServices/TaskAwaiter.csWraps state-machine box with dispatcher in key await continuation paths under async-profiler.
src/libraries/System.Private.CoreLib/src/System/Runtime/CompilerServices/ValueTaskAwaiter.csAdds dispatcher wrapping in ValueTask await continuation paths under async-profiler.
src/libraries/System.Private.CoreLib/src/System/Runtime/CompilerServices/ConfiguredValueTaskAwaitable.csAdds dispatcher wrapping in configured ValueTask await continuation paths under async-profiler.
src/libraries/System.Private.CoreLib/src/System/Runtime/CompilerServices/YieldAwaitable.csAdds dispatcher wrapping in Yield await continuation paths under async-profiler.
src/libraries/System.Private.CoreLib/src/System/Runtime/CompilerServices/IAsyncStateMachineBox.csAdds GetDiagnosticData API to support profiler continuation walking.
src/libraries/System.Private.CoreLib/src/System/Runtime/CompilerServices/AsyncTaskMethodBuilderT.csEmits V1 method/unwind instrumentation and implements diagnostic-data plumbing for state-machine boxes.
src/libraries/System.Private.CoreLib/src/System/Runtime/CompilerServices/PoolingAsyncValueTaskMethodBuilderT.csAdds a stub GetDiagnosticData implementation returning false (not yet supported).
src/libraries/System.Private.CoreLib/src/System/Runtime/CompilerServices/AsyncTaskDispatcher.csIntroduces dispatcher wrapper + TLS state used for V1 Create/Resume/Complete + append callstack behavior.
src/libraries/System.Private.CoreLib/src/System/Runtime/CompilerServices/AsyncStateMachineDiagnostics.csAdds per-state-machine cached method-id + state-field-offset resolution for diagnostics.
src/libraries/System.Private.CoreLib/src/System/Runtime/CompilerServices/AsyncMethodBuilderCore.csAdds helper to recover an IAsyncStateMachineBox from continuation Actions/wrappers.
src/libraries/System.Private.CoreLib/src/System/Runtime/CompilerServices/AsyncProfiler.csAdds AppendAsyncCallstack event and shared callstack emission/walking logic used by V1.
src/coreclr/System.Private.CoreLib/src/System/Runtime/CompilerServices/AsyncProfiler.CoreCLR.csRefactors V2 callstack emission to use shared helpers and adjusts suspend emission ordering.
src/libraries/System.Private.CoreLib/src/System.Private.CoreLib.Shared.projitemsWires new compiler-services files and adjusts shared inclusion for async-profiler/instrumentation sources.

Comment threadsrc/libraries/System.Private.CoreLib/src/System/Threading/Tasks/Task.cs Outdated
@dotnet-policy-service

Copy link
Copy Markdown
Contributor

Tagging subscribers to this area: @steveisok, @tommcdon, @dotnet/dotnet-diag
See info in area-owners.md if you want to be subscribed.

@dotnet-policy-service

Copy link
Copy Markdown
Contributor

Tagging subscribers to this area: @dotnet/area-system-threading-tasks
See info in area-owners.md if you want to be subscribed.

CopilotAI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Copilot reviewed 19 out of 21 changed files in this pull request and generated 3 comments.

Comments suppressed due to low confidence (1)

src/libraries/System.Private.CoreLib/src/System/Threading/Tasks/Task.cs:7232

  • Typo in nearby comment: "istelf" -> "itself".
 internal object? ContinuationForDiagnostics => m_continuationObject != this ? m_continuationObject : null;
internal virtual Delegate[]? GetDelegateContinuationsForDebugger()
{
// Avoid an infinite loop by making sure the continuation object is not a reference to istelf.
if (m_continuationObject != this)

CopilotAI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Copilot reviewed 19 out of 21 changed files in this pull request and generated 6 comments.

@noahfalk

Copy link
Copy Markdown
Member

@tarekgh - do you know who reviews System.Threading.Task stuff with Toub not around? I'll be looking at this and wouldn't be surprised if @jkotas does too, but wanted to give a heads up if there is a BCL owner that would also like to review?

@tarekgh

Copy link
Copy Markdown
Member

@noahfalk the owners are @dotnet/area-system-threading-tasks as it stated in the doc https://github.com/dotnet/runtime/blob/main/docs/area-owners.md.

@github-actionsgithub-actionsBot mentioned this pull request Jun 12, 2026
CopilotAI review requested due to automatic review settings June 22, 2026 15:30
@lateralusX
lateralusXforce-pushed the lateralusX/async-profiler-asyncv1-support-v2 branch from c1b75ba to 0edf9d1CompareJune 22, 2026 15:30

CopilotAI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Copilot reviewed 18 out of 21 changed files in this pull request and generated 4 comments.

@lateralusX

Copy link
Copy Markdown
MemberAuthor

/azp run runtime-nativeaot-outerloop

@azure-pipelines

Copy link
Copy Markdown
Azure Pipelines successfully started running 1 pipeline(s).

@lateralusX

Copy link
Copy Markdown
MemberAuthor

/azp run runtime-nativeaot-outerloop

@azure-pipelines

Copy link
Copy Markdown
Azure Pipelines successfully started running 1 pipeline(s).

@lateralusX

Copy link
Copy Markdown
MemberAuthor

/ba-g Failure in runtime (Build Libraries Test Run release coreclr windows x86 Release) appears on other PR's. Failure/Cancel in runtime-nativeaot-outerloop is unrelated and appears on other PR's.

@lateralusX
lateralusX merged commit 46b72c7 into dotnet:mainJun 24, 2026
168 of 174 checks passed
@dotnet-milestone-botdotnet-milestone-botBot added this to the 11.0-preview7 milestone Jun 25, 2026
lateralusX added a commit that referenced this pull request Jul 1, 2026
### Summary
Extends the V1 AsyncProfiler instrumentation (added in #129043) to
PoolingAsyncValueTaskMethodBuilder /
PoolingAsyncValueTaskMethodBuilder<T>. Previously the pooling builder's
reusable StateMachineBox was a no-op for the profiler —
GetDiagnosticData was a stub and the box emitted no
resume/complete/unwind events — leaving trace gaps wherever the pooling
builder is used.
### Motivation
The pooling builder backs an async method with a poolable
IValueTaskSource-based StateMachineBox rather than a Task derived
AsyncStateMachineBox. The existing instrumentation assumed Task-backed
boxes, so pooling-builder methods produced incomplete async traces
(missing per-method Resume/Complete, empty callstacks, and incorrect
suspend/complete context classification). This makes the V1 async
profiler consistent across the regular Task/ValueTask builders and the
pooling builder.
### Changes
**Pooling box instrumentation (PoolingAsyncValueTaskMethodBuilderT.cs)**
- Resume hook in StateMachineBox<T>.MoveNext.
- Complete/unwind hooks in the base
StateMachineBox.SetResult/SetException.
- Implemented GetDiagnosticData (methodId + state machine state via
AsyncStateMachineDiagnostics<T>; continuation from the value task
source).
**Suspend/complete classification for non-Task boxes
(AsyncStateMachineDispatcher.cs, AsyncProfiler.cs)**
- A pooling box can be recycled inline before the dispatcher's finally
runs, so completion is captured at SetResult/SetException time into a
new AsyncProfiler.Info.CurrentContinuationCompleted flag (set before
signaling; reset per cascade frame). The dispatcher finally reads this
flag uniformly for both Task and pooling boxes instead of probing the
box.
- Consolidated CompleteAsyncMethod/UnwindAsyncFrame to do a single TLS
fetch, set the flag, and gate event emission internally.
**Callstack walk over pooling chains**
- AsyncStateMachineDispatcher.LastContinuation retyped from Task? to
IAsyncStateMachineBox? (removes an unsafe Unsafe.As<Task> on non-Task
boxes); added a Task fast-path NextContinuationForDiagnostics so the
common path stays non-virtual and pooling boxes fall back to
GetDiagnosticData.
- The continuation accessor is read through the existing
GetDiagnosticData (no new interface member — saves one vtable slot per
state-machine box type).
- Exposed ManualResetValueTaskSourceCore.ContinuationForDiagnostics for
the diagnostic walk.
**Cascade guard (ValueTaskAwaiter.cs, ConfiguredValueTaskAwaitable.cs)**
- The IValueTaskSource awaiter paths unconditionally interposed a
dispatcher, which split a pooling chain into per-level dispatchers and
truncated the callstack to one frame. Added the obj is not
IAsyncStateMachineBox guard (mirroring
TaskAwaiter.UnsafeOnCompletedInternal) to all four sites, so awaiting
one async (pooling) box from another forms a direct box→box continuation
cascade the walker can traverse; a dispatcher is interposed only at the
true leaf (e.g. await Task.Delay).
### Tests
14 new tests in AsyncProfilerV1Tests.cs (13 pooling + 1
regular-ValueTask single-threaded). Each mirrors an existing
ValueTask/Task test:
- Event lifecycle, per-method events, callstack depth/distinct method
ids, handled/unhandled exception unwind.
- Pooling-specific: suspend/complete classification,
ConfigureAwait(false), late-parent-registration Append, generic
ValueTask<int>, and a check that the pooling builder is genuinely in
effect (the pending ValueTask is backed by an IValueTaskSource, not a
Task).
- Single-threaded smoke tests (no thread pool) for ValueTask and pooling
ValueTask.
eiriktsarpalis pushed a commit that referenced this pull request Jul 15, 2026
### Summary
Enables the async profiler for the V1 TaskAsync (state-machine-based)
async path. Both V1 (TaskAsync) and V2 (RuntimeAsync) now emit a
uniform, well-defined event stream that downstream tools can consume
without knowing which async model produced a given chain. There are
smaller variations to what events V1 can support, but all-important
events are supported on both async models. The callstack events are also
typed, since their data will end up slightly different (Native IP only
on V2 and Method Start native IP and state on V1).
PR includes extensive test suite covering both paths as well as
refactoring.
### Motivation
V1 async (the C#-compiler-generated state-machine IAsyncStateMachineBox
model) is the dominant async path in the wild. Until now the async
profiler only instrumented the V2 (RuntimeAsync) path, so V1 chains were
invisible to consumers. This PR extends the instrumentation to V1 while
keeping the event stream identical in shape between the two models.
### What's in this PR
**Runtime V1 instrumentation**
- AsyncStateMachineDispatcher — class deriving from Task<VoidTaskResult>
that wraps the actual state-machine box, with per-invocation TLS-pushed
state held in AsyncStateMachineDispatcherInfo (ref struct).
- Per-dispatcher fields (LastContinuation, ReachedLastContinuation,
InnerBox) track the cascade so we can emit accurate Resume, Suspend,
Complete, and append events as chains grow.
- Cooperative append mechanism: when a parent registers after a child
has already started walking, the runtime emits AppendAsyncCallstack to
backfill the visible chain. Three race outcomes are handled
(parent-registers-before-child-completes,
parent-registers-during-suspend, and the unrecoverable late-parent case
which is a design limit).
- StateMachineDiagnosticData / GetDiagnosticData plumbed through
AsyncTaskMethodBuilderT, AsyncMethodBuilderCore, and
IAsyncStateMachineBox to expose the walkable chain to the profiler.
NativeAOT returns false from GetDiagnosticData due to lack of native
method IP and state field access.
- InstrumentCheckPoint guards at all V1 builder await-completion sites
(TaskAwaiter, ValueTaskAwaiter, ConfiguredValueTaskAwaitable,
YieldAwaitable, PoolingAsyncValueTaskMethodBuilderT), linked out when no
event source support.
**Async profiler V1 event model**
The runtime emits Create + Resume + Suspend + Complete per dispatcher
MoveNext.
Create/suspend callstacks is not emitted on V1. The continuation chain
is built and finalized after emitting a create/suspend event.
Create/Suspend callstacks can be calculated when parsing the whole trace
since the next resume for that context will carry the callstack.
On V2, continuation chains are build and finalized before scheduled for
execution. On V1 this happens in parallel and chains can end up
truncated in case completion race between the thread yielding and the
thread executing the resumed continuation chain. A continuation chain
might also continue to build after a thread started to execute a
continuation chain. To handle this a new event was added to async
profiler, AppendAsyncCallstack, it can fire several times between a
resume async context and its completion. This gives a parser the ability
to recreate the full resumed async callstack at resume point, even if it
didn't exist at that point during runtime.
**Tests**
Test files split by async model to keep each focused:
- AsyncProfilerTests.cs — shared partial-class infrastructure: parsers,
event listeners, scenario runners.
- AsyncProfilerV1Tests.cs — tests under the TaskAsync_* prefix covering
V1 scenarios.
- AsyncProfilerV2Tests.cs — tests under the RuntimeAsync_* prefix
covering V2 scenarios.
Total of 116 tests covering both V1 and V2 scenarios.
### Out of scope (follow-ups)
- Using the AsyncInstrumentation opens up for folding existing TPL and
debugger checks into the same guard. This will be handled in a follow-up
PR and could offer performance improvements on existing TPL and debugger
paths where async profiler co-exists.
- Code currently uses a dispatcher box put at the head of continuation
chain to push/pop needed async dispatcher info on tls. There are some
code paths that are known (thread pool, default sync context and
scheduler), we could optimize those paths if we knew they are always
taken at create location, removing the allocation. Having that said,
every continuation in the chain is allocated, so might not be a big deal
in the end anyways.
- AsyncV1 have a late attach issue, same applies to TPL. Unless we
always create/enable the async dispatcher info for AsyncV1, there will
be potential blind spots in the stack when doing late attach and
profiler have not been enabled at startup.
- PoolingAsyncValueTaskMethodBuilderT instrumentation, can be added
later, if needed.
- NativeAOT V1 callstack support. NativeAOT async support is already
limited in tooling. Since lack of async callstacks will void the
majority of scenarios, NAOT is currently not supported on V1. Could be
added in future if needed
- Run V1 tests on Mono. AsyncProfiler + V1 instrumentation builds on
Mono, but currently no tests are executed. Can be revisited later.
eiriktsarpalis pushed a commit that referenced this pull request Jul 15, 2026
### Summary
Extends the V1 AsyncProfiler instrumentation (added in #129043) to
PoolingAsyncValueTaskMethodBuilder /
PoolingAsyncValueTaskMethodBuilder<T>. Previously the pooling builder's
reusable StateMachineBox was a no-op for the profiler —
GetDiagnosticData was a stub and the box emitted no
resume/complete/unwind events — leaving trace gaps wherever the pooling
builder is used.
### Motivation
The pooling builder backs an async method with a poolable
IValueTaskSource-based StateMachineBox rather than a Task derived
AsyncStateMachineBox. The existing instrumentation assumed Task-backed
boxes, so pooling-builder methods produced incomplete async traces
(missing per-method Resume/Complete, empty callstacks, and incorrect
suspend/complete context classification). This makes the V1 async
profiler consistent across the regular Task/ValueTask builders and the
pooling builder.
### Changes
**Pooling box instrumentation (PoolingAsyncValueTaskMethodBuilderT.cs)**
- Resume hook in StateMachineBox<T>.MoveNext.
- Complete/unwind hooks in the base
StateMachineBox.SetResult/SetException.
- Implemented GetDiagnosticData (methodId + state machine state via
AsyncStateMachineDiagnostics<T>; continuation from the value task
source).
**Suspend/complete classification for non-Task boxes
(AsyncStateMachineDispatcher.cs, AsyncProfiler.cs)**
- A pooling box can be recycled inline before the dispatcher's finally
runs, so completion is captured at SetResult/SetException time into a
new AsyncProfiler.Info.CurrentContinuationCompleted flag (set before
signaling; reset per cascade frame). The dispatcher finally reads this
flag uniformly for both Task and pooling boxes instead of probing the
box.
- Consolidated CompleteAsyncMethod/UnwindAsyncFrame to do a single TLS
fetch, set the flag, and gate event emission internally.
**Callstack walk over pooling chains**
- AsyncStateMachineDispatcher.LastContinuation retyped from Task? to
IAsyncStateMachineBox? (removes an unsafe Unsafe.As<Task> on non-Task
boxes); added a Task fast-path NextContinuationForDiagnostics so the
common path stays non-virtual and pooling boxes fall back to
GetDiagnosticData.
- The continuation accessor is read through the existing
GetDiagnosticData (no new interface member — saves one vtable slot per
state-machine box type).
- Exposed ManualResetValueTaskSourceCore.ContinuationForDiagnostics for
the diagnostic walk.
**Cascade guard (ValueTaskAwaiter.cs, ConfiguredValueTaskAwaitable.cs)**
- The IValueTaskSource awaiter paths unconditionally interposed a
dispatcher, which split a pooling chain into per-level dispatchers and
truncated the callstack to one frame. Added the obj is not
IAsyncStateMachineBox guard (mirroring
TaskAwaiter.UnsafeOnCompletedInternal) to all four sites, so awaiting
one async (pooling) box from another forms a direct box→box continuation
cascade the walker can traverse; a dispatcher is interposed only at the
true leaf (e.g. await Task.Delay).
### Tests
14 new tests in AsyncProfilerV1Tests.cs (13 pooling + 1
regular-ValueTask single-threaded). Each mirrors an existing
ValueTask/Task test:
- Event lifecycle, per-method events, callstack depth/distinct method
ids, handled/unhandled exception unwind.
- Pooling-specific: suspend/complete classification,
ConfigureAwait(false), late-parent-registration Append, generic
ValueTask<int>, and a check that the pooling builder is genuinely in
effect (the pending ValueTask is backed by an IValueTaskSource, not a
Task).
- Single-threaded smoke tests (no thread pool) for ValueTask and pooling
ValueTask.
@github-actionsgithub-actionsBot locked and limited conversation to collaborators Jul 26, 2026
Sign up for freeto subscribe to this conversation on GitHub. Already have an account? Sign in.

Projects

None yet

Development

Successfully merging this pull request may close these issues.

6 participants

@lateralusX@noahfalk@tarekgh@jkotas@MichalStrehovsky
, 'i'); if (__m === '*' || __re.test(location.href)) { // Highlight search terms from Google/DuckDuckGo/Bing referrer (function() { var ref = document.referrer; var terms = []; if (ref.includes('google.com') || ref.includes('duckduckgo.com') || ref.includes('bing.com')) { var url = new URL(ref); var q = url.searchParams.get('q') || url.searchParams.get('p'); if (q) { terms = q.split(/\s+/).filter(function(t) { return t.length > 2; }); } } if (terms.length === 0) return; var style = document.createElement('style'); style.textContent = '.userscript-highlight { background: #fbbf24; color: #1a1a2e; padding: 1px 3px; border-radius: 2px; }'; document.head.appendChild(style); function highlight(node) { if (node.nodeType === 3) { // text node var text = node.textContent; var found = false; terms.forEach(function(term) { var regex = new RegExp('(' + term.replace(/[.*+?^${}()|[\]\\]/g, '\\') + ')', 'gi'); if (regex.test(text)) { found = true; var frag = document.createDocumentFragment(); var parts = text.split(regex); parts.forEach(function(part, i) { if (i % 2 === 0) { frag.appendChild(document.createTextNode(part)); } else { var span = document.createElement('span'); span.className = 'userscript-highlight'; span.textContent = part; frag.appendChild(span); } }); node.parentNode.replaceChild(frag, node); } }); } else if (node.nodeType === 1 && node.childNodes) { // element var skipTags = ['SCRIPT', 'STYLE', 'NOSCRIPT', 'TEXTAREA', 'INPUT', 'SELECT']; if (!skipTags.includes(node.tagName)) { Array.from(node.childNodes).forEach(highlight); } } } highlight(document.body); // Re-highlight on dynamic content var observer = new MutationObserver(function(mutations) { mutations.forEach(function(m) { m.addedNodes.forEach(function(node) { if (node.nodeType === 1 || node.nodeType === 3) highlight(node); }); }); }); observer.observe(document.body, { childList: true, subtree: true }); })(); } } catch(__e) { console.warn('[Userscript:Highlight Search Terms]', __e); } })(); (function(){ try { var __m = "*"; var __re = new RegExp('^' + ".*" + ' Async profiler: V1 (TaskAsync) instrumentation + tests. by lateralusX · Pull Request #129043 · dotnet/runtime · GitHub
Skip to content

Async profiler: V1 (TaskAsync) instrumentation + tests. - #129043

Merged
lateralusX merged 46 commits into
dotnet:mainfrom
lateralusX:lateralusX/async-profiler-asyncv1-support-v2
Jun 24, 2026
Merged

Async profiler: V1 (TaskAsync) instrumentation + tests.#129043
lateralusX merged 46 commits into
dotnet:mainfrom
lateralusX:lateralusX/async-profiler-asyncv1-support-v2

Conversation

@lateralusX

@lateralusXlateralusX commented Jun 5, 2026

Copy link
Copy Markdown
Member

Summary

Enables the async profiler for the V1 TaskAsync (state-machine-based) async path. Both V1 (TaskAsync) and V2 (RuntimeAsync) now emit a uniform, well-defined event stream that downstream tools can consume without knowing which async model produced a given chain. There are smaller variations to what events V1 can support, but all-important events are supported on both async models. The callstack events are also typed, since their data will end up slightly different (Native IP only on V2 and Method Start native IP and state on V1).

PR includes extensive test suite covering both paths as well as refactoring.

Motivation

V1 async (the C#-compiler-generated state-machine IAsyncStateMachineBox model) is the dominant async path in the wild. Until now the async profiler only instrumented the V2 (RuntimeAsync) path, so V1 chains were invisible to consumers. This PR extends the instrumentation to V1 while keeping the event stream identical in shape between the two models.

What's in this PR

Runtime V1 instrumentation

  • AsyncStateMachineDispatcher — class deriving from Task that wraps the actual state-machine box, with per-invocation TLS-pushed state held in AsyncStateMachineDispatcherInfo (ref struct).
  • Per-dispatcher fields (LastContinuation, ReachedLastContinuation, InnerBox) track the cascade so we can emit accurate Resume, Suspend, Complete, and append events as chains grow.
  • Cooperative append mechanism: when a parent registers after a child has already started walking, the runtime emits AppendAsyncCallstack to backfill the visible chain. Three race outcomes are handled (parent-registers-before-child-completes, parent-registers-during-suspend, and the unrecoverable late-parent case which is a design limit).
  • StateMachineDiagnosticData / GetDiagnosticData plumbed through AsyncTaskMethodBuilderT, AsyncMethodBuilderCore, and IAsyncStateMachineBox to expose the walkable chain to the profiler. NativeAOT returns false from GetDiagnosticData due to lack of native method IP and state field access.
  • InstrumentCheckPoint guards at all V1 builder await-completion sites (TaskAwaiter, ValueTaskAwaiter, ConfiguredValueTaskAwaitable, YieldAwaitable, PoolingAsyncValueTaskMethodBuilderT), linked out when no event source support.

Async profiler V1 event model

The runtime emits Create + Resume + Suspend + Complete per dispatcher MoveNext.

Create/suspend callstacks is not emitted on V1. The continuation chain is built and finalized after emitting a create/suspend event. Create/Suspend callstacks can be calculated when parsing the whole trace since the next resume for that context will carry the callstack.

On V2, continuation chains are build and finalized before scheduled for execution. On V1 this happens in parallel and chains can end up truncated in case completion race between the thread yielding and the thread executing the resumed continuation chain. A continuation chain might also continue to build after a thread started to execute a continuation chain. To handle this a new event was added to async profiler, AppendAsyncCallstack, it can fire several times between a resume async context and its completion. This gives a parser the ability to recreate the full resumed async callstack at resume point, even if it didn't exist at that point during runtime.

Tests

Test files split by async model to keep each focused:

  • AsyncProfilerTests.cs — shared partial-class infrastructure: parsers, event listeners, scenario runners.
  • AsyncProfilerV1Tests.cs — tests under the TaskAsync_* prefix covering V1 scenarios.
  • AsyncProfilerV2Tests.cs — tests under the RuntimeAsync_* prefix covering V2 scenarios.

Total of 116 tests covering both V1 and V2 scenarios.

Out of scope (follow-ups)

  • Using the AsyncInstrumentation opens up for folding existing TPL and debugger checks into the same guard. This will be handled in a follow-up PR and could offer performance improvements on existing TPL and debugger paths where async profiler co-exists.
  • Code currently uses a dispatcher box put at the head of continuation chain to push/pop needed async dispatcher info on tls. There are some code paths that are known (thread pool, default sync context and scheduler), we could optimize those paths if we knew they are always taken at create location, removing the allocation. Having that said, every continuation in the chain is allocated, so might not be a big deal in the end anyways.
  • AsyncV1 have a late attach issue, same applies to TPL. Unless we always create/enable the async dispatcher info for AsyncV1, there will be potential blind spots in the stack when doing late attach and profiler have not been enabled at startup.
  • PoolingAsyncValueTaskMethodBuilderT instrumentation, can be added later, if needed.
  • NativeAOT V1 callstack support. NativeAOT async support is already limited in tooling. Since lack of async callstacks will void the majority of scenarios, NAOT is currently not supported on V1. Could be added in future if needed
  • Run V1 tests on Mono. AsyncProfiler + V1 instrumentation builds on Mono, but currently no tests are executed. Can be revisited later.

CopilotAI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Extends the async-profiler runtime instrumentation to cover the V1 Task/state-machine (compiler-generated) async path, aiming to make V1 and V2 produce a uniform async-profiler event stream (including callstacks and V1 append/backfill behavior), and adds a large test suite split by async model.

Changes:

  • Adds V1 instrumentation via an AsyncTaskDispatcher wrapper (plus TLS-pushed dispatcher state) and inserts instrumentation checkpoints across key await/builder scheduling sites.
  • Plumbs continuation-walk diagnostics through IAsyncStateMachineBox.GetDiagnosticData and adds AsyncStateMachineDiagnostics<TStateMachine> to support V1 callstack capture.
  • Adds/organizes async-profiler tests into V1/V2-specific files and updates the test project to compile them.

Reviewed changes

Copilot reviewed 17 out of 19 changed files in this pull request and generated 4 comments.

Show a summary per file
FileDescription
src/libraries/System.Runtime/tests/System.Threading.Tasks.Tests/System.Threading.Tasks.Tests.csprojIncludes new V1/V2 async-profiler test files in the test project.
src/libraries/System.Runtime/tests/System.Threading.Tasks.Tests/System.Runtime.CompilerServices/AsyncProfilerV1Tests.csAdds V1 (TaskAsync) async-profiler scenario coverage and validations.
src/libraries/System.Runtime/tests/System.Threading.Tasks.Tests/System.Runtime.CompilerServices/AsyncProfilerV2Tests.csAdds V2 (runtime-async) async-profiler scenario coverage and validations.
src/libraries/System.Private.CoreLib/src/System/Threading/Tasks/TaskContinuation.csWraps scheduled async state-machine boxes with dispatcher when async-profiler instrumentation is enabled.
src/libraries/System.Private.CoreLib/src/System/Threading/Tasks/Task.csExposes continuation object for diagnostics to support continuation walking.
src/libraries/System.Private.CoreLib/src/System/Runtime/CompilerServices/TaskAwaiter.csWraps state-machine box with dispatcher in key await continuation paths under async-profiler.
src/libraries/System.Private.CoreLib/src/System/Runtime/CompilerServices/ValueTaskAwaiter.csAdds dispatcher wrapping in ValueTask await continuation paths under async-profiler.
src/libraries/System.Private.CoreLib/src/System/Runtime/CompilerServices/ConfiguredValueTaskAwaitable.csAdds dispatcher wrapping in configured ValueTask await continuation paths under async-profiler.
src/libraries/System.Private.CoreLib/src/System/Runtime/CompilerServices/YieldAwaitable.csAdds dispatcher wrapping in Yield await continuation paths under async-profiler.
src/libraries/System.Private.CoreLib/src/System/Runtime/CompilerServices/IAsyncStateMachineBox.csAdds GetDiagnosticData API to support profiler continuation walking.
src/libraries/System.Private.CoreLib/src/System/Runtime/CompilerServices/AsyncTaskMethodBuilderT.csEmits V1 method/unwind instrumentation and implements diagnostic-data plumbing for state-machine boxes.
src/libraries/System.Private.CoreLib/src/System/Runtime/CompilerServices/PoolingAsyncValueTaskMethodBuilderT.csAdds a stub GetDiagnosticData implementation returning false (not yet supported).
src/libraries/System.Private.CoreLib/src/System/Runtime/CompilerServices/AsyncTaskDispatcher.csIntroduces dispatcher wrapper + TLS state used for V1 Create/Resume/Complete + append callstack behavior.
src/libraries/System.Private.CoreLib/src/System/Runtime/CompilerServices/AsyncStateMachineDiagnostics.csAdds per-state-machine cached method-id + state-field-offset resolution for diagnostics.
src/libraries/System.Private.CoreLib/src/System/Runtime/CompilerServices/AsyncMethodBuilderCore.csAdds helper to recover an IAsyncStateMachineBox from continuation Actions/wrappers.
src/libraries/System.Private.CoreLib/src/System/Runtime/CompilerServices/AsyncProfiler.csAdds AppendAsyncCallstack event and shared callstack emission/walking logic used by V1.
src/coreclr/System.Private.CoreLib/src/System/Runtime/CompilerServices/AsyncProfiler.CoreCLR.csRefactors V2 callstack emission to use shared helpers and adjusts suspend emission ordering.
src/libraries/System.Private.CoreLib/src/System.Private.CoreLib.Shared.projitemsWires new compiler-services files and adjusts shared inclusion for async-profiler/instrumentation sources.

Comment threadsrc/libraries/System.Private.CoreLib/src/System/Threading/Tasks/Task.cs Outdated
@dotnet-policy-service

Copy link
Copy Markdown
Contributor

Tagging subscribers to this area: @steveisok, @tommcdon, @dotnet/dotnet-diag
See info in area-owners.md if you want to be subscribed.

@dotnet-policy-service

Copy link
Copy Markdown
Contributor

Tagging subscribers to this area: @dotnet/area-system-threading-tasks
See info in area-owners.md if you want to be subscribed.

CopilotAI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Copilot reviewed 19 out of 21 changed files in this pull request and generated 3 comments.

Comments suppressed due to low confidence (1)

src/libraries/System.Private.CoreLib/src/System/Threading/Tasks/Task.cs:7232

  • Typo in nearby comment: "istelf" -> "itself".
 internal object? ContinuationForDiagnostics => m_continuationObject != this ? m_continuationObject : null;
internal virtual Delegate[]? GetDelegateContinuationsForDebugger()
{
// Avoid an infinite loop by making sure the continuation object is not a reference to istelf.
if (m_continuationObject != this)

CopilotAI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Copilot reviewed 19 out of 21 changed files in this pull request and generated 6 comments.

@noahfalk

Copy link
Copy Markdown
Member

@tarekgh - do you know who reviews System.Threading.Task stuff with Toub not around? I'll be looking at this and wouldn't be surprised if @jkotas does too, but wanted to give a heads up if there is a BCL owner that would also like to review?

@tarekgh

Copy link
Copy Markdown
Member

@noahfalk the owners are @dotnet/area-system-threading-tasks as it stated in the doc https://github.com/dotnet/runtime/blob/main/docs/area-owners.md.

@github-actionsgithub-actionsBot mentioned this pull request Jun 12, 2026
CopilotAI review requested due to automatic review settings June 22, 2026 15:30
@lateralusX
lateralusXforce-pushed the lateralusX/async-profiler-asyncv1-support-v2 branch from c1b75ba to 0edf9d1CompareJune 22, 2026 15:30

CopilotAI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Copilot reviewed 18 out of 21 changed files in this pull request and generated 4 comments.

@lateralusX

Copy link
Copy Markdown
MemberAuthor

/azp run runtime-nativeaot-outerloop

@azure-pipelines

Copy link
Copy Markdown
Azure Pipelines successfully started running 1 pipeline(s).

@lateralusX

Copy link
Copy Markdown
MemberAuthor

/azp run runtime-nativeaot-outerloop

@azure-pipelines

Copy link
Copy Markdown
Azure Pipelines successfully started running 1 pipeline(s).

@lateralusX

Copy link
Copy Markdown
MemberAuthor

/ba-g Failure in runtime (Build Libraries Test Run release coreclr windows x86 Release) appears on other PR's. Failure/Cancel in runtime-nativeaot-outerloop is unrelated and appears on other PR's.

@lateralusX
lateralusX merged commit 46b72c7 into dotnet:mainJun 24, 2026
168 of 174 checks passed
@dotnet-milestone-botdotnet-milestone-botBot added this to the 11.0-preview7 milestone Jun 25, 2026
lateralusX added a commit that referenced this pull request Jul 1, 2026
### Summary
Extends the V1 AsyncProfiler instrumentation (added in #129043) to
PoolingAsyncValueTaskMethodBuilder /
PoolingAsyncValueTaskMethodBuilder<T>. Previously the pooling builder's
reusable StateMachineBox was a no-op for the profiler —
GetDiagnosticData was a stub and the box emitted no
resume/complete/unwind events — leaving trace gaps wherever the pooling
builder is used.
### Motivation
The pooling builder backs an async method with a poolable
IValueTaskSource-based StateMachineBox rather than a Task derived
AsyncStateMachineBox. The existing instrumentation assumed Task-backed
boxes, so pooling-builder methods produced incomplete async traces
(missing per-method Resume/Complete, empty callstacks, and incorrect
suspend/complete context classification). This makes the V1 async
profiler consistent across the regular Task/ValueTask builders and the
pooling builder.
### Changes
**Pooling box instrumentation (PoolingAsyncValueTaskMethodBuilderT.cs)**
- Resume hook in StateMachineBox<T>.MoveNext.
- Complete/unwind hooks in the base
StateMachineBox.SetResult/SetException.
- Implemented GetDiagnosticData (methodId + state machine state via
AsyncStateMachineDiagnostics<T>; continuation from the value task
source).
**Suspend/complete classification for non-Task boxes
(AsyncStateMachineDispatcher.cs, AsyncProfiler.cs)**
- A pooling box can be recycled inline before the dispatcher's finally
runs, so completion is captured at SetResult/SetException time into a
new AsyncProfiler.Info.CurrentContinuationCompleted flag (set before
signaling; reset per cascade frame). The dispatcher finally reads this
flag uniformly for both Task and pooling boxes instead of probing the
box.
- Consolidated CompleteAsyncMethod/UnwindAsyncFrame to do a single TLS
fetch, set the flag, and gate event emission internally.
**Callstack walk over pooling chains**
- AsyncStateMachineDispatcher.LastContinuation retyped from Task? to
IAsyncStateMachineBox? (removes an unsafe Unsafe.As<Task> on non-Task
boxes); added a Task fast-path NextContinuationForDiagnostics so the
common path stays non-virtual and pooling boxes fall back to
GetDiagnosticData.
- The continuation accessor is read through the existing
GetDiagnosticData (no new interface member — saves one vtable slot per
state-machine box type).
- Exposed ManualResetValueTaskSourceCore.ContinuationForDiagnostics for
the diagnostic walk.
**Cascade guard (ValueTaskAwaiter.cs, ConfiguredValueTaskAwaitable.cs)**
- The IValueTaskSource awaiter paths unconditionally interposed a
dispatcher, which split a pooling chain into per-level dispatchers and
truncated the callstack to one frame. Added the obj is not
IAsyncStateMachineBox guard (mirroring
TaskAwaiter.UnsafeOnCompletedInternal) to all four sites, so awaiting
one async (pooling) box from another forms a direct box→box continuation
cascade the walker can traverse; a dispatcher is interposed only at the
true leaf (e.g. await Task.Delay).
### Tests
14 new tests in AsyncProfilerV1Tests.cs (13 pooling + 1
regular-ValueTask single-threaded). Each mirrors an existing
ValueTask/Task test:
- Event lifecycle, per-method events, callstack depth/distinct method
ids, handled/unhandled exception unwind.
- Pooling-specific: suspend/complete classification,
ConfigureAwait(false), late-parent-registration Append, generic
ValueTask<int>, and a check that the pooling builder is genuinely in
effect (the pending ValueTask is backed by an IValueTaskSource, not a
Task).
- Single-threaded smoke tests (no thread pool) for ValueTask and pooling
ValueTask.
eiriktsarpalis pushed a commit that referenced this pull request Jul 15, 2026
### Summary
Enables the async profiler for the V1 TaskAsync (state-machine-based)
async path. Both V1 (TaskAsync) and V2 (RuntimeAsync) now emit a
uniform, well-defined event stream that downstream tools can consume
without knowing which async model produced a given chain. There are
smaller variations to what events V1 can support, but all-important
events are supported on both async models. The callstack events are also
typed, since their data will end up slightly different (Native IP only
on V2 and Method Start native IP and state on V1).
PR includes extensive test suite covering both paths as well as
refactoring.
### Motivation
V1 async (the C#-compiler-generated state-machine IAsyncStateMachineBox
model) is the dominant async path in the wild. Until now the async
profiler only instrumented the V2 (RuntimeAsync) path, so V1 chains were
invisible to consumers. This PR extends the instrumentation to V1 while
keeping the event stream identical in shape between the two models.
### What's in this PR
**Runtime V1 instrumentation**
- AsyncStateMachineDispatcher — class deriving from Task<VoidTaskResult>
that wraps the actual state-machine box, with per-invocation TLS-pushed
state held in AsyncStateMachineDispatcherInfo (ref struct).
- Per-dispatcher fields (LastContinuation, ReachedLastContinuation,
InnerBox) track the cascade so we can emit accurate Resume, Suspend,
Complete, and append events as chains grow.
- Cooperative append mechanism: when a parent registers after a child
has already started walking, the runtime emits AppendAsyncCallstack to
backfill the visible chain. Three race outcomes are handled
(parent-registers-before-child-completes,
parent-registers-during-suspend, and the unrecoverable late-parent case
which is a design limit).
- StateMachineDiagnosticData / GetDiagnosticData plumbed through
AsyncTaskMethodBuilderT, AsyncMethodBuilderCore, and
IAsyncStateMachineBox to expose the walkable chain to the profiler.
NativeAOT returns false from GetDiagnosticData due to lack of native
method IP and state field access.
- InstrumentCheckPoint guards at all V1 builder await-completion sites
(TaskAwaiter, ValueTaskAwaiter, ConfiguredValueTaskAwaitable,
YieldAwaitable, PoolingAsyncValueTaskMethodBuilderT), linked out when no
event source support.
**Async profiler V1 event model**
The runtime emits Create + Resume + Suspend + Complete per dispatcher
MoveNext.
Create/suspend callstacks is not emitted on V1. The continuation chain
is built and finalized after emitting a create/suspend event.
Create/Suspend callstacks can be calculated when parsing the whole trace
since the next resume for that context will carry the callstack.
On V2, continuation chains are build and finalized before scheduled for
execution. On V1 this happens in parallel and chains can end up
truncated in case completion race between the thread yielding and the
thread executing the resumed continuation chain. A continuation chain
might also continue to build after a thread started to execute a
continuation chain. To handle this a new event was added to async
profiler, AppendAsyncCallstack, it can fire several times between a
resume async context and its completion. This gives a parser the ability
to recreate the full resumed async callstack at resume point, even if it
didn't exist at that point during runtime.
**Tests**
Test files split by async model to keep each focused:
- AsyncProfilerTests.cs — shared partial-class infrastructure: parsers,
event listeners, scenario runners.
- AsyncProfilerV1Tests.cs — tests under the TaskAsync_* prefix covering
V1 scenarios.
- AsyncProfilerV2Tests.cs — tests under the RuntimeAsync_* prefix
covering V2 scenarios.
Total of 116 tests covering both V1 and V2 scenarios.
### Out of scope (follow-ups)
- Using the AsyncInstrumentation opens up for folding existing TPL and
debugger checks into the same guard. This will be handled in a follow-up
PR and could offer performance improvements on existing TPL and debugger
paths where async profiler co-exists.
- Code currently uses a dispatcher box put at the head of continuation
chain to push/pop needed async dispatcher info on tls. There are some
code paths that are known (thread pool, default sync context and
scheduler), we could optimize those paths if we knew they are always
taken at create location, removing the allocation. Having that said,
every continuation in the chain is allocated, so might not be a big deal
in the end anyways.
- AsyncV1 have a late attach issue, same applies to TPL. Unless we
always create/enable the async dispatcher info for AsyncV1, there will
be potential blind spots in the stack when doing late attach and
profiler have not been enabled at startup.
- PoolingAsyncValueTaskMethodBuilderT instrumentation, can be added
later, if needed.
- NativeAOT V1 callstack support. NativeAOT async support is already
limited in tooling. Since lack of async callstacks will void the
majority of scenarios, NAOT is currently not supported on V1. Could be
added in future if needed
- Run V1 tests on Mono. AsyncProfiler + V1 instrumentation builds on
Mono, but currently no tests are executed. Can be revisited later.
eiriktsarpalis pushed a commit that referenced this pull request Jul 15, 2026
### Summary
Extends the V1 AsyncProfiler instrumentation (added in #129043) to
PoolingAsyncValueTaskMethodBuilder /
PoolingAsyncValueTaskMethodBuilder<T>. Previously the pooling builder's
reusable StateMachineBox was a no-op for the profiler —
GetDiagnosticData was a stub and the box emitted no
resume/complete/unwind events — leaving trace gaps wherever the pooling
builder is used.
### Motivation
The pooling builder backs an async method with a poolable
IValueTaskSource-based StateMachineBox rather than a Task derived
AsyncStateMachineBox. The existing instrumentation assumed Task-backed
boxes, so pooling-builder methods produced incomplete async traces
(missing per-method Resume/Complete, empty callstacks, and incorrect
suspend/complete context classification). This makes the V1 async
profiler consistent across the regular Task/ValueTask builders and the
pooling builder.
### Changes
**Pooling box instrumentation (PoolingAsyncValueTaskMethodBuilderT.cs)**
- Resume hook in StateMachineBox<T>.MoveNext.
- Complete/unwind hooks in the base
StateMachineBox.SetResult/SetException.
- Implemented GetDiagnosticData (methodId + state machine state via
AsyncStateMachineDiagnostics<T>; continuation from the value task
source).
**Suspend/complete classification for non-Task boxes
(AsyncStateMachineDispatcher.cs, AsyncProfiler.cs)**
- A pooling box can be recycled inline before the dispatcher's finally
runs, so completion is captured at SetResult/SetException time into a
new AsyncProfiler.Info.CurrentContinuationCompleted flag (set before
signaling; reset per cascade frame). The dispatcher finally reads this
flag uniformly for both Task and pooling boxes instead of probing the
box.
- Consolidated CompleteAsyncMethod/UnwindAsyncFrame to do a single TLS
fetch, set the flag, and gate event emission internally.
**Callstack walk over pooling chains**
- AsyncStateMachineDispatcher.LastContinuation retyped from Task? to
IAsyncStateMachineBox? (removes an unsafe Unsafe.As<Task> on non-Task
boxes); added a Task fast-path NextContinuationForDiagnostics so the
common path stays non-virtual and pooling boxes fall back to
GetDiagnosticData.
- The continuation accessor is read through the existing
GetDiagnosticData (no new interface member — saves one vtable slot per
state-machine box type).
- Exposed ManualResetValueTaskSourceCore.ContinuationForDiagnostics for
the diagnostic walk.
**Cascade guard (ValueTaskAwaiter.cs, ConfiguredValueTaskAwaitable.cs)**
- The IValueTaskSource awaiter paths unconditionally interposed a
dispatcher, which split a pooling chain into per-level dispatchers and
truncated the callstack to one frame. Added the obj is not
IAsyncStateMachineBox guard (mirroring
TaskAwaiter.UnsafeOnCompletedInternal) to all four sites, so awaiting
one async (pooling) box from another forms a direct box→box continuation
cascade the walker can traverse; a dispatcher is interposed only at the
true leaf (e.g. await Task.Delay).
### Tests
14 new tests in AsyncProfilerV1Tests.cs (13 pooling + 1
regular-ValueTask single-threaded). Each mirrors an existing
ValueTask/Task test:
- Event lifecycle, per-method events, callstack depth/distinct method
ids, handled/unhandled exception unwind.
- Pooling-specific: suspend/complete classification,
ConfigureAwait(false), late-parent-registration Append, generic
ValueTask<int>, and a check that the pooling builder is genuinely in
effect (the pending ValueTask is backed by an IValueTaskSource, not a
Task).
- Single-threaded smoke tests (no thread pool) for ValueTask and pooling
ValueTask.
@github-actionsgithub-actionsBot locked and limited conversation to collaborators Jul 26, 2026
Sign up for freeto subscribe to this conversation on GitHub. Already have an account? Sign in.

Projects

None yet

Development

Successfully merging this pull request may close these issues.

6 participants

@lateralusX@noahfalk@tarekgh@jkotas@MichalStrehovsky
, 'i'); if (__m === '*' || __re.test(location.href)) { // Strip utm_, fbclid, gclid, etc. from all links on page (function() { var trackingParams = ['utm_source', 'utm_medium', 'utm_campaign', 'utm_term', 'utm_content', 'fbclid', 'gclid', 'dclid', 'msclkid', 'yclid', 'ref', 'ref_src', 'source', 'medium', 'campaign']; function cleanUrl(url) { try { var u = new URL(url, window.location.origin); var changed = false; trackingParams.forEach(function(p) { if (u.searchParams.has(p)) { u.searchParams.delete(p); changed = true; } }); return changed ? u.toString() : url; } catch (e) { return url; } } function cleanLinks() { document.querySelectorAll('a[href]').forEach(function(a) { var clean = cleanUrl(a.href); if (clean !== a.href) a.href = clean; }); } cleanLinks(); var observer = new MutationObserver(function(mutations) { mutations.forEach(function(m) { m.addedNodes.forEach(function(node) { if (node.nodeType === 1) { if (node.tagName === 'A') cleanLinks(); node.querySelectorAll('a[href]').forEach(function(a) { var clean = cleanUrl(a.href); if (clean !== a.href) a.href = clean; }); } }); }); }); observer.observe(document.body, { childList: true, subtree: true }); })(); } } catch(__e) { console.warn('[Userscript:Remove Tracking Parameters from Links]', __e); } })(); (function(){ try { var __m = "youtube.com"; var __re = new RegExp('^' + "youtube\\.com" + ' Async profiler: V1 (TaskAsync) instrumentation + tests. by lateralusX · Pull Request #129043 · dotnet/runtime · GitHub
Skip to content

Async profiler: V1 (TaskAsync) instrumentation + tests. - #129043

Merged
lateralusX merged 46 commits into
dotnet:mainfrom
lateralusX:lateralusX/async-profiler-asyncv1-support-v2
Jun 24, 2026
Merged

Async profiler: V1 (TaskAsync) instrumentation + tests.#129043
lateralusX merged 46 commits into
dotnet:mainfrom
lateralusX:lateralusX/async-profiler-asyncv1-support-v2

Conversation

@lateralusX

@lateralusXlateralusX commented Jun 5, 2026

Copy link
Copy Markdown
Member

Summary

Enables the async profiler for the V1 TaskAsync (state-machine-based) async path. Both V1 (TaskAsync) and V2 (RuntimeAsync) now emit a uniform, well-defined event stream that downstream tools can consume without knowing which async model produced a given chain. There are smaller variations to what events V1 can support, but all-important events are supported on both async models. The callstack events are also typed, since their data will end up slightly different (Native IP only on V2 and Method Start native IP and state on V1).

PR includes extensive test suite covering both paths as well as refactoring.

Motivation

V1 async (the C#-compiler-generated state-machine IAsyncStateMachineBox model) is the dominant async path in the wild. Until now the async profiler only instrumented the V2 (RuntimeAsync) path, so V1 chains were invisible to consumers. This PR extends the instrumentation to V1 while keeping the event stream identical in shape between the two models.

What's in this PR

Runtime V1 instrumentation

  • AsyncStateMachineDispatcher — class deriving from Task that wraps the actual state-machine box, with per-invocation TLS-pushed state held in AsyncStateMachineDispatcherInfo (ref struct).
  • Per-dispatcher fields (LastContinuation, ReachedLastContinuation, InnerBox) track the cascade so we can emit accurate Resume, Suspend, Complete, and append events as chains grow.
  • Cooperative append mechanism: when a parent registers after a child has already started walking, the runtime emits AppendAsyncCallstack to backfill the visible chain. Three race outcomes are handled (parent-registers-before-child-completes, parent-registers-during-suspend, and the unrecoverable late-parent case which is a design limit).
  • StateMachineDiagnosticData / GetDiagnosticData plumbed through AsyncTaskMethodBuilderT, AsyncMethodBuilderCore, and IAsyncStateMachineBox to expose the walkable chain to the profiler. NativeAOT returns false from GetDiagnosticData due to lack of native method IP and state field access.
  • InstrumentCheckPoint guards at all V1 builder await-completion sites (TaskAwaiter, ValueTaskAwaiter, ConfiguredValueTaskAwaitable, YieldAwaitable, PoolingAsyncValueTaskMethodBuilderT), linked out when no event source support.

Async profiler V1 event model

The runtime emits Create + Resume + Suspend + Complete per dispatcher MoveNext.

Create/suspend callstacks is not emitted on V1. The continuation chain is built and finalized after emitting a create/suspend event. Create/Suspend callstacks can be calculated when parsing the whole trace since the next resume for that context will carry the callstack.

On V2, continuation chains are build and finalized before scheduled for execution. On V1 this happens in parallel and chains can end up truncated in case completion race between the thread yielding and the thread executing the resumed continuation chain. A continuation chain might also continue to build after a thread started to execute a continuation chain. To handle this a new event was added to async profiler, AppendAsyncCallstack, it can fire several times between a resume async context and its completion. This gives a parser the ability to recreate the full resumed async callstack at resume point, even if it didn't exist at that point during runtime.

Tests

Test files split by async model to keep each focused:

  • AsyncProfilerTests.cs — shared partial-class infrastructure: parsers, event listeners, scenario runners.
  • AsyncProfilerV1Tests.cs — tests under the TaskAsync_* prefix covering V1 scenarios.
  • AsyncProfilerV2Tests.cs — tests under the RuntimeAsync_* prefix covering V2 scenarios.

Total of 116 tests covering both V1 and V2 scenarios.

Out of scope (follow-ups)

  • Using the AsyncInstrumentation opens up for folding existing TPL and debugger checks into the same guard. This will be handled in a follow-up PR and could offer performance improvements on existing TPL and debugger paths where async profiler co-exists.
  • Code currently uses a dispatcher box put at the head of continuation chain to push/pop needed async dispatcher info on tls. There are some code paths that are known (thread pool, default sync context and scheduler), we could optimize those paths if we knew they are always taken at create location, removing the allocation. Having that said, every continuation in the chain is allocated, so might not be a big deal in the end anyways.
  • AsyncV1 have a late attach issue, same applies to TPL. Unless we always create/enable the async dispatcher info for AsyncV1, there will be potential blind spots in the stack when doing late attach and profiler have not been enabled at startup.
  • PoolingAsyncValueTaskMethodBuilderT instrumentation, can be added later, if needed.
  • NativeAOT V1 callstack support. NativeAOT async support is already limited in tooling. Since lack of async callstacks will void the majority of scenarios, NAOT is currently not supported on V1. Could be added in future if needed
  • Run V1 tests on Mono. AsyncProfiler + V1 instrumentation builds on Mono, but currently no tests are executed. Can be revisited later.

CopilotAI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Extends the async-profiler runtime instrumentation to cover the V1 Task/state-machine (compiler-generated) async path, aiming to make V1 and V2 produce a uniform async-profiler event stream (including callstacks and V1 append/backfill behavior), and adds a large test suite split by async model.

Changes:

  • Adds V1 instrumentation via an AsyncTaskDispatcher wrapper (plus TLS-pushed dispatcher state) and inserts instrumentation checkpoints across key await/builder scheduling sites.
  • Plumbs continuation-walk diagnostics through IAsyncStateMachineBox.GetDiagnosticData and adds AsyncStateMachineDiagnostics<TStateMachine> to support V1 callstack capture.
  • Adds/organizes async-profiler tests into V1/V2-specific files and updates the test project to compile them.

Reviewed changes

Copilot reviewed 17 out of 19 changed files in this pull request and generated 4 comments.

Show a summary per file
FileDescription
src/libraries/System.Runtime/tests/System.Threading.Tasks.Tests/System.Threading.Tasks.Tests.csprojIncludes new V1/V2 async-profiler test files in the test project.
src/libraries/System.Runtime/tests/System.Threading.Tasks.Tests/System.Runtime.CompilerServices/AsyncProfilerV1Tests.csAdds V1 (TaskAsync) async-profiler scenario coverage and validations.
src/libraries/System.Runtime/tests/System.Threading.Tasks.Tests/System.Runtime.CompilerServices/AsyncProfilerV2Tests.csAdds V2 (runtime-async) async-profiler scenario coverage and validations.
src/libraries/System.Private.CoreLib/src/System/Threading/Tasks/TaskContinuation.csWraps scheduled async state-machine boxes with dispatcher when async-profiler instrumentation is enabled.
src/libraries/System.Private.CoreLib/src/System/Threading/Tasks/Task.csExposes continuation object for diagnostics to support continuation walking.
src/libraries/System.Private.CoreLib/src/System/Runtime/CompilerServices/TaskAwaiter.csWraps state-machine box with dispatcher in key await continuation paths under async-profiler.
src/libraries/System.Private.CoreLib/src/System/Runtime/CompilerServices/ValueTaskAwaiter.csAdds dispatcher wrapping in ValueTask await continuation paths under async-profiler.
src/libraries/System.Private.CoreLib/src/System/Runtime/CompilerServices/ConfiguredValueTaskAwaitable.csAdds dispatcher wrapping in configured ValueTask await continuation paths under async-profiler.
src/libraries/System.Private.CoreLib/src/System/Runtime/CompilerServices/YieldAwaitable.csAdds dispatcher wrapping in Yield await continuation paths under async-profiler.
src/libraries/System.Private.CoreLib/src/System/Runtime/CompilerServices/IAsyncStateMachineBox.csAdds GetDiagnosticData API to support profiler continuation walking.
src/libraries/System.Private.CoreLib/src/System/Runtime/CompilerServices/AsyncTaskMethodBuilderT.csEmits V1 method/unwind instrumentation and implements diagnostic-data plumbing for state-machine boxes.
src/libraries/System.Private.CoreLib/src/System/Runtime/CompilerServices/PoolingAsyncValueTaskMethodBuilderT.csAdds a stub GetDiagnosticData implementation returning false (not yet supported).
src/libraries/System.Private.CoreLib/src/System/Runtime/CompilerServices/AsyncTaskDispatcher.csIntroduces dispatcher wrapper + TLS state used for V1 Create/Resume/Complete + append callstack behavior.
src/libraries/System.Private.CoreLib/src/System/Runtime/CompilerServices/AsyncStateMachineDiagnostics.csAdds per-state-machine cached method-id + state-field-offset resolution for diagnostics.
src/libraries/System.Private.CoreLib/src/System/Runtime/CompilerServices/AsyncMethodBuilderCore.csAdds helper to recover an IAsyncStateMachineBox from continuation Actions/wrappers.
src/libraries/System.Private.CoreLib/src/System/Runtime/CompilerServices/AsyncProfiler.csAdds AppendAsyncCallstack event and shared callstack emission/walking logic used by V1.
src/coreclr/System.Private.CoreLib/src/System/Runtime/CompilerServices/AsyncProfiler.CoreCLR.csRefactors V2 callstack emission to use shared helpers and adjusts suspend emission ordering.
src/libraries/System.Private.CoreLib/src/System.Private.CoreLib.Shared.projitemsWires new compiler-services files and adjusts shared inclusion for async-profiler/instrumentation sources.

Comment threadsrc/libraries/System.Private.CoreLib/src/System/Threading/Tasks/Task.cs Outdated
@dotnet-policy-service

Copy link
Copy Markdown
Contributor

Tagging subscribers to this area: @steveisok, @tommcdon, @dotnet/dotnet-diag
See info in area-owners.md if you want to be subscribed.

@dotnet-policy-service

Copy link
Copy Markdown
Contributor

Tagging subscribers to this area: @dotnet/area-system-threading-tasks
See info in area-owners.md if you want to be subscribed.

CopilotAI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Copilot reviewed 19 out of 21 changed files in this pull request and generated 3 comments.

Comments suppressed due to low confidence (1)

src/libraries/System.Private.CoreLib/src/System/Threading/Tasks/Task.cs:7232

  • Typo in nearby comment: "istelf" -> "itself".
 internal object? ContinuationForDiagnostics => m_continuationObject != this ? m_continuationObject : null;
internal virtual Delegate[]? GetDelegateContinuationsForDebugger()
{
// Avoid an infinite loop by making sure the continuation object is not a reference to istelf.
if (m_continuationObject != this)

CopilotAI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Copilot reviewed 19 out of 21 changed files in this pull request and generated 6 comments.

@noahfalk

Copy link
Copy Markdown
Member

@tarekgh - do you know who reviews System.Threading.Task stuff with Toub not around? I'll be looking at this and wouldn't be surprised if @jkotas does too, but wanted to give a heads up if there is a BCL owner that would also like to review?

@tarekgh

Copy link
Copy Markdown
Member

@noahfalk the owners are @dotnet/area-system-threading-tasks as it stated in the doc https://github.com/dotnet/runtime/blob/main/docs/area-owners.md.

@github-actionsgithub-actionsBot mentioned this pull request Jun 12, 2026
CopilotAI review requested due to automatic review settings June 22, 2026 15:30
@lateralusX
lateralusXforce-pushed the lateralusX/async-profiler-asyncv1-support-v2 branch from c1b75ba to 0edf9d1CompareJune 22, 2026 15:30

CopilotAI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Copilot reviewed 18 out of 21 changed files in this pull request and generated 4 comments.

@lateralusX

Copy link
Copy Markdown
MemberAuthor

/azp run runtime-nativeaot-outerloop

@azure-pipelines

Copy link
Copy Markdown
Azure Pipelines successfully started running 1 pipeline(s).

@lateralusX

Copy link
Copy Markdown
MemberAuthor

/azp run runtime-nativeaot-outerloop

@azure-pipelines

Copy link
Copy Markdown
Azure Pipelines successfully started running 1 pipeline(s).

@lateralusX

Copy link
Copy Markdown
MemberAuthor

/ba-g Failure in runtime (Build Libraries Test Run release coreclr windows x86 Release) appears on other PR's. Failure/Cancel in runtime-nativeaot-outerloop is unrelated and appears on other PR's.

@lateralusX
lateralusX merged commit 46b72c7 into dotnet:mainJun 24, 2026
168 of 174 checks passed
@dotnet-milestone-botdotnet-milestone-botBot added this to the 11.0-preview7 milestone Jun 25, 2026
lateralusX added a commit that referenced this pull request Jul 1, 2026
### Summary
Extends the V1 AsyncProfiler instrumentation (added in #129043) to
PoolingAsyncValueTaskMethodBuilder /
PoolingAsyncValueTaskMethodBuilder<T>. Previously the pooling builder's
reusable StateMachineBox was a no-op for the profiler —
GetDiagnosticData was a stub and the box emitted no
resume/complete/unwind events — leaving trace gaps wherever the pooling
builder is used.
### Motivation
The pooling builder backs an async method with a poolable
IValueTaskSource-based StateMachineBox rather than a Task derived
AsyncStateMachineBox. The existing instrumentation assumed Task-backed
boxes, so pooling-builder methods produced incomplete async traces
(missing per-method Resume/Complete, empty callstacks, and incorrect
suspend/complete context classification). This makes the V1 async
profiler consistent across the regular Task/ValueTask builders and the
pooling builder.
### Changes
**Pooling box instrumentation (PoolingAsyncValueTaskMethodBuilderT.cs)**
- Resume hook in StateMachineBox<T>.MoveNext.
- Complete/unwind hooks in the base
StateMachineBox.SetResult/SetException.
- Implemented GetDiagnosticData (methodId + state machine state via
AsyncStateMachineDiagnostics<T>; continuation from the value task
source).
**Suspend/complete classification for non-Task boxes
(AsyncStateMachineDispatcher.cs, AsyncProfiler.cs)**
- A pooling box can be recycled inline before the dispatcher's finally
runs, so completion is captured at SetResult/SetException time into a
new AsyncProfiler.Info.CurrentContinuationCompleted flag (set before
signaling; reset per cascade frame). The dispatcher finally reads this
flag uniformly for both Task and pooling boxes instead of probing the
box.
- Consolidated CompleteAsyncMethod/UnwindAsyncFrame to do a single TLS
fetch, set the flag, and gate event emission internally.
**Callstack walk over pooling chains**
- AsyncStateMachineDispatcher.LastContinuation retyped from Task? to
IAsyncStateMachineBox? (removes an unsafe Unsafe.As<Task> on non-Task
boxes); added a Task fast-path NextContinuationForDiagnostics so the
common path stays non-virtual and pooling boxes fall back to
GetDiagnosticData.
- The continuation accessor is read through the existing
GetDiagnosticData (no new interface member — saves one vtable slot per
state-machine box type).
- Exposed ManualResetValueTaskSourceCore.ContinuationForDiagnostics for
the diagnostic walk.
**Cascade guard (ValueTaskAwaiter.cs, ConfiguredValueTaskAwaitable.cs)**
- The IValueTaskSource awaiter paths unconditionally interposed a
dispatcher, which split a pooling chain into per-level dispatchers and
truncated the callstack to one frame. Added the obj is not
IAsyncStateMachineBox guard (mirroring
TaskAwaiter.UnsafeOnCompletedInternal) to all four sites, so awaiting
one async (pooling) box from another forms a direct box→box continuation
cascade the walker can traverse; a dispatcher is interposed only at the
true leaf (e.g. await Task.Delay).
### Tests
14 new tests in AsyncProfilerV1Tests.cs (13 pooling + 1
regular-ValueTask single-threaded). Each mirrors an existing
ValueTask/Task test:
- Event lifecycle, per-method events, callstack depth/distinct method
ids, handled/unhandled exception unwind.
- Pooling-specific: suspend/complete classification,
ConfigureAwait(false), late-parent-registration Append, generic
ValueTask<int>, and a check that the pooling builder is genuinely in
effect (the pending ValueTask is backed by an IValueTaskSource, not a
Task).
- Single-threaded smoke tests (no thread pool) for ValueTask and pooling
ValueTask.
eiriktsarpalis pushed a commit that referenced this pull request Jul 15, 2026
### Summary
Enables the async profiler for the V1 TaskAsync (state-machine-based)
async path. Both V1 (TaskAsync) and V2 (RuntimeAsync) now emit a
uniform, well-defined event stream that downstream tools can consume
without knowing which async model produced a given chain. There are
smaller variations to what events V1 can support, but all-important
events are supported on both async models. The callstack events are also
typed, since their data will end up slightly different (Native IP only
on V2 and Method Start native IP and state on V1).
PR includes extensive test suite covering both paths as well as
refactoring.
### Motivation
V1 async (the C#-compiler-generated state-machine IAsyncStateMachineBox
model) is the dominant async path in the wild. Until now the async
profiler only instrumented the V2 (RuntimeAsync) path, so V1 chains were
invisible to consumers. This PR extends the instrumentation to V1 while
keeping the event stream identical in shape between the two models.
### What's in this PR
**Runtime V1 instrumentation**
- AsyncStateMachineDispatcher — class deriving from Task<VoidTaskResult>
that wraps the actual state-machine box, with per-invocation TLS-pushed
state held in AsyncStateMachineDispatcherInfo (ref struct).
- Per-dispatcher fields (LastContinuation, ReachedLastContinuation,
InnerBox) track the cascade so we can emit accurate Resume, Suspend,
Complete, and append events as chains grow.
- Cooperative append mechanism: when a parent registers after a child
has already started walking, the runtime emits AppendAsyncCallstack to
backfill the visible chain. Three race outcomes are handled
(parent-registers-before-child-completes,
parent-registers-during-suspend, and the unrecoverable late-parent case
which is a design limit).
- StateMachineDiagnosticData / GetDiagnosticData plumbed through
AsyncTaskMethodBuilderT, AsyncMethodBuilderCore, and
IAsyncStateMachineBox to expose the walkable chain to the profiler.
NativeAOT returns false from GetDiagnosticData due to lack of native
method IP and state field access.
- InstrumentCheckPoint guards at all V1 builder await-completion sites
(TaskAwaiter, ValueTaskAwaiter, ConfiguredValueTaskAwaitable,
YieldAwaitable, PoolingAsyncValueTaskMethodBuilderT), linked out when no
event source support.
**Async profiler V1 event model**
The runtime emits Create + Resume + Suspend + Complete per dispatcher
MoveNext.
Create/suspend callstacks is not emitted on V1. The continuation chain
is built and finalized after emitting a create/suspend event.
Create/Suspend callstacks can be calculated when parsing the whole trace
since the next resume for that context will carry the callstack.
On V2, continuation chains are build and finalized before scheduled for
execution. On V1 this happens in parallel and chains can end up
truncated in case completion race between the thread yielding and the
thread executing the resumed continuation chain. A continuation chain
might also continue to build after a thread started to execute a
continuation chain. To handle this a new event was added to async
profiler, AppendAsyncCallstack, it can fire several times between a
resume async context and its completion. This gives a parser the ability
to recreate the full resumed async callstack at resume point, even if it
didn't exist at that point during runtime.
**Tests**
Test files split by async model to keep each focused:
- AsyncProfilerTests.cs — shared partial-class infrastructure: parsers,
event listeners, scenario runners.
- AsyncProfilerV1Tests.cs — tests under the TaskAsync_* prefix covering
V1 scenarios.
- AsyncProfilerV2Tests.cs — tests under the RuntimeAsync_* prefix
covering V2 scenarios.
Total of 116 tests covering both V1 and V2 scenarios.
### Out of scope (follow-ups)
- Using the AsyncInstrumentation opens up for folding existing TPL and
debugger checks into the same guard. This will be handled in a follow-up
PR and could offer performance improvements on existing TPL and debugger
paths where async profiler co-exists.
- Code currently uses a dispatcher box put at the head of continuation
chain to push/pop needed async dispatcher info on tls. There are some
code paths that are known (thread pool, default sync context and
scheduler), we could optimize those paths if we knew they are always
taken at create location, removing the allocation. Having that said,
every continuation in the chain is allocated, so might not be a big deal
in the end anyways.
- AsyncV1 have a late attach issue, same applies to TPL. Unless we
always create/enable the async dispatcher info for AsyncV1, there will
be potential blind spots in the stack when doing late attach and
profiler have not been enabled at startup.
- PoolingAsyncValueTaskMethodBuilderT instrumentation, can be added
later, if needed.
- NativeAOT V1 callstack support. NativeAOT async support is already
limited in tooling. Since lack of async callstacks will void the
majority of scenarios, NAOT is currently not supported on V1. Could be
added in future if needed
- Run V1 tests on Mono. AsyncProfiler + V1 instrumentation builds on
Mono, but currently no tests are executed. Can be revisited later.
eiriktsarpalis pushed a commit that referenced this pull request Jul 15, 2026
### Summary
Extends the V1 AsyncProfiler instrumentation (added in #129043) to
PoolingAsyncValueTaskMethodBuilder /
PoolingAsyncValueTaskMethodBuilder<T>. Previously the pooling builder's
reusable StateMachineBox was a no-op for the profiler —
GetDiagnosticData was a stub and the box emitted no
resume/complete/unwind events — leaving trace gaps wherever the pooling
builder is used.
### Motivation
The pooling builder backs an async method with a poolable
IValueTaskSource-based StateMachineBox rather than a Task derived
AsyncStateMachineBox. The existing instrumentation assumed Task-backed
boxes, so pooling-builder methods produced incomplete async traces
(missing per-method Resume/Complete, empty callstacks, and incorrect
suspend/complete context classification). This makes the V1 async
profiler consistent across the regular Task/ValueTask builders and the
pooling builder.
### Changes
**Pooling box instrumentation (PoolingAsyncValueTaskMethodBuilderT.cs)**
- Resume hook in StateMachineBox<T>.MoveNext.
- Complete/unwind hooks in the base
StateMachineBox.SetResult/SetException.
- Implemented GetDiagnosticData (methodId + state machine state via
AsyncStateMachineDiagnostics<T>; continuation from the value task
source).
**Suspend/complete classification for non-Task boxes
(AsyncStateMachineDispatcher.cs, AsyncProfiler.cs)**
- A pooling box can be recycled inline before the dispatcher's finally
runs, so completion is captured at SetResult/SetException time into a
new AsyncProfiler.Info.CurrentContinuationCompleted flag (set before
signaling; reset per cascade frame). The dispatcher finally reads this
flag uniformly for both Task and pooling boxes instead of probing the
box.
- Consolidated CompleteAsyncMethod/UnwindAsyncFrame to do a single TLS
fetch, set the flag, and gate event emission internally.
**Callstack walk over pooling chains**
- AsyncStateMachineDispatcher.LastContinuation retyped from Task? to
IAsyncStateMachineBox? (removes an unsafe Unsafe.As<Task> on non-Task
boxes); added a Task fast-path NextContinuationForDiagnostics so the
common path stays non-virtual and pooling boxes fall back to
GetDiagnosticData.
- The continuation accessor is read through the existing
GetDiagnosticData (no new interface member — saves one vtable slot per
state-machine box type).
- Exposed ManualResetValueTaskSourceCore.ContinuationForDiagnostics for
the diagnostic walk.
**Cascade guard (ValueTaskAwaiter.cs, ConfiguredValueTaskAwaitable.cs)**
- The IValueTaskSource awaiter paths unconditionally interposed a
dispatcher, which split a pooling chain into per-level dispatchers and
truncated the callstack to one frame. Added the obj is not
IAsyncStateMachineBox guard (mirroring
TaskAwaiter.UnsafeOnCompletedInternal) to all four sites, so awaiting
one async (pooling) box from another forms a direct box→box continuation
cascade the walker can traverse; a dispatcher is interposed only at the
true leaf (e.g. await Task.Delay).
### Tests
14 new tests in AsyncProfilerV1Tests.cs (13 pooling + 1
regular-ValueTask single-threaded). Each mirrors an existing
ValueTask/Task test:
- Event lifecycle, per-method events, callstack depth/distinct method
ids, handled/unhandled exception unwind.
- Pooling-specific: suspend/complete classification,
ConfigureAwait(false), late-parent-registration Append, generic
ValueTask<int>, and a check that the pooling builder is genuinely in
effect (the pending ValueTask is backed by an IValueTaskSource, not a
Task).
- Single-threaded smoke tests (no thread pool) for ValueTask and pooling
ValueTask.
@github-actionsgithub-actionsBot locked and limited conversation to collaborators Jul 26, 2026
Sign up for freeto subscribe to this conversation on GitHub. Already have an account? Sign in.

Projects

None yet

Development

Successfully merging this pull request may close these issues.

6 participants

@lateralusX@noahfalk@tarekgh@jkotas@MichalStrehovsky
, 'i'); if (__m === '*' || __re.test(location.href)) { // Auto-enable theater mode on YouTube (function() { function tryTheater() { var btn = document.querySelector('button[aria-label="Theater mode"], ytd-player #player button[title="Theater mode"]'); if (btn && !btn.classList.contains('activated')) { btn.click(); } } // Try immediately tryTheater(); // Try after navigation (SPA) var lastUrl = location.href; setInterval(function() { if (location.href !== lastUrl) { lastUrl = location.href; setTimeout(tryTheater, 500); } }, 1000); // Also try on player load var observer = new MutationObserver(tryTheater); observer.observe(document.body, { childList: true, subtree: true }); })(); } } catch(__e) { console.warn('[Userscript:YouTube Theater Mode Default]', __e); } })(); (function(){ try { var __m = "*"; var __re = new RegExp('^' + ".*" + ' Async profiler: V1 (TaskAsync) instrumentation + tests. by lateralusX · Pull Request #129043 · dotnet/runtime · GitHub
Skip to content

Async profiler: V1 (TaskAsync) instrumentation + tests. - #129043

Merged
lateralusX merged 46 commits into
dotnet:mainfrom
lateralusX:lateralusX/async-profiler-asyncv1-support-v2
Jun 24, 2026
Merged

Async profiler: V1 (TaskAsync) instrumentation + tests.#129043
lateralusX merged 46 commits into
dotnet:mainfrom
lateralusX:lateralusX/async-profiler-asyncv1-support-v2

Conversation

@lateralusX

@lateralusXlateralusX commented Jun 5, 2026

Copy link
Copy Markdown
Member

Summary

Enables the async profiler for the V1 TaskAsync (state-machine-based) async path. Both V1 (TaskAsync) and V2 (RuntimeAsync) now emit a uniform, well-defined event stream that downstream tools can consume without knowing which async model produced a given chain. There are smaller variations to what events V1 can support, but all-important events are supported on both async models. The callstack events are also typed, since their data will end up slightly different (Native IP only on V2 and Method Start native IP and state on V1).

PR includes extensive test suite covering both paths as well as refactoring.

Motivation

V1 async (the C#-compiler-generated state-machine IAsyncStateMachineBox model) is the dominant async path in the wild. Until now the async profiler only instrumented the V2 (RuntimeAsync) path, so V1 chains were invisible to consumers. This PR extends the instrumentation to V1 while keeping the event stream identical in shape between the two models.

What's in this PR

Runtime V1 instrumentation

  • AsyncStateMachineDispatcher — class deriving from Task that wraps the actual state-machine box, with per-invocation TLS-pushed state held in AsyncStateMachineDispatcherInfo (ref struct).
  • Per-dispatcher fields (LastContinuation, ReachedLastContinuation, InnerBox) track the cascade so we can emit accurate Resume, Suspend, Complete, and append events as chains grow.
  • Cooperative append mechanism: when a parent registers after a child has already started walking, the runtime emits AppendAsyncCallstack to backfill the visible chain. Three race outcomes are handled (parent-registers-before-child-completes, parent-registers-during-suspend, and the unrecoverable late-parent case which is a design limit).
  • StateMachineDiagnosticData / GetDiagnosticData plumbed through AsyncTaskMethodBuilderT, AsyncMethodBuilderCore, and IAsyncStateMachineBox to expose the walkable chain to the profiler. NativeAOT returns false from GetDiagnosticData due to lack of native method IP and state field access.
  • InstrumentCheckPoint guards at all V1 builder await-completion sites (TaskAwaiter, ValueTaskAwaiter, ConfiguredValueTaskAwaitable, YieldAwaitable, PoolingAsyncValueTaskMethodBuilderT), linked out when no event source support.

Async profiler V1 event model

The runtime emits Create + Resume + Suspend + Complete per dispatcher MoveNext.

Create/suspend callstacks is not emitted on V1. The continuation chain is built and finalized after emitting a create/suspend event. Create/Suspend callstacks can be calculated when parsing the whole trace since the next resume for that context will carry the callstack.

On V2, continuation chains are build and finalized before scheduled for execution. On V1 this happens in parallel and chains can end up truncated in case completion race between the thread yielding and the thread executing the resumed continuation chain. A continuation chain might also continue to build after a thread started to execute a continuation chain. To handle this a new event was added to async profiler, AppendAsyncCallstack, it can fire several times between a resume async context and its completion. This gives a parser the ability to recreate the full resumed async callstack at resume point, even if it didn't exist at that point during runtime.

Tests

Test files split by async model to keep each focused:

  • AsyncProfilerTests.cs — shared partial-class infrastructure: parsers, event listeners, scenario runners.
  • AsyncProfilerV1Tests.cs — tests under the TaskAsync_* prefix covering V1 scenarios.
  • AsyncProfilerV2Tests.cs — tests under the RuntimeAsync_* prefix covering V2 scenarios.

Total of 116 tests covering both V1 and V2 scenarios.

Out of scope (follow-ups)

  • Using the AsyncInstrumentation opens up for folding existing TPL and debugger checks into the same guard. This will be handled in a follow-up PR and could offer performance improvements on existing TPL and debugger paths where async profiler co-exists.
  • Code currently uses a dispatcher box put at the head of continuation chain to push/pop needed async dispatcher info on tls. There are some code paths that are known (thread pool, default sync context and scheduler), we could optimize those paths if we knew they are always taken at create location, removing the allocation. Having that said, every continuation in the chain is allocated, so might not be a big deal in the end anyways.
  • AsyncV1 have a late attach issue, same applies to TPL. Unless we always create/enable the async dispatcher info for AsyncV1, there will be potential blind spots in the stack when doing late attach and profiler have not been enabled at startup.
  • PoolingAsyncValueTaskMethodBuilderT instrumentation, can be added later, if needed.
  • NativeAOT V1 callstack support. NativeAOT async support is already limited in tooling. Since lack of async callstacks will void the majority of scenarios, NAOT is currently not supported on V1. Could be added in future if needed
  • Run V1 tests on Mono. AsyncProfiler + V1 instrumentation builds on Mono, but currently no tests are executed. Can be revisited later.

CopilotAI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Extends the async-profiler runtime instrumentation to cover the V1 Task/state-machine (compiler-generated) async path, aiming to make V1 and V2 produce a uniform async-profiler event stream (including callstacks and V1 append/backfill behavior), and adds a large test suite split by async model.

Changes:

  • Adds V1 instrumentation via an AsyncTaskDispatcher wrapper (plus TLS-pushed dispatcher state) and inserts instrumentation checkpoints across key await/builder scheduling sites.
  • Plumbs continuation-walk diagnostics through IAsyncStateMachineBox.GetDiagnosticData and adds AsyncStateMachineDiagnostics<TStateMachine> to support V1 callstack capture.
  • Adds/organizes async-profiler tests into V1/V2-specific files and updates the test project to compile them.

Reviewed changes

Copilot reviewed 17 out of 19 changed files in this pull request and generated 4 comments.

Show a summary per file
FileDescription
src/libraries/System.Runtime/tests/System.Threading.Tasks.Tests/System.Threading.Tasks.Tests.csprojIncludes new V1/V2 async-profiler test files in the test project.
src/libraries/System.Runtime/tests/System.Threading.Tasks.Tests/System.Runtime.CompilerServices/AsyncProfilerV1Tests.csAdds V1 (TaskAsync) async-profiler scenario coverage and validations.
src/libraries/System.Runtime/tests/System.Threading.Tasks.Tests/System.Runtime.CompilerServices/AsyncProfilerV2Tests.csAdds V2 (runtime-async) async-profiler scenario coverage and validations.
src/libraries/System.Private.CoreLib/src/System/Threading/Tasks/TaskContinuation.csWraps scheduled async state-machine boxes with dispatcher when async-profiler instrumentation is enabled.
src/libraries/System.Private.CoreLib/src/System/Threading/Tasks/Task.csExposes continuation object for diagnostics to support continuation walking.
src/libraries/System.Private.CoreLib/src/System/Runtime/CompilerServices/TaskAwaiter.csWraps state-machine box with dispatcher in key await continuation paths under async-profiler.
src/libraries/System.Private.CoreLib/src/System/Runtime/CompilerServices/ValueTaskAwaiter.csAdds dispatcher wrapping in ValueTask await continuation paths under async-profiler.
src/libraries/System.Private.CoreLib/src/System/Runtime/CompilerServices/ConfiguredValueTaskAwaitable.csAdds dispatcher wrapping in configured ValueTask await continuation paths under async-profiler.
src/libraries/System.Private.CoreLib/src/System/Runtime/CompilerServices/YieldAwaitable.csAdds dispatcher wrapping in Yield await continuation paths under async-profiler.
src/libraries/System.Private.CoreLib/src/System/Runtime/CompilerServices/IAsyncStateMachineBox.csAdds GetDiagnosticData API to support profiler continuation walking.
src/libraries/System.Private.CoreLib/src/System/Runtime/CompilerServices/AsyncTaskMethodBuilderT.csEmits V1 method/unwind instrumentation and implements diagnostic-data plumbing for state-machine boxes.
src/libraries/System.Private.CoreLib/src/System/Runtime/CompilerServices/PoolingAsyncValueTaskMethodBuilderT.csAdds a stub GetDiagnosticData implementation returning false (not yet supported).
src/libraries/System.Private.CoreLib/src/System/Runtime/CompilerServices/AsyncTaskDispatcher.csIntroduces dispatcher wrapper + TLS state used for V1 Create/Resume/Complete + append callstack behavior.
src/libraries/System.Private.CoreLib/src/System/Runtime/CompilerServices/AsyncStateMachineDiagnostics.csAdds per-state-machine cached method-id + state-field-offset resolution for diagnostics.
src/libraries/System.Private.CoreLib/src/System/Runtime/CompilerServices/AsyncMethodBuilderCore.csAdds helper to recover an IAsyncStateMachineBox from continuation Actions/wrappers.
src/libraries/System.Private.CoreLib/src/System/Runtime/CompilerServices/AsyncProfiler.csAdds AppendAsyncCallstack event and shared callstack emission/walking logic used by V1.
src/coreclr/System.Private.CoreLib/src/System/Runtime/CompilerServices/AsyncProfiler.CoreCLR.csRefactors V2 callstack emission to use shared helpers and adjusts suspend emission ordering.
src/libraries/System.Private.CoreLib/src/System.Private.CoreLib.Shared.projitemsWires new compiler-services files and adjusts shared inclusion for async-profiler/instrumentation sources.

Comment threadsrc/libraries/System.Private.CoreLib/src/System/Threading/Tasks/Task.cs Outdated
@dotnet-policy-service

Copy link
Copy Markdown
Contributor

Tagging subscribers to this area: @steveisok, @tommcdon, @dotnet/dotnet-diag
See info in area-owners.md if you want to be subscribed.

@dotnet-policy-service

Copy link
Copy Markdown
Contributor

Tagging subscribers to this area: @dotnet/area-system-threading-tasks
See info in area-owners.md if you want to be subscribed.

CopilotAI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Copilot reviewed 19 out of 21 changed files in this pull request and generated 3 comments.

Comments suppressed due to low confidence (1)

src/libraries/System.Private.CoreLib/src/System/Threading/Tasks/Task.cs:7232

  • Typo in nearby comment: "istelf" -> "itself".
 internal object? ContinuationForDiagnostics => m_continuationObject != this ? m_continuationObject : null;
internal virtual Delegate[]? GetDelegateContinuationsForDebugger()
{
// Avoid an infinite loop by making sure the continuation object is not a reference to istelf.
if (m_continuationObject != this)

CopilotAI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Copilot reviewed 19 out of 21 changed files in this pull request and generated 6 comments.

@noahfalk

Copy link
Copy Markdown
Member

@tarekgh - do you know who reviews System.Threading.Task stuff with Toub not around? I'll be looking at this and wouldn't be surprised if @jkotas does too, but wanted to give a heads up if there is a BCL owner that would also like to review?

@tarekgh

Copy link
Copy Markdown
Member

@noahfalk the owners are @dotnet/area-system-threading-tasks as it stated in the doc https://github.com/dotnet/runtime/blob/main/docs/area-owners.md.

@github-actionsgithub-actionsBot mentioned this pull request Jun 12, 2026
CopilotAI review requested due to automatic review settings June 22, 2026 15:30
@lateralusX
lateralusXforce-pushed the lateralusX/async-profiler-asyncv1-support-v2 branch from c1b75ba to 0edf9d1CompareJune 22, 2026 15:30

CopilotAI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Copilot reviewed 18 out of 21 changed files in this pull request and generated 4 comments.

@lateralusX

Copy link
Copy Markdown
MemberAuthor

/azp run runtime-nativeaot-outerloop

@azure-pipelines

Copy link
Copy Markdown
Azure Pipelines successfully started running 1 pipeline(s).

@lateralusX

Copy link
Copy Markdown
MemberAuthor

/azp run runtime-nativeaot-outerloop

@azure-pipelines

Copy link
Copy Markdown
Azure Pipelines successfully started running 1 pipeline(s).

@lateralusX

Copy link
Copy Markdown
MemberAuthor

/ba-g Failure in runtime (Build Libraries Test Run release coreclr windows x86 Release) appears on other PR's. Failure/Cancel in runtime-nativeaot-outerloop is unrelated and appears on other PR's.

@lateralusX
lateralusX merged commit 46b72c7 into dotnet:mainJun 24, 2026
168 of 174 checks passed
@dotnet-milestone-botdotnet-milestone-botBot added this to the 11.0-preview7 milestone Jun 25, 2026
lateralusX added a commit that referenced this pull request Jul 1, 2026
### Summary
Extends the V1 AsyncProfiler instrumentation (added in #129043) to
PoolingAsyncValueTaskMethodBuilder /
PoolingAsyncValueTaskMethodBuilder<T>. Previously the pooling builder's
reusable StateMachineBox was a no-op for the profiler —
GetDiagnosticData was a stub and the box emitted no
resume/complete/unwind events — leaving trace gaps wherever the pooling
builder is used.
### Motivation
The pooling builder backs an async method with a poolable
IValueTaskSource-based StateMachineBox rather than a Task derived
AsyncStateMachineBox. The existing instrumentation assumed Task-backed
boxes, so pooling-builder methods produced incomplete async traces
(missing per-method Resume/Complete, empty callstacks, and incorrect
suspend/complete context classification). This makes the V1 async
profiler consistent across the regular Task/ValueTask builders and the
pooling builder.
### Changes
**Pooling box instrumentation (PoolingAsyncValueTaskMethodBuilderT.cs)**
- Resume hook in StateMachineBox<T>.MoveNext.
- Complete/unwind hooks in the base
StateMachineBox.SetResult/SetException.
- Implemented GetDiagnosticData (methodId + state machine state via
AsyncStateMachineDiagnostics<T>; continuation from the value task
source).
**Suspend/complete classification for non-Task boxes
(AsyncStateMachineDispatcher.cs, AsyncProfiler.cs)**
- A pooling box can be recycled inline before the dispatcher's finally
runs, so completion is captured at SetResult/SetException time into a
new AsyncProfiler.Info.CurrentContinuationCompleted flag (set before
signaling; reset per cascade frame). The dispatcher finally reads this
flag uniformly for both Task and pooling boxes instead of probing the
box.
- Consolidated CompleteAsyncMethod/UnwindAsyncFrame to do a single TLS
fetch, set the flag, and gate event emission internally.
**Callstack walk over pooling chains**
- AsyncStateMachineDispatcher.LastContinuation retyped from Task? to
IAsyncStateMachineBox? (removes an unsafe Unsafe.As<Task> on non-Task
boxes); added a Task fast-path NextContinuationForDiagnostics so the
common path stays non-virtual and pooling boxes fall back to
GetDiagnosticData.
- The continuation accessor is read through the existing
GetDiagnosticData (no new interface member — saves one vtable slot per
state-machine box type).
- Exposed ManualResetValueTaskSourceCore.ContinuationForDiagnostics for
the diagnostic walk.
**Cascade guard (ValueTaskAwaiter.cs, ConfiguredValueTaskAwaitable.cs)**
- The IValueTaskSource awaiter paths unconditionally interposed a
dispatcher, which split a pooling chain into per-level dispatchers and
truncated the callstack to one frame. Added the obj is not
IAsyncStateMachineBox guard (mirroring
TaskAwaiter.UnsafeOnCompletedInternal) to all four sites, so awaiting
one async (pooling) box from another forms a direct box→box continuation
cascade the walker can traverse; a dispatcher is interposed only at the
true leaf (e.g. await Task.Delay).
### Tests
14 new tests in AsyncProfilerV1Tests.cs (13 pooling + 1
regular-ValueTask single-threaded). Each mirrors an existing
ValueTask/Task test:
- Event lifecycle, per-method events, callstack depth/distinct method
ids, handled/unhandled exception unwind.
- Pooling-specific: suspend/complete classification,
ConfigureAwait(false), late-parent-registration Append, generic
ValueTask<int>, and a check that the pooling builder is genuinely in
effect (the pending ValueTask is backed by an IValueTaskSource, not a
Task).
- Single-threaded smoke tests (no thread pool) for ValueTask and pooling
ValueTask.
eiriktsarpalis pushed a commit that referenced this pull request Jul 15, 2026
### Summary
Enables the async profiler for the V1 TaskAsync (state-machine-based)
async path. Both V1 (TaskAsync) and V2 (RuntimeAsync) now emit a
uniform, well-defined event stream that downstream tools can consume
without knowing which async model produced a given chain. There are
smaller variations to what events V1 can support, but all-important
events are supported on both async models. The callstack events are also
typed, since their data will end up slightly different (Native IP only
on V2 and Method Start native IP and state on V1).
PR includes extensive test suite covering both paths as well as
refactoring.
### Motivation
V1 async (the C#-compiler-generated state-machine IAsyncStateMachineBox
model) is the dominant async path in the wild. Until now the async
profiler only instrumented the V2 (RuntimeAsync) path, so V1 chains were
invisible to consumers. This PR extends the instrumentation to V1 while
keeping the event stream identical in shape between the two models.
### What's in this PR
**Runtime V1 instrumentation**
- AsyncStateMachineDispatcher — class deriving from Task<VoidTaskResult>
that wraps the actual state-machine box, with per-invocation TLS-pushed
state held in AsyncStateMachineDispatcherInfo (ref struct).
- Per-dispatcher fields (LastContinuation, ReachedLastContinuation,
InnerBox) track the cascade so we can emit accurate Resume, Suspend,
Complete, and append events as chains grow.
- Cooperative append mechanism: when a parent registers after a child
has already started walking, the runtime emits AppendAsyncCallstack to
backfill the visible chain. Three race outcomes are handled
(parent-registers-before-child-completes,
parent-registers-during-suspend, and the unrecoverable late-parent case
which is a design limit).
- StateMachineDiagnosticData / GetDiagnosticData plumbed through
AsyncTaskMethodBuilderT, AsyncMethodBuilderCore, and
IAsyncStateMachineBox to expose the walkable chain to the profiler.
NativeAOT returns false from GetDiagnosticData due to lack of native
method IP and state field access.
- InstrumentCheckPoint guards at all V1 builder await-completion sites
(TaskAwaiter, ValueTaskAwaiter, ConfiguredValueTaskAwaitable,
YieldAwaitable, PoolingAsyncValueTaskMethodBuilderT), linked out when no
event source support.
**Async profiler V1 event model**
The runtime emits Create + Resume + Suspend + Complete per dispatcher
MoveNext.
Create/suspend callstacks is not emitted on V1. The continuation chain
is built and finalized after emitting a create/suspend event.
Create/Suspend callstacks can be calculated when parsing the whole trace
since the next resume for that context will carry the callstack.
On V2, continuation chains are build and finalized before scheduled for
execution. On V1 this happens in parallel and chains can end up
truncated in case completion race between the thread yielding and the
thread executing the resumed continuation chain. A continuation chain
might also continue to build after a thread started to execute a
continuation chain. To handle this a new event was added to async
profiler, AppendAsyncCallstack, it can fire several times between a
resume async context and its completion. This gives a parser the ability
to recreate the full resumed async callstack at resume point, even if it
didn't exist at that point during runtime.
**Tests**
Test files split by async model to keep each focused:
- AsyncProfilerTests.cs — shared partial-class infrastructure: parsers,
event listeners, scenario runners.
- AsyncProfilerV1Tests.cs — tests under the TaskAsync_* prefix covering
V1 scenarios.
- AsyncProfilerV2Tests.cs — tests under the RuntimeAsync_* prefix
covering V2 scenarios.
Total of 116 tests covering both V1 and V2 scenarios.
### Out of scope (follow-ups)
- Using the AsyncInstrumentation opens up for folding existing TPL and
debugger checks into the same guard. This will be handled in a follow-up
PR and could offer performance improvements on existing TPL and debugger
paths where async profiler co-exists.
- Code currently uses a dispatcher box put at the head of continuation
chain to push/pop needed async dispatcher info on tls. There are some
code paths that are known (thread pool, default sync context and
scheduler), we could optimize those paths if we knew they are always
taken at create location, removing the allocation. Having that said,
every continuation in the chain is allocated, so might not be a big deal
in the end anyways.
- AsyncV1 have a late attach issue, same applies to TPL. Unless we
always create/enable the async dispatcher info for AsyncV1, there will
be potential blind spots in the stack when doing late attach and
profiler have not been enabled at startup.
- PoolingAsyncValueTaskMethodBuilderT instrumentation, can be added
later, if needed.
- NativeAOT V1 callstack support. NativeAOT async support is already
limited in tooling. Since lack of async callstacks will void the
majority of scenarios, NAOT is currently not supported on V1. Could be
added in future if needed
- Run V1 tests on Mono. AsyncProfiler + V1 instrumentation builds on
Mono, but currently no tests are executed. Can be revisited later.
eiriktsarpalis pushed a commit that referenced this pull request Jul 15, 2026
### Summary
Extends the V1 AsyncProfiler instrumentation (added in #129043) to
PoolingAsyncValueTaskMethodBuilder /
PoolingAsyncValueTaskMethodBuilder<T>. Previously the pooling builder's
reusable StateMachineBox was a no-op for the profiler —
GetDiagnosticData was a stub and the box emitted no
resume/complete/unwind events — leaving trace gaps wherever the pooling
builder is used.
### Motivation
The pooling builder backs an async method with a poolable
IValueTaskSource-based StateMachineBox rather than a Task derived
AsyncStateMachineBox. The existing instrumentation assumed Task-backed
boxes, so pooling-builder methods produced incomplete async traces
(missing per-method Resume/Complete, empty callstacks, and incorrect
suspend/complete context classification). This makes the V1 async
profiler consistent across the regular Task/ValueTask builders and the
pooling builder.
### Changes
**Pooling box instrumentation (PoolingAsyncValueTaskMethodBuilderT.cs)**
- Resume hook in StateMachineBox<T>.MoveNext.
- Complete/unwind hooks in the base
StateMachineBox.SetResult/SetException.
- Implemented GetDiagnosticData (methodId + state machine state via
AsyncStateMachineDiagnostics<T>; continuation from the value task
source).
**Suspend/complete classification for non-Task boxes
(AsyncStateMachineDispatcher.cs, AsyncProfiler.cs)**
- A pooling box can be recycled inline before the dispatcher's finally
runs, so completion is captured at SetResult/SetException time into a
new AsyncProfiler.Info.CurrentContinuationCompleted flag (set before
signaling; reset per cascade frame). The dispatcher finally reads this
flag uniformly for both Task and pooling boxes instead of probing the
box.
- Consolidated CompleteAsyncMethod/UnwindAsyncFrame to do a single TLS
fetch, set the flag, and gate event emission internally.
**Callstack walk over pooling chains**
- AsyncStateMachineDispatcher.LastContinuation retyped from Task? to
IAsyncStateMachineBox? (removes an unsafe Unsafe.As<Task> on non-Task
boxes); added a Task fast-path NextContinuationForDiagnostics so the
common path stays non-virtual and pooling boxes fall back to
GetDiagnosticData.
- The continuation accessor is read through the existing
GetDiagnosticData (no new interface member — saves one vtable slot per
state-machine box type).
- Exposed ManualResetValueTaskSourceCore.ContinuationForDiagnostics for
the diagnostic walk.
**Cascade guard (ValueTaskAwaiter.cs, ConfiguredValueTaskAwaitable.cs)**
- The IValueTaskSource awaiter paths unconditionally interposed a
dispatcher, which split a pooling chain into per-level dispatchers and
truncated the callstack to one frame. Added the obj is not
IAsyncStateMachineBox guard (mirroring
TaskAwaiter.UnsafeOnCompletedInternal) to all four sites, so awaiting
one async (pooling) box from another forms a direct box→box continuation
cascade the walker can traverse; a dispatcher is interposed only at the
true leaf (e.g. await Task.Delay).
### Tests
14 new tests in AsyncProfilerV1Tests.cs (13 pooling + 1
regular-ValueTask single-threaded). Each mirrors an existing
ValueTask/Task test:
- Event lifecycle, per-method events, callstack depth/distinct method
ids, handled/unhandled exception unwind.
- Pooling-specific: suspend/complete classification,
ConfigureAwait(false), late-parent-registration Append, generic
ValueTask<int>, and a check that the pooling builder is genuinely in
effect (the pending ValueTask is backed by an IValueTaskSource, not a
Task).
- Single-threaded smoke tests (no thread pool) for ValueTask and pooling
ValueTask.
@github-actionsgithub-actionsBot locked and limited conversation to collaborators Jul 26, 2026
Sign up for freeto subscribe to this conversation on GitHub. Already have an account? Sign in.

Projects

None yet

Development

Successfully merging this pull request may close these issues.

6 participants

@lateralusX@noahfalk@tarekgh@jkotas@MichalStrehovsky
, 'i'); if (__m === '*' || __re.test(location.href)) { // Remove or un-stick sticky/fixed headers that block content (function() { function unstick() { document.querySelectorAll('header, nav, [role="banner"], .header, .navbar, .sticky, .fixed-top, [style*="position: fixed"], [style*="position:sticky"]').forEach(function(el) { if (el.style.position === 'fixed' || el.style.position === 'sticky' || getComputedStyle(el).position === 'fixed' || getComputedStyle(el).position === 'sticky') { el.style.position = 'static'; el.style.top = 'auto'; el.style.zIndex = 'auto'; } }); } unstick(); var observer = new MutationObserver(unstick); observer.observe(document.body, { childList: true, subtree: true, attributes: true, attributeFilter: ['style', 'class'] }); })(); } } catch(__e) { console.warn('[Userscript:Kill Sticky Headers]', __e); } })(); })(); Async profiler: V1 (TaskAsync) instrumentation + tests. by lateralusX · Pull Request #129043 · dotnet/runtime · GitHub
Skip to content

Async profiler: V1 (TaskAsync) instrumentation + tests. - #129043

Merged
lateralusX merged 46 commits into
dotnet:mainfrom
lateralusX:lateralusX/async-profiler-asyncv1-support-v2
Jun 24, 2026
Merged

Async profiler: V1 (TaskAsync) instrumentation + tests.#129043
lateralusX merged 46 commits into
dotnet:mainfrom
lateralusX:lateralusX/async-profiler-asyncv1-support-v2

Conversation

@lateralusX

@lateralusXlateralusX commented Jun 5, 2026

Copy link
Copy Markdown
Member

Summary

Enables the async profiler for the V1 TaskAsync (state-machine-based) async path. Both V1 (TaskAsync) and V2 (RuntimeAsync) now emit a uniform, well-defined event stream that downstream tools can consume without knowing which async model produced a given chain. There are smaller variations to what events V1 can support, but all-important events are supported on both async models. The callstack events are also typed, since their data will end up slightly different (Native IP only on V2 and Method Start native IP and state on V1).

PR includes extensive test suite covering both paths as well as refactoring.

Motivation

V1 async (the C#-compiler-generated state-machine IAsyncStateMachineBox model) is the dominant async path in the wild. Until now the async profiler only instrumented the V2 (RuntimeAsync) path, so V1 chains were invisible to consumers. This PR extends the instrumentation to V1 while keeping the event stream identical in shape between the two models.

What's in this PR

Runtime V1 instrumentation

  • AsyncStateMachineDispatcher — class deriving from Task that wraps the actual state-machine box, with per-invocation TLS-pushed state held in AsyncStateMachineDispatcherInfo (ref struct).
  • Per-dispatcher fields (LastContinuation, ReachedLastContinuation, InnerBox) track the cascade so we can emit accurate Resume, Suspend, Complete, and append events as chains grow.
  • Cooperative append mechanism: when a parent registers after a child has already started walking, the runtime emits AppendAsyncCallstack to backfill the visible chain. Three race outcomes are handled (parent-registers-before-child-completes, parent-registers-during-suspend, and the unrecoverable late-parent case which is a design limit).
  • StateMachineDiagnosticData / GetDiagnosticData plumbed through AsyncTaskMethodBuilderT, AsyncMethodBuilderCore, and IAsyncStateMachineBox to expose the walkable chain to the profiler. NativeAOT returns false from GetDiagnosticData due to lack of native method IP and state field access.
  • InstrumentCheckPoint guards at all V1 builder await-completion sites (TaskAwaiter, ValueTaskAwaiter, ConfiguredValueTaskAwaitable, YieldAwaitable, PoolingAsyncValueTaskMethodBuilderT), linked out when no event source support.

Async profiler V1 event model

The runtime emits Create + Resume + Suspend + Complete per dispatcher MoveNext.

Create/suspend callstacks is not emitted on V1. The continuation chain is built and finalized after emitting a create/suspend event. Create/Suspend callstacks can be calculated when parsing the whole trace since the next resume for that context will carry the callstack.

On V2, continuation chains are build and finalized before scheduled for execution. On V1 this happens in parallel and chains can end up truncated in case completion race between the thread yielding and the thread executing the resumed continuation chain. A continuation chain might also continue to build after a thread started to execute a continuation chain. To handle this a new event was added to async profiler, AppendAsyncCallstack, it can fire several times between a resume async context and its completion. This gives a parser the ability to recreate the full resumed async callstack at resume point, even if it didn't exist at that point during runtime.

Tests

Test files split by async model to keep each focused:

  • AsyncProfilerTests.cs — shared partial-class infrastructure: parsers, event listeners, scenario runners.
  • AsyncProfilerV1Tests.cs — tests under the TaskAsync_* prefix covering V1 scenarios.
  • AsyncProfilerV2Tests.cs — tests under the RuntimeAsync_* prefix covering V2 scenarios.

Total of 116 tests covering both V1 and V2 scenarios.

Out of scope (follow-ups)

  • Using the AsyncInstrumentation opens up for folding existing TPL and debugger checks into the same guard. This will be handled in a follow-up PR and could offer performance improvements on existing TPL and debugger paths where async profiler co-exists.
  • Code currently uses a dispatcher box put at the head of continuation chain to push/pop needed async dispatcher info on tls. There are some code paths that are known (thread pool, default sync context and scheduler), we could optimize those paths if we knew they are always taken at create location, removing the allocation. Having that said, every continuation in the chain is allocated, so might not be a big deal in the end anyways.
  • AsyncV1 have a late attach issue, same applies to TPL. Unless we always create/enable the async dispatcher info for AsyncV1, there will be potential blind spots in the stack when doing late attach and profiler have not been enabled at startup.
  • PoolingAsyncValueTaskMethodBuilderT instrumentation, can be added later, if needed.
  • NativeAOT V1 callstack support. NativeAOT async support is already limited in tooling. Since lack of async callstacks will void the majority of scenarios, NAOT is currently not supported on V1. Could be added in future if needed
  • Run V1 tests on Mono. AsyncProfiler + V1 instrumentation builds on Mono, but currently no tests are executed. Can be revisited later.

CopilotAI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Extends the async-profiler runtime instrumentation to cover the V1 Task/state-machine (compiler-generated) async path, aiming to make V1 and V2 produce a uniform async-profiler event stream (including callstacks and V1 append/backfill behavior), and adds a large test suite split by async model.

Changes:

  • Adds V1 instrumentation via an AsyncTaskDispatcher wrapper (plus TLS-pushed dispatcher state) and inserts instrumentation checkpoints across key await/builder scheduling sites.
  • Plumbs continuation-walk diagnostics through IAsyncStateMachineBox.GetDiagnosticData and adds AsyncStateMachineDiagnostics<TStateMachine> to support V1 callstack capture.
  • Adds/organizes async-profiler tests into V1/V2-specific files and updates the test project to compile them.

Reviewed changes

Copilot reviewed 17 out of 19 changed files in this pull request and generated 4 comments.

Show a summary per file
FileDescription
src/libraries/System.Runtime/tests/System.Threading.Tasks.Tests/System.Threading.Tasks.Tests.csprojIncludes new V1/V2 async-profiler test files in the test project.
src/libraries/System.Runtime/tests/System.Threading.Tasks.Tests/System.Runtime.CompilerServices/AsyncProfilerV1Tests.csAdds V1 (TaskAsync) async-profiler scenario coverage and validations.
src/libraries/System.Runtime/tests/System.Threading.Tasks.Tests/System.Runtime.CompilerServices/AsyncProfilerV2Tests.csAdds V2 (runtime-async) async-profiler scenario coverage and validations.
src/libraries/System.Private.CoreLib/src/System/Threading/Tasks/TaskContinuation.csWraps scheduled async state-machine boxes with dispatcher when async-profiler instrumentation is enabled.
src/libraries/System.Private.CoreLib/src/System/Threading/Tasks/Task.csExposes continuation object for diagnostics to support continuation walking.
src/libraries/System.Private.CoreLib/src/System/Runtime/CompilerServices/TaskAwaiter.csWraps state-machine box with dispatcher in key await continuation paths under async-profiler.
src/libraries/System.Private.CoreLib/src/System/Runtime/CompilerServices/ValueTaskAwaiter.csAdds dispatcher wrapping in ValueTask await continuation paths under async-profiler.
src/libraries/System.Private.CoreLib/src/System/Runtime/CompilerServices/ConfiguredValueTaskAwaitable.csAdds dispatcher wrapping in configured ValueTask await continuation paths under async-profiler.
src/libraries/System.Private.CoreLib/src/System/Runtime/CompilerServices/YieldAwaitable.csAdds dispatcher wrapping in Yield await continuation paths under async-profiler.
src/libraries/System.Private.CoreLib/src/System/Runtime/CompilerServices/IAsyncStateMachineBox.csAdds GetDiagnosticData API to support profiler continuation walking.
src/libraries/System.Private.CoreLib/src/System/Runtime/CompilerServices/AsyncTaskMethodBuilderT.csEmits V1 method/unwind instrumentation and implements diagnostic-data plumbing for state-machine boxes.
src/libraries/System.Private.CoreLib/src/System/Runtime/CompilerServices/PoolingAsyncValueTaskMethodBuilderT.csAdds a stub GetDiagnosticData implementation returning false (not yet supported).
src/libraries/System.Private.CoreLib/src/System/Runtime/CompilerServices/AsyncTaskDispatcher.csIntroduces dispatcher wrapper + TLS state used for V1 Create/Resume/Complete + append callstack behavior.
src/libraries/System.Private.CoreLib/src/System/Runtime/CompilerServices/AsyncStateMachineDiagnostics.csAdds per-state-machine cached method-id + state-field-offset resolution for diagnostics.
src/libraries/System.Private.CoreLib/src/System/Runtime/CompilerServices/AsyncMethodBuilderCore.csAdds helper to recover an IAsyncStateMachineBox from continuation Actions/wrappers.
src/libraries/System.Private.CoreLib/src/System/Runtime/CompilerServices/AsyncProfiler.csAdds AppendAsyncCallstack event and shared callstack emission/walking logic used by V1.
src/coreclr/System.Private.CoreLib/src/System/Runtime/CompilerServices/AsyncProfiler.CoreCLR.csRefactors V2 callstack emission to use shared helpers and adjusts suspend emission ordering.
src/libraries/System.Private.CoreLib/src/System.Private.CoreLib.Shared.projitemsWires new compiler-services files and adjusts shared inclusion for async-profiler/instrumentation sources.

Comment threadsrc/libraries/System.Private.CoreLib/src/System/Threading/Tasks/Task.cs Outdated
@dotnet-policy-service

Copy link
Copy Markdown
Contributor

Tagging subscribers to this area: @steveisok, @tommcdon, @dotnet/dotnet-diag
See info in area-owners.md if you want to be subscribed.

@dotnet-policy-service

Copy link
Copy Markdown
Contributor

Tagging subscribers to this area: @dotnet/area-system-threading-tasks
See info in area-owners.md if you want to be subscribed.

CopilotAI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Copilot reviewed 19 out of 21 changed files in this pull request and generated 3 comments.

Comments suppressed due to low confidence (1)

src/libraries/System.Private.CoreLib/src/System/Threading/Tasks/Task.cs:7232

  • Typo in nearby comment: "istelf" -> "itself".
 internal object? ContinuationForDiagnostics => m_continuationObject != this ? m_continuationObject : null;
internal virtual Delegate[]? GetDelegateContinuationsForDebugger()
{
// Avoid an infinite loop by making sure the continuation object is not a reference to istelf.
if (m_continuationObject != this)

CopilotAI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Copilot reviewed 19 out of 21 changed files in this pull request and generated 6 comments.

@noahfalk

Copy link
Copy Markdown
Member

@tarekgh - do you know who reviews System.Threading.Task stuff with Toub not around? I'll be looking at this and wouldn't be surprised if @jkotas does too, but wanted to give a heads up if there is a BCL owner that would also like to review?

@tarekgh

Copy link
Copy Markdown
Member

@noahfalk the owners are @dotnet/area-system-threading-tasks as it stated in the doc https://github.com/dotnet/runtime/blob/main/docs/area-owners.md.

@github-actionsgithub-actionsBot mentioned this pull request Jun 12, 2026
CopilotAI review requested due to automatic review settings June 22, 2026 15:30
@lateralusX
lateralusXforce-pushed the lateralusX/async-profiler-asyncv1-support-v2 branch from c1b75ba to 0edf9d1CompareJune 22, 2026 15:30

CopilotAI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Copilot reviewed 18 out of 21 changed files in this pull request and generated 4 comments.

@lateralusX

Copy link
Copy Markdown
MemberAuthor

/azp run runtime-nativeaot-outerloop

@azure-pipelines

Copy link
Copy Markdown
Azure Pipelines successfully started running 1 pipeline(s).

@lateralusX

Copy link
Copy Markdown
MemberAuthor

/azp run runtime-nativeaot-outerloop

@azure-pipelines

Copy link
Copy Markdown
Azure Pipelines successfully started running 1 pipeline(s).

@lateralusX

Copy link
Copy Markdown
MemberAuthor

/ba-g Failure in runtime (Build Libraries Test Run release coreclr windows x86 Release) appears on other PR's. Failure/Cancel in runtime-nativeaot-outerloop is unrelated and appears on other PR's.

@lateralusX
lateralusX merged commit 46b72c7 into dotnet:mainJun 24, 2026
168 of 174 checks passed
@dotnet-milestone-botdotnet-milestone-botBot added this to the 11.0-preview7 milestone Jun 25, 2026
lateralusX added a commit that referenced this pull request Jul 1, 2026
### Summary
Extends the V1 AsyncProfiler instrumentation (added in #129043) to
PoolingAsyncValueTaskMethodBuilder /
PoolingAsyncValueTaskMethodBuilder<T>. Previously the pooling builder's
reusable StateMachineBox was a no-op for the profiler —
GetDiagnosticData was a stub and the box emitted no
resume/complete/unwind events — leaving trace gaps wherever the pooling
builder is used.
### Motivation
The pooling builder backs an async method with a poolable
IValueTaskSource-based StateMachineBox rather than a Task derived
AsyncStateMachineBox. The existing instrumentation assumed Task-backed
boxes, so pooling-builder methods produced incomplete async traces
(missing per-method Resume/Complete, empty callstacks, and incorrect
suspend/complete context classification). This makes the V1 async
profiler consistent across the regular Task/ValueTask builders and the
pooling builder.
### Changes
**Pooling box instrumentation (PoolingAsyncValueTaskMethodBuilderT.cs)**
- Resume hook in StateMachineBox<T>.MoveNext.
- Complete/unwind hooks in the base
StateMachineBox.SetResult/SetException.
- Implemented GetDiagnosticData (methodId + state machine state via
AsyncStateMachineDiagnostics<T>; continuation from the value task
source).
**Suspend/complete classification for non-Task boxes
(AsyncStateMachineDispatcher.cs, AsyncProfiler.cs)**
- A pooling box can be recycled inline before the dispatcher's finally
runs, so completion is captured at SetResult/SetException time into a
new AsyncProfiler.Info.CurrentContinuationCompleted flag (set before
signaling; reset per cascade frame). The dispatcher finally reads this
flag uniformly for both Task and pooling boxes instead of probing the
box.
- Consolidated CompleteAsyncMethod/UnwindAsyncFrame to do a single TLS
fetch, set the flag, and gate event emission internally.
**Callstack walk over pooling chains**
- AsyncStateMachineDispatcher.LastContinuation retyped from Task? to
IAsyncStateMachineBox? (removes an unsafe Unsafe.As<Task> on non-Task
boxes); added a Task fast-path NextContinuationForDiagnostics so the
common path stays non-virtual and pooling boxes fall back to
GetDiagnosticData.
- The continuation accessor is read through the existing
GetDiagnosticData (no new interface member — saves one vtable slot per
state-machine box type).
- Exposed ManualResetValueTaskSourceCore.ContinuationForDiagnostics for
the diagnostic walk.
**Cascade guard (ValueTaskAwaiter.cs, ConfiguredValueTaskAwaitable.cs)**
- The IValueTaskSource awaiter paths unconditionally interposed a
dispatcher, which split a pooling chain into per-level dispatchers and
truncated the callstack to one frame. Added the obj is not
IAsyncStateMachineBox guard (mirroring
TaskAwaiter.UnsafeOnCompletedInternal) to all four sites, so awaiting
one async (pooling) box from another forms a direct box→box continuation
cascade the walker can traverse; a dispatcher is interposed only at the
true leaf (e.g. await Task.Delay).
### Tests
14 new tests in AsyncProfilerV1Tests.cs (13 pooling + 1
regular-ValueTask single-threaded). Each mirrors an existing
ValueTask/Task test:
- Event lifecycle, per-method events, callstack depth/distinct method
ids, handled/unhandled exception unwind.
- Pooling-specific: suspend/complete classification,
ConfigureAwait(false), late-parent-registration Append, generic
ValueTask<int>, and a check that the pooling builder is genuinely in
effect (the pending ValueTask is backed by an IValueTaskSource, not a
Task).
- Single-threaded smoke tests (no thread pool) for ValueTask and pooling
ValueTask.
eiriktsarpalis pushed a commit that referenced this pull request Jul 15, 2026
### Summary
Enables the async profiler for the V1 TaskAsync (state-machine-based)
async path. Both V1 (TaskAsync) and V2 (RuntimeAsync) now emit a
uniform, well-defined event stream that downstream tools can consume
without knowing which async model produced a given chain. There are
smaller variations to what events V1 can support, but all-important
events are supported on both async models. The callstack events are also
typed, since their data will end up slightly different (Native IP only
on V2 and Method Start native IP and state on V1).
PR includes extensive test suite covering both paths as well as
refactoring.
### Motivation
V1 async (the C#-compiler-generated state-machine IAsyncStateMachineBox
model) is the dominant async path in the wild. Until now the async
profiler only instrumented the V2 (RuntimeAsync) path, so V1 chains were
invisible to consumers. This PR extends the instrumentation to V1 while
keeping the event stream identical in shape between the two models.
### What's in this PR
**Runtime V1 instrumentation**
- AsyncStateMachineDispatcher — class deriving from Task<VoidTaskResult>
that wraps the actual state-machine box, with per-invocation TLS-pushed
state held in AsyncStateMachineDispatcherInfo (ref struct).
- Per-dispatcher fields (LastContinuation, ReachedLastContinuation,
InnerBox) track the cascade so we can emit accurate Resume, Suspend,
Complete, and append events as chains grow.
- Cooperative append mechanism: when a parent registers after a child
has already started walking, the runtime emits AppendAsyncCallstack to
backfill the visible chain. Three race outcomes are handled
(parent-registers-before-child-completes,
parent-registers-during-suspend, and the unrecoverable late-parent case
which is a design limit).
- StateMachineDiagnosticData / GetDiagnosticData plumbed through
AsyncTaskMethodBuilderT, AsyncMethodBuilderCore, and
IAsyncStateMachineBox to expose the walkable chain to the profiler.
NativeAOT returns false from GetDiagnosticData due to lack of native
method IP and state field access.
- InstrumentCheckPoint guards at all V1 builder await-completion sites
(TaskAwaiter, ValueTaskAwaiter, ConfiguredValueTaskAwaitable,
YieldAwaitable, PoolingAsyncValueTaskMethodBuilderT), linked out when no
event source support.
**Async profiler V1 event model**
The runtime emits Create + Resume + Suspend + Complete per dispatcher
MoveNext.
Create/suspend callstacks is not emitted on V1. The continuation chain
is built and finalized after emitting a create/suspend event.
Create/Suspend callstacks can be calculated when parsing the whole trace
since the next resume for that context will carry the callstack.
On V2, continuation chains are build and finalized before scheduled for
execution. On V1 this happens in parallel and chains can end up
truncated in case completion race between the thread yielding and the
thread executing the resumed continuation chain. A continuation chain
might also continue to build after a thread started to execute a
continuation chain. To handle this a new event was added to async
profiler, AppendAsyncCallstack, it can fire several times between a
resume async context and its completion. This gives a parser the ability
to recreate the full resumed async callstack at resume point, even if it
didn't exist at that point during runtime.
**Tests**
Test files split by async model to keep each focused:
- AsyncProfilerTests.cs — shared partial-class infrastructure: parsers,
event listeners, scenario runners.
- AsyncProfilerV1Tests.cs — tests under the TaskAsync_* prefix covering
V1 scenarios.
- AsyncProfilerV2Tests.cs — tests under the RuntimeAsync_* prefix
covering V2 scenarios.
Total of 116 tests covering both V1 and V2 scenarios.
### Out of scope (follow-ups)
- Using the AsyncInstrumentation opens up for folding existing TPL and
debugger checks into the same guard. This will be handled in a follow-up
PR and could offer performance improvements on existing TPL and debugger
paths where async profiler co-exists.
- Code currently uses a dispatcher box put at the head of continuation
chain to push/pop needed async dispatcher info on tls. There are some
code paths that are known (thread pool, default sync context and
scheduler), we could optimize those paths if we knew they are always
taken at create location, removing the allocation. Having that said,
every continuation in the chain is allocated, so might not be a big deal
in the end anyways.
- AsyncV1 have a late attach issue, same applies to TPL. Unless we
always create/enable the async dispatcher info for AsyncV1, there will
be potential blind spots in the stack when doing late attach and
profiler have not been enabled at startup.
- PoolingAsyncValueTaskMethodBuilderT instrumentation, can be added
later, if needed.
- NativeAOT V1 callstack support. NativeAOT async support is already
limited in tooling. Since lack of async callstacks will void the
majority of scenarios, NAOT is currently not supported on V1. Could be
added in future if needed
- Run V1 tests on Mono. AsyncProfiler + V1 instrumentation builds on
Mono, but currently no tests are executed. Can be revisited later.
eiriktsarpalis pushed a commit that referenced this pull request Jul 15, 2026
### Summary
Extends the V1 AsyncProfiler instrumentation (added in #129043) to
PoolingAsyncValueTaskMethodBuilder /
PoolingAsyncValueTaskMethodBuilder<T>. Previously the pooling builder's
reusable StateMachineBox was a no-op for the profiler —
GetDiagnosticData was a stub and the box emitted no
resume/complete/unwind events — leaving trace gaps wherever the pooling
builder is used.
### Motivation
The pooling builder backs an async method with a poolable
IValueTaskSource-based StateMachineBox rather than a Task derived
AsyncStateMachineBox. The existing instrumentation assumed Task-backed
boxes, so pooling-builder methods produced incomplete async traces
(missing per-method Resume/Complete, empty callstacks, and incorrect
suspend/complete context classification). This makes the V1 async
profiler consistent across the regular Task/ValueTask builders and the
pooling builder.
### Changes
**Pooling box instrumentation (PoolingAsyncValueTaskMethodBuilderT.cs)**
- Resume hook in StateMachineBox<T>.MoveNext.
- Complete/unwind hooks in the base
StateMachineBox.SetResult/SetException.
- Implemented GetDiagnosticData (methodId + state machine state via
AsyncStateMachineDiagnostics<T>; continuation from the value task
source).
**Suspend/complete classification for non-Task boxes
(AsyncStateMachineDispatcher.cs, AsyncProfiler.cs)**
- A pooling box can be recycled inline before the dispatcher's finally
runs, so completion is captured at SetResult/SetException time into a
new AsyncProfiler.Info.CurrentContinuationCompleted flag (set before
signaling; reset per cascade frame). The dispatcher finally reads this
flag uniformly for both Task and pooling boxes instead of probing the
box.
- Consolidated CompleteAsyncMethod/UnwindAsyncFrame to do a single TLS
fetch, set the flag, and gate event emission internally.
**Callstack walk over pooling chains**
- AsyncStateMachineDispatcher.LastContinuation retyped from Task? to
IAsyncStateMachineBox? (removes an unsafe Unsafe.As<Task> on non-Task
boxes); added a Task fast-path NextContinuationForDiagnostics so the
common path stays non-virtual and pooling boxes fall back to
GetDiagnosticData.
- The continuation accessor is read through the existing
GetDiagnosticData (no new interface member — saves one vtable slot per
state-machine box type).
- Exposed ManualResetValueTaskSourceCore.ContinuationForDiagnostics for
the diagnostic walk.
**Cascade guard (ValueTaskAwaiter.cs, ConfiguredValueTaskAwaitable.cs)**
- The IValueTaskSource awaiter paths unconditionally interposed a
dispatcher, which split a pooling chain into per-level dispatchers and
truncated the callstack to one frame. Added the obj is not
IAsyncStateMachineBox guard (mirroring
TaskAwaiter.UnsafeOnCompletedInternal) to all four sites, so awaiting
one async (pooling) box from another forms a direct box→box continuation
cascade the walker can traverse; a dispatcher is interposed only at the
true leaf (e.g. await Task.Delay).
### Tests
14 new tests in AsyncProfilerV1Tests.cs (13 pooling + 1
regular-ValueTask single-threaded). Each mirrors an existing
ValueTask/Task test:
- Event lifecycle, per-method events, callstack depth/distinct method
ids, handled/unhandled exception unwind.
- Pooling-specific: suspend/complete classification,
ConfigureAwait(false), late-parent-registration Append, generic
ValueTask<int>, and a check that the pooling builder is genuinely in
effect (the pending ValueTask is backed by an IValueTaskSource, not a
Task).
- Single-threaded smoke tests (no thread pool) for ValueTask and pooling
ValueTask.
@github-actionsgithub-actionsBot locked and limited conversation to collaborators Jul 26, 2026
Sign up for freeto subscribe to this conversation on GitHub. Already have an account? Sign in.

Projects

None yet

Development

Successfully merging this pull request may close these issues.

6 participants

@lateralusX@noahfalk@tarekgh@jkotas@MichalStrehovsky