Skip to content

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

Open
lateralusX wants to merge 8 commits intodotnet:mainfrom
lateralusX:lateralusX/runtime-async-instrumentation
Open

Add low-cost instrumented version of RuntimeAsyncTask::DispatchContinuations reclaiming lost performance.#126091
lateralusX wants to merge 8 commits intodotnet:mainfrom
lateralusX:lateralusX/runtime-async-instrumentation

Conversation

@lateralusX
Copy link
Member

@lateralusX lateralusX commented Mar 25, 2026

#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 introduces DispatchContinuations<TRuntimeAsyncTaskInstrumentation>() where TRuntimeAsyncTaskInstrumentation : struct, IRuntimeAsyncTaskInstrumentation using generic value type specialization using an interface for the instrumentation locations needed in the method creating two different versions of codegen. When instrumentation is disabled, most of the instrumentations are empty with minimal overhead on the execution of the method.

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.

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, but the check on method entry needs to do one more compare detecting if debugger flag has been toggled. The post continuation completion check is just a reload 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 method. Since it shares the majority of the implementation of the loop itself, there is no code duplication. Another approach will be to just duplicate the method, but that would lead to maintaining two copies of the method.

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

Metric Old New Diff
Total bytes (S.P.C) 16 740 KB 16 684 KB -56 KB
JIT Size (RuntimeAsyncTask::DispatchContinuations) 1778 B 1431 B -347 B
Benchmark 337ms 362ms -25ms (~ -7%)

Measurements done on Windows x64.

S.P.C is 56 KB smaller with this PR, so the extra instrumented codegen version of RuntimeAsyncTask::DispatchContinuations is not included in R2R image. The methods triggering the generic instantiation of the instrumented method have been explicitly marked BypassReadyToRun.

JIT Size is 347 bytes smaller on the default none instrumented version of RuntimeAsyncTask::DispatchContinuations, all previous instrumentation has been moved out into the instrumentation interface, 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 specialization, DispatchContinuations<EnableRuntimeAsyncTaskInstrumentation> are protected by a IsSupported flag. On JIT this is always true and will be folded away, but on Native AOT it will use feature flags ( Debugger.IsSupported || EventSource.IsSupported), that are false by default, meaning that none of the instrumented versions of RuntimeAsyncTask::DispatchContinuations will be included, and together with the JIT size savings to RuntimeAsyncTask::DispatchContinuations, Native AOT apps are expected to be slightly smaller.

Most changes in this PR are around setting up the interface used as instrumentation points and extract out existing instrumentation into the instrumentation implementation of the interface. 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 RuntimeAsyncTaskInstrumentation type.

Add instrumentation probes to RuntimeAsyncTask DispatchContinuations
loop. This is an extremely hot loop, in the centre of dispatching
async continuations.

dotnet#123727 introduced a regression
of ~7% when adding additional instrumentation for debugger/tpl into the
loop.

This commit uses generic value type specialization to setup an interface
for the probes that JIT can use to create two versions of codegen
for this hot method, most of the probes will be transformed to noop
when profiling/debugging is disabled, introduce minimal overhead to
critical hot code path.

Dispatch loop checks on entry if instrumentation is enabled, if so it
will switch to instrumented version of the function. It also checks
on each completion of a continuation if instrumentation flags changed
and if that is the case it will again switch to the instrumented version.

In total it performs small set of instructions to "upgrade" the method
on entry, and also a fast check against a static on each loop to support
late attach scenarios.

This change will make sure the dispatch continuations loop is protected
from future performance regressions when more instrumentation gets added.
@dotnet-policy-service
Copy link
Contributor

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

Copy link
Contributor

Copilot AI left a comment

Choose a reason for hiding this comment

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

Pull request overview

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.

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

…s/System.Runtime.CompilerServices/RuntimeAsyncTests.cs

Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>
Copilot AI review requested due to automatic review settings March 25, 2026 14:53
…s/Task.cs

Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>
…s/System.Runtime.CompilerServices/RuntimeAsyncTests.cs

Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>
Copy link
Contributor

Copilot AI left a comment

Choose a reason for hiding this comment

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

Pull request overview

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

…s/Task.cs

Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>
Copilot AI review requested due to automatic review settings March 25, 2026 15:15
Copy link
Contributor

Copilot AI left a comment

Choose a reason for hiding this comment

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

Pull request overview

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

@lateralusX
Copy link
Member Author

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.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants