Skip to content

[Async v2] Support Environment.StackTrace#125396

Draft
tommcdon wants to merge 4 commits intodotnet:mainfrom
tommcdon:dev/tommcdon/env_stacktrace
Draft

[Async v2] Support Environment.StackTrace#125396
tommcdon wants to merge 4 commits intodotnet:mainfrom
tommcdon:dev/tommcdon/env_stacktrace

Conversation

@tommcdon
Copy link
Member

Draft PR for initial feedback. A separate proposal will be created for the design change review/approval.

Summary

Enhance Environment.StackTrace (and the underlying StackTrace/StackFrame APIs) 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.StackTrace shows only what is physically on the stack:

at System.Environment.get_StackTrace()
at MyApp.MiddleMethod()
at System.Runtime.CompilerServices.YieldAwaitable+YieldAwaiter.RunAction(Object)
at System.Threading.QueueUserWorkItemCallbackDefaultContext.Execute()
at System.Threading.ThreadPoolWorkQueue.Dispatch()
at System.Threading.PortableThreadPool+WorkerThread.WorkerDoWork(...)
at System.Threading.PortableThreadPool+WorkerThread.WorkerThreadStart()
at System.Threading.Thread.StartCallback()

The logical callers — which async methods are awaiting MiddleMethod — are completely absent. A developer has no way to determine why MiddleMethod is running.

Desired behavior

at System.Environment.get_StackTrace()
at MyApp.MiddleMethod() in MyApp.cs:line 42
at MyApp.OuterMethod() in MyApp.cs:line 30       ← continuation frame
at MyApp.EntryPoint() in MyApp.cs:line 15        ← continuation frame

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

  • Debugging: Log files with Environment.StackTrace are a useful diagnostic tool. Without the async caller chain, developers cannot trace the origin of a call.
  • Parity with Exception stack traces: Exception stack traces in async v2 already inject continuation frames via STEF_CONTINUATION in excep.cpp. Environment.StackTrace should have equivalent behavior.
  • Observability tooling: APM tools, logging frameworks, and profilers that consume StackTrace objects 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 await that doesn't complete synchronously), the JIT creates a Continuation object that captures:

Field Type Purpose
Next Continuation? Next continuation in the linked list (the method that is awaiting this method)
ResumeInfo ResumeInfo* Pointer to resume function and diagnostic IP

The ResumeInfo struct contains:

Field Type Purpose
Resume delegate*<Continuation, ref byte, Continuation?> Function pointer to the IL stub that resumes the awaiting method
DiagnosticIP void* An instruction pointer within the suspending method, suitable for native-to-IL offset mapping

Dispatch

When an async method completes, RuntimeAsyncTaskCore.DispatchContinuations() iterates the continuation chain. It maintains a thread-local AsyncDispatcherInfo pointer (AsyncDispatcherInfo.t_current) that tracks the current dispatch state, including the NextContinuation to be processed.

IsAsyncMethod()

A MethodDesc is considered an async v2 method if it has AsyncMethodData with the AsyncCall flag 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 with STEF_CONTINUATION:

// When pCf == NULL, this is an async v2 continuation frame
else
{
    stackTraceElem.flags |= STEF_CONTINUATION;
}

tommcdon and others added 4 commits March 6, 2026 18:30
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>
@tommcdon tommcdon added this to the 11.0.0 milestone Mar 10, 2026
@tommcdon tommcdon self-assigned this Mar 10, 2026
Copilot AI review requested due to automatic review settings March 10, 2026 18:28
@dotnet-policy-service
Copy link
Contributor

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

Copy link
Contributor

Copilot AI left a comment

Choose a reason for hiding this comment

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

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_CONTINUATION frames while filtering out DispatchContinuations.
  • NativeAOT: augment the collected IP array by truncating dispatch frames and appending continuation DiagnosticIPs when AsyncDispatcherInfo.t_current is present.
  • Tests: update existing reflection stack-frame expectations and add a new env-stacktrace regression 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.

Comment on lines +263 to +276
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;

Copy link

Copilot AI Mar 10, 2026

Choose a reason for hiding this comment

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

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).

Copilot uses AI. Check for mistakes.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants