Skip to content

[Efficiency Improver] perf: eliminate yield-iterator allocations in test execution hot path #8085

@Evangelink

Description

@Evangelink

🤖 This PR was created by Daily Efficiency Improver, an automated AI assistant focused on reducing the energy consumption and computational footprint of this repository.


Goal and Rationale

Eliminate heap allocations that occur on every test execution in the MSTest test runner hot path. These allocations scale linearly with test count and contribute to GC pressure, which wastes CPU cycles (and thus energy) on collection work.

Focus area: Code-Level Efficiency

Changes

1. TestMethodInfo.GetRetryAttribute() — 1 allocation per TestMethodInfo construction eliminated

Before:

IEnumerable<RetryBaseAttribute> attributes = ReflectHelper.Instance.GetAttributes<RetryBaseAttribute>(MethodInfo);
using IEnumerator<RetryBaseAttribute> enumerator = attributes.GetEnumerator();

GetAttributes<T>() is a yield return iterator — calling it allocates a compiler-generated state machine object on the heap. For 10,000 tests, that's 10,000 avoidable allocations at construction time.

After:

Attribute[] cachedAttributes = ReflectHelper.Instance.GetCustomAttributesCached(MethodInfo);
RetryBaseAttribute? found = null;
foreach (Attribute attr in cachedAttributes)
{
    if (attr is RetryBaseAttribute retryAttr) { ... }
}

Direct iteration of the already-cached Attribute[] — zero extra allocation. Preserves the duplicate-attribute detection and the custom TypeInspectionException.

2. TestMethodRunner.TryExecuteDataSourceBasedTestsAsync() — 2 allocations per test execution eliminated (common case)

Before:

DataSourceAttribute[] dataSourceAttribute = _testMethodInfo.GetAttributes<DataSourceAttribute>();
if (dataSourceAttribute is { Length: 1 }) { ... }

GetAttributes<DataSourceAttribute>() allocates a yield iterator state machine and a DataSourceAttribute[] array via [.. iterator]. For most tests (no data source), these are 2 wasted allocations per test execution.

After:

if (!ReflectHelper.Instance.IsAttributeDefined<DataSourceAttribute>(_testMethodInfo.MethodInfo))
{
    return false;
}

IsAttributeDefined<T>() iterates the cached array in-place — zero allocation. Since DataSourceAttribute is sealed and has AllowMultiple = false, presence equals exactly one instance.

Energy Efficiency Evidence

Proxy metric: Heap allocation count (measures GC pressure, which translates directly to CPU energy use for garbage collection)

Location Before After Savings
GetRetryAttribute() 1 state machine alloc per test construction 0 1 alloc eliminated
TryExecuteDataSourceBasedTestsAsync() 2 allocs per test execution (non-DataSource) 0 2 allocs eliminated

At scale (10,000 tests): ~30,000 heap allocations eliminated per test run (10k constructions + 20k executions), reducing GC trigger frequency and associated CPU overhead.

Methodology: Static code analysis of allocation sites. Validated by reading the IL model: yield return generates a compiler-synthesized class that is heap-allocated on each GetEnumerator() call; [.. IEnumerable<T>] additionally allocates a new array.

Green Software Foundation

  • Hardware Efficiency: Fewer allocations = less DRAM bandwidth and GC CPU per joule
  • Energy Proportionality: GC cost scales with allocation rate; reducing it improves the energy/work ratio as test suite size grows

Trade-offs

  • Slightly more verbose code in GetRetryAttribute() but equivalent logic; the comment explains why.
  • No behaviour change: same result, same error paths, same exceptions.

Reproducibility

# Measure allocation count before/after via dotnet-counters:
dotnet counters monitor --process-id <PID> System.Runtime[gc-heap-size,gen-0-gc-count]

Test Status

  • Build: dotnet build MSTestAdapter.PlatformServices.csproj -p:SignAssembly=false — 0 warnings, 0 errors (net8.0, net9.0)
  • Build: dotnet build MSTest.TestAdapter.csproj -p:SignAssembly=false — 0 warnings, 0 errors (net8.0, net9.0)
  • CI is authoritative for full test runs

Generated by Daily Efficiency Improver · ● 3.6M ·


Note

This was originally intended as a pull request, but the git push operation failed.

Workflow Run: View run details and download patch artifact

The patch file is available in the agent artifact in the workflow run linked above.

To create a pull request with the changes:

# Download the artifact from the workflow run
gh run download 25650636054 -n agent -D /tmp/agent-25650636054

# Create a new branch
git checkout -b efficiency/avoid-yield-iterator-get-retry-attribute-be0151402d39b1b6

# Apply the patch (--3way handles cross-repo patches where files may already exist)
git am --3way /tmp/agent-25650636054/aw-efficiency-avoid-yield-iterator-get-retry-attribute.patch

# Push the branch to origin
git push origin efficiency/avoid-yield-iterator-get-retry-attribute-be0151402d39b1b6

# Create the pull request
gh pr create --title '[Efficiency Improver] perf: eliminate yield-iterator allocations in test execution hot path' --base main --head efficiency/avoid-yield-iterator-get-retry-attribute-be0151402d39b1b6 --repo microsoft/testfx
Show patch preview (101 of 101 lines)
From 5ee2f140680aa644dd745fca452df3109bc1ab78 Mon Sep 17 00:00:00 2001
From: "github-actions[bot]" <github-actions[bot]@users.noreply.github.com>
Date: Mon, 11 May 2026 04:50:33 +0000
Subject: [PATCH] perf: eliminate yield-iterator allocations in test execution
 hot path

GetRetryAttribute() called GetAttributes<RetryBaseAttribute>() which allocates
a compiler-generated state machine (IEnumerator<T> via yield return) on every
test method construction. Replace with direct iteration of the cached Attribute[]
from GetCustomAttributesCached(). Preserves duplicate-attribute detection and
the custom TypeInspectionException.

TryExecuteDataSourceBasedTestsAsync() called _testMethodInfo.GetAttributes<DataSourceAttribute>()
which allocates a yield iterator state machine AND a DataSourceAttribute[] array.
Replace with ReflectHelper.Instance.IsAttributeDefined<DataSourceAttribute>(),
which iterates the same cached array without any allocation. Since DataSourceAttribute
is sealed and does not allow multiple, presence == exactly one instance.

Proxy metric: heap allocation count
- GetRetryAttribute: 1 allocation eliminated per TestMethodInfo construction
- TryExecuteDataSourceBasedTestsAsync: 2 allocations eliminated per test execution
  (for tests without a DataSourceAttribute -- the common case)
- Estimated: ~480 KB GC pressure reduction per 10,000-test run

GSF principle: Hardware Efficiency -- fewer allocations = less DRAM and GC CPU
per joule; Energy Proportionality -- GC cost scales with allocation rate.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
---
 .../Execution/TestMethodInfo.cs               | 24 ++++++++++---------
 .../Execution/TestMethodRunner.cs             | 11 +++++----
 2 files changed, 19 insertions(+), 16 deletions(-)

diff --git a/src/Adapter/MSTestAdapter.PlatformServices/Execution/TestMethodInfo.cs b/src/Adapter/MSTestAdapter.PlatformServices/Execution/TestMethodInfo.cs
index e45ed74e5..af4aef0eb 100644
--- a/src/Adapter/MSTestA
... (truncated)

Metadata

Metadata

Assignees

No one assigned

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions