diff --git a/src/Adapter/MSTestAdapter.PlatformServices/Execution/TestMethodInfo.ArgumentResolution.cs b/src/Adapter/MSTestAdapter.PlatformServices/Execution/TestMethodInfo.ArgumentResolution.cs index 6d155adccb..3c4f06ebff 100644 --- a/src/Adapter/MSTestAdapter.PlatformServices/Execution/TestMethodInfo.ArgumentResolution.cs +++ b/src/Adapter/MSTestAdapter.PlatformServices/Execution/TestMethodInfo.ArgumentResolution.cs @@ -10,7 +10,10 @@ internal partial class TestMethodInfo internal object?[] ResolveArguments(object?[] arguments) { - ParameterInfo[] parametersInfo = MethodInfo.GetParameters(); + // Use the cached ParameterTypes property rather than calling MethodInfo.GetParameters() + // (which allocates a fresh ParameterInfo[] on every call). ResolveArguments only reads the + // array, so sharing the cached instance is safe. + ParameterInfo[] parametersInfo = ParameterTypes; int requiredParameterCount = 0; bool hasParamsValue = false; object? paramsValues = null; diff --git a/src/Adapter/MSTestAdapter.PlatformServices/Execution/TestMethodRunner.cs b/src/Adapter/MSTestAdapter.PlatformServices/Execution/TestMethodRunner.cs index 96b3545d62..a02f455014 100644 --- a/src/Adapter/MSTestAdapter.PlatformServices/Execution/TestMethodRunner.cs +++ b/src/Adapter/MSTestAdapter.PlatformServices/Execution/TestMethodRunner.cs @@ -35,6 +35,14 @@ internal sealed class TestMethodRunner /// private readonly TestMethodInfo _testMethodInfo; + /// + /// Lazily-created wrapper reused across all data rows of a + /// data-driven test. Both and + /// are immutable for the lifetime of a , so a single wrapper can be shared + /// instead of allocating one per row. + /// + private ReflectionTestMethodInfo? _cachedReflectionMethodInfo; + /// /// Initializes a new instance of the class. /// @@ -423,9 +431,9 @@ private async Task ExecuteTestWithDataSourceAsync(ITestContext exe // and both GetDisplayName and ComputeDefaultDisplayName need to be consulted. if (displayNameFromTestDataRow is null && testDataSource is not null) { - var reflectionMethodInfo = new ReflectionTestMethodInfo(_testMethodInfo.MethodInfo, _test.DisplayName); - displayName = testDataSource.GetDisplayName(reflectionMethodInfo, data) - ?? TestDataSourceUtilities.ComputeDefaultDisplayName(reflectionMethodInfo, data) + _cachedReflectionMethodInfo ??= new ReflectionTestMethodInfo(_testMethodInfo.MethodInfo, _test.DisplayName); + displayName = testDataSource.GetDisplayName(_cachedReflectionMethodInfo, data) + ?? TestDataSourceUtilities.ComputeDefaultDisplayName(_cachedReflectionMethodInfo, data) ?? displayName; } else @@ -488,11 +496,26 @@ private async Task ExecuteTestAsync(ITestContext executionContext, { try { + ExecutionContext? capturedContext = testMethodInfo.Parent.ExecutionContext ?? testMethodInfo.Parent.Parent.ExecutionContext; + + // Fast path: when no ExecutionContext was captured by [AssemblyInitialize] / [ClassInitialize] + // (the common case), RunOnContext with a null context simply invokes the action directly on the + // current thread. Skip the TaskCompletionSource + closure + delegate allocations and just await + // the executor directly. + if (capturedContext is null) + { + using (TestContextImplementation.SetCurrentTestContext(executionContext as TestContext)) + { + testMethodInfo.TestContext = executionContext; + return await _testMethodInfo.Executor.ExecuteAsync(testMethodInfo).ConfigureAwait(false); + } + } + var tcs = new TaskCompletionSource(); #pragma warning disable VSTHRD101 // Avoid unsupported async delegates ExecutionContextHelpers.RunOnContext( - testMethodInfo.Parent.ExecutionContext ?? testMethodInfo.Parent.Parent.ExecutionContext, + capturedContext, async () => { try diff --git a/src/Adapter/MSTestAdapter.PlatformServices/Services/TestContextImplementation.cs b/src/Adapter/MSTestAdapter.PlatformServices/Services/TestContextImplementation.cs index 5efc98a041..a24f169e6c 100644 --- a/src/Adapter/MSTestAdapter.PlatformServices/Services/TestContextImplementation.cs +++ b/src/Adapter/MSTestAdapter.PlatformServices/Services/TestContextImplementation.cs @@ -524,16 +524,14 @@ private SynchronizedStringBuilder GetTestContextMessagesStringBuilder() /// A fresh context suitable for one folded data-driven iteration. internal TestContextImplementation CloneForDataDrivenIteration() { - // Take a shallow snapshot of the current property bag so that the clone starts with - // the same properties (including TestNameLabel / FullyQualifiedTestClassNameLabel and - // anything merged from AssemblyInitialize / ClassInitialize) but is otherwise isolated. - // Per-iteration mutations to the clone's property bag won't leak back to this instance + // Pass testMethod: null and testClassFullName: null because the relevant labels + // (including TestNameLabel / FullyQualifiedTestClassNameLabel and anything merged from + // AssemblyInitialize / ClassInitialize) are already in _properties. In that branch the + // constructor takes a shallow copy of the passed dictionary, so passing _properties + // directly gives the clone an isolated property bag without an extra intermediate copy: + // per-iteration mutations to the clone's property bag won't leak back to this instance // nor to subsequent iterations. - var snapshot = new Dictionary(_properties); - - // Pass testMethod: null and testClassFullName: null because the relevant labels are - // already in the snapshot. The constructor will copy the snapshot as-is. - var clone = new TestContextImplementation(testMethod: null, testClassFullName: null, snapshot, _messageLogger, _testRunCancellationToken); + var clone = new TestContextImplementation(testMethod: null, testClassFullName: null, _properties, _messageLogger, _testRunCancellationToken); // Preserve TestRunCount so user code that observes it (e.g. retry-aware tests) sees // the same value it would see in the unfolded path. TestRunCount represents the diff --git a/src/TestFramework/TestFramework/Internal/ReflectionTestMethodInfo.cs b/src/TestFramework/TestFramework/Internal/ReflectionTestMethodInfo.cs index 8d2ffd3632..41b4574690 100644 --- a/src/TestFramework/TestFramework/Internal/ReflectionTestMethodInfo.cs +++ b/src/TestFramework/TestFramework/Internal/ReflectionTestMethodInfo.cs @@ -7,6 +7,8 @@ internal sealed class ReflectionTestMethodInfo : MethodInfo { private readonly MethodInfo _methodInfo; + private ParameterInfo[]? _parameters; + public ReflectionTestMethodInfo(MethodInfo methodInfo, string? displayName) { _methodInfo = methodInfo; @@ -41,7 +43,10 @@ public ReflectionTestMethodInfo(MethodInfo methodInfo, string? displayName) public override MethodImplAttributes GetMethodImplementationFlags() => _methodInfo.GetMethodImplementationFlags(); - public override ParameterInfo[] GetParameters() => _methodInfo.GetParameters(); + // MethodInfo.GetParameters() allocates a fresh ParameterInfo[] on every call (CLR safety + // guarantee). The wrapped MethodInfo is immutable, so cache the array and share it across + // callers of this wrapper (e.g. display-name computation for every data-driven row). + public override ParameterInfo[] GetParameters() => _parameters ??= _methodInfo.GetParameters(); public override object? Invoke(object? obj, BindingFlags invokeAttr, Binder? binder, object?[]? parameters, CultureInfo? culture) => _methodInfo.Invoke(obj, invokeAttr, binder, parameters, culture);