Skip to content

Add low-cost instrumented version of RuntimeAsyncTask::DispatchContinuations reclaiming lost performance. - #126091

Merged
lateralusX merged 23 commits into
dotnet:mainfrom
lateralusX:lateralusX/runtime-async-instrumentation
Apr 16, 2026
Merged

Add low-cost instrumented version of RuntimeAsyncTask::DispatchContinuations reclaiming lost performance.#126091
lateralusX merged 23 commits into
dotnet:mainfrom
lateralusX:lateralusX/runtime-async-instrumentation

Conversation

@lateralusX

@lateralusXlateralusX commented Mar 25, 2026

Copy link
Copy Markdown
Member

#123727 introduced a regression of ~7% adding additional instrumentation for Debugger/TPL into RuntimeAsyncTask::DispatchContinuations. Going forward, there will be even more instrumentation needed when implementing async profiling and that would increase the overhead even more, so we need a way to isolate the instrumented vs none instrumented version of this method and regain some of the lost performance.

This PR duplicates DispatchContinuations into two methods, regular and instrumented. Previous version of PR used a generic value type specialization, but it was decided that the simplification not using generic value type specialization is worth the duplication of the function.

To be able to "upgrade" from none instrumented to instrumented version of DispatchContinuations, there are checks at method entry and after each completed continuation detecting if method should switch to instrumented version. Both checks are small and fast just a load of a flag in a static variable and checked if it's not 0.

This change will make sure the RuntimeAsyncTask::DispatchContinuations is protected from future performance regressions when more instrumentation gets added into the instrumented version.

Running the same benchmark, #123727 (comment) now shows the following numbers on old vs new implementation:

MetricOldNewDiff
Total bytes (S.P.C)16 740 KB16 756 KB+16 KB
JIT Size (RuntimeAsyncTask::DispatchContinuations)1778 B1399 B-379 B
Benchmark337ms362ms-25ms (~ -7%)

Measurements done on Windows x64.

S.P.C is 16 KB larger with this PR, due to duplicated DispatchContinuation method.

JIT Size is 379 bytes smaller on none instrumented version of RuntimeAsyncTask::DispatchContinuations, all previous instrumentation has been moved to instrumented version, completely eliminated in default method.

Benchmark shows that this PR recover most of the performance previously lost in #123727.

Code paths triggering the use of instrumented version, InstrumentedDispatchContinuations are protected by a IsSupported flag, so can be trimmed away, removing references to InstrumentedDispatchContinuations.

Most changes in this PR are around extracting out existing instrumentation into the instrumentation implementation. PR also optimize some of the debugger instrumentations previously implemented reducing locking in scenarios where continuation chains are handled.

PR adds a number of new tests validating that the current debugger and TPL instrumentation is still working.

PR also adds preparation for async profiler instrumentation in the AsyncInstrumentation type. This type will be used by more scenarios going forward.

@dotnet-policy-service

Copy link
Copy Markdown
Contributor

Tagging subscribers to this area: @dotnet/area-system-runtime
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

This PR refactors runtime-async continuation dispatch to support a low-overhead “uninstrumented” fast path while still enabling Debugger/TPL (and future profiler) instrumentation via a separate, JIT-specialized codegen path, with updated flag plumbing and added tests to validate behavior and cleanup.

Changes:

  • Introduces a generic, instrumentation-specialized DispatchContinuations<TRuntimeAsyncTaskInstrumentation>() path and centralized runtime-async instrumentation flag management.
  • Refactors Task’s runtime-async timestamp bookkeeping APIs to better support continuation chains and exception/unwind cleanup.
  • Adds/expands RuntimeAsync tests for timestamp cleanup, debugger detach behavior, continuation timestamp visibility, and TPL EventSource events; wires TPL EventSource enable/disable to update instrumentation flags.

Reviewed changes

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

FileDescription
src/libraries/System.Runtime/tests/System.Threading.Tasks.Tests/System.Runtime.CompilerServices/RuntimeAsyncTests.csAdds coverage for runtime-async instrumentation behavior (timestamps, detach, unwind/cancel, and TPL events).
src/libraries/System.Private.CoreLib/src/System/Threading/Tasks/TplEventSource.csUpdates runtime-async instrumentation flags when TPL EventSource commands change enabled keywords.
src/libraries/System.Private.CoreLib/src/System/Threading/Tasks/Task.csRefactors runtime-async timestamp dictionaries and adds helpers for chain timestamp propagation and cleanup.
src/coreclr/System.Private.CoreLib/src/System/Runtime/CompilerServices/AsyncHelpers.CoreCLR.csAdds the generic instrumentation abstraction and splits dispatch/finalize into specialized uninstrumented vs instrumented implementations.

Comment threadsrc/libraries/System.Private.CoreLib/src/System/Threading/Tasks/Task.cs Outdated
CopilotAI review requested due to automatic review settings March 25, 2026 14:53

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 4 out of 4 changed files in this pull request and generated 3 comments.

Comment threadsrc/libraries/System.Private.CoreLib/src/System/Threading/Tasks/Task.cs Outdated
CopilotAI review requested due to automatic review settings March 25, 2026 15:15

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 4 out of 4 changed files in this pull request and generated 2 comments.

@lateralusX

Copy link
Copy Markdown
MemberAuthor

Most failures are due to test _reflection::Async2Reflection.FromStack that currently asserts that DispatchContinuations is on stack and is currently not aware of the change to a generic method. If we stick with the generic method, then the assert in this test needs to be updated to reflect the name change.

@rcj1

rcj1 commented Mar 27, 2026

Copy link
Copy Markdown
Contributor

s_asyncDebuggingEnabled implies that these TPL events have been enabled, with two exceptions:

  1. Failure to create ETW session (too many existing ETW sessions, etc)
  2. Manual shutdown of VS-created ETW session or other exceptional error that causes ETW session to close

We can assume that s_asyncDebuggingEnabled implies required TPL events enabled, and save the setting and checking of these flags. The downside is a tiny amount of overhead spent checking whether TPL events are enabled in these two exceptional cases during debugging - in my opinion, an acceptable tradeoff for simplification and any possible optimization of the hot path.

If someone believes that scenario 1 is not very uncommon, it is possible to change Concord such that they will only be out of sync in case 2.

@lateralusX

lateralusX commented Mar 30, 2026

Copy link
Copy Markdown
MemberAuthor

s_asyncDebuggingEnabled implies that these TPL events have been enabled, with two exceptions:

  1. Failure to create ETW session (too many existing ETW sessions, etc)
  2. Manual shutdown of VS-created ETW session or other exceptional error that causes ETW session to close

We can assume that s_asyncDebuggingEnabled implies required TPL events enabled, and save the setting and checking of these flags. The downside is a tiny amount of overhead spent checking whether TPL events are enabled in these two exceptional cases during debugging - in my opinion, an acceptable tradeoff for simplification and any possible optimization of the hot path.

If someone believes that scenario 1 is not very uncommon, it is possible to change Concord such that they will only be out of sync in case 2.

Question is if pure TPL will have meaning here, like adding TPL events for runtime async activities in case a client only consumes TPL events to recreate async callstacks, or if that was never the intent with this change and it should only support the debugger features, then we should definitely put all logic under the same flag.

If we could assume the feature will trigger the TPL event source, we could handle it all in the on command event onTPL event source, and we can do the fast check only on flags, maybe that was what you proposed @rcj1? In that case this feature won't light up without the TPL session enabled, but that might be, OK? It would speed up the instrumentation gate check since it will only look at the flags again and ignore s_asyncDebuggingEnabled as part of dispatch loop.

@jakobbotsch

Copy link
Copy Markdown
Member

I took this path to get away from duplicating the complete DispatchContinuations method into a regular and instrumented version, reducing the maintenance of ~100 lines of duplicated high performing unsafe code. It can be argued that duplicating the DispatchContinuations is a small price to pay giving a little clearer implementation. If that is something we all agree on and accept, I'm happy to pursue that path as well, but wanted to start with the zero cost abstraction, no duplication path.

I worry that all the AggressiveInlining static abstracts introduce non-trivial startup cost when it comes to compiling all the dispatchers. It should be possible to measure this via some polymorphically recursive async methods that creates a lot of rarely used instantiations.

The other thing with the current approach -- the inversion of control makes the dispatcher quite hard to follow for me. But I can live with this given the improved performance.

@jakobbotsch

Copy link
Copy Markdown
Member

I worry that all the AggressiveInlining static abstracts introduce non-trivial startup cost when it comes to compiling all the dispatchers. It should be possible to measure this via some polymorphically recursive async methods that creates a lot of rarely used instantiations.

Doesn't look too bad, I tried a binary counter micro benchmark that loads 4096 of the dispatchers:

usingSystem;usingSystem.Diagnostics;usingSystem.Threading.Tasks;namespaceAsyncMicro;publicclassProgram{staticvoidMain(){Stopwatchtimer=Stopwatch.StartNew();newProgram().Recurse<byte,byte,byte,byte,byte,byte,byte,byte,byte,byte,byte,byte>(0).GetAwaiter().GetResult();Console.WriteLine("Took {0} ms",timer.ElapsedMilliseconds);}privateasyncTask<V<T11,T10,T9,T8,T7,T6,T5,T4,T3,T2,T1,T0>>Recurse<T11,T10,T9,T8,T7,T6,T5,T4,T3,T2,T1,T0>(intn){awaitTask.Yield();//Console.WriteLine(n);Taskt;if(typeof(T0)==typeof(byte)){t=Recurse<T11,T10,T9,T8,T7,T6,T5,T4,T3,T2,T1,int>(n+1);}elseif(typeof(T1)==typeof(byte)){t=Recurse<T11,T10,T9,T8,T7,T6,T5,T4,T3,T2,int,byte>(n+1);}elseif(typeof(T2)==typeof(byte)){t=Recurse<T11,T10,T9,T8,T7,T6,T5,T4,T3,int,byte,byte>(n+1);}elseif(typeof(T3)==typeof(byte)){t=Recurse<T11,T10,T9,T8,T7,T6,T5,T4,int,byte,byte,byte>(n+1);}elseif(typeof(T4)==typeof(byte)){t=Recurse<T11,T10,T9,T8,T7,T6,T5,int,byte,byte,byte,byte>(n+1);}elseif(typeof(T5)==typeof(byte)){t=Recurse<T11,T10,T9,T8,T7,T6,int,byte,byte,byte,byte,byte>(n+1);}elseif(typeof(T6)==typeof(byte)){t=Recurse<T11,T10,T9,T8,T7,int,byte,byte,byte,byte,byte,byte>(n+1);}elseif(typeof(T7)==typeof(byte)){t=Recurse<T11,T10,T9,T8,int,byte,byte,byte,byte,byte,byte,byte>(n+1);}elseif(typeof(T8)==typeof(byte)){t=Recurse<T11,T10,T9,int,byte,byte,byte,byte,byte,byte,byte,byte>(n+1);}elseif(typeof(T9)==typeof(byte)){t=Recurse<T11,T10,int,byte,byte,byte,byte,byte,byte,byte,byte,byte>(n+1);}elseif(typeof(T10)==typeof(byte)){t=Recurse<T11,int,byte,byte,byte,byte,byte,byte,byte,byte,byte,byte>(n+1);}elseif(typeof(T11)==typeof(byte)){t=Recurse<int,byte,byte,byte,byte,byte,byte,byte,byte,byte,byte,byte>(n+1);}else{returndefault;}awaitt;returndefault;}privatestructV<T11,T10,T9,T8,T7,T6,T5,T4,T3,T2,T1,T0>{publicintX;}}

The difference is only around 100 ms on this PR.
Base: Took 9205 ms
Diff: Took 9303 ms

(P.S. a simpler binary recursing generic hits some exponential behavior... Need to investigate that.)

@lateralusX

Copy link
Copy Markdown
MemberAuthor

The other thing with the current approach -- the inversion of control makes the dispatcher quite hard to follow for me. But I can live with this given the improved performance.

The alternative is to duplicate the DispatchContinuation implementation into two different methods, one default (with upgrade capabilities) and one instrumented version. In the end it depends on whats important, this PR currently take the reduce code duplication path, with slightly more complex dispatcher implementation (due to the instrumentation callbacks), and I think that is fine. Dropping the R2R exclusion reducing one indirect call on the instrumented path will simplify the instrumented path a little more as well, with very small size increase of S.P.C.

CopilotAI review requested due to automatic review settings April 10, 2026 08:16
@lateralusX
lateralusXforce-pushed the lateralusX/runtime-async-instrumentation branch from 069b76c to 75f5f5eCompareApril 10, 2026 08:16

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 6 out of 6 changed files in this pull request and generated no new comments.

@rcj1

rcj1 commented Apr 13, 2026

Copy link
Copy Markdown
Contributor

After testing the changes locally, I realize propagating these changes through to VS will be more complex than I initially thought. @tommcdon and I built changes making a lot of assumptions about the location of DispatchContinuations. We should fix whatever is broken by insertion of InstrumentedDispatchContinuations.

@lateralusX

lateralusX commented Apr 14, 2026

Copy link
Copy Markdown
MemberAuthor

After testing the changes locally, I realize propagating these changes through to VS will be more complex than I initially thought. @tommcdon and I built changes making a lot of assumptions about the location of DispatchContinuations. We should fix whatever is broken by insertion of InstrumentedDispatchContinuations.

@rcj1 can we go ahead and merge this and you pick up the needed VS changes later or do we need to hold off on this PR until you verified/fixed VS? It would be nice to get this PR merged since its blocking other PR's from following at the moment.

This was referenced Apr 14, 2026
CopilotAI review requested due to automatic review settings April 14, 2026 18:51
@lateralusX
lateralusXforce-pushed the lateralusX/runtime-async-instrumentation branch from 01867b8 to 75f5f5eCompareApril 14, 2026 18:51

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 6 out of 6 changed files in this pull request and generated 1 comment.

@lateralusX

lateralusX commented Apr 14, 2026

Copy link
Copy Markdown
MemberAuthor

All comments resolved, dropped linker tests from PR after validating that all instrumentation code gets linked out in IL trimmer and Native AOT scenarios. Good to go once CI pass?

@lateralusX

Copy link
Copy Markdown
MemberAuthor

/ba-g recently fixed known issue #126922

@lateralusX

Copy link
Copy Markdown
MemberAuthor

@rcj1, @jkotas, @jakobbotsch, @noahfalk, if there are no more objections, I will go ahead and merge this PR.

@lateralusX
lateralusX merged commit 462878e into dotnet:mainApr 16, 2026
173 of 176 checks passed
tommcdon added a commit to tommcdon/runtime that referenced this pull request Apr 21, 2026
…rDebug
When the debugger sets Task.s_asyncDebuggingEnabled directly via
ICorDebug (SetManagedTaskEtwEventsEnabled), the AsyncInstrumentation
flags system added by PR dotnet#126091 was not aware of this. The new flags
only got set via ETW OnEventCommand, so non-ETW RuntimeAsync configs
had s_asyncDebuggerActiveFlags stuck at Disabled, preventing
NotifyDebuggerOfRuntimeAsyncState from being called.
Fix: In InitializeFlags(), check Task.s_asyncDebuggingEnabled as a
fallback when s_asyncDebuggerActiveFlags is still Disabled. This
ensures the debugger's request is honored regardless of whether ETW
events have been enabled.
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
@github-actionsgithub-actionsBot locked and limited conversation to collaborators May 17, 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.

7 participants

@lateralusX@rcj1@jakobbotsch@noahfalk@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" + '
Add low-cost instrumented version of RuntimeAsyncTask::DispatchContinuations reclaiming lost performance. by lateralusX · Pull Request #126091 · dotnet/runtime · GitHub
Skip to content

Add low-cost instrumented version of RuntimeAsyncTask::DispatchContinuations reclaiming lost performance. - #126091

Merged
lateralusX merged 23 commits into
dotnet:mainfrom
lateralusX:lateralusX/runtime-async-instrumentation
Apr 16, 2026
Merged

Add low-cost instrumented version of RuntimeAsyncTask::DispatchContinuations reclaiming lost performance.#126091
lateralusX merged 23 commits into
dotnet:mainfrom
lateralusX:lateralusX/runtime-async-instrumentation

Conversation

@lateralusX

@lateralusXlateralusX commented Mar 25, 2026

Copy link
Copy Markdown
Member

#123727 introduced a regression of ~7% adding additional instrumentation for Debugger/TPL into RuntimeAsyncTask::DispatchContinuations. Going forward, there will be even more instrumentation needed when implementing async profiling and that would increase the overhead even more, so we need a way to isolate the instrumented vs none instrumented version of this method and regain some of the lost performance.

This PR duplicates DispatchContinuations into two methods, regular and instrumented. Previous version of PR used a generic value type specialization, but it was decided that the simplification not using generic value type specialization is worth the duplication of the function.

To be able to "upgrade" from none instrumented to instrumented version of DispatchContinuations, there are checks at method entry and after each completed continuation detecting if method should switch to instrumented version. Both checks are small and fast just a load of a flag in a static variable and checked if it's not 0.

This change will make sure the RuntimeAsyncTask::DispatchContinuations is protected from future performance regressions when more instrumentation gets added into the instrumented version.

Running the same benchmark, #123727 (comment) now shows the following numbers on old vs new implementation:

MetricOldNewDiff
Total bytes (S.P.C)16 740 KB16 756 KB+16 KB
JIT Size (RuntimeAsyncTask::DispatchContinuations)1778 B1399 B-379 B
Benchmark337ms362ms-25ms (~ -7%)

Measurements done on Windows x64.

S.P.C is 16 KB larger with this PR, due to duplicated DispatchContinuation method.

JIT Size is 379 bytes smaller on none instrumented version of RuntimeAsyncTask::DispatchContinuations, all previous instrumentation has been moved to instrumented version, completely eliminated in default method.

Benchmark shows that this PR recover most of the performance previously lost in #123727.

Code paths triggering the use of instrumented version, InstrumentedDispatchContinuations are protected by a IsSupported flag, so can be trimmed away, removing references to InstrumentedDispatchContinuations.

Most changes in this PR are around extracting out existing instrumentation into the instrumentation implementation. PR also optimize some of the debugger instrumentations previously implemented reducing locking in scenarios where continuation chains are handled.

PR adds a number of new tests validating that the current debugger and TPL instrumentation is still working.

PR also adds preparation for async profiler instrumentation in the AsyncInstrumentation type. This type will be used by more scenarios going forward.

@dotnet-policy-service

Copy link
Copy Markdown
Contributor

Tagging subscribers to this area: @dotnet/area-system-runtime
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

This PR refactors runtime-async continuation dispatch to support a low-overhead “uninstrumented” fast path while still enabling Debugger/TPL (and future profiler) instrumentation via a separate, JIT-specialized codegen path, with updated flag plumbing and added tests to validate behavior and cleanup.

Changes:

  • Introduces a generic, instrumentation-specialized DispatchContinuations<TRuntimeAsyncTaskInstrumentation>() path and centralized runtime-async instrumentation flag management.
  • Refactors Task’s runtime-async timestamp bookkeeping APIs to better support continuation chains and exception/unwind cleanup.
  • Adds/expands RuntimeAsync tests for timestamp cleanup, debugger detach behavior, continuation timestamp visibility, and TPL EventSource events; wires TPL EventSource enable/disable to update instrumentation flags.

Reviewed changes

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

FileDescription
src/libraries/System.Runtime/tests/System.Threading.Tasks.Tests/System.Runtime.CompilerServices/RuntimeAsyncTests.csAdds coverage for runtime-async instrumentation behavior (timestamps, detach, unwind/cancel, and TPL events).
src/libraries/System.Private.CoreLib/src/System/Threading/Tasks/TplEventSource.csUpdates runtime-async instrumentation flags when TPL EventSource commands change enabled keywords.
src/libraries/System.Private.CoreLib/src/System/Threading/Tasks/Task.csRefactors runtime-async timestamp dictionaries and adds helpers for chain timestamp propagation and cleanup.
src/coreclr/System.Private.CoreLib/src/System/Runtime/CompilerServices/AsyncHelpers.CoreCLR.csAdds the generic instrumentation abstraction and splits dispatch/finalize into specialized uninstrumented vs instrumented implementations.

Comment threadsrc/libraries/System.Private.CoreLib/src/System/Threading/Tasks/Task.cs Outdated
CopilotAI review requested due to automatic review settings March 25, 2026 14:53

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 4 out of 4 changed files in this pull request and generated 3 comments.

Comment threadsrc/libraries/System.Private.CoreLib/src/System/Threading/Tasks/Task.cs Outdated
CopilotAI review requested due to automatic review settings March 25, 2026 15:15

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 4 out of 4 changed files in this pull request and generated 2 comments.

@lateralusX

Copy link
Copy Markdown
MemberAuthor

Most failures are due to test _reflection::Async2Reflection.FromStack that currently asserts that DispatchContinuations is on stack and is currently not aware of the change to a generic method. If we stick with the generic method, then the assert in this test needs to be updated to reflect the name change.

@rcj1

rcj1 commented Mar 27, 2026

Copy link
Copy Markdown
Contributor

s_asyncDebuggingEnabled implies that these TPL events have been enabled, with two exceptions:

  1. Failure to create ETW session (too many existing ETW sessions, etc)
  2. Manual shutdown of VS-created ETW session or other exceptional error that causes ETW session to close

We can assume that s_asyncDebuggingEnabled implies required TPL events enabled, and save the setting and checking of these flags. The downside is a tiny amount of overhead spent checking whether TPL events are enabled in these two exceptional cases during debugging - in my opinion, an acceptable tradeoff for simplification and any possible optimization of the hot path.

If someone believes that scenario 1 is not very uncommon, it is possible to change Concord such that they will only be out of sync in case 2.

@lateralusX

lateralusX commented Mar 30, 2026

Copy link
Copy Markdown
MemberAuthor

s_asyncDebuggingEnabled implies that these TPL events have been enabled, with two exceptions:

  1. Failure to create ETW session (too many existing ETW sessions, etc)
  2. Manual shutdown of VS-created ETW session or other exceptional error that causes ETW session to close

We can assume that s_asyncDebuggingEnabled implies required TPL events enabled, and save the setting and checking of these flags. The downside is a tiny amount of overhead spent checking whether TPL events are enabled in these two exceptional cases during debugging - in my opinion, an acceptable tradeoff for simplification and any possible optimization of the hot path.

If someone believes that scenario 1 is not very uncommon, it is possible to change Concord such that they will only be out of sync in case 2.

Question is if pure TPL will have meaning here, like adding TPL events for runtime async activities in case a client only consumes TPL events to recreate async callstacks, or if that was never the intent with this change and it should only support the debugger features, then we should definitely put all logic under the same flag.

If we could assume the feature will trigger the TPL event source, we could handle it all in the on command event onTPL event source, and we can do the fast check only on flags, maybe that was what you proposed @rcj1? In that case this feature won't light up without the TPL session enabled, but that might be, OK? It would speed up the instrumentation gate check since it will only look at the flags again and ignore s_asyncDebuggingEnabled as part of dispatch loop.

@jakobbotsch

Copy link
Copy Markdown
Member

I took this path to get away from duplicating the complete DispatchContinuations method into a regular and instrumented version, reducing the maintenance of ~100 lines of duplicated high performing unsafe code. It can be argued that duplicating the DispatchContinuations is a small price to pay giving a little clearer implementation. If that is something we all agree on and accept, I'm happy to pursue that path as well, but wanted to start with the zero cost abstraction, no duplication path.

I worry that all the AggressiveInlining static abstracts introduce non-trivial startup cost when it comes to compiling all the dispatchers. It should be possible to measure this via some polymorphically recursive async methods that creates a lot of rarely used instantiations.

The other thing with the current approach -- the inversion of control makes the dispatcher quite hard to follow for me. But I can live with this given the improved performance.

@jakobbotsch

Copy link
Copy Markdown
Member

I worry that all the AggressiveInlining static abstracts introduce non-trivial startup cost when it comes to compiling all the dispatchers. It should be possible to measure this via some polymorphically recursive async methods that creates a lot of rarely used instantiations.

Doesn't look too bad, I tried a binary counter micro benchmark that loads 4096 of the dispatchers:

usingSystem;usingSystem.Diagnostics;usingSystem.Threading.Tasks;namespaceAsyncMicro;publicclassProgram{staticvoidMain(){Stopwatchtimer=Stopwatch.StartNew();newProgram().Recurse<byte,byte,byte,byte,byte,byte,byte,byte,byte,byte,byte,byte>(0).GetAwaiter().GetResult();Console.WriteLine("Took {0} ms",timer.ElapsedMilliseconds);}privateasyncTask<V<T11,T10,T9,T8,T7,T6,T5,T4,T3,T2,T1,T0>>Recurse<T11,T10,T9,T8,T7,T6,T5,T4,T3,T2,T1,T0>(intn){awaitTask.Yield();//Console.WriteLine(n);Taskt;if(typeof(T0)==typeof(byte)){t=Recurse<T11,T10,T9,T8,T7,T6,T5,T4,T3,T2,T1,int>(n+1);}elseif(typeof(T1)==typeof(byte)){t=Recurse<T11,T10,T9,T8,T7,T6,T5,T4,T3,T2,int,byte>(n+1);}elseif(typeof(T2)==typeof(byte)){t=Recurse<T11,T10,T9,T8,T7,T6,T5,T4,T3,int,byte,byte>(n+1);}elseif(typeof(T3)==typeof(byte)){t=Recurse<T11,T10,T9,T8,T7,T6,T5,T4,int,byte,byte,byte>(n+1);}elseif(typeof(T4)==typeof(byte)){t=Recurse<T11,T10,T9,T8,T7,T6,T5,int,byte,byte,byte,byte>(n+1);}elseif(typeof(T5)==typeof(byte)){t=Recurse<T11,T10,T9,T8,T7,T6,int,byte,byte,byte,byte,byte>(n+1);}elseif(typeof(T6)==typeof(byte)){t=Recurse<T11,T10,T9,T8,T7,int,byte,byte,byte,byte,byte,byte>(n+1);}elseif(typeof(T7)==typeof(byte)){t=Recurse<T11,T10,T9,T8,int,byte,byte,byte,byte,byte,byte,byte>(n+1);}elseif(typeof(T8)==typeof(byte)){t=Recurse<T11,T10,T9,int,byte,byte,byte,byte,byte,byte,byte,byte>(n+1);}elseif(typeof(T9)==typeof(byte)){t=Recurse<T11,T10,int,byte,byte,byte,byte,byte,byte,byte,byte,byte>(n+1);}elseif(typeof(T10)==typeof(byte)){t=Recurse<T11,int,byte,byte,byte,byte,byte,byte,byte,byte,byte,byte>(n+1);}elseif(typeof(T11)==typeof(byte)){t=Recurse<int,byte,byte,byte,byte,byte,byte,byte,byte,byte,byte,byte>(n+1);}else{returndefault;}awaitt;returndefault;}privatestructV<T11,T10,T9,T8,T7,T6,T5,T4,T3,T2,T1,T0>{publicintX;}}

The difference is only around 100 ms on this PR.
Base: Took 9205 ms
Diff: Took 9303 ms

(P.S. a simpler binary recursing generic hits some exponential behavior... Need to investigate that.)

@lateralusX

Copy link
Copy Markdown
MemberAuthor

The other thing with the current approach -- the inversion of control makes the dispatcher quite hard to follow for me. But I can live with this given the improved performance.

The alternative is to duplicate the DispatchContinuation implementation into two different methods, one default (with upgrade capabilities) and one instrumented version. In the end it depends on whats important, this PR currently take the reduce code duplication path, with slightly more complex dispatcher implementation (due to the instrumentation callbacks), and I think that is fine. Dropping the R2R exclusion reducing one indirect call on the instrumented path will simplify the instrumented path a little more as well, with very small size increase of S.P.C.

CopilotAI review requested due to automatic review settings April 10, 2026 08:16
@lateralusX
lateralusXforce-pushed the lateralusX/runtime-async-instrumentation branch from 069b76c to 75f5f5eCompareApril 10, 2026 08:16

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 6 out of 6 changed files in this pull request and generated no new comments.

@rcj1

rcj1 commented Apr 13, 2026

Copy link
Copy Markdown
Contributor

After testing the changes locally, I realize propagating these changes through to VS will be more complex than I initially thought. @tommcdon and I built changes making a lot of assumptions about the location of DispatchContinuations. We should fix whatever is broken by insertion of InstrumentedDispatchContinuations.

@lateralusX

lateralusX commented Apr 14, 2026

Copy link
Copy Markdown
MemberAuthor

After testing the changes locally, I realize propagating these changes through to VS will be more complex than I initially thought. @tommcdon and I built changes making a lot of assumptions about the location of DispatchContinuations. We should fix whatever is broken by insertion of InstrumentedDispatchContinuations.

@rcj1 can we go ahead and merge this and you pick up the needed VS changes later or do we need to hold off on this PR until you verified/fixed VS? It would be nice to get this PR merged since its blocking other PR's from following at the moment.

This was referenced Apr 14, 2026
CopilotAI review requested due to automatic review settings April 14, 2026 18:51
@lateralusX
lateralusXforce-pushed the lateralusX/runtime-async-instrumentation branch from 01867b8 to 75f5f5eCompareApril 14, 2026 18:51

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 6 out of 6 changed files in this pull request and generated 1 comment.

@lateralusX

lateralusX commented Apr 14, 2026

Copy link
Copy Markdown
MemberAuthor

All comments resolved, dropped linker tests from PR after validating that all instrumentation code gets linked out in IL trimmer and Native AOT scenarios. Good to go once CI pass?

@lateralusX

Copy link
Copy Markdown
MemberAuthor

/ba-g recently fixed known issue #126922

@lateralusX

Copy link
Copy Markdown
MemberAuthor

@rcj1, @jkotas, @jakobbotsch, @noahfalk, if there are no more objections, I will go ahead and merge this PR.

@lateralusX
lateralusX merged commit 462878e into dotnet:mainApr 16, 2026
173 of 176 checks passed
tommcdon added a commit to tommcdon/runtime that referenced this pull request Apr 21, 2026
…rDebug
When the debugger sets Task.s_asyncDebuggingEnabled directly via
ICorDebug (SetManagedTaskEtwEventsEnabled), the AsyncInstrumentation
flags system added by PR dotnet#126091 was not aware of this. The new flags
only got set via ETW OnEventCommand, so non-ETW RuntimeAsync configs
had s_asyncDebuggerActiveFlags stuck at Disabled, preventing
NotifyDebuggerOfRuntimeAsyncState from being called.
Fix: In InitializeFlags(), check Task.s_asyncDebuggingEnabled as a
fallback when s_asyncDebuggerActiveFlags is still Disabled. This
ensures the debugger's request is honored regardless of whether ETW
events have been enabled.
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
@github-actionsgithub-actionsBot locked and limited conversation to collaborators May 17, 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.

7 participants

@lateralusX@rcj1@jakobbotsch@noahfalk@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('^' + ".*" + ' Add low-cost instrumented version of RuntimeAsyncTask::DispatchContinuations reclaiming lost performance. by lateralusX · Pull Request #126091 · dotnet/runtime · GitHub
Skip to content

Add low-cost instrumented version of RuntimeAsyncTask::DispatchContinuations reclaiming lost performance. - #126091

Merged
lateralusX merged 23 commits into
dotnet:mainfrom
lateralusX:lateralusX/runtime-async-instrumentation
Apr 16, 2026
Merged

Add low-cost instrumented version of RuntimeAsyncTask::DispatchContinuations reclaiming lost performance.#126091
lateralusX merged 23 commits into
dotnet:mainfrom
lateralusX:lateralusX/runtime-async-instrumentation

Conversation

@lateralusX

@lateralusXlateralusX commented Mar 25, 2026

Copy link
Copy Markdown
Member

#123727 introduced a regression of ~7% adding additional instrumentation for Debugger/TPL into RuntimeAsyncTask::DispatchContinuations. Going forward, there will be even more instrumentation needed when implementing async profiling and that would increase the overhead even more, so we need a way to isolate the instrumented vs none instrumented version of this method and regain some of the lost performance.

This PR duplicates DispatchContinuations into two methods, regular and instrumented. Previous version of PR used a generic value type specialization, but it was decided that the simplification not using generic value type specialization is worth the duplication of the function.

To be able to "upgrade" from none instrumented to instrumented version of DispatchContinuations, there are checks at method entry and after each completed continuation detecting if method should switch to instrumented version. Both checks are small and fast just a load of a flag in a static variable and checked if it's not 0.

This change will make sure the RuntimeAsyncTask::DispatchContinuations is protected from future performance regressions when more instrumentation gets added into the instrumented version.

Running the same benchmark, #123727 (comment) now shows the following numbers on old vs new implementation:

MetricOldNewDiff
Total bytes (S.P.C)16 740 KB16 756 KB+16 KB
JIT Size (RuntimeAsyncTask::DispatchContinuations)1778 B1399 B-379 B
Benchmark337ms362ms-25ms (~ -7%)

Measurements done on Windows x64.

S.P.C is 16 KB larger with this PR, due to duplicated DispatchContinuation method.

JIT Size is 379 bytes smaller on none instrumented version of RuntimeAsyncTask::DispatchContinuations, all previous instrumentation has been moved to instrumented version, completely eliminated in default method.

Benchmark shows that this PR recover most of the performance previously lost in #123727.

Code paths triggering the use of instrumented version, InstrumentedDispatchContinuations are protected by a IsSupported flag, so can be trimmed away, removing references to InstrumentedDispatchContinuations.

Most changes in this PR are around extracting out existing instrumentation into the instrumentation implementation. PR also optimize some of the debugger instrumentations previously implemented reducing locking in scenarios where continuation chains are handled.

PR adds a number of new tests validating that the current debugger and TPL instrumentation is still working.

PR also adds preparation for async profiler instrumentation in the AsyncInstrumentation type. This type will be used by more scenarios going forward.

@dotnet-policy-service

Copy link
Copy Markdown
Contributor

Tagging subscribers to this area: @dotnet/area-system-runtime
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

This PR refactors runtime-async continuation dispatch to support a low-overhead “uninstrumented” fast path while still enabling Debugger/TPL (and future profiler) instrumentation via a separate, JIT-specialized codegen path, with updated flag plumbing and added tests to validate behavior and cleanup.

Changes:

  • Introduces a generic, instrumentation-specialized DispatchContinuations<TRuntimeAsyncTaskInstrumentation>() path and centralized runtime-async instrumentation flag management.
  • Refactors Task’s runtime-async timestamp bookkeeping APIs to better support continuation chains and exception/unwind cleanup.
  • Adds/expands RuntimeAsync tests for timestamp cleanup, debugger detach behavior, continuation timestamp visibility, and TPL EventSource events; wires TPL EventSource enable/disable to update instrumentation flags.

Reviewed changes

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

FileDescription
src/libraries/System.Runtime/tests/System.Threading.Tasks.Tests/System.Runtime.CompilerServices/RuntimeAsyncTests.csAdds coverage for runtime-async instrumentation behavior (timestamps, detach, unwind/cancel, and TPL events).
src/libraries/System.Private.CoreLib/src/System/Threading/Tasks/TplEventSource.csUpdates runtime-async instrumentation flags when TPL EventSource commands change enabled keywords.
src/libraries/System.Private.CoreLib/src/System/Threading/Tasks/Task.csRefactors runtime-async timestamp dictionaries and adds helpers for chain timestamp propagation and cleanup.
src/coreclr/System.Private.CoreLib/src/System/Runtime/CompilerServices/AsyncHelpers.CoreCLR.csAdds the generic instrumentation abstraction and splits dispatch/finalize into specialized uninstrumented vs instrumented implementations.

Comment threadsrc/libraries/System.Private.CoreLib/src/System/Threading/Tasks/Task.cs Outdated
CopilotAI review requested due to automatic review settings March 25, 2026 14:53

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 4 out of 4 changed files in this pull request and generated 3 comments.

Comment threadsrc/libraries/System.Private.CoreLib/src/System/Threading/Tasks/Task.cs Outdated
CopilotAI review requested due to automatic review settings March 25, 2026 15:15

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 4 out of 4 changed files in this pull request and generated 2 comments.

@lateralusX

Copy link
Copy Markdown
MemberAuthor

Most failures are due to test _reflection::Async2Reflection.FromStack that currently asserts that DispatchContinuations is on stack and is currently not aware of the change to a generic method. If we stick with the generic method, then the assert in this test needs to be updated to reflect the name change.

@rcj1

rcj1 commented Mar 27, 2026

Copy link
Copy Markdown
Contributor

s_asyncDebuggingEnabled implies that these TPL events have been enabled, with two exceptions:

  1. Failure to create ETW session (too many existing ETW sessions, etc)
  2. Manual shutdown of VS-created ETW session or other exceptional error that causes ETW session to close

We can assume that s_asyncDebuggingEnabled implies required TPL events enabled, and save the setting and checking of these flags. The downside is a tiny amount of overhead spent checking whether TPL events are enabled in these two exceptional cases during debugging - in my opinion, an acceptable tradeoff for simplification and any possible optimization of the hot path.

If someone believes that scenario 1 is not very uncommon, it is possible to change Concord such that they will only be out of sync in case 2.

@lateralusX

lateralusX commented Mar 30, 2026

Copy link
Copy Markdown
MemberAuthor

s_asyncDebuggingEnabled implies that these TPL events have been enabled, with two exceptions:

  1. Failure to create ETW session (too many existing ETW sessions, etc)
  2. Manual shutdown of VS-created ETW session or other exceptional error that causes ETW session to close

We can assume that s_asyncDebuggingEnabled implies required TPL events enabled, and save the setting and checking of these flags. The downside is a tiny amount of overhead spent checking whether TPL events are enabled in these two exceptional cases during debugging - in my opinion, an acceptable tradeoff for simplification and any possible optimization of the hot path.

If someone believes that scenario 1 is not very uncommon, it is possible to change Concord such that they will only be out of sync in case 2.

Question is if pure TPL will have meaning here, like adding TPL events for runtime async activities in case a client only consumes TPL events to recreate async callstacks, or if that was never the intent with this change and it should only support the debugger features, then we should definitely put all logic under the same flag.

If we could assume the feature will trigger the TPL event source, we could handle it all in the on command event onTPL event source, and we can do the fast check only on flags, maybe that was what you proposed @rcj1? In that case this feature won't light up without the TPL session enabled, but that might be, OK? It would speed up the instrumentation gate check since it will only look at the flags again and ignore s_asyncDebuggingEnabled as part of dispatch loop.

@jakobbotsch

Copy link
Copy Markdown
Member

I took this path to get away from duplicating the complete DispatchContinuations method into a regular and instrumented version, reducing the maintenance of ~100 lines of duplicated high performing unsafe code. It can be argued that duplicating the DispatchContinuations is a small price to pay giving a little clearer implementation. If that is something we all agree on and accept, I'm happy to pursue that path as well, but wanted to start with the zero cost abstraction, no duplication path.

I worry that all the AggressiveInlining static abstracts introduce non-trivial startup cost when it comes to compiling all the dispatchers. It should be possible to measure this via some polymorphically recursive async methods that creates a lot of rarely used instantiations.

The other thing with the current approach -- the inversion of control makes the dispatcher quite hard to follow for me. But I can live with this given the improved performance.

@jakobbotsch

Copy link
Copy Markdown
Member

I worry that all the AggressiveInlining static abstracts introduce non-trivial startup cost when it comes to compiling all the dispatchers. It should be possible to measure this via some polymorphically recursive async methods that creates a lot of rarely used instantiations.

Doesn't look too bad, I tried a binary counter micro benchmark that loads 4096 of the dispatchers:

usingSystem;usingSystem.Diagnostics;usingSystem.Threading.Tasks;namespaceAsyncMicro;publicclassProgram{staticvoidMain(){Stopwatchtimer=Stopwatch.StartNew();newProgram().Recurse<byte,byte,byte,byte,byte,byte,byte,byte,byte,byte,byte,byte>(0).GetAwaiter().GetResult();Console.WriteLine("Took {0} ms",timer.ElapsedMilliseconds);}privateasyncTask<V<T11,T10,T9,T8,T7,T6,T5,T4,T3,T2,T1,T0>>Recurse<T11,T10,T9,T8,T7,T6,T5,T4,T3,T2,T1,T0>(intn){awaitTask.Yield();//Console.WriteLine(n);Taskt;if(typeof(T0)==typeof(byte)){t=Recurse<T11,T10,T9,T8,T7,T6,T5,T4,T3,T2,T1,int>(n+1);}elseif(typeof(T1)==typeof(byte)){t=Recurse<T11,T10,T9,T8,T7,T6,T5,T4,T3,T2,int,byte>(n+1);}elseif(typeof(T2)==typeof(byte)){t=Recurse<T11,T10,T9,T8,T7,T6,T5,T4,T3,int,byte,byte>(n+1);}elseif(typeof(T3)==typeof(byte)){t=Recurse<T11,T10,T9,T8,T7,T6,T5,T4,int,byte,byte,byte>(n+1);}elseif(typeof(T4)==typeof(byte)){t=Recurse<T11,T10,T9,T8,T7,T6,T5,int,byte,byte,byte,byte>(n+1);}elseif(typeof(T5)==typeof(byte)){t=Recurse<T11,T10,T9,T8,T7,T6,int,byte,byte,byte,byte,byte>(n+1);}elseif(typeof(T6)==typeof(byte)){t=Recurse<T11,T10,T9,T8,T7,int,byte,byte,byte,byte,byte,byte>(n+1);}elseif(typeof(T7)==typeof(byte)){t=Recurse<T11,T10,T9,T8,int,byte,byte,byte,byte,byte,byte,byte>(n+1);}elseif(typeof(T8)==typeof(byte)){t=Recurse<T11,T10,T9,int,byte,byte,byte,byte,byte,byte,byte,byte>(n+1);}elseif(typeof(T9)==typeof(byte)){t=Recurse<T11,T10,int,byte,byte,byte,byte,byte,byte,byte,byte,byte>(n+1);}elseif(typeof(T10)==typeof(byte)){t=Recurse<T11,int,byte,byte,byte,byte,byte,byte,byte,byte,byte,byte>(n+1);}elseif(typeof(T11)==typeof(byte)){t=Recurse<int,byte,byte,byte,byte,byte,byte,byte,byte,byte,byte,byte>(n+1);}else{returndefault;}awaitt;returndefault;}privatestructV<T11,T10,T9,T8,T7,T6,T5,T4,T3,T2,T1,T0>{publicintX;}}

The difference is only around 100 ms on this PR.
Base: Took 9205 ms
Diff: Took 9303 ms

(P.S. a simpler binary recursing generic hits some exponential behavior... Need to investigate that.)

@lateralusX

Copy link
Copy Markdown
MemberAuthor

The other thing with the current approach -- the inversion of control makes the dispatcher quite hard to follow for me. But I can live with this given the improved performance.

The alternative is to duplicate the DispatchContinuation implementation into two different methods, one default (with upgrade capabilities) and one instrumented version. In the end it depends on whats important, this PR currently take the reduce code duplication path, with slightly more complex dispatcher implementation (due to the instrumentation callbacks), and I think that is fine. Dropping the R2R exclusion reducing one indirect call on the instrumented path will simplify the instrumented path a little more as well, with very small size increase of S.P.C.

CopilotAI review requested due to automatic review settings April 10, 2026 08:16
@lateralusX
lateralusXforce-pushed the lateralusX/runtime-async-instrumentation branch from 069b76c to 75f5f5eCompareApril 10, 2026 08:16

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 6 out of 6 changed files in this pull request and generated no new comments.

@rcj1

rcj1 commented Apr 13, 2026

Copy link
Copy Markdown
Contributor

After testing the changes locally, I realize propagating these changes through to VS will be more complex than I initially thought. @tommcdon and I built changes making a lot of assumptions about the location of DispatchContinuations. We should fix whatever is broken by insertion of InstrumentedDispatchContinuations.

@lateralusX

lateralusX commented Apr 14, 2026

Copy link
Copy Markdown
MemberAuthor

After testing the changes locally, I realize propagating these changes through to VS will be more complex than I initially thought. @tommcdon and I built changes making a lot of assumptions about the location of DispatchContinuations. We should fix whatever is broken by insertion of InstrumentedDispatchContinuations.

@rcj1 can we go ahead and merge this and you pick up the needed VS changes later or do we need to hold off on this PR until you verified/fixed VS? It would be nice to get this PR merged since its blocking other PR's from following at the moment.

This was referenced Apr 14, 2026
CopilotAI review requested due to automatic review settings April 14, 2026 18:51
@lateralusX
lateralusXforce-pushed the lateralusX/runtime-async-instrumentation branch from 01867b8 to 75f5f5eCompareApril 14, 2026 18:51

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 6 out of 6 changed files in this pull request and generated 1 comment.

@lateralusX

lateralusX commented Apr 14, 2026

Copy link
Copy Markdown
MemberAuthor

All comments resolved, dropped linker tests from PR after validating that all instrumentation code gets linked out in IL trimmer and Native AOT scenarios. Good to go once CI pass?

@lateralusX

Copy link
Copy Markdown
MemberAuthor

/ba-g recently fixed known issue #126922

@lateralusX

Copy link
Copy Markdown
MemberAuthor

@rcj1, @jkotas, @jakobbotsch, @noahfalk, if there are no more objections, I will go ahead and merge this PR.

@lateralusX
lateralusX merged commit 462878e into dotnet:mainApr 16, 2026
173 of 176 checks passed
tommcdon added a commit to tommcdon/runtime that referenced this pull request Apr 21, 2026
…rDebug
When the debugger sets Task.s_asyncDebuggingEnabled directly via
ICorDebug (SetManagedTaskEtwEventsEnabled), the AsyncInstrumentation
flags system added by PR dotnet#126091 was not aware of this. The new flags
only got set via ETW OnEventCommand, so non-ETW RuntimeAsync configs
had s_asyncDebuggerActiveFlags stuck at Disabled, preventing
NotifyDebuggerOfRuntimeAsyncState from being called.
Fix: In InitializeFlags(), check Task.s_asyncDebuggingEnabled as a
fallback when s_asyncDebuggerActiveFlags is still Disabled. This
ensures the debugger's request is honored regardless of whether ETW
events have been enabled.
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
@github-actionsgithub-actionsBot locked and limited conversation to collaborators May 17, 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.

7 participants

@lateralusX@rcj1@jakobbotsch@noahfalk@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('^' + ".*" + ' Add low-cost instrumented version of RuntimeAsyncTask::DispatchContinuations reclaiming lost performance. by lateralusX · Pull Request #126091 · dotnet/runtime · GitHub
Skip to content

Add low-cost instrumented version of RuntimeAsyncTask::DispatchContinuations reclaiming lost performance. - #126091

Merged
lateralusX merged 23 commits into
dotnet:mainfrom
lateralusX:lateralusX/runtime-async-instrumentation
Apr 16, 2026
Merged

Add low-cost instrumented version of RuntimeAsyncTask::DispatchContinuations reclaiming lost performance.#126091
lateralusX merged 23 commits into
dotnet:mainfrom
lateralusX:lateralusX/runtime-async-instrumentation

Conversation

@lateralusX

@lateralusXlateralusX commented Mar 25, 2026

Copy link
Copy Markdown
Member

#123727 introduced a regression of ~7% adding additional instrumentation for Debugger/TPL into RuntimeAsyncTask::DispatchContinuations. Going forward, there will be even more instrumentation needed when implementing async profiling and that would increase the overhead even more, so we need a way to isolate the instrumented vs none instrumented version of this method and regain some of the lost performance.

This PR duplicates DispatchContinuations into two methods, regular and instrumented. Previous version of PR used a generic value type specialization, but it was decided that the simplification not using generic value type specialization is worth the duplication of the function.

To be able to "upgrade" from none instrumented to instrumented version of DispatchContinuations, there are checks at method entry and after each completed continuation detecting if method should switch to instrumented version. Both checks are small and fast just a load of a flag in a static variable and checked if it's not 0.

This change will make sure the RuntimeAsyncTask::DispatchContinuations is protected from future performance regressions when more instrumentation gets added into the instrumented version.

Running the same benchmark, #123727 (comment) now shows the following numbers on old vs new implementation:

MetricOldNewDiff
Total bytes (S.P.C)16 740 KB16 756 KB+16 KB
JIT Size (RuntimeAsyncTask::DispatchContinuations)1778 B1399 B-379 B
Benchmark337ms362ms-25ms (~ -7%)

Measurements done on Windows x64.

S.P.C is 16 KB larger with this PR, due to duplicated DispatchContinuation method.

JIT Size is 379 bytes smaller on none instrumented version of RuntimeAsyncTask::DispatchContinuations, all previous instrumentation has been moved to instrumented version, completely eliminated in default method.

Benchmark shows that this PR recover most of the performance previously lost in #123727.

Code paths triggering the use of instrumented version, InstrumentedDispatchContinuations are protected by a IsSupported flag, so can be trimmed away, removing references to InstrumentedDispatchContinuations.

Most changes in this PR are around extracting out existing instrumentation into the instrumentation implementation. PR also optimize some of the debugger instrumentations previously implemented reducing locking in scenarios where continuation chains are handled.

PR adds a number of new tests validating that the current debugger and TPL instrumentation is still working.

PR also adds preparation for async profiler instrumentation in the AsyncInstrumentation type. This type will be used by more scenarios going forward.

@dotnet-policy-service

Copy link
Copy Markdown
Contributor

Tagging subscribers to this area: @dotnet/area-system-runtime
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

This PR refactors runtime-async continuation dispatch to support a low-overhead “uninstrumented” fast path while still enabling Debugger/TPL (and future profiler) instrumentation via a separate, JIT-specialized codegen path, with updated flag plumbing and added tests to validate behavior and cleanup.

Changes:

  • Introduces a generic, instrumentation-specialized DispatchContinuations<TRuntimeAsyncTaskInstrumentation>() path and centralized runtime-async instrumentation flag management.
  • Refactors Task’s runtime-async timestamp bookkeeping APIs to better support continuation chains and exception/unwind cleanup.
  • Adds/expands RuntimeAsync tests for timestamp cleanup, debugger detach behavior, continuation timestamp visibility, and TPL EventSource events; wires TPL EventSource enable/disable to update instrumentation flags.

Reviewed changes

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

FileDescription
src/libraries/System.Runtime/tests/System.Threading.Tasks.Tests/System.Runtime.CompilerServices/RuntimeAsyncTests.csAdds coverage for runtime-async instrumentation behavior (timestamps, detach, unwind/cancel, and TPL events).
src/libraries/System.Private.CoreLib/src/System/Threading/Tasks/TplEventSource.csUpdates runtime-async instrumentation flags when TPL EventSource commands change enabled keywords.
src/libraries/System.Private.CoreLib/src/System/Threading/Tasks/Task.csRefactors runtime-async timestamp dictionaries and adds helpers for chain timestamp propagation and cleanup.
src/coreclr/System.Private.CoreLib/src/System/Runtime/CompilerServices/AsyncHelpers.CoreCLR.csAdds the generic instrumentation abstraction and splits dispatch/finalize into specialized uninstrumented vs instrumented implementations.

Comment threadsrc/libraries/System.Private.CoreLib/src/System/Threading/Tasks/Task.cs Outdated
CopilotAI review requested due to automatic review settings March 25, 2026 14:53

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 4 out of 4 changed files in this pull request and generated 3 comments.

Comment threadsrc/libraries/System.Private.CoreLib/src/System/Threading/Tasks/Task.cs Outdated
CopilotAI review requested due to automatic review settings March 25, 2026 15:15

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 4 out of 4 changed files in this pull request and generated 2 comments.

@lateralusX

Copy link
Copy Markdown
MemberAuthor

Most failures are due to test _reflection::Async2Reflection.FromStack that currently asserts that DispatchContinuations is on stack and is currently not aware of the change to a generic method. If we stick with the generic method, then the assert in this test needs to be updated to reflect the name change.

@rcj1

rcj1 commented Mar 27, 2026

Copy link
Copy Markdown
Contributor

s_asyncDebuggingEnabled implies that these TPL events have been enabled, with two exceptions:

  1. Failure to create ETW session (too many existing ETW sessions, etc)
  2. Manual shutdown of VS-created ETW session or other exceptional error that causes ETW session to close

We can assume that s_asyncDebuggingEnabled implies required TPL events enabled, and save the setting and checking of these flags. The downside is a tiny amount of overhead spent checking whether TPL events are enabled in these two exceptional cases during debugging - in my opinion, an acceptable tradeoff for simplification and any possible optimization of the hot path.

If someone believes that scenario 1 is not very uncommon, it is possible to change Concord such that they will only be out of sync in case 2.

@lateralusX

lateralusX commented Mar 30, 2026

Copy link
Copy Markdown
MemberAuthor

s_asyncDebuggingEnabled implies that these TPL events have been enabled, with two exceptions:

  1. Failure to create ETW session (too many existing ETW sessions, etc)
  2. Manual shutdown of VS-created ETW session or other exceptional error that causes ETW session to close

We can assume that s_asyncDebuggingEnabled implies required TPL events enabled, and save the setting and checking of these flags. The downside is a tiny amount of overhead spent checking whether TPL events are enabled in these two exceptional cases during debugging - in my opinion, an acceptable tradeoff for simplification and any possible optimization of the hot path.

If someone believes that scenario 1 is not very uncommon, it is possible to change Concord such that they will only be out of sync in case 2.

Question is if pure TPL will have meaning here, like adding TPL events for runtime async activities in case a client only consumes TPL events to recreate async callstacks, or if that was never the intent with this change and it should only support the debugger features, then we should definitely put all logic under the same flag.

If we could assume the feature will trigger the TPL event source, we could handle it all in the on command event onTPL event source, and we can do the fast check only on flags, maybe that was what you proposed @rcj1? In that case this feature won't light up without the TPL session enabled, but that might be, OK? It would speed up the instrumentation gate check since it will only look at the flags again and ignore s_asyncDebuggingEnabled as part of dispatch loop.

@jakobbotsch

Copy link
Copy Markdown
Member

I took this path to get away from duplicating the complete DispatchContinuations method into a regular and instrumented version, reducing the maintenance of ~100 lines of duplicated high performing unsafe code. It can be argued that duplicating the DispatchContinuations is a small price to pay giving a little clearer implementation. If that is something we all agree on and accept, I'm happy to pursue that path as well, but wanted to start with the zero cost abstraction, no duplication path.

I worry that all the AggressiveInlining static abstracts introduce non-trivial startup cost when it comes to compiling all the dispatchers. It should be possible to measure this via some polymorphically recursive async methods that creates a lot of rarely used instantiations.

The other thing with the current approach -- the inversion of control makes the dispatcher quite hard to follow for me. But I can live with this given the improved performance.

@jakobbotsch

Copy link
Copy Markdown
Member

I worry that all the AggressiveInlining static abstracts introduce non-trivial startup cost when it comes to compiling all the dispatchers. It should be possible to measure this via some polymorphically recursive async methods that creates a lot of rarely used instantiations.

Doesn't look too bad, I tried a binary counter micro benchmark that loads 4096 of the dispatchers:

usingSystem;usingSystem.Diagnostics;usingSystem.Threading.Tasks;namespaceAsyncMicro;publicclassProgram{staticvoidMain(){Stopwatchtimer=Stopwatch.StartNew();newProgram().Recurse<byte,byte,byte,byte,byte,byte,byte,byte,byte,byte,byte,byte>(0).GetAwaiter().GetResult();Console.WriteLine("Took {0} ms",timer.ElapsedMilliseconds);}privateasyncTask<V<T11,T10,T9,T8,T7,T6,T5,T4,T3,T2,T1,T0>>Recurse<T11,T10,T9,T8,T7,T6,T5,T4,T3,T2,T1,T0>(intn){awaitTask.Yield();//Console.WriteLine(n);Taskt;if(typeof(T0)==typeof(byte)){t=Recurse<T11,T10,T9,T8,T7,T6,T5,T4,T3,T2,T1,int>(n+1);}elseif(typeof(T1)==typeof(byte)){t=Recurse<T11,T10,T9,T8,T7,T6,T5,T4,T3,T2,int,byte>(n+1);}elseif(typeof(T2)==typeof(byte)){t=Recurse<T11,T10,T9,T8,T7,T6,T5,T4,T3,int,byte,byte>(n+1);}elseif(typeof(T3)==typeof(byte)){t=Recurse<T11,T10,T9,T8,T7,T6,T5,T4,int,byte,byte,byte>(n+1);}elseif(typeof(T4)==typeof(byte)){t=Recurse<T11,T10,T9,T8,T7,T6,T5,int,byte,byte,byte,byte>(n+1);}elseif(typeof(T5)==typeof(byte)){t=Recurse<T11,T10,T9,T8,T7,T6,int,byte,byte,byte,byte,byte>(n+1);}elseif(typeof(T6)==typeof(byte)){t=Recurse<T11,T10,T9,T8,T7,int,byte,byte,byte,byte,byte,byte>(n+1);}elseif(typeof(T7)==typeof(byte)){t=Recurse<T11,T10,T9,T8,int,byte,byte,byte,byte,byte,byte,byte>(n+1);}elseif(typeof(T8)==typeof(byte)){t=Recurse<T11,T10,T9,int,byte,byte,byte,byte,byte,byte,byte,byte>(n+1);}elseif(typeof(T9)==typeof(byte)){t=Recurse<T11,T10,int,byte,byte,byte,byte,byte,byte,byte,byte,byte>(n+1);}elseif(typeof(T10)==typeof(byte)){t=Recurse<T11,int,byte,byte,byte,byte,byte,byte,byte,byte,byte,byte>(n+1);}elseif(typeof(T11)==typeof(byte)){t=Recurse<int,byte,byte,byte,byte,byte,byte,byte,byte,byte,byte,byte>(n+1);}else{returndefault;}awaitt;returndefault;}privatestructV<T11,T10,T9,T8,T7,T6,T5,T4,T3,T2,T1,T0>{publicintX;}}

The difference is only around 100 ms on this PR.
Base: Took 9205 ms
Diff: Took 9303 ms

(P.S. a simpler binary recursing generic hits some exponential behavior... Need to investigate that.)

@lateralusX

Copy link
Copy Markdown
MemberAuthor

The other thing with the current approach -- the inversion of control makes the dispatcher quite hard to follow for me. But I can live with this given the improved performance.

The alternative is to duplicate the DispatchContinuation implementation into two different methods, one default (with upgrade capabilities) and one instrumented version. In the end it depends on whats important, this PR currently take the reduce code duplication path, with slightly more complex dispatcher implementation (due to the instrumentation callbacks), and I think that is fine. Dropping the R2R exclusion reducing one indirect call on the instrumented path will simplify the instrumented path a little more as well, with very small size increase of S.P.C.

CopilotAI review requested due to automatic review settings April 10, 2026 08:16
@lateralusX
lateralusXforce-pushed the lateralusX/runtime-async-instrumentation branch from 069b76c to 75f5f5eCompareApril 10, 2026 08:16

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 6 out of 6 changed files in this pull request and generated no new comments.

@rcj1

rcj1 commented Apr 13, 2026

Copy link
Copy Markdown
Contributor

After testing the changes locally, I realize propagating these changes through to VS will be more complex than I initially thought. @tommcdon and I built changes making a lot of assumptions about the location of DispatchContinuations. We should fix whatever is broken by insertion of InstrumentedDispatchContinuations.

@lateralusX

lateralusX commented Apr 14, 2026

Copy link
Copy Markdown
MemberAuthor

After testing the changes locally, I realize propagating these changes through to VS will be more complex than I initially thought. @tommcdon and I built changes making a lot of assumptions about the location of DispatchContinuations. We should fix whatever is broken by insertion of InstrumentedDispatchContinuations.

@rcj1 can we go ahead and merge this and you pick up the needed VS changes later or do we need to hold off on this PR until you verified/fixed VS? It would be nice to get this PR merged since its blocking other PR's from following at the moment.

This was referenced Apr 14, 2026
CopilotAI review requested due to automatic review settings April 14, 2026 18:51
@lateralusX
lateralusXforce-pushed the lateralusX/runtime-async-instrumentation branch from 01867b8 to 75f5f5eCompareApril 14, 2026 18:51

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 6 out of 6 changed files in this pull request and generated 1 comment.

@lateralusX

lateralusX commented Apr 14, 2026

Copy link
Copy Markdown
MemberAuthor

All comments resolved, dropped linker tests from PR after validating that all instrumentation code gets linked out in IL trimmer and Native AOT scenarios. Good to go once CI pass?

@lateralusX

Copy link
Copy Markdown
MemberAuthor

/ba-g recently fixed known issue #126922

@lateralusX

Copy link
Copy Markdown
MemberAuthor

@rcj1, @jkotas, @jakobbotsch, @noahfalk, if there are no more objections, I will go ahead and merge this PR.

@lateralusX
lateralusX merged commit 462878e into dotnet:mainApr 16, 2026
173 of 176 checks passed
tommcdon added a commit to tommcdon/runtime that referenced this pull request Apr 21, 2026
…rDebug
When the debugger sets Task.s_asyncDebuggingEnabled directly via
ICorDebug (SetManagedTaskEtwEventsEnabled), the AsyncInstrumentation
flags system added by PR dotnet#126091 was not aware of this. The new flags
only got set via ETW OnEventCommand, so non-ETW RuntimeAsync configs
had s_asyncDebuggerActiveFlags stuck at Disabled, preventing
NotifyDebuggerOfRuntimeAsyncState from being called.
Fix: In InitializeFlags(), check Task.s_asyncDebuggingEnabled as a
fallback when s_asyncDebuggerActiveFlags is still Disabled. This
ensures the debugger's request is honored regardless of whether ETW
events have been enabled.
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
@github-actionsgithub-actionsBot locked and limited conversation to collaborators May 17, 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.

7 participants

@lateralusX@rcj1@jakobbotsch@noahfalk@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" + ' Add low-cost instrumented version of RuntimeAsyncTask::DispatchContinuations reclaiming lost performance. by lateralusX · Pull Request #126091 · dotnet/runtime · GitHub
Skip to content

Add low-cost instrumented version of RuntimeAsyncTask::DispatchContinuations reclaiming lost performance. - #126091

Merged
lateralusX merged 23 commits into
dotnet:mainfrom
lateralusX:lateralusX/runtime-async-instrumentation
Apr 16, 2026
Merged

Add low-cost instrumented version of RuntimeAsyncTask::DispatchContinuations reclaiming lost performance.#126091
lateralusX merged 23 commits into
dotnet:mainfrom
lateralusX:lateralusX/runtime-async-instrumentation

Conversation

@lateralusX

@lateralusXlateralusX commented Mar 25, 2026

Copy link
Copy Markdown
Member

#123727 introduced a regression of ~7% adding additional instrumentation for Debugger/TPL into RuntimeAsyncTask::DispatchContinuations. Going forward, there will be even more instrumentation needed when implementing async profiling and that would increase the overhead even more, so we need a way to isolate the instrumented vs none instrumented version of this method and regain some of the lost performance.

This PR duplicates DispatchContinuations into two methods, regular and instrumented. Previous version of PR used a generic value type specialization, but it was decided that the simplification not using generic value type specialization is worth the duplication of the function.

To be able to "upgrade" from none instrumented to instrumented version of DispatchContinuations, there are checks at method entry and after each completed continuation detecting if method should switch to instrumented version. Both checks are small and fast just a load of a flag in a static variable and checked if it's not 0.

This change will make sure the RuntimeAsyncTask::DispatchContinuations is protected from future performance regressions when more instrumentation gets added into the instrumented version.

Running the same benchmark, #123727 (comment) now shows the following numbers on old vs new implementation:

MetricOldNewDiff
Total bytes (S.P.C)16 740 KB16 756 KB+16 KB
JIT Size (RuntimeAsyncTask::DispatchContinuations)1778 B1399 B-379 B
Benchmark337ms362ms-25ms (~ -7%)

Measurements done on Windows x64.

S.P.C is 16 KB larger with this PR, due to duplicated DispatchContinuation method.

JIT Size is 379 bytes smaller on none instrumented version of RuntimeAsyncTask::DispatchContinuations, all previous instrumentation has been moved to instrumented version, completely eliminated in default method.

Benchmark shows that this PR recover most of the performance previously lost in #123727.

Code paths triggering the use of instrumented version, InstrumentedDispatchContinuations are protected by a IsSupported flag, so can be trimmed away, removing references to InstrumentedDispatchContinuations.

Most changes in this PR are around extracting out existing instrumentation into the instrumentation implementation. PR also optimize some of the debugger instrumentations previously implemented reducing locking in scenarios where continuation chains are handled.

PR adds a number of new tests validating that the current debugger and TPL instrumentation is still working.

PR also adds preparation for async profiler instrumentation in the AsyncInstrumentation type. This type will be used by more scenarios going forward.

@dotnet-policy-service

Copy link
Copy Markdown
Contributor

Tagging subscribers to this area: @dotnet/area-system-runtime
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

This PR refactors runtime-async continuation dispatch to support a low-overhead “uninstrumented” fast path while still enabling Debugger/TPL (and future profiler) instrumentation via a separate, JIT-specialized codegen path, with updated flag plumbing and added tests to validate behavior and cleanup.

Changes:

  • Introduces a generic, instrumentation-specialized DispatchContinuations<TRuntimeAsyncTaskInstrumentation>() path and centralized runtime-async instrumentation flag management.
  • Refactors Task’s runtime-async timestamp bookkeeping APIs to better support continuation chains and exception/unwind cleanup.
  • Adds/expands RuntimeAsync tests for timestamp cleanup, debugger detach behavior, continuation timestamp visibility, and TPL EventSource events; wires TPL EventSource enable/disable to update instrumentation flags.

Reviewed changes

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

FileDescription
src/libraries/System.Runtime/tests/System.Threading.Tasks.Tests/System.Runtime.CompilerServices/RuntimeAsyncTests.csAdds coverage for runtime-async instrumentation behavior (timestamps, detach, unwind/cancel, and TPL events).
src/libraries/System.Private.CoreLib/src/System/Threading/Tasks/TplEventSource.csUpdates runtime-async instrumentation flags when TPL EventSource commands change enabled keywords.
src/libraries/System.Private.CoreLib/src/System/Threading/Tasks/Task.csRefactors runtime-async timestamp dictionaries and adds helpers for chain timestamp propagation and cleanup.
src/coreclr/System.Private.CoreLib/src/System/Runtime/CompilerServices/AsyncHelpers.CoreCLR.csAdds the generic instrumentation abstraction and splits dispatch/finalize into specialized uninstrumented vs instrumented implementations.

Comment threadsrc/libraries/System.Private.CoreLib/src/System/Threading/Tasks/Task.cs Outdated
CopilotAI review requested due to automatic review settings March 25, 2026 14:53

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 4 out of 4 changed files in this pull request and generated 3 comments.

Comment threadsrc/libraries/System.Private.CoreLib/src/System/Threading/Tasks/Task.cs Outdated
CopilotAI review requested due to automatic review settings March 25, 2026 15:15

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 4 out of 4 changed files in this pull request and generated 2 comments.

@lateralusX

Copy link
Copy Markdown
MemberAuthor

Most failures are due to test _reflection::Async2Reflection.FromStack that currently asserts that DispatchContinuations is on stack and is currently not aware of the change to a generic method. If we stick with the generic method, then the assert in this test needs to be updated to reflect the name change.

@rcj1

rcj1 commented Mar 27, 2026

Copy link
Copy Markdown
Contributor

s_asyncDebuggingEnabled implies that these TPL events have been enabled, with two exceptions:

  1. Failure to create ETW session (too many existing ETW sessions, etc)
  2. Manual shutdown of VS-created ETW session or other exceptional error that causes ETW session to close

We can assume that s_asyncDebuggingEnabled implies required TPL events enabled, and save the setting and checking of these flags. The downside is a tiny amount of overhead spent checking whether TPL events are enabled in these two exceptional cases during debugging - in my opinion, an acceptable tradeoff for simplification and any possible optimization of the hot path.

If someone believes that scenario 1 is not very uncommon, it is possible to change Concord such that they will only be out of sync in case 2.

@lateralusX

lateralusX commented Mar 30, 2026

Copy link
Copy Markdown
MemberAuthor

s_asyncDebuggingEnabled implies that these TPL events have been enabled, with two exceptions:

  1. Failure to create ETW session (too many existing ETW sessions, etc)
  2. Manual shutdown of VS-created ETW session or other exceptional error that causes ETW session to close

We can assume that s_asyncDebuggingEnabled implies required TPL events enabled, and save the setting and checking of these flags. The downside is a tiny amount of overhead spent checking whether TPL events are enabled in these two exceptional cases during debugging - in my opinion, an acceptable tradeoff for simplification and any possible optimization of the hot path.

If someone believes that scenario 1 is not very uncommon, it is possible to change Concord such that they will only be out of sync in case 2.

Question is if pure TPL will have meaning here, like adding TPL events for runtime async activities in case a client only consumes TPL events to recreate async callstacks, or if that was never the intent with this change and it should only support the debugger features, then we should definitely put all logic under the same flag.

If we could assume the feature will trigger the TPL event source, we could handle it all in the on command event onTPL event source, and we can do the fast check only on flags, maybe that was what you proposed @rcj1? In that case this feature won't light up without the TPL session enabled, but that might be, OK? It would speed up the instrumentation gate check since it will only look at the flags again and ignore s_asyncDebuggingEnabled as part of dispatch loop.

@jakobbotsch

Copy link
Copy Markdown
Member

I took this path to get away from duplicating the complete DispatchContinuations method into a regular and instrumented version, reducing the maintenance of ~100 lines of duplicated high performing unsafe code. It can be argued that duplicating the DispatchContinuations is a small price to pay giving a little clearer implementation. If that is something we all agree on and accept, I'm happy to pursue that path as well, but wanted to start with the zero cost abstraction, no duplication path.

I worry that all the AggressiveInlining static abstracts introduce non-trivial startup cost when it comes to compiling all the dispatchers. It should be possible to measure this via some polymorphically recursive async methods that creates a lot of rarely used instantiations.

The other thing with the current approach -- the inversion of control makes the dispatcher quite hard to follow for me. But I can live with this given the improved performance.

@jakobbotsch

Copy link
Copy Markdown
Member

I worry that all the AggressiveInlining static abstracts introduce non-trivial startup cost when it comes to compiling all the dispatchers. It should be possible to measure this via some polymorphically recursive async methods that creates a lot of rarely used instantiations.

Doesn't look too bad, I tried a binary counter micro benchmark that loads 4096 of the dispatchers:

usingSystem;usingSystem.Diagnostics;usingSystem.Threading.Tasks;namespaceAsyncMicro;publicclassProgram{staticvoidMain(){Stopwatchtimer=Stopwatch.StartNew();newProgram().Recurse<byte,byte,byte,byte,byte,byte,byte,byte,byte,byte,byte,byte>(0).GetAwaiter().GetResult();Console.WriteLine("Took {0} ms",timer.ElapsedMilliseconds);}privateasyncTask<V<T11,T10,T9,T8,T7,T6,T5,T4,T3,T2,T1,T0>>Recurse<T11,T10,T9,T8,T7,T6,T5,T4,T3,T2,T1,T0>(intn){awaitTask.Yield();//Console.WriteLine(n);Taskt;if(typeof(T0)==typeof(byte)){t=Recurse<T11,T10,T9,T8,T7,T6,T5,T4,T3,T2,T1,int>(n+1);}elseif(typeof(T1)==typeof(byte)){t=Recurse<T11,T10,T9,T8,T7,T6,T5,T4,T3,T2,int,byte>(n+1);}elseif(typeof(T2)==typeof(byte)){t=Recurse<T11,T10,T9,T8,T7,T6,T5,T4,T3,int,byte,byte>(n+1);}elseif(typeof(T3)==typeof(byte)){t=Recurse<T11,T10,T9,T8,T7,T6,T5,T4,int,byte,byte,byte>(n+1);}elseif(typeof(T4)==typeof(byte)){t=Recurse<T11,T10,T9,T8,T7,T6,T5,int,byte,byte,byte,byte>(n+1);}elseif(typeof(T5)==typeof(byte)){t=Recurse<T11,T10,T9,T8,T7,T6,int,byte,byte,byte,byte,byte>(n+1);}elseif(typeof(T6)==typeof(byte)){t=Recurse<T11,T10,T9,T8,T7,int,byte,byte,byte,byte,byte,byte>(n+1);}elseif(typeof(T7)==typeof(byte)){t=Recurse<T11,T10,T9,T8,int,byte,byte,byte,byte,byte,byte,byte>(n+1);}elseif(typeof(T8)==typeof(byte)){t=Recurse<T11,T10,T9,int,byte,byte,byte,byte,byte,byte,byte,byte>(n+1);}elseif(typeof(T9)==typeof(byte)){t=Recurse<T11,T10,int,byte,byte,byte,byte,byte,byte,byte,byte,byte>(n+1);}elseif(typeof(T10)==typeof(byte)){t=Recurse<T11,int,byte,byte,byte,byte,byte,byte,byte,byte,byte,byte>(n+1);}elseif(typeof(T11)==typeof(byte)){t=Recurse<int,byte,byte,byte,byte,byte,byte,byte,byte,byte,byte,byte>(n+1);}else{returndefault;}awaitt;returndefault;}privatestructV<T11,T10,T9,T8,T7,T6,T5,T4,T3,T2,T1,T0>{publicintX;}}

The difference is only around 100 ms on this PR.
Base: Took 9205 ms
Diff: Took 9303 ms

(P.S. a simpler binary recursing generic hits some exponential behavior... Need to investigate that.)

@lateralusX

Copy link
Copy Markdown
MemberAuthor

The other thing with the current approach -- the inversion of control makes the dispatcher quite hard to follow for me. But I can live with this given the improved performance.

The alternative is to duplicate the DispatchContinuation implementation into two different methods, one default (with upgrade capabilities) and one instrumented version. In the end it depends on whats important, this PR currently take the reduce code duplication path, with slightly more complex dispatcher implementation (due to the instrumentation callbacks), and I think that is fine. Dropping the R2R exclusion reducing one indirect call on the instrumented path will simplify the instrumented path a little more as well, with very small size increase of S.P.C.

CopilotAI review requested due to automatic review settings April 10, 2026 08:16
@lateralusX
lateralusXforce-pushed the lateralusX/runtime-async-instrumentation branch from 069b76c to 75f5f5eCompareApril 10, 2026 08:16

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 6 out of 6 changed files in this pull request and generated no new comments.

@rcj1

rcj1 commented Apr 13, 2026

Copy link
Copy Markdown
Contributor

After testing the changes locally, I realize propagating these changes through to VS will be more complex than I initially thought. @tommcdon and I built changes making a lot of assumptions about the location of DispatchContinuations. We should fix whatever is broken by insertion of InstrumentedDispatchContinuations.

@lateralusX

lateralusX commented Apr 14, 2026

Copy link
Copy Markdown
MemberAuthor

After testing the changes locally, I realize propagating these changes through to VS will be more complex than I initially thought. @tommcdon and I built changes making a lot of assumptions about the location of DispatchContinuations. We should fix whatever is broken by insertion of InstrumentedDispatchContinuations.

@rcj1 can we go ahead and merge this and you pick up the needed VS changes later or do we need to hold off on this PR until you verified/fixed VS? It would be nice to get this PR merged since its blocking other PR's from following at the moment.

This was referenced Apr 14, 2026
CopilotAI review requested due to automatic review settings April 14, 2026 18:51
@lateralusX
lateralusXforce-pushed the lateralusX/runtime-async-instrumentation branch from 01867b8 to 75f5f5eCompareApril 14, 2026 18:51

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 6 out of 6 changed files in this pull request and generated 1 comment.

@lateralusX

lateralusX commented Apr 14, 2026

Copy link
Copy Markdown
MemberAuthor

All comments resolved, dropped linker tests from PR after validating that all instrumentation code gets linked out in IL trimmer and Native AOT scenarios. Good to go once CI pass?

@lateralusX

Copy link
Copy Markdown
MemberAuthor

/ba-g recently fixed known issue #126922

@lateralusX

Copy link
Copy Markdown
MemberAuthor

@rcj1, @jkotas, @jakobbotsch, @noahfalk, if there are no more objections, I will go ahead and merge this PR.

@lateralusX
lateralusX merged commit 462878e into dotnet:mainApr 16, 2026
173 of 176 checks passed
tommcdon added a commit to tommcdon/runtime that referenced this pull request Apr 21, 2026
…rDebug
When the debugger sets Task.s_asyncDebuggingEnabled directly via
ICorDebug (SetManagedTaskEtwEventsEnabled), the AsyncInstrumentation
flags system added by PR dotnet#126091 was not aware of this. The new flags
only got set via ETW OnEventCommand, so non-ETW RuntimeAsync configs
had s_asyncDebuggerActiveFlags stuck at Disabled, preventing
NotifyDebuggerOfRuntimeAsyncState from being called.
Fix: In InitializeFlags(), check Task.s_asyncDebuggingEnabled as a
fallback when s_asyncDebuggerActiveFlags is still Disabled. This
ensures the debugger's request is honored regardless of whether ETW
events have been enabled.
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
@github-actionsgithub-actionsBot locked and limited conversation to collaborators May 17, 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.

7 participants

@lateralusX@rcj1@jakobbotsch@noahfalk@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('^' + ".*" + ' Add low-cost instrumented version of RuntimeAsyncTask::DispatchContinuations reclaiming lost performance. by lateralusX · Pull Request #126091 · dotnet/runtime · GitHub
Skip to content

Add low-cost instrumented version of RuntimeAsyncTask::DispatchContinuations reclaiming lost performance. - #126091

Merged
lateralusX merged 23 commits into
dotnet:mainfrom
lateralusX:lateralusX/runtime-async-instrumentation
Apr 16, 2026
Merged

Add low-cost instrumented version of RuntimeAsyncTask::DispatchContinuations reclaiming lost performance.#126091
lateralusX merged 23 commits into
dotnet:mainfrom
lateralusX:lateralusX/runtime-async-instrumentation

Conversation

@lateralusX

@lateralusXlateralusX commented Mar 25, 2026

Copy link
Copy Markdown
Member

#123727 introduced a regression of ~7% adding additional instrumentation for Debugger/TPL into RuntimeAsyncTask::DispatchContinuations. Going forward, there will be even more instrumentation needed when implementing async profiling and that would increase the overhead even more, so we need a way to isolate the instrumented vs none instrumented version of this method and regain some of the lost performance.

This PR duplicates DispatchContinuations into two methods, regular and instrumented. Previous version of PR used a generic value type specialization, but it was decided that the simplification not using generic value type specialization is worth the duplication of the function.

To be able to "upgrade" from none instrumented to instrumented version of DispatchContinuations, there are checks at method entry and after each completed continuation detecting if method should switch to instrumented version. Both checks are small and fast just a load of a flag in a static variable and checked if it's not 0.

This change will make sure the RuntimeAsyncTask::DispatchContinuations is protected from future performance regressions when more instrumentation gets added into the instrumented version.

Running the same benchmark, #123727 (comment) now shows the following numbers on old vs new implementation:

MetricOldNewDiff
Total bytes (S.P.C)16 740 KB16 756 KB+16 KB
JIT Size (RuntimeAsyncTask::DispatchContinuations)1778 B1399 B-379 B
Benchmark337ms362ms-25ms (~ -7%)

Measurements done on Windows x64.

S.P.C is 16 KB larger with this PR, due to duplicated DispatchContinuation method.

JIT Size is 379 bytes smaller on none instrumented version of RuntimeAsyncTask::DispatchContinuations, all previous instrumentation has been moved to instrumented version, completely eliminated in default method.

Benchmark shows that this PR recover most of the performance previously lost in #123727.

Code paths triggering the use of instrumented version, InstrumentedDispatchContinuations are protected by a IsSupported flag, so can be trimmed away, removing references to InstrumentedDispatchContinuations.

Most changes in this PR are around extracting out existing instrumentation into the instrumentation implementation. PR also optimize some of the debugger instrumentations previously implemented reducing locking in scenarios where continuation chains are handled.

PR adds a number of new tests validating that the current debugger and TPL instrumentation is still working.

PR also adds preparation for async profiler instrumentation in the AsyncInstrumentation type. This type will be used by more scenarios going forward.

@dotnet-policy-service

Copy link
Copy Markdown
Contributor

Tagging subscribers to this area: @dotnet/area-system-runtime
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

This PR refactors runtime-async continuation dispatch to support a low-overhead “uninstrumented” fast path while still enabling Debugger/TPL (and future profiler) instrumentation via a separate, JIT-specialized codegen path, with updated flag plumbing and added tests to validate behavior and cleanup.

Changes:

  • Introduces a generic, instrumentation-specialized DispatchContinuations<TRuntimeAsyncTaskInstrumentation>() path and centralized runtime-async instrumentation flag management.
  • Refactors Task’s runtime-async timestamp bookkeeping APIs to better support continuation chains and exception/unwind cleanup.
  • Adds/expands RuntimeAsync tests for timestamp cleanup, debugger detach behavior, continuation timestamp visibility, and TPL EventSource events; wires TPL EventSource enable/disable to update instrumentation flags.

Reviewed changes

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

FileDescription
src/libraries/System.Runtime/tests/System.Threading.Tasks.Tests/System.Runtime.CompilerServices/RuntimeAsyncTests.csAdds coverage for runtime-async instrumentation behavior (timestamps, detach, unwind/cancel, and TPL events).
src/libraries/System.Private.CoreLib/src/System/Threading/Tasks/TplEventSource.csUpdates runtime-async instrumentation flags when TPL EventSource commands change enabled keywords.
src/libraries/System.Private.CoreLib/src/System/Threading/Tasks/Task.csRefactors runtime-async timestamp dictionaries and adds helpers for chain timestamp propagation and cleanup.
src/coreclr/System.Private.CoreLib/src/System/Runtime/CompilerServices/AsyncHelpers.CoreCLR.csAdds the generic instrumentation abstraction and splits dispatch/finalize into specialized uninstrumented vs instrumented implementations.

Comment threadsrc/libraries/System.Private.CoreLib/src/System/Threading/Tasks/Task.cs Outdated
CopilotAI review requested due to automatic review settings March 25, 2026 14:53

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 4 out of 4 changed files in this pull request and generated 3 comments.

Comment threadsrc/libraries/System.Private.CoreLib/src/System/Threading/Tasks/Task.cs Outdated
CopilotAI review requested due to automatic review settings March 25, 2026 15:15

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 4 out of 4 changed files in this pull request and generated 2 comments.

@lateralusX

Copy link
Copy Markdown
MemberAuthor

Most failures are due to test _reflection::Async2Reflection.FromStack that currently asserts that DispatchContinuations is on stack and is currently not aware of the change to a generic method. If we stick with the generic method, then the assert in this test needs to be updated to reflect the name change.

@rcj1

rcj1 commented Mar 27, 2026

Copy link
Copy Markdown
Contributor

s_asyncDebuggingEnabled implies that these TPL events have been enabled, with two exceptions:

  1. Failure to create ETW session (too many existing ETW sessions, etc)
  2. Manual shutdown of VS-created ETW session or other exceptional error that causes ETW session to close

We can assume that s_asyncDebuggingEnabled implies required TPL events enabled, and save the setting and checking of these flags. The downside is a tiny amount of overhead spent checking whether TPL events are enabled in these two exceptional cases during debugging - in my opinion, an acceptable tradeoff for simplification and any possible optimization of the hot path.

If someone believes that scenario 1 is not very uncommon, it is possible to change Concord such that they will only be out of sync in case 2.

@lateralusX

lateralusX commented Mar 30, 2026

Copy link
Copy Markdown
MemberAuthor

s_asyncDebuggingEnabled implies that these TPL events have been enabled, with two exceptions:

  1. Failure to create ETW session (too many existing ETW sessions, etc)
  2. Manual shutdown of VS-created ETW session or other exceptional error that causes ETW session to close

We can assume that s_asyncDebuggingEnabled implies required TPL events enabled, and save the setting and checking of these flags. The downside is a tiny amount of overhead spent checking whether TPL events are enabled in these two exceptional cases during debugging - in my opinion, an acceptable tradeoff for simplification and any possible optimization of the hot path.

If someone believes that scenario 1 is not very uncommon, it is possible to change Concord such that they will only be out of sync in case 2.

Question is if pure TPL will have meaning here, like adding TPL events for runtime async activities in case a client only consumes TPL events to recreate async callstacks, or if that was never the intent with this change and it should only support the debugger features, then we should definitely put all logic under the same flag.

If we could assume the feature will trigger the TPL event source, we could handle it all in the on command event onTPL event source, and we can do the fast check only on flags, maybe that was what you proposed @rcj1? In that case this feature won't light up without the TPL session enabled, but that might be, OK? It would speed up the instrumentation gate check since it will only look at the flags again and ignore s_asyncDebuggingEnabled as part of dispatch loop.

@jakobbotsch

Copy link
Copy Markdown
Member

I took this path to get away from duplicating the complete DispatchContinuations method into a regular and instrumented version, reducing the maintenance of ~100 lines of duplicated high performing unsafe code. It can be argued that duplicating the DispatchContinuations is a small price to pay giving a little clearer implementation. If that is something we all agree on and accept, I'm happy to pursue that path as well, but wanted to start with the zero cost abstraction, no duplication path.

I worry that all the AggressiveInlining static abstracts introduce non-trivial startup cost when it comes to compiling all the dispatchers. It should be possible to measure this via some polymorphically recursive async methods that creates a lot of rarely used instantiations.

The other thing with the current approach -- the inversion of control makes the dispatcher quite hard to follow for me. But I can live with this given the improved performance.

@jakobbotsch

Copy link
Copy Markdown
Member

I worry that all the AggressiveInlining static abstracts introduce non-trivial startup cost when it comes to compiling all the dispatchers. It should be possible to measure this via some polymorphically recursive async methods that creates a lot of rarely used instantiations.

Doesn't look too bad, I tried a binary counter micro benchmark that loads 4096 of the dispatchers:

usingSystem;usingSystem.Diagnostics;usingSystem.Threading.Tasks;namespaceAsyncMicro;publicclassProgram{staticvoidMain(){Stopwatchtimer=Stopwatch.StartNew();newProgram().Recurse<byte,byte,byte,byte,byte,byte,byte,byte,byte,byte,byte,byte>(0).GetAwaiter().GetResult();Console.WriteLine("Took {0} ms",timer.ElapsedMilliseconds);}privateasyncTask<V<T11,T10,T9,T8,T7,T6,T5,T4,T3,T2,T1,T0>>Recurse<T11,T10,T9,T8,T7,T6,T5,T4,T3,T2,T1,T0>(intn){awaitTask.Yield();//Console.WriteLine(n);Taskt;if(typeof(T0)==typeof(byte)){t=Recurse<T11,T10,T9,T8,T7,T6,T5,T4,T3,T2,T1,int>(n+1);}elseif(typeof(T1)==typeof(byte)){t=Recurse<T11,T10,T9,T8,T7,T6,T5,T4,T3,T2,int,byte>(n+1);}elseif(typeof(T2)==typeof(byte)){t=Recurse<T11,T10,T9,T8,T7,T6,T5,T4,T3,int,byte,byte>(n+1);}elseif(typeof(T3)==typeof(byte)){t=Recurse<T11,T10,T9,T8,T7,T6,T5,T4,int,byte,byte,byte>(n+1);}elseif(typeof(T4)==typeof(byte)){t=Recurse<T11,T10,T9,T8,T7,T6,T5,int,byte,byte,byte,byte>(n+1);}elseif(typeof(T5)==typeof(byte)){t=Recurse<T11,T10,T9,T8,T7,T6,int,byte,byte,byte,byte,byte>(n+1);}elseif(typeof(T6)==typeof(byte)){t=Recurse<T11,T10,T9,T8,T7,int,byte,byte,byte,byte,byte,byte>(n+1);}elseif(typeof(T7)==typeof(byte)){t=Recurse<T11,T10,T9,T8,int,byte,byte,byte,byte,byte,byte,byte>(n+1);}elseif(typeof(T8)==typeof(byte)){t=Recurse<T11,T10,T9,int,byte,byte,byte,byte,byte,byte,byte,byte>(n+1);}elseif(typeof(T9)==typeof(byte)){t=Recurse<T11,T10,int,byte,byte,byte,byte,byte,byte,byte,byte,byte>(n+1);}elseif(typeof(T10)==typeof(byte)){t=Recurse<T11,int,byte,byte,byte,byte,byte,byte,byte,byte,byte,byte>(n+1);}elseif(typeof(T11)==typeof(byte)){t=Recurse<int,byte,byte,byte,byte,byte,byte,byte,byte,byte,byte,byte>(n+1);}else{returndefault;}awaitt;returndefault;}privatestructV<T11,T10,T9,T8,T7,T6,T5,T4,T3,T2,T1,T0>{publicintX;}}

The difference is only around 100 ms on this PR.
Base: Took 9205 ms
Diff: Took 9303 ms

(P.S. a simpler binary recursing generic hits some exponential behavior... Need to investigate that.)

@lateralusX

Copy link
Copy Markdown
MemberAuthor

The other thing with the current approach -- the inversion of control makes the dispatcher quite hard to follow for me. But I can live with this given the improved performance.

The alternative is to duplicate the DispatchContinuation implementation into two different methods, one default (with upgrade capabilities) and one instrumented version. In the end it depends on whats important, this PR currently take the reduce code duplication path, with slightly more complex dispatcher implementation (due to the instrumentation callbacks), and I think that is fine. Dropping the R2R exclusion reducing one indirect call on the instrumented path will simplify the instrumented path a little more as well, with very small size increase of S.P.C.

CopilotAI review requested due to automatic review settings April 10, 2026 08:16
@lateralusX
lateralusXforce-pushed the lateralusX/runtime-async-instrumentation branch from 069b76c to 75f5f5eCompareApril 10, 2026 08:16

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 6 out of 6 changed files in this pull request and generated no new comments.

@rcj1

rcj1 commented Apr 13, 2026

Copy link
Copy Markdown
Contributor

After testing the changes locally, I realize propagating these changes through to VS will be more complex than I initially thought. @tommcdon and I built changes making a lot of assumptions about the location of DispatchContinuations. We should fix whatever is broken by insertion of InstrumentedDispatchContinuations.

@lateralusX

lateralusX commented Apr 14, 2026

Copy link
Copy Markdown
MemberAuthor

After testing the changes locally, I realize propagating these changes through to VS will be more complex than I initially thought. @tommcdon and I built changes making a lot of assumptions about the location of DispatchContinuations. We should fix whatever is broken by insertion of InstrumentedDispatchContinuations.

@rcj1 can we go ahead and merge this and you pick up the needed VS changes later or do we need to hold off on this PR until you verified/fixed VS? It would be nice to get this PR merged since its blocking other PR's from following at the moment.

This was referenced Apr 14, 2026
CopilotAI review requested due to automatic review settings April 14, 2026 18:51
@lateralusX
lateralusXforce-pushed the lateralusX/runtime-async-instrumentation branch from 01867b8 to 75f5f5eCompareApril 14, 2026 18:51

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 6 out of 6 changed files in this pull request and generated 1 comment.

@lateralusX

lateralusX commented Apr 14, 2026

Copy link
Copy Markdown
MemberAuthor

All comments resolved, dropped linker tests from PR after validating that all instrumentation code gets linked out in IL trimmer and Native AOT scenarios. Good to go once CI pass?

@lateralusX

Copy link
Copy Markdown
MemberAuthor

/ba-g recently fixed known issue #126922

@lateralusX

Copy link
Copy Markdown
MemberAuthor

@rcj1, @jkotas, @jakobbotsch, @noahfalk, if there are no more objections, I will go ahead and merge this PR.

@lateralusX
lateralusX merged commit 462878e into dotnet:mainApr 16, 2026
173 of 176 checks passed
tommcdon added a commit to tommcdon/runtime that referenced this pull request Apr 21, 2026
…rDebug
When the debugger sets Task.s_asyncDebuggingEnabled directly via
ICorDebug (SetManagedTaskEtwEventsEnabled), the AsyncInstrumentation
flags system added by PR dotnet#126091 was not aware of this. The new flags
only got set via ETW OnEventCommand, so non-ETW RuntimeAsync configs
had s_asyncDebuggerActiveFlags stuck at Disabled, preventing
NotifyDebuggerOfRuntimeAsyncState from being called.
Fix: In InitializeFlags(), check Task.s_asyncDebuggingEnabled as a
fallback when s_asyncDebuggerActiveFlags is still Disabled. This
ensures the debugger's request is honored regardless of whether ETW
events have been enabled.
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
@github-actionsgithub-actionsBot locked and limited conversation to collaborators May 17, 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.

7 participants

@lateralusX@rcj1@jakobbotsch@noahfalk@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); } })(); (function(){ try { var __m = "*"; var __re = new RegExp('^' + ".*" + ' Add low-cost instrumented version of RuntimeAsyncTask::DispatchContinuations reclaiming lost performance. by lateralusX · Pull Request #126091 · dotnet/runtime · GitHub
Skip to content

Add low-cost instrumented version of RuntimeAsyncTask::DispatchContinuations reclaiming lost performance. - #126091

Merged
lateralusX merged 23 commits into
dotnet:mainfrom
lateralusX:lateralusX/runtime-async-instrumentation
Apr 16, 2026
Merged

Add low-cost instrumented version of RuntimeAsyncTask::DispatchContinuations reclaiming lost performance.#126091
lateralusX merged 23 commits into
dotnet:mainfrom
lateralusX:lateralusX/runtime-async-instrumentation

Conversation

@lateralusX

@lateralusXlateralusX commented Mar 25, 2026

Copy link
Copy Markdown
Member

#123727 introduced a regression of ~7% adding additional instrumentation for Debugger/TPL into RuntimeAsyncTask::DispatchContinuations. Going forward, there will be even more instrumentation needed when implementing async profiling and that would increase the overhead even more, so we need a way to isolate the instrumented vs none instrumented version of this method and regain some of the lost performance.

This PR duplicates DispatchContinuations into two methods, regular and instrumented. Previous version of PR used a generic value type specialization, but it was decided that the simplification not using generic value type specialization is worth the duplication of the function.

To be able to "upgrade" from none instrumented to instrumented version of DispatchContinuations, there are checks at method entry and after each completed continuation detecting if method should switch to instrumented version. Both checks are small and fast just a load of a flag in a static variable and checked if it's not 0.

This change will make sure the RuntimeAsyncTask::DispatchContinuations is protected from future performance regressions when more instrumentation gets added into the instrumented version.

Running the same benchmark, #123727 (comment) now shows the following numbers on old vs new implementation:

MetricOldNewDiff
Total bytes (S.P.C)16 740 KB16 756 KB+16 KB
JIT Size (RuntimeAsyncTask::DispatchContinuations)1778 B1399 B-379 B
Benchmark337ms362ms-25ms (~ -7%)

Measurements done on Windows x64.

S.P.C is 16 KB larger with this PR, due to duplicated DispatchContinuation method.

JIT Size is 379 bytes smaller on none instrumented version of RuntimeAsyncTask::DispatchContinuations, all previous instrumentation has been moved to instrumented version, completely eliminated in default method.

Benchmark shows that this PR recover most of the performance previously lost in #123727.

Code paths triggering the use of instrumented version, InstrumentedDispatchContinuations are protected by a IsSupported flag, so can be trimmed away, removing references to InstrumentedDispatchContinuations.

Most changes in this PR are around extracting out existing instrumentation into the instrumentation implementation. PR also optimize some of the debugger instrumentations previously implemented reducing locking in scenarios where continuation chains are handled.

PR adds a number of new tests validating that the current debugger and TPL instrumentation is still working.

PR also adds preparation for async profiler instrumentation in the AsyncInstrumentation type. This type will be used by more scenarios going forward.

@dotnet-policy-service

Copy link
Copy Markdown
Contributor

Tagging subscribers to this area: @dotnet/area-system-runtime
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

This PR refactors runtime-async continuation dispatch to support a low-overhead “uninstrumented” fast path while still enabling Debugger/TPL (and future profiler) instrumentation via a separate, JIT-specialized codegen path, with updated flag plumbing and added tests to validate behavior and cleanup.

Changes:

  • Introduces a generic, instrumentation-specialized DispatchContinuations<TRuntimeAsyncTaskInstrumentation>() path and centralized runtime-async instrumentation flag management.
  • Refactors Task’s runtime-async timestamp bookkeeping APIs to better support continuation chains and exception/unwind cleanup.
  • Adds/expands RuntimeAsync tests for timestamp cleanup, debugger detach behavior, continuation timestamp visibility, and TPL EventSource events; wires TPL EventSource enable/disable to update instrumentation flags.

Reviewed changes

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

FileDescription
src/libraries/System.Runtime/tests/System.Threading.Tasks.Tests/System.Runtime.CompilerServices/RuntimeAsyncTests.csAdds coverage for runtime-async instrumentation behavior (timestamps, detach, unwind/cancel, and TPL events).
src/libraries/System.Private.CoreLib/src/System/Threading/Tasks/TplEventSource.csUpdates runtime-async instrumentation flags when TPL EventSource commands change enabled keywords.
src/libraries/System.Private.CoreLib/src/System/Threading/Tasks/Task.csRefactors runtime-async timestamp dictionaries and adds helpers for chain timestamp propagation and cleanup.
src/coreclr/System.Private.CoreLib/src/System/Runtime/CompilerServices/AsyncHelpers.CoreCLR.csAdds the generic instrumentation abstraction and splits dispatch/finalize into specialized uninstrumented vs instrumented implementations.

Comment threadsrc/libraries/System.Private.CoreLib/src/System/Threading/Tasks/Task.cs Outdated
CopilotAI review requested due to automatic review settings March 25, 2026 14:53

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 4 out of 4 changed files in this pull request and generated 3 comments.

Comment threadsrc/libraries/System.Private.CoreLib/src/System/Threading/Tasks/Task.cs Outdated
CopilotAI review requested due to automatic review settings March 25, 2026 15:15

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 4 out of 4 changed files in this pull request and generated 2 comments.

@lateralusX

Copy link
Copy Markdown
MemberAuthor

Most failures are due to test _reflection::Async2Reflection.FromStack that currently asserts that DispatchContinuations is on stack and is currently not aware of the change to a generic method. If we stick with the generic method, then the assert in this test needs to be updated to reflect the name change.

@rcj1

rcj1 commented Mar 27, 2026

Copy link
Copy Markdown
Contributor

s_asyncDebuggingEnabled implies that these TPL events have been enabled, with two exceptions:

  1. Failure to create ETW session (too many existing ETW sessions, etc)
  2. Manual shutdown of VS-created ETW session or other exceptional error that causes ETW session to close

We can assume that s_asyncDebuggingEnabled implies required TPL events enabled, and save the setting and checking of these flags. The downside is a tiny amount of overhead spent checking whether TPL events are enabled in these two exceptional cases during debugging - in my opinion, an acceptable tradeoff for simplification and any possible optimization of the hot path.

If someone believes that scenario 1 is not very uncommon, it is possible to change Concord such that they will only be out of sync in case 2.

@lateralusX

lateralusX commented Mar 30, 2026

Copy link
Copy Markdown
MemberAuthor

s_asyncDebuggingEnabled implies that these TPL events have been enabled, with two exceptions:

  1. Failure to create ETW session (too many existing ETW sessions, etc)
  2. Manual shutdown of VS-created ETW session or other exceptional error that causes ETW session to close

We can assume that s_asyncDebuggingEnabled implies required TPL events enabled, and save the setting and checking of these flags. The downside is a tiny amount of overhead spent checking whether TPL events are enabled in these two exceptional cases during debugging - in my opinion, an acceptable tradeoff for simplification and any possible optimization of the hot path.

If someone believes that scenario 1 is not very uncommon, it is possible to change Concord such that they will only be out of sync in case 2.

Question is if pure TPL will have meaning here, like adding TPL events for runtime async activities in case a client only consumes TPL events to recreate async callstacks, or if that was never the intent with this change and it should only support the debugger features, then we should definitely put all logic under the same flag.

If we could assume the feature will trigger the TPL event source, we could handle it all in the on command event onTPL event source, and we can do the fast check only on flags, maybe that was what you proposed @rcj1? In that case this feature won't light up without the TPL session enabled, but that might be, OK? It would speed up the instrumentation gate check since it will only look at the flags again and ignore s_asyncDebuggingEnabled as part of dispatch loop.

@jakobbotsch

Copy link
Copy Markdown
Member

I took this path to get away from duplicating the complete DispatchContinuations method into a regular and instrumented version, reducing the maintenance of ~100 lines of duplicated high performing unsafe code. It can be argued that duplicating the DispatchContinuations is a small price to pay giving a little clearer implementation. If that is something we all agree on and accept, I'm happy to pursue that path as well, but wanted to start with the zero cost abstraction, no duplication path.

I worry that all the AggressiveInlining static abstracts introduce non-trivial startup cost when it comes to compiling all the dispatchers. It should be possible to measure this via some polymorphically recursive async methods that creates a lot of rarely used instantiations.

The other thing with the current approach -- the inversion of control makes the dispatcher quite hard to follow for me. But I can live with this given the improved performance.

@jakobbotsch

Copy link
Copy Markdown
Member

I worry that all the AggressiveInlining static abstracts introduce non-trivial startup cost when it comes to compiling all the dispatchers. It should be possible to measure this via some polymorphically recursive async methods that creates a lot of rarely used instantiations.

Doesn't look too bad, I tried a binary counter micro benchmark that loads 4096 of the dispatchers:

usingSystem;usingSystem.Diagnostics;usingSystem.Threading.Tasks;namespaceAsyncMicro;publicclassProgram{staticvoidMain(){Stopwatchtimer=Stopwatch.StartNew();newProgram().Recurse<byte,byte,byte,byte,byte,byte,byte,byte,byte,byte,byte,byte>(0).GetAwaiter().GetResult();Console.WriteLine("Took {0} ms",timer.ElapsedMilliseconds);}privateasyncTask<V<T11,T10,T9,T8,T7,T6,T5,T4,T3,T2,T1,T0>>Recurse<T11,T10,T9,T8,T7,T6,T5,T4,T3,T2,T1,T0>(intn){awaitTask.Yield();//Console.WriteLine(n);Taskt;if(typeof(T0)==typeof(byte)){t=Recurse<T11,T10,T9,T8,T7,T6,T5,T4,T3,T2,T1,int>(n+1);}elseif(typeof(T1)==typeof(byte)){t=Recurse<T11,T10,T9,T8,T7,T6,T5,T4,T3,T2,int,byte>(n+1);}elseif(typeof(T2)==typeof(byte)){t=Recurse<T11,T10,T9,T8,T7,T6,T5,T4,T3,int,byte,byte>(n+1);}elseif(typeof(T3)==typeof(byte)){t=Recurse<T11,T10,T9,T8,T7,T6,T5,T4,int,byte,byte,byte>(n+1);}elseif(typeof(T4)==typeof(byte)){t=Recurse<T11,T10,T9,T8,T7,T6,T5,int,byte,byte,byte,byte>(n+1);}elseif(typeof(T5)==typeof(byte)){t=Recurse<T11,T10,T9,T8,T7,T6,int,byte,byte,byte,byte,byte>(n+1);}elseif(typeof(T6)==typeof(byte)){t=Recurse<T11,T10,T9,T8,T7,int,byte,byte,byte,byte,byte,byte>(n+1);}elseif(typeof(T7)==typeof(byte)){t=Recurse<T11,T10,T9,T8,int,byte,byte,byte,byte,byte,byte,byte>(n+1);}elseif(typeof(T8)==typeof(byte)){t=Recurse<T11,T10,T9,int,byte,byte,byte,byte,byte,byte,byte,byte>(n+1);}elseif(typeof(T9)==typeof(byte)){t=Recurse<T11,T10,int,byte,byte,byte,byte,byte,byte,byte,byte,byte>(n+1);}elseif(typeof(T10)==typeof(byte)){t=Recurse<T11,int,byte,byte,byte,byte,byte,byte,byte,byte,byte,byte>(n+1);}elseif(typeof(T11)==typeof(byte)){t=Recurse<int,byte,byte,byte,byte,byte,byte,byte,byte,byte,byte,byte>(n+1);}else{returndefault;}awaitt;returndefault;}privatestructV<T11,T10,T9,T8,T7,T6,T5,T4,T3,T2,T1,T0>{publicintX;}}

The difference is only around 100 ms on this PR.
Base: Took 9205 ms
Diff: Took 9303 ms

(P.S. a simpler binary recursing generic hits some exponential behavior... Need to investigate that.)

@lateralusX

Copy link
Copy Markdown
MemberAuthor

The other thing with the current approach -- the inversion of control makes the dispatcher quite hard to follow for me. But I can live with this given the improved performance.

The alternative is to duplicate the DispatchContinuation implementation into two different methods, one default (with upgrade capabilities) and one instrumented version. In the end it depends on whats important, this PR currently take the reduce code duplication path, with slightly more complex dispatcher implementation (due to the instrumentation callbacks), and I think that is fine. Dropping the R2R exclusion reducing one indirect call on the instrumented path will simplify the instrumented path a little more as well, with very small size increase of S.P.C.

CopilotAI review requested due to automatic review settings April 10, 2026 08:16
@lateralusX
lateralusXforce-pushed the lateralusX/runtime-async-instrumentation branch from 069b76c to 75f5f5eCompareApril 10, 2026 08:16

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 6 out of 6 changed files in this pull request and generated no new comments.

@rcj1

rcj1 commented Apr 13, 2026

Copy link
Copy Markdown
Contributor

After testing the changes locally, I realize propagating these changes through to VS will be more complex than I initially thought. @tommcdon and I built changes making a lot of assumptions about the location of DispatchContinuations. We should fix whatever is broken by insertion of InstrumentedDispatchContinuations.

@lateralusX

lateralusX commented Apr 14, 2026

Copy link
Copy Markdown
MemberAuthor

After testing the changes locally, I realize propagating these changes through to VS will be more complex than I initially thought. @tommcdon and I built changes making a lot of assumptions about the location of DispatchContinuations. We should fix whatever is broken by insertion of InstrumentedDispatchContinuations.

@rcj1 can we go ahead and merge this and you pick up the needed VS changes later or do we need to hold off on this PR until you verified/fixed VS? It would be nice to get this PR merged since its blocking other PR's from following at the moment.

This was referenced Apr 14, 2026
CopilotAI review requested due to automatic review settings April 14, 2026 18:51
@lateralusX
lateralusXforce-pushed the lateralusX/runtime-async-instrumentation branch from 01867b8 to 75f5f5eCompareApril 14, 2026 18:51

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 6 out of 6 changed files in this pull request and generated 1 comment.

@lateralusX

lateralusX commented Apr 14, 2026

Copy link
Copy Markdown
MemberAuthor

All comments resolved, dropped linker tests from PR after validating that all instrumentation code gets linked out in IL trimmer and Native AOT scenarios. Good to go once CI pass?

@lateralusX

Copy link
Copy Markdown
MemberAuthor

/ba-g recently fixed known issue #126922

@lateralusX

Copy link
Copy Markdown
MemberAuthor

@rcj1, @jkotas, @jakobbotsch, @noahfalk, if there are no more objections, I will go ahead and merge this PR.

@lateralusX
lateralusX merged commit 462878e into dotnet:mainApr 16, 2026
173 of 176 checks passed
tommcdon added a commit to tommcdon/runtime that referenced this pull request Apr 21, 2026
…rDebug
When the debugger sets Task.s_asyncDebuggingEnabled directly via
ICorDebug (SetManagedTaskEtwEventsEnabled), the AsyncInstrumentation
flags system added by PR dotnet#126091 was not aware of this. The new flags
only got set via ETW OnEventCommand, so non-ETW RuntimeAsync configs
had s_asyncDebuggerActiveFlags stuck at Disabled, preventing
NotifyDebuggerOfRuntimeAsyncState from being called.
Fix: In InitializeFlags(), check Task.s_asyncDebuggingEnabled as a
fallback when s_asyncDebuggerActiveFlags is still Disabled. This
ensures the debugger's request is honored regardless of whether ETW
events have been enabled.
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
@github-actionsgithub-actionsBot locked and limited conversation to collaborators May 17, 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.

7 participants

@lateralusX@rcj1@jakobbotsch@noahfalk@jkotas@MichalStrehovsky
, '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); } })(); })(); Add low-cost instrumented version of RuntimeAsyncTask::DispatchContinuations reclaiming lost performance. by lateralusX · Pull Request #126091 · dotnet/runtime · GitHub
Skip to content

Add low-cost instrumented version of RuntimeAsyncTask::DispatchContinuations reclaiming lost performance. - #126091

Merged
lateralusX merged 23 commits into
dotnet:mainfrom
lateralusX:lateralusX/runtime-async-instrumentation
Apr 16, 2026
Merged

Add low-cost instrumented version of RuntimeAsyncTask::DispatchContinuations reclaiming lost performance.#126091
lateralusX merged 23 commits into
dotnet:mainfrom
lateralusX:lateralusX/runtime-async-instrumentation

Conversation

@lateralusX

@lateralusXlateralusX commented Mar 25, 2026

Copy link
Copy Markdown
Member

#123727 introduced a regression of ~7% adding additional instrumentation for Debugger/TPL into RuntimeAsyncTask::DispatchContinuations. Going forward, there will be even more instrumentation needed when implementing async profiling and that would increase the overhead even more, so we need a way to isolate the instrumented vs none instrumented version of this method and regain some of the lost performance.

This PR duplicates DispatchContinuations into two methods, regular and instrumented. Previous version of PR used a generic value type specialization, but it was decided that the simplification not using generic value type specialization is worth the duplication of the function.

To be able to "upgrade" from none instrumented to instrumented version of DispatchContinuations, there are checks at method entry and after each completed continuation detecting if method should switch to instrumented version. Both checks are small and fast just a load of a flag in a static variable and checked if it's not 0.

This change will make sure the RuntimeAsyncTask::DispatchContinuations is protected from future performance regressions when more instrumentation gets added into the instrumented version.

Running the same benchmark, #123727 (comment) now shows the following numbers on old vs new implementation:

MetricOldNewDiff
Total bytes (S.P.C)16 740 KB16 756 KB+16 KB
JIT Size (RuntimeAsyncTask::DispatchContinuations)1778 B1399 B-379 B
Benchmark337ms362ms-25ms (~ -7%)

Measurements done on Windows x64.

S.P.C is 16 KB larger with this PR, due to duplicated DispatchContinuation method.

JIT Size is 379 bytes smaller on none instrumented version of RuntimeAsyncTask::DispatchContinuations, all previous instrumentation has been moved to instrumented version, completely eliminated in default method.

Benchmark shows that this PR recover most of the performance previously lost in #123727.

Code paths triggering the use of instrumented version, InstrumentedDispatchContinuations are protected by a IsSupported flag, so can be trimmed away, removing references to InstrumentedDispatchContinuations.

Most changes in this PR are around extracting out existing instrumentation into the instrumentation implementation. PR also optimize some of the debugger instrumentations previously implemented reducing locking in scenarios where continuation chains are handled.

PR adds a number of new tests validating that the current debugger and TPL instrumentation is still working.

PR also adds preparation for async profiler instrumentation in the AsyncInstrumentation type. This type will be used by more scenarios going forward.

@dotnet-policy-service

Copy link
Copy Markdown
Contributor

Tagging subscribers to this area: @dotnet/area-system-runtime
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

This PR refactors runtime-async continuation dispatch to support a low-overhead “uninstrumented” fast path while still enabling Debugger/TPL (and future profiler) instrumentation via a separate, JIT-specialized codegen path, with updated flag plumbing and added tests to validate behavior and cleanup.

Changes:

  • Introduces a generic, instrumentation-specialized DispatchContinuations<TRuntimeAsyncTaskInstrumentation>() path and centralized runtime-async instrumentation flag management.
  • Refactors Task’s runtime-async timestamp bookkeeping APIs to better support continuation chains and exception/unwind cleanup.
  • Adds/expands RuntimeAsync tests for timestamp cleanup, debugger detach behavior, continuation timestamp visibility, and TPL EventSource events; wires TPL EventSource enable/disable to update instrumentation flags.

Reviewed changes

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

FileDescription
src/libraries/System.Runtime/tests/System.Threading.Tasks.Tests/System.Runtime.CompilerServices/RuntimeAsyncTests.csAdds coverage for runtime-async instrumentation behavior (timestamps, detach, unwind/cancel, and TPL events).
src/libraries/System.Private.CoreLib/src/System/Threading/Tasks/TplEventSource.csUpdates runtime-async instrumentation flags when TPL EventSource commands change enabled keywords.
src/libraries/System.Private.CoreLib/src/System/Threading/Tasks/Task.csRefactors runtime-async timestamp dictionaries and adds helpers for chain timestamp propagation and cleanup.
src/coreclr/System.Private.CoreLib/src/System/Runtime/CompilerServices/AsyncHelpers.CoreCLR.csAdds the generic instrumentation abstraction and splits dispatch/finalize into specialized uninstrumented vs instrumented implementations.

Comment threadsrc/libraries/System.Private.CoreLib/src/System/Threading/Tasks/Task.cs Outdated
CopilotAI review requested due to automatic review settings March 25, 2026 14:53

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 4 out of 4 changed files in this pull request and generated 3 comments.

Comment threadsrc/libraries/System.Private.CoreLib/src/System/Threading/Tasks/Task.cs Outdated
CopilotAI review requested due to automatic review settings March 25, 2026 15:15

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 4 out of 4 changed files in this pull request and generated 2 comments.

@lateralusX

Copy link
Copy Markdown
MemberAuthor

Most failures are due to test _reflection::Async2Reflection.FromStack that currently asserts that DispatchContinuations is on stack and is currently not aware of the change to a generic method. If we stick with the generic method, then the assert in this test needs to be updated to reflect the name change.

@rcj1

rcj1 commented Mar 27, 2026

Copy link
Copy Markdown
Contributor

s_asyncDebuggingEnabled implies that these TPL events have been enabled, with two exceptions:

  1. Failure to create ETW session (too many existing ETW sessions, etc)
  2. Manual shutdown of VS-created ETW session or other exceptional error that causes ETW session to close

We can assume that s_asyncDebuggingEnabled implies required TPL events enabled, and save the setting and checking of these flags. The downside is a tiny amount of overhead spent checking whether TPL events are enabled in these two exceptional cases during debugging - in my opinion, an acceptable tradeoff for simplification and any possible optimization of the hot path.

If someone believes that scenario 1 is not very uncommon, it is possible to change Concord such that they will only be out of sync in case 2.

@lateralusX

lateralusX commented Mar 30, 2026

Copy link
Copy Markdown
MemberAuthor

s_asyncDebuggingEnabled implies that these TPL events have been enabled, with two exceptions:

  1. Failure to create ETW session (too many existing ETW sessions, etc)
  2. Manual shutdown of VS-created ETW session or other exceptional error that causes ETW session to close

We can assume that s_asyncDebuggingEnabled implies required TPL events enabled, and save the setting and checking of these flags. The downside is a tiny amount of overhead spent checking whether TPL events are enabled in these two exceptional cases during debugging - in my opinion, an acceptable tradeoff for simplification and any possible optimization of the hot path.

If someone believes that scenario 1 is not very uncommon, it is possible to change Concord such that they will only be out of sync in case 2.

Question is if pure TPL will have meaning here, like adding TPL events for runtime async activities in case a client only consumes TPL events to recreate async callstacks, or if that was never the intent with this change and it should only support the debugger features, then we should definitely put all logic under the same flag.

If we could assume the feature will trigger the TPL event source, we could handle it all in the on command event onTPL event source, and we can do the fast check only on flags, maybe that was what you proposed @rcj1? In that case this feature won't light up without the TPL session enabled, but that might be, OK? It would speed up the instrumentation gate check since it will only look at the flags again and ignore s_asyncDebuggingEnabled as part of dispatch loop.

@jakobbotsch

Copy link
Copy Markdown
Member

I took this path to get away from duplicating the complete DispatchContinuations method into a regular and instrumented version, reducing the maintenance of ~100 lines of duplicated high performing unsafe code. It can be argued that duplicating the DispatchContinuations is a small price to pay giving a little clearer implementation. If that is something we all agree on and accept, I'm happy to pursue that path as well, but wanted to start with the zero cost abstraction, no duplication path.

I worry that all the AggressiveInlining static abstracts introduce non-trivial startup cost when it comes to compiling all the dispatchers. It should be possible to measure this via some polymorphically recursive async methods that creates a lot of rarely used instantiations.

The other thing with the current approach -- the inversion of control makes the dispatcher quite hard to follow for me. But I can live with this given the improved performance.

@jakobbotsch

Copy link
Copy Markdown
Member

I worry that all the AggressiveInlining static abstracts introduce non-trivial startup cost when it comes to compiling all the dispatchers. It should be possible to measure this via some polymorphically recursive async methods that creates a lot of rarely used instantiations.

Doesn't look too bad, I tried a binary counter micro benchmark that loads 4096 of the dispatchers:

usingSystem;usingSystem.Diagnostics;usingSystem.Threading.Tasks;namespaceAsyncMicro;publicclassProgram{staticvoidMain(){Stopwatchtimer=Stopwatch.StartNew();newProgram().Recurse<byte,byte,byte,byte,byte,byte,byte,byte,byte,byte,byte,byte>(0).GetAwaiter().GetResult();Console.WriteLine("Took {0} ms",timer.ElapsedMilliseconds);}privateasyncTask<V<T11,T10,T9,T8,T7,T6,T5,T4,T3,T2,T1,T0>>Recurse<T11,T10,T9,T8,T7,T6,T5,T4,T3,T2,T1,T0>(intn){awaitTask.Yield();//Console.WriteLine(n);Taskt;if(typeof(T0)==typeof(byte)){t=Recurse<T11,T10,T9,T8,T7,T6,T5,T4,T3,T2,T1,int>(n+1);}elseif(typeof(T1)==typeof(byte)){t=Recurse<T11,T10,T9,T8,T7,T6,T5,T4,T3,T2,int,byte>(n+1);}elseif(typeof(T2)==typeof(byte)){t=Recurse<T11,T10,T9,T8,T7,T6,T5,T4,T3,int,byte,byte>(n+1);}elseif(typeof(T3)==typeof(byte)){t=Recurse<T11,T10,T9,T8,T7,T6,T5,T4,int,byte,byte,byte>(n+1);}elseif(typeof(T4)==typeof(byte)){t=Recurse<T11,T10,T9,T8,T7,T6,T5,int,byte,byte,byte,byte>(n+1);}elseif(typeof(T5)==typeof(byte)){t=Recurse<T11,T10,T9,T8,T7,T6,int,byte,byte,byte,byte,byte>(n+1);}elseif(typeof(T6)==typeof(byte)){t=Recurse<T11,T10,T9,T8,T7,int,byte,byte,byte,byte,byte,byte>(n+1);}elseif(typeof(T7)==typeof(byte)){t=Recurse<T11,T10,T9,T8,int,byte,byte,byte,byte,byte,byte,byte>(n+1);}elseif(typeof(T8)==typeof(byte)){t=Recurse<T11,T10,T9,int,byte,byte,byte,byte,byte,byte,byte,byte>(n+1);}elseif(typeof(T9)==typeof(byte)){t=Recurse<T11,T10,int,byte,byte,byte,byte,byte,byte,byte,byte,byte>(n+1);}elseif(typeof(T10)==typeof(byte)){t=Recurse<T11,int,byte,byte,byte,byte,byte,byte,byte,byte,byte,byte>(n+1);}elseif(typeof(T11)==typeof(byte)){t=Recurse<int,byte,byte,byte,byte,byte,byte,byte,byte,byte,byte,byte>(n+1);}else{returndefault;}awaitt;returndefault;}privatestructV<T11,T10,T9,T8,T7,T6,T5,T4,T3,T2,T1,T0>{publicintX;}}

The difference is only around 100 ms on this PR.
Base: Took 9205 ms
Diff: Took 9303 ms

(P.S. a simpler binary recursing generic hits some exponential behavior... Need to investigate that.)

@lateralusX

Copy link
Copy Markdown
MemberAuthor

The other thing with the current approach -- the inversion of control makes the dispatcher quite hard to follow for me. But I can live with this given the improved performance.

The alternative is to duplicate the DispatchContinuation implementation into two different methods, one default (with upgrade capabilities) and one instrumented version. In the end it depends on whats important, this PR currently take the reduce code duplication path, with slightly more complex dispatcher implementation (due to the instrumentation callbacks), and I think that is fine. Dropping the R2R exclusion reducing one indirect call on the instrumented path will simplify the instrumented path a little more as well, with very small size increase of S.P.C.

CopilotAI review requested due to automatic review settings April 10, 2026 08:16
@lateralusX
lateralusXforce-pushed the lateralusX/runtime-async-instrumentation branch from 069b76c to 75f5f5eCompareApril 10, 2026 08:16

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 6 out of 6 changed files in this pull request and generated no new comments.

@rcj1

rcj1 commented Apr 13, 2026

Copy link
Copy Markdown
Contributor

After testing the changes locally, I realize propagating these changes through to VS will be more complex than I initially thought. @tommcdon and I built changes making a lot of assumptions about the location of DispatchContinuations. We should fix whatever is broken by insertion of InstrumentedDispatchContinuations.

@lateralusX

lateralusX commented Apr 14, 2026

Copy link
Copy Markdown
MemberAuthor

After testing the changes locally, I realize propagating these changes through to VS will be more complex than I initially thought. @tommcdon and I built changes making a lot of assumptions about the location of DispatchContinuations. We should fix whatever is broken by insertion of InstrumentedDispatchContinuations.

@rcj1 can we go ahead and merge this and you pick up the needed VS changes later or do we need to hold off on this PR until you verified/fixed VS? It would be nice to get this PR merged since its blocking other PR's from following at the moment.

This was referenced Apr 14, 2026
CopilotAI review requested due to automatic review settings April 14, 2026 18:51
@lateralusX
lateralusXforce-pushed the lateralusX/runtime-async-instrumentation branch from 01867b8 to 75f5f5eCompareApril 14, 2026 18:51

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 6 out of 6 changed files in this pull request and generated 1 comment.

@lateralusX

lateralusX commented Apr 14, 2026

Copy link
Copy Markdown
MemberAuthor

All comments resolved, dropped linker tests from PR after validating that all instrumentation code gets linked out in IL trimmer and Native AOT scenarios. Good to go once CI pass?

@lateralusX

Copy link
Copy Markdown
MemberAuthor

/ba-g recently fixed known issue #126922

@lateralusX

Copy link
Copy Markdown
MemberAuthor

@rcj1, @jkotas, @jakobbotsch, @noahfalk, if there are no more objections, I will go ahead and merge this PR.

@lateralusX
lateralusX merged commit 462878e into dotnet:mainApr 16, 2026
173 of 176 checks passed
tommcdon added a commit to tommcdon/runtime that referenced this pull request Apr 21, 2026
…rDebug
When the debugger sets Task.s_asyncDebuggingEnabled directly via
ICorDebug (SetManagedTaskEtwEventsEnabled), the AsyncInstrumentation
flags system added by PR dotnet#126091 was not aware of this. The new flags
only got set via ETW OnEventCommand, so non-ETW RuntimeAsync configs
had s_asyncDebuggerActiveFlags stuck at Disabled, preventing
NotifyDebuggerOfRuntimeAsyncState from being called.
Fix: In InitializeFlags(), check Task.s_asyncDebuggingEnabled as a
fallback when s_asyncDebuggerActiveFlags is still Disabled. This
ensures the debugger's request is honored regardless of whether ETW
events have been enabled.
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
@github-actionsgithub-actionsBot locked and limited conversation to collaborators May 17, 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.

7 participants

@lateralusX@rcj1@jakobbotsch@noahfalk@jkotas@MichalStrehovsky