From 9d8b54e18b87ae1ffb867199604494bd64a79df2 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Amaury=20Lev=C3=A9?= Date: Tue, 30 Jun 2026 00:43:39 +0200 Subject: [PATCH 1/2] perf: cache MethodInfo.GetParameters() to avoid per-row array allocations in data-driven tests MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 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> --- .../Discovery/AssemblyEnumerator.cs | 4 +++- .../Execution/TestMethodInfo.cs | 6 +++++- 2 files changed, 8 insertions(+), 2 deletions(-) diff --git a/src/Adapter/MSTestAdapter.PlatformServices/Discovery/AssemblyEnumerator.cs b/src/Adapter/MSTestAdapter.PlatformServices/Discovery/AssemblyEnumerator.cs index 89bf1c82ed..efdb766f24 100644 --- a/src/Adapter/MSTestAdapter.PlatformServices/Discovery/AssemblyEnumerator.cs +++ b/src/Adapter/MSTestAdapter.PlatformServices/Discovery/AssemblyEnumerator.cs @@ -289,12 +289,14 @@ private static bool TryUnfoldITestDataSource(ITestDataSource dataSource, UnitTes var discoveredTests = new List(); bool dataSourceHasData = false; + // PERF: Hoist outside the loop — MethodInfo.GetParameters() returns a fresh array copy on each + // call (CLR safety guarantee), so calling it per row allocates N identical arrays for N data rows. + ParameterInfo[] parameters = methodInfo.GetParameters(); foreach (object?[] dataOrTestDataRow in dataEnumerable) { dataSourceHasData = true; object?[] d = dataOrTestDataRow; - ParameterInfo[] parameters = methodInfo.GetParameters(); // The effective ignore message for this row is the row-level ignore message (from // TestDataRow), falling back to the whole-source ignore message. It must be diff --git a/src/Adapter/MSTestAdapter.PlatformServices/Execution/TestMethodInfo.cs b/src/Adapter/MSTestAdapter.PlatformServices/Execution/TestMethodInfo.cs index 165a49355d..984f92d289 100644 --- a/src/Adapter/MSTestAdapter.PlatformServices/Execution/TestMethodInfo.cs +++ b/src/Adapter/MSTestAdapter.PlatformServices/Execution/TestMethodInfo.cs @@ -60,7 +60,11 @@ internal ITestContext TestContext /// /// Gets the parameter types of the test method. /// - public ParameterInfo[] ParameterTypes => MethodInfo.GetParameters(); + /// + /// Lazy-cached: MethodInfo.GetParameters() returns a fresh array copy on every call + /// (CLR safety guarantee), so caching avoids N redundant copies for data-driven tests with N rows. + /// + public ParameterInfo[] ParameterTypes => field ??= MethodInfo.GetParameters(); /// /// Gets the return type of the test method. From e415e3dc774b1bbde06336364cc625fd0570dd9e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Amaury=20Lev=C3=A9?= Date: Tue, 30 Jun 2026 07:35:04 +0200 Subject: [PATCH 2/2] Return fresh array from ITestMethod.ParameterTypes to protect cache 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> --- .../Execution/TestMethodInfo.cs | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/src/Adapter/MSTestAdapter.PlatformServices/Execution/TestMethodInfo.cs b/src/Adapter/MSTestAdapter.PlatformServices/Execution/TestMethodInfo.cs index 984f92d289..df0879e8cb 100644 --- a/src/Adapter/MSTestAdapter.PlatformServices/Execution/TestMethodInfo.cs +++ b/src/Adapter/MSTestAdapter.PlatformServices/Execution/TestMethodInfo.cs @@ -63,9 +63,15 @@ internal ITestContext TestContext /// /// Lazy-cached: MethodInfo.GetParameters() returns a fresh array copy on every call /// (CLR safety guarantee), so caching avoids N redundant copies for data-driven tests with N rows. + /// This cached array is shared across internal call sites and MUST NOT be mutated. The explicit + /// implementation hands external consumers a fresh copy to + /// preserve the previous "fresh array per call" behavior and protect the cache from mutation. /// public ParameterInfo[] ParameterTypes => field ??= MethodInfo.GetParameters(); + /// + ParameterInfo[] ITestMethod.ParameterTypes => (ParameterInfo[])ParameterTypes.Clone(); + /// /// Gets the return type of the test method. ///