Skip to content

perf: skip TCS bridge in ExecuteTestAsync when no ExecutionContext is captured#9636

Open
Evangelink wants to merge 1 commit into
mainfrom
dev/amauryleve/skip-tcs-bridge-execution-context
Open

perf: skip TCS bridge in ExecuteTestAsync when no ExecutionContext is captured#9636
Evangelink wants to merge 1 commit into
mainfrom
dev/amauryleve/skip-tcs-bridge-execution-context

Conversation

@Evangelink

Copy link
Copy Markdown
Member

Fixes #9635

Summary

ExecuteTestAsync in TestMethodRunner used a TaskCompletionSource<TestResult[]> bridge to hop test execution onto a captured ExecutionContext (set by [ClassInitialize] or [AssemblyInitialize]). However, ExecutionContextHelpers.RunOnContext already handles a null context by invoking the action inline — so the TaskCompletionSource, async-lambda closure, and Action delegate were being allocated for every test, even in the common case where no ExecutionContext was captured.

Change

Compute the captured context once and short-circuit the bridge when it is null:

  • Fast path (common case, capturedContext is null): execute directly, skipping the TCS bridge and its ~3 heap allocations (~114 bytes) per test.
  • Slow path (a lifecycle method captured a context): unchanged TCS bridge via ExecutionContext.Run.

Verification

  • MSTestAdapter.PlatformServices builds clean (0 warnings, 0 errors).
  • All 23 TestMethodRunnerTests pass unchanged.

Co-authored-by: Copilot App 223556219+Copilot@users.noreply.github.com

… captured

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 test even when unnecessary.

Add a fast path that skips all three allocations when the captured context is null. The slow path (TCS bridge via ExecutionContext.Run) is preserved unchanged for the rare case where a lifecycle method captured a context.

Fixes #9635

Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>
Copilot AI review requested due to automatic review settings July 5, 2026 19:36
@Evangelink Evangelink added the state/needs-review Awaiting review from the team. label Jul 5, 2026
@Evangelink Evangelink enabled auto-merge (squash) July 5, 2026 19:37

Copilot AI 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 optimizes MSTest adapter execution by avoiding unnecessary allocations in TestMethodRunner.ExecuteTestAsync when no ExecutionContext was captured by lifecycle methods, improving the common-case performance of running tests.

Changes:

  • Compute the captured ExecutionContext once and add a fast path for capturedContext is null.
  • Skip the TaskCompletionSource<TestResult[]> bridge (and associated async-void delegate allocations) on the fast path, while preserving the existing slow path for captured contexts.
Show a summary per file
File Description
src/Adapter/MSTestAdapter.PlatformServices/Execution/TestMethodRunner.cs Adds a null-context fast path to avoid per-test TCS/closure/delegate allocations while preserving the captured-context behavior.

Review details

  • Files reviewed: 1/1 changed files
  • Comments generated: 1
  • Review effort level: Low

Comment on lines +499 to +513
ExecutionContext? capturedContext = testMethodInfo.Parent.ExecutionContext
?? testMethodInfo.Parent.Parent.ExecutionContext;

// Fast path: when no ExecutionContext was captured (the common case),
// ExecutionContextHelpers.RunOnContext would simply call the action inline.
// Skip the TaskCompletionSource bridge, async-lambda closure, and Action
// delegate allocations entirely.
if (capturedContext is null)
{
using (TestContextImplementation.SetCurrentTestContext(executionContext as TestContext))
{
testMethodInfo.TestContext = executionContext;
return await _testMethodInfo.Executor.ExecuteAsync(testMethodInfo).ConfigureAwait(false);
}
}

@github-actions github-actions Bot 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.

Note

🤖 Automated review 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 Expert Code Review workflow. To request a follow-up action, reply by tagging @copilot directly.

Review Summary — All Clean ✅

This is a well-crafted, low-risk performance optimization. I applied all 22 review dimensions and found no actionable issues.

Key Verification Points

Dimension Verdict Notes
1. Algorithmic Correctness Fast path is semantically equivalent to RunOnContext with null context (which calls action() inline)
2. Threading/Concurrency ExecutionContext properties are set during ClassInitialize/AssemblyInitialize — stable by the time ExecuteTestAsync runs
3. Edge Cases / Null Safety testMethodInfo.Parent (TestClassInfo) and .Parent.Parent (TestAssemblyInfo) are both non-nullable; no null-deref risk
5. Error Handling Exceptions propagate through await to the same outer catch (Exception ex) in both paths — identical behavior
7. Performance Eliminates ~3 allocations (TCS, async-lambda closure, Action delegate) per test in the common case
11. Test Coverage Existing 23 TestMethodRunnerTests exercise this path (most use no captured context); behavioral no-op justifies no new tests

Notes

  • Code duplication between fast and slow paths (SetCurrentTestContext + TestContext assignment + ExecuteAsync) is intentional and well-commented — it's the mechanism enabling the allocation savings.
  • The fast path actually improves safety slightly by avoiding the async void delegate pattern (the VSTHRD101 suppression) in the common case.
  • The extracted capturedContext local also eliminates double-evaluation of the ?? chain, which is a minor but welcome cleanup for the slow path too.

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

Labels

state/needs-review Awaiting review from the team.

Projects

None yet

Development

Successfully merging this pull request may close these issues.

[perf-improver] perf: skip TCS bridge in ExecuteTestAsync when no ExecutionContext is captured

3 participants