Cache TestMethodIdentifierProperty per TestMethod to avoid redundant parsing - #10366
Cache TestMethodIdentifierProperty per TestMethod to avoid redundant parsing#10366Evangelink wants to merge 1 commit into
Conversation
MSTestTestNodeConverter.AddTestMethodIdentifier called ManagedNameParser.ParseManagedMethodName and allocated a new TestMethodIdentifierProperty on every node conversion, even though the result is a pure function of TestMethod.ManagedTypeName and TestMethod.ManagedMethodName, which are immutable for the lifetime of the instance. The same TestMethod is converted several times per executed test: once for the in-progress node and once per result node (one per data row for data-driven tests). Cache the built property in a static ConditionalWeakTable keyed on the TestMethod instance so the parse, the two substring allocations and the property allocation are paid once per test method instead of once per node. The cache lives in the converter rather than as a lazy property on TestMethod because TestMethodIdentifierProperty is a Microsoft.Testing.Platform type and MSTestAdapter.PlatformServices does not reference the platform. Fixes #10363 Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com> Copilot-Session: 01fd9297-53cd-4532-b294-a8da5723825d
There was a problem hiding this comment.
Pull request overview
Caches parsed MSTest method identifiers during MTP node conversion to reduce repeated parsing and allocations.
Changes:
- Adds a weak per-
TestMethodidentifier cache. - Adds regression and cache-isolation tests.
Show a summary per file
| File | Description |
|---|---|
MSTestTestNodeConverter.cs |
Implements identifier caching. |
MSTestTestNodeConverterTests.cs |
Tests reuse, isolation, and empty type handling. |
Review details
- Files reviewed: 2/2 changed files
- Comments generated: 1
- Review effort level: Balanced
| TestMethodIdentifierProperty testMethodIdentifier = TestMethodIdentifierCache.GetValue(testMethod, BuildTestMethodIdentifier); | ||
| testNode.Properties.Add(testMethodIdentifier); |
There was a problem hiding this comment.
Note
🤖 Automated review by GitHub Copilot. Generated by the Expert Code Review workflow. To request a follow-up action, reply by tagging @copilot directly.
Summary
Clean, well-motivated performance optimization. No issues found.
Correctness ✅ — The guard logic is preserved: HasManagedMethodAndTypeProperties already ensures ManagedMethodName is non-null/non-whitespace, and the new StringEx.IsNullOrEmpty(testMethod.ManagedTypeName) check covers the edge case where FullClassName is empty (previously caught by the second null check inside the method). The ! suppressions in BuildTestMethodIdentifier are safe given the caller's pre-validation.
Thread safety ✅ — ConditionalWeakTable.GetValue guarantees a single published value even under concurrent factory execution. No mutable shared state is introduced.
Lifetime / leaks ✅ — Weak keys ensure entries are collected with the TestMethod. No strong reference cycle.
Shared array note — The remarks correctly call out that ParameterTypeFullNames is a shared array. Since all consumers are read-only today this is safe, but worth keeping in mind for future changes.
Tests ✅ — Good coverage: reuse across node types, distinct instances for distinct keys, and the new empty-ManagedTypeName edge case.
No blocking concerns. LGTM.
🧪 Test quality grade — PR #10366
This advisory comment was generated automatically. Grades are heuristic and informational — they do not block merging. Re-run with
|
Fixes #10363
Problem
MSTestTestNodeConverter.AddTestMethodIdentifiercalledManagedNameParser.ParseManagedMethodNameand allocated a newTestMethodIdentifierPropertyon every node conversion — even though the result is a pure function ofTestMethod.ManagedTypeNameandTestMethod.ManagedMethodName, both of which are immutable for the lifetime of the instance.The same
TestMethodinstance is converted several times per executed test:TestExecutionManager.ExecuteTestsWithTestRunnerAsyncreports the originalcurrentTestelement toRecordStartAsync(in-progress node) and then toSendTestResultsAsync, which loops over the results and callsRecordResultAsynconce per row. So a plain test parses twice, and a data-driven test parses once per data row plus one.Change
Cache the built property in a static
ConditionalWeakTable<TestMethod, TestMethodIdentifierProperty>keyed on theTestMethodinstance. Per repeated conversion this removes:ParseManagedMethodNamecall (scans the managed signature, allocates the parameter-type list/array)stringallocations (thenamespaceandtypeNamesubstrings)TestMethodIdentifierPropertyallocationThe cache lives in the converter rather than as a lazy property on
TestMethod(the wayTestMethod.ManagedTypeNamecaches itself) becauseTestMethodIdentifierPropertyis a Microsoft.Testing.Platform type andMSTestAdapter.PlatformServicesdoes not reference the platform; withinMSTest.TestAdapterthe platform is only reachable from theTestingPlatformAdapterfolder via the file-levelRS0030suppression.Behavior is unchanged: the null/empty guards are equivalent (
HasManagedMethodAndTypePropertiesalready subsumes the removedIsNullOrEmpty(ManagedMethodName)check), andInvalidManagedNameExceptionstill propagates out of the factory with nothing cached, so it is re-thrown identically on the next call.Safety notes
ManagedMethodNameandFullClassNameare assigned only in theTestMethodconstructor;Clone(),CloneWithSource(),CloneWithUpdatedSource()and the[Serializable]app-domain path all produce new key objects with identical values, so the cache can only miss, never go stale.TestMethodIdentifierProperty(includingParameterTypeFullNames) insrc/Platform/**is read-only. That invariant is called out in the XML doc, since the type is not deeply immutable.TestMethodbecomes unreachable, so there is no unbounded growth.ConditionalWeakTable.GetValueis thread-safe; concurrent misses may each run the factory, but a single value is published.#pragma warning disable IDE0028is required: a collection expression onConditionalWeakTableisCS9174onnet462, which lacksIEnumerable<KeyValuePair<,>>on that type.Tests
Three tests added to
MSTestTestNodeConverterTests:ToResultTestNode_ReusesTestMethodIdentifier_FromInProgressNode— regression guard; fails without the cache.ToDiscoveredTestNode_DoesNotShareTestMethodIdentifier_AcrossDistinctTestMethods— the cache must not conflate two test methods that share a class name.ToDiscoveredTestNode_DoesNotAddTestMethodIdentifier_WhenManagedTypeNameIsEmpty— pins the pre-existing empty-ManagedTypeNameguard that moved into the fast path.Full repo
build.cmdis clean (0 warnings, 0 errors);MSTestAdapter.UnitTestsis 67/67 green on bothnet9.0andnet462.