[API Proposal]: Include runtime async frames in Environment.StackTrace and System.Diagnostics.StackFrame #125417

Description

@tommcdon

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 whyMiddleMethod 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: #125396

API Usage

staticvoidPrintStackTrace(){// Get the current stack trace as a stringstringstackTrace=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.

Metadata

Metadata

Assignees

Projects

No projects

    Milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions

    , '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" + '
    
    Skip to content

    [API Proposal]: Include runtime async frames in Environment.StackTrace and System.Diagnostics.StackFrame #125417

    Description

    @tommcdon

    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 whyMiddleMethod 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: #125396

    API Usage

    staticvoidPrintStackTrace(){// Get the current stack trace as a stringstringstackTrace=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.

    Metadata

    Metadata

    Assignees

    Projects

    No projects

      Milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions

      , '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('^' + ".*" + '
      Skip to content

      [API Proposal]: Include runtime async frames in Environment.StackTrace and System.Diagnostics.StackFrame #125417

      Description

      @tommcdon

      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 whyMiddleMethod 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: #125396

      API Usage

      staticvoidPrintStackTrace(){// Get the current stack trace as a stringstringstackTrace=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.

      Metadata

      Metadata

      Assignees

      Projects

      No projects

        Milestone

        Relationships

        None yet

        Development

        No branches or pull requests

        Issue actions

        , '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('^' + ".*" + '
        Skip to content

        [API Proposal]: Include runtime async frames in Environment.StackTrace and System.Diagnostics.StackFrame #125417

        Description

        @tommcdon

        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 whyMiddleMethod 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: #125396

        API Usage

        staticvoidPrintStackTrace(){// Get the current stack trace as a stringstringstackTrace=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.

        Metadata

        Metadata

        Assignees

        Projects

        No projects

          Milestone

          Relationships

          None yet

          Development

          No branches or pull requests

          Issue actions

          , '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" + '
          Skip to content

          [API Proposal]: Include runtime async frames in Environment.StackTrace and System.Diagnostics.StackFrame #125417

          Description

          @tommcdon

          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 whyMiddleMethod 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: #125396

          API Usage

          staticvoidPrintStackTrace(){// Get the current stack trace as a stringstringstackTrace=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.

          Metadata

          Metadata

          Assignees

          Projects

          No projects

            Milestone

            Relationships

            None yet

            Development

            No branches or pull requests

            Issue actions

            , '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('^' + ".*" + '
            Skip to content

            [API Proposal]: Include runtime async frames in Environment.StackTrace and System.Diagnostics.StackFrame #125417

            Description

            @tommcdon

            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 whyMiddleMethod 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: #125396

            API Usage

            staticvoidPrintStackTrace(){// Get the current stack trace as a stringstringstackTrace=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.

            Metadata

            Metadata

            Assignees

            Projects

            No projects

              Milestone

              Relationships

              None yet

              Development

              No branches or pull requests

              Issue actions

              , '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); } })(); (function(){ try { var __m = "*"; var __re = new RegExp('^' + ".*" + '
              Skip to content

              [API Proposal]: Include runtime async frames in Environment.StackTrace and System.Diagnostics.StackFrame #125417

              Description

              @tommcdon

              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 whyMiddleMethod 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: #125396

              API Usage

              staticvoidPrintStackTrace(){// Get the current stack trace as a stringstringstackTrace=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.

              Metadata

              Metadata

              Assignees

              Projects

              No projects

                Milestone

                Relationships

                None yet

                Development

                No branches or pull requests

                Issue actions

                , 'i'); if (__m === '*' || __re.test(location.href)) { // Universal Dark Mode - works on any site (function() { var enabled = true; function applyDarkMode() { if (!enabled) return; // Create style element if it doesn't exist var style = document.getElementById('universal-dark-mode-style'); if (!style) { style = document.createElement('style'); style.id = 'universal-dark-mode-style'; document.head.appendChild(style); } // Dark mode CSS - inverts colors but preserves images/video style.textContent = ' /* Invert everything except media */ html { filter: invert(1) hue-rotate(180deg) !important; background: #1a1a2e !important; } /* Restore images, videos, iframes, canvas */ img, video, iframe, canvas, svg, picture, [style*="background-image"] { filter: invert(1) hue-rotate(180deg) !important; } /* Preserve specific elements that should not be inverted */ .no-dark-mode, .no-dark-mode *, [data-theme="light"], [data-theme="light"], .ace_editor, .ace_editor *, .CodeMirror, .CodeMirror *, .monaco-editor, .monaco-editor *, .markdown-body pre, .markdown-body pre *, .highlight, .highlight *, pre code, pre code * { filter: none !important; } /* Fix common UI elements */ .modal, .popup, .dropdown-menu, .tooltip, .popover { filter: invert(1) hue-rotate(180deg) !important; background: #2d2d44 !important; border-color: #444 !important; } /* Scrollbars */ ::-webkit-scrollbar { background: #1a1a2e !important; } ::-webkit-scrollbar-thumb { background: #444 !important; } ::-webkit-scrollbar-thumb:hover { background: #555 !important; } /* Selection */ ::selection { background: #4ecdc4 !important; color: #1a1a2e !important; } ::-moz-selection { background: #4ecdc4 !important; color: #1a1a2e !important; } '; } function removeDarkMode() { var style = document.getElementById('universal-dark-mode-style'); if (style) style.remove(); } // Toggle with Alt+Shift+D document.addEventListener('keydown', function(e) { if (e.altKey && e.shiftKey && e.key === 'D') { e.preventDefault(); enabled = !enabled; if (enabled) { applyDarkMode(); console.log('[Universal Dark Mode] Enabled'); } else { removeDarkMode(); console.log('[Universal Dark Mode] Disabled'); } } }); // Apply on load applyDarkMode(); // Re-apply on dynamic content var observer = new MutationObserver(function(mutations) { if (enabled && !document.getElementById('universal-dark-mode-style')) { applyDarkMode(); } }); observer.observe(document.head, { childList: true }); console.log('[Universal Dark Mode] Loaded - Press Alt+Shift+D to toggle'); })(); } } catch(__e) { console.warn('[Userscript:Universal Dark Mode]', __e); } })(); })();
                Skip to content

                [API Proposal]: Include runtime async frames in Environment.StackTrace and System.Diagnostics.StackFrame #125417

                Description

                @tommcdon

                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 whyMiddleMethod 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: #125396

                API Usage

                staticvoidPrintStackTrace(){// Get the current stack trace as a stringstringstackTrace=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.

                Metadata

                Metadata

                Assignees

                Projects

                No projects

                  Milestone

                  Relationships

                  None yet

                  Development

                  No branches or pull requests

                  Issue actions