Skip to content

[efficiency-improver] perf: cache MethodInfo.GetParameters() to avoid per-row array allocations in data-driven tests#9514

Open
Evangelink wants to merge 2 commits into
mainfrom
efficiency/cache-get-parameters-c6e75b85c5406dcd
Open

[efficiency-improver] perf: cache MethodInfo.GetParameters() to avoid per-row array allocations in data-driven tests#9514
Evangelink wants to merge 2 commits into
mainfrom
efficiency/cache-get-parameters-c6e75b85c5406dcd

Conversation

@Evangelink

Copy link
Copy Markdown
Member

Goal

Eliminate redundant ParameterInfo[] heap allocations in the data-driven test execution and discovery paths.

Focus Area

Code-Level Efficiency — reducing unnecessary object creation that causes GC pressure.

Background

MethodInfo.GetParameters() is a CLR-mandated copy-on-every-call API: the runtime keeps an internal ParameterInfo[] but returns a fresh copy to each caller to prevent external mutation of internal reflection state. This means every call allocates a new array on the heap, even when the method signature hasn't changed.

Two separate call sites in the MSTest adapter were calling GetParameters() repeatedly for the same method while iterating over data rows:

1. AssemblyEnumerator.TryUnfoldITestDataSource (discovery path)

foreach (object?[] dataOrTestDataRow in dataEnumerable)
{
    ParameterInfo[] parameters = methodInfo.GetParameters();  // ← inside the loop
    ...

For a test with N data rows, this allocates N identical ParameterInfo[] objects during test discovery.

2. TestMethodInfo.ParameterTypes (execution path)

public ParameterInfo[] ParameterTypes => MethodInfo.GetParameters();  // ← eager, no cache

In TestMethodRunner.ExecuteTestWithDataSourceAsync, ParameterTypes is accessed up to 3 times per data row:

TestDataSourceHelpers.TryHandleITestDataRow(data, _testMethodInfo.ParameterTypes, ...)    // row × 1
TestDataSourceHelpers.IsDataConsideredSingleArgumentValue(data, _testMethodInfo.ParameterTypes) // row × 1
TestDataSourceHelpers.TryHandleTupleDataSource(data[0], _testMethodInfo.ParameterTypes, ...)  // row × 1

Fix

AssemblyEnumerator.cs: hoist GetParameters() above the foreach — one call before the loop, reused across all rows.

TestMethodInfo.cs: use the C# 14 field keyword to lazily cache the result once per TestMethodInfo instance (the backing MethodInfo is get-only and set once in the constructor, so the cache is valid for the lifetime of the object):

public ParameterInfo[] ParameterTypes => field ??= MethodInfo.GetParameters();

Energy Efficiency Evidence

Metric Before After
ParameterInfo[] allocs — discovery (N rows) N arrays 1 array
ParameterInfo[] allocs — execution (N rows) up to 3N arrays 1 array

Proxy metric: heap allocation count. Fewer short-lived heap objects → fewer GC mark/sweep cycles → fewer CPU cycles spent collecting garbage → lower energy per test run.

For a data-driven test with 100 rows, this reduces ParameterInfo[] allocations from ~400 to 1.

Green Software Foundation — Hardware Efficiency: making better use of already-allocated memory by reusing a cached value rather than churning through repeated identical allocations improves work-per-watt.

Trade-offs

None significant. The field keyword lazy-initialisation is a one-liner that's idiomatic C# 14. The hoisted GetParameters() call is straightforward. MethodInfo is get-only on TestMethodInfo, so the cached value is permanently valid.

Reproducibility

Any test suite with [DynamicData] or custom ITestDataSource test methods will exercise both paths. Run ./build.sh -pack -test -integrationTest to exercise acceptance tests that use data-driven tests.

Test Status

./build.sh -test — all unit tests pass (net8.0 + net9.0).

🤖 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 Efficiency Improver workflow. · 4.4K AIC · ⌖ 40.1 AIC · ⊞ 58.8K · [◷]( · )

Add this agentic workflows to your repo

To install this agentic workflow, run

gh aw add githubnext/agentics/workflows/efficiency-improver.md@main

…ions in data-driven tests

MethodInfo.GetParameters() returns a fresh ParameterInfo[] copy on every call (CLR
safety guarantee to prevent callers from mutating internal state). For a data-driven
test method with N rows this means:

  - During discovery (TryUnfoldITestDataSource): N identical ParameterInfo[] arrays
    allocated inside the foreach loop, where only one is ever needed.
  - During execution (TestMethodRunner via TestMethodInfo.ParameterTypes): up to 3
    arrays per row from three separate property accesses in ExecuteTestWithDataSourceAsync.

Fixes:
1. AssemblyEnumerator.TryUnfoldITestDataSource: hoist GetParameters() before the
   foreach loop so the array is computed once and reused for all rows.
2. TestMethodInfo.ParameterTypes: lazy-cache using the C# 14 'field' keyword
   (LangVersion=preview) so the array is computed once per TestMethodInfo instance
   and all subsequent data-row accesses return the cached reference.

Energy proxy: memory allocation — fewer short-lived heap objects reduces GC
pressure (mark/sweep cycles), which reduces CPU cycles spent in the collector.
For a test with 100 data rows this cuts ParameterInfo[] allocations from ~400 to ~1.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Copilot AI review requested due to automatic review settings June 29, 2026 22:43
@Evangelink Evangelink added area/performance Runtime / build performance / efficiency. type/automation Created or maintained by an agentic workflow. labels Jun 29, 2026

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 targets MSTest adapter performance by reducing repeated MethodInfo.GetParameters() calls (which allocate a new ParameterInfo[] on each invocation) in both discovery and execution paths for data-driven tests.

Changes:

  • Hoists GetParameters() out of the per-row discovery loop in AssemblyEnumerator.TryUnfoldITestDataSource.
  • Lazily caches GetParameters() in TestMethodInfo.ParameterTypes to avoid repeated allocations during execution.
Show a summary per file
File Description
src/Adapter/MSTestAdapter.PlatformServices/Execution/TestMethodInfo.cs Adds lazy caching for parameter metadata; potential API/behavioral implications due to returning a cached mutable array.
src/Adapter/MSTestAdapter.PlatformServices/Discovery/AssemblyEnumerator.cs Hoists parameter retrieval outside the data row iteration to avoid per-row allocations during discovery.

Review details

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

@Evangelink Evangelink marked this pull request as ready for review June 30, 2026 05:30
@Evangelink Evangelink enabled auto-merge (squash) June 30, 2026 05:30
@Evangelink Evangelink added the state/needs-review Awaiting review from the team. label Jun 30, 2026
Keep the cached ParameterInfo[] for internal call sites (the data-driven hot path uses the concrete TestMethodInfo type), but explicitly implement ITestMethod.ParameterTypes to clone the array. This preserves the previous 'fresh array per call' behavior for external interface consumers and prevents them from mutating the shared cache.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

area/performance Runtime / build performance / efficiency. state/needs-review Awaiting review from the team. type/automation Created or maintained by an agentic workflow.

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants