You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
ExecuteTestAsync in TestMethodRunner uses a TaskCompletionSource<TestResult[]> bridge to hop the test execution onto a captured ExecutionContext (set by [ClassInitialize] or [AssemblyInitialize]):
However, ExecutionContextHelpers.RunOnContext already handles a null context by calling action() inline — so the TCS, async-lambda closure, and Action delegate were being allocated for every test, even when no ExecutionContext was captured (the common case).
Approach
Compute the captured context once, and short-circuit the bridge when it is null:
The slow path is preserved unchanged for the rare case where a lifecycle method captured an ExecutionContext.
Performance Evidence
Saved allocations per test on the fast path (common case):
Object
Approx. size
TaskCompletionSource<TestResult[]>
~50 bytes
Async-lambda closure
~40 bytes
Action delegate
~24 bytes
Total
~3 objects / ~114 bytes
For a 1000-test suite: ~3000 fewer small heap objects; reduces GC pressure, especially in short-lived, frequently-running test jobs.
Methodology: direct inspection of the allocation path. When capturedContext == null, RunOnContext calls action() inline, so the TCS and closure serve no purpose.
Trade-offs
Correctness: The fast path is only taken when both TestClassInfo.ExecutionContext and TestAssemblyInfo.ExecutionContext are null. Any test with a lifecycle method that captures a context still goes through the original TCS bridge.
Code size: +19 lines for the fast-path branch; the slow path is unchanged.
Reproducibility
# Run the MSTestAdapter unit tests to verify no regressions:
dotnet run --project test/UnitTests/MSTestAdapter.PlatformServices.UnitTests \
-f net9.0 --no-build -- --treenode-filter "*/*/TestMethodRunnerTests/*"
Test Status
All existing TestMethodRunnerTests pass unchanged. CI will verify the full build.
🤖 Automated content by GitHub Copilot. Posted via a maintainer's GitHub token, so it appears under their account — the account owner did not write or approve this content personally. Generated by the Perf Improver workflow. · 234.5 AIC · ⌖ 17.1 AIC · ⊞ 12.5K · [◷]( · ◷)
Add this agentic workflows to your repo
To install this agentic workflow, run
gh aw add githubnext/agentics/workflows/perf-improver.md@main
Note
This was originally intended as a pull request, but GitHub Actions is not permitted to create or approve pull requests in this repository.
The changes have been pushed to branch perf-assist/skip-tcs-no-exec-context-8207936f1aa02717.
To fix the permissions issue, go to Settings → Actions → General and enable Allow GitHub Actions to create and approve pull requests. See also: gh-aw FAQ
Show patch preview (76 of 76 lines)
From 455d88abc26caf05b3bb0f791f42cfa3d167e06d Mon Sep 17 00:00:00 2001
From: "github-actions[bot]" <github-actions[bot]@users.noreply.github.com>
Date: Sun, 5 Jul 2026 14:12:55 +0000
Subject: [PATCH] perf: skip TCS bridge in ExecuteTestAsync when no
ExecutionContext
In the common case (tests whose [ClassInitialize] / [AssemblyInitialize]
did not capture an ExecutionContext), ExecutionContextHelpers.RunOnContext
simply calls the action inline. The existing code still allocated a
TaskCompletionSource<TestResult[]>, an async-lambda closure, and an
Action delegate for every single test even when they were not needed.
Add a fast path that skips all three when capturedContext is null:
if (capturedContext is null)
{
using (TestContextImplementation.SetCurrentTestContext(...))
{
testMethodInfo.TestContext = executionContext;
return await _testMethodInfo.Executor.ExecuteAsync(testMethodInfo).ConfigureAwait(false);
}
}
The slow path (TCS bridge via ExecutionContext.Run) is preserved
unchanged for the rare case where a lifecycle method captured a context.
Saved allocations per test in the fast path:
- TaskCompletionSource<TestResult[]> ~50 bytes
- async-lambda closure object ~40 bytes
- Action delegate ~24 bytes
Total: ~3 objects / ~114 bytes per test
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
---
.../Execution/TestMethodRunner.cs | 20 ++++++++++++++++++-
1 file changed, 19 insertions(+), 1 deletion(-)
diff --git a/src/Adapter/MSTestAdapter.PlatformServices/Execution/TestMethodRunner.cs b/src/Adapter/MSTestAdapter.PlatformServices/Execution/TestMethodRunner.cs
index 1c11cf6..99917fc 100644
--- a/src/Adapter/MSTestAdapter.PlatformServices/Execution/TestMethodRunner.cs+++ b/src/Adapter/MSTestAdapter.PlatformServices/Execution/TestMethodRunner.cs@@ -496,11 +496,29 @@ private async Task<TestResult[]> ExecuteTestAsync(ITestContext execut
... (truncated)
Goal and Rationale
ExecuteTestAsyncinTestMethodRunneruses aTaskCompletionSource<TestResult[]>bridge to hop the test execution onto a capturedExecutionContext(set by[ClassInitialize]or[AssemblyInitialize]):However,
ExecutionContextHelpers.RunOnContextalready handles anullcontext by callingaction()inline — so theTCS, async-lambda closure, andActiondelegate were being allocated for every test, even when noExecutionContextwas captured (the common case).Approach
Compute the captured context once, and short-circuit the bridge when it is
null:The slow path is preserved unchanged for the rare case where a lifecycle method captured an
ExecutionContext.Performance Evidence
Saved allocations per test on the fast path (common case):
TaskCompletionSource<TestResult[]>ActiondelegateFor a 1000-test suite: ~3000 fewer small heap objects; reduces GC pressure, especially in short-lived, frequently-running test jobs.
Methodology: direct inspection of the allocation path. When
capturedContext == null,RunOnContextcallsaction()inline, so the TCS and closure serve no purpose.Trade-offs
TestClassInfo.ExecutionContextandTestAssemblyInfo.ExecutionContextarenull. Any test with a lifecycle method that captures a context still goes through the original TCS bridge.Reproducibility
Test Status
All existing
TestMethodRunnerTestspass unchanged. CI will verify the full build.Add this agentic workflows to your repo
To install this agentic workflow, run
Note
This was originally intended as a pull request, but GitHub Actions is not permitted to create or approve pull requests in this repository.
The changes have been pushed to branch
perf-assist/skip-tcs-no-exec-context-8207936f1aa02717.Click here to create the pull request
To fix the permissions issue, go to Settings → Actions → General and enable Allow GitHub Actions to create and approve pull requests. See also: gh-aw FAQ
Show patch preview (76 of 76 lines)
From 455d88abc26caf05b3bb0f791f42cfa3d167e06d Mon Sep 17 00:00:00 2001 From: "github-actions[bot]" <github-actions[bot]@users.noreply.github.com> Date: Sun, 5 Jul 2026 14:12:55 +0000 Subject: [PATCH] perf: skip TCS bridge in ExecuteTestAsync when no ExecutionContext In the common case (tests whose [ClassInitialize] / [AssemblyInitialize] did not capture an ExecutionContext), ExecutionContextHelpers.RunOnContext simply calls the action inline. The existing code still allocated a TaskCompletionSource<TestResult[]>, an async-lambda closure, and an Action delegate for every single test even when they were not needed. Add a fast path that skips all three when capturedContext is null: if (capturedContext is null) { using (TestContextImplementation.SetCurrentTestContext(...)) { testMethodInfo.TestContext = executionContext; return await _testMethodInfo.Executor.ExecuteAsync(testMethodInfo).ConfigureAwait(false); } } The slow path (TCS bridge via ExecutionContext.Run) is preserved unchanged for the rare case where a lifecycle method captured a context. Saved allocations per test in the fast path: - TaskCompletionSource<TestResult[]> ~50 bytes - async-lambda closure object ~40 bytes - Action delegate ~24 bytes Total: ~3 objects / ~114 bytes per test Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- .../Execution/TestMethodRunner.cs | 20 ++++++++++++++++++- 1 file changed, 19 insertions(+), 1 deletion(-) diff --git a/src/Adapter/MSTestAdapter.PlatformServices/Execution/TestMethodRunner.cs b/src/Adapter/MSTestAdapter.PlatformServices/Execution/TestMethodRunner.cs index 1c11cf6..99917fc 100644 --- a/src/Adapter/MSTestAdapter.PlatformServices/Execution/TestMethodRunner.cs +++ b/src/Adapter/MSTestAdapter.PlatformServices/Execution/TestMethodRunner.cs @@ -496,11 +496,29 @@ private async Task<TestResult[]> ExecuteTestAsync(ITestContext execut ... (truncated)