[API Proposal]: Include runtime async frames in Environment.StackTrace and System.Diagnostics.StackFrame
- Dominant language
- C#
- Stars
- 18.3k
- Forks
- 5.6k
- PR merge metrics
- PR metrics pending
Description
### Background
Customers often use logging to diagnose issues in production code. Environment.StackTrace is one of the possible ways to log current callstack and method location. Async v1 code often times be undiagnosable due to thread switches, for example calling Environment.StackTrace immediately after `await Task.Delay` would be resumed on a new thread, and so there would be no indication on how execution led to the current method.
Runtime Async (also known as Async V2) gives us a new opportunity to improve the scenario. Previously in Async V1, async execution state was tracked in Roslyn compiler-generated code, requiring decoding of async state machines to track caller/callee chains. Now with Async V2, the runtime tracks async execution and so we can now extract the resume locations and output this information as part of a synchronous callstack.
### 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
- Observability tooling: APM tools, logging frameworks, and profilers that consume `StackTrace` objects need the logical call chain to build meaningful traces.
### API Proposal
No API changes are suggested - this is changing behavior of an existing API to provide more information. API's affected are:
* System.Diagnostics.StackFrame
* System.Diagnostics.StackTrace.ctor
* System.Environment.StackTrace
Draft PR with proposed changes is here: https://github.com/dotnet/runtime/pull/125396
### API Usage
```csharp
static void PrintStackTrace()
{
// Get the current stack trace as a string
string stackTrace = Environment.StackTrace;
Console.WriteLine("Current Stack Trace:");
Console.WriteLine(stackTrace);
}
```
### Alternative Designs
We could consider adding overloads that make the behavior opt-in or opt-out
### Risks
Including runtime async frames might unexpectedly slow down performance for those that log callstacks in production scenarios, or bloat the log files themselves causing an extra expense on log storage.
Risk is mitigated by:
1. Runtime async is currently opt-in at the application level (at the time of writing in .NET 11)
2. Diagnostic benefits should outweigh performance or log size concerns. To mitigate this risk, the feature should include an opt-out app config switch to revert to fully non-async callstacks. We can also consider API overloads as well depending on feedback.
Contributor guide
Assessment
This issue has not been assessed yet.