[Async v2] Support Environment.StackTrace#125396
[Async v2] Support Environment.StackTrace#125396tommcdon wants to merge 4 commits intodotnet:mainfrom
Conversation
output native sequence points wip WIP Testing More debug output testing Temp disable il offset lookup on exceptions Walk continuation resume chain and append to stackwalk data structure Use AsyncResumeILStubResolver to get the target method address Rebase fixes and collect native offsets for Env.StackTrace Inject ResumeData frames GetStackFramesData::Elements Truncate stack when async v2 continuation data is present Fix bad merge Addional fixes from previous merge Update to latest Continuation changes in main
Validates that Environment.StackTrace correctly includes async method frames and filters out internal DispatchContinuations frames when a runtime async method resumes via continuation dispatch. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
When executing inside a runtime async (v2) continuation dispatch, the stack trace is augmented by truncating internal dispatch frames and appending continuation DiagnosticIP values from the async continuation chain. This parallels the CoreCLR implementation in debugdebugger.cpp but operates on the NativeAOT IP-address-based stack trace model. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
- Fix ExtractContinuationData to load AsyncDispatcherInfo type by name instead of searching the wrong MethodTable (RuntimeAsyncTask<T>) - Fix field name: t_dispatcherInfo -> t_current (matching managed code) - Fix type name: use strstr for RuntimeAsyncTask substring matching instead of exact match against non-existent RuntimeAsyncTaskCore - Add IsDynamicMethod() safety check before AsDynamicMethodDesc() to skip non-stub continuations (Task infrastructure callbacks) - Walk full dispatcher chain via Next pointer (was TODO with break) - Tighten NativeAOT type matching to AsyncHelpers+RuntimeAsyncTask - Update env-stacktrace test to verify OuterMethod appears via continuation injection (not just physical stack presence) - Update reflection tests to expect logical async caller at frame 1 when an async caller exists in the continuation chain Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
|
Tagging subscribers to this area: @steveisok, @dotnet/area-system-diagnostics |
There was a problem hiding this comment.
Pull request overview
Adds support for reconstructing the logical async v2 continuation chain in Environment.StackTrace / StackTrace so stack traces taken during continuation dispatch include awaiting callers and hide internal dispatch machinery.
Changes:
- CoreCLR: capture async v2 continuation resume points during stack walking and inject them as
STEF_CONTINUATIONframes while filtering outDispatchContinuations. - NativeAOT: augment the collected IP array by truncating dispatch frames and appending continuation
DiagnosticIPs whenAsyncDispatcherInfo.t_currentis present. - Tests: update existing reflection stack-frame expectations and add a new
env-stacktraceregression test.
Reviewed changes
Copilot reviewed 6 out of 6 changed files in this pull request and generated 1 comment.
Show a summary per file
| File | Description |
|---|---|
| src/tests/async/reflection/reflection.cs | Updates StackFrame.GetMethod() expectations to reflect injected logical async caller frames. |
| src/tests/async/env-stacktrace/env-stacktrace.csproj | Adds a new async test project for Environment.StackTrace async-v2 behavior. |
| src/tests/async/env-stacktrace/env-stacktrace.cs | New test validating that logical async callers appear and DispatchContinuations is filtered. |
| src/coreclr/vm/debugdebugger.h | Extends stack-walk data to track presence of async frames and collect continuation resume points. |
| src/coreclr/vm/debugdebugger.cpp | Extracts continuation chain from AsyncDispatcherInfo.t_current and injects continuation frames during stack trace collection. |
| src/coreclr/nativeaot/System.Private.CoreLib/src/System/Diagnostics/StackTrace.NativeAot.cs | Adds NativeAOT stack trace augmentation to append async continuation frames and truncate dispatch machinery. |
You can also share your feedback on Copilot code review. Take the survey.
| OBJECTREF continuation = pDispatcherInfo->pContinuation; | ||
| while (continuation != NULL) | ||
| { | ||
| typedef struct | ||
| { | ||
| PCODE Resume; | ||
| PCODE DiagnosticIP; | ||
| } ResumeInfoLayout; | ||
|
|
||
| gc.continuation = continuation; | ||
| OBJECTREF pNext = nullptr; | ||
| ResumeInfoLayout * pResumeNext = nullptr; | ||
| int numFound = 0; | ||
|
|
There was a problem hiding this comment.
In ExtractContinuationData, the loop uses local OBJECTREF variables (e.g., continuation and pNext) across calls that can GC (e.g., IL stub resolver access and SArray::Append). Only gc.continuation is protected, so if a GC occurs after reading pNext (or while still using continuation), these unprotected locals can become stale and lead to incorrect traversal or crashes. Consider protecting both the current and next OBJECTREFs (e.g., keep them in the GCPROTECT struct and exclusively use those protected slots when reading Next / advancing the loop).
| } | ||
| CONTRACTL_END; | ||
|
|
||
| // AsyncDispatcherInfo.t_current is a [ThreadStatic] field on AsyncDispatcherInfo, |
There was a problem hiding this comment.
This should use corelib binder (see corelib.h).
Draft PR for initial feedback. A separate proposal will be created for the design change review/approval.
Summary
Enhance
Environment.StackTrace(and the underlyingStackTrace/StackFrameAPIs) to reconstruct the logical async call chain when executing inside a runtime async (v2) method that has been resumed via continuation dispatch. Today, the stack trace shows only physical stack frames — thread pool internals and the immediately executing method — losing the caller context that is useful for logging.Motivation
Current behavior (without this change)
When a runtime async v2 method yields and is later resumed by the thread pool,
Environment.StackTraceshows only what is physically on the stack:The logical callers — which async methods are awaiting
MiddleMethod— are completely absent. A developer has no way to determine whyMiddleMethodis running.Desired behavior
Internal dispatch machinery (
DispatchContinuations, thread pool frames) is hidden, and the logical async caller chain is reconstructed from the runtime's continuation data.Why this matters
Environment.StackTraceare a useful diagnostic tool. Without the async caller chain, developers cannot trace the origin of a call.STEF_CONTINUATIONinexcep.cpp.Environment.StackTraceshould have equivalent behavior.StackTraceobjects need the logical call chain to build meaningful traces.Background: Runtime Async v2 Continuation Infrastructure
Continuation chain
When a runtime async method suspends (e.g., at an
awaitthat doesn't complete synchronously), the JIT creates aContinuationobject that captures:NextContinuation?ResumeInfoResumeInfo*The
ResumeInfostruct contains:Resumedelegate*<Continuation, ref byte, Continuation?>DiagnosticIPvoid*Dispatch
When an async method completes,
RuntimeAsyncTaskCore.DispatchContinuations()iterates the continuation chain. It maintains a thread-localAsyncDispatcherInfopointer (AsyncDispatcherInfo.t_current) that tracks the current dispatch state, including theNextContinuationto be processed.IsAsyncMethod()A
MethodDescis considered an async v2 method if it hasAsyncMethodDatawith theAsyncCallflag set. This is populated during JIT compilation when the method uses runtime async calling conventions.Existing precedent: Exception stack traces
In
excep.cpp, when building exception stack traces, continuation frames are already injected withSTEF_CONTINUATION: