Uh oh!
There was an error while loading. Please reload this page.
Cache the parsed managed name per TestMethod to avoid redundant parsing - #10366
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
Uh oh!
There was an error while loading. Please reload this page.
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.
This comment has been minimized.
This comment has been minimized.
TestMethodIdentifierProperty publicly exposes its ParameterTypeFullNames array, so handing a single cached instance to several TestNodes let any consumer that writes to that array corrupt every other node built from the same TestMethod - a behavior change for external platform extensions. Cache the parsed pieces (namespace, type name, method name, arity and parameter types) instead and build a fresh property, with its own parameter array, per node. The expensive part - ParseManagedMethodName plus the two substring allocations - is still paid once per TestMethod. An empty parameter array cannot be mutated, so the common parameterless case still allocates no array. 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.
Review details
Suppressed comments (2)
test/UnitTests/MSTestAdapter.UnitTests/MSTestTestNodeConverterTests.cs:346
- This assertion only checks structural equality, so it also passes with the pre-change implementation that reparses every node; none of these tests currently fail if
ParsedManagedNameCacheis removed. Add reference-identity assertions for cached parse outputs such asNamespaceandTypeNameso the intended reuse is covered while retaining the separate parameter-array assertion below.
result.Should().Be(inProgress);
src/Adapter/MSTest.TestAdapter/TestingPlatformAdapter/MSTestTestNodeConverter.cs:196
- The implementation caches
ParsedManagedNamebut still callsToProperty()for every conversion, allocating a newTestMethodIdentifierProperty(and cloning non-empty parameter arrays). This materially differs from the PR title/description, which says the built property is cached and counts its allocation among the savings. Please update the PR metadata and performance accounting to describe the parse-only cache and its actual allocation savings.
// The method group conversion is cached by the compiler, so the lookup does not allocate a delegate.
TestMethodIdentifierProperty testMethodIdentifier = ParsedManagedNameCache.GetValue(testMethod, ParsedManagedName.Parse).ToProperty();
- Files reviewed: 2/2 changed files
- Comments generated: 0 new
- Review effort level: Balanced
This comment has been minimized.
This comment has been minimized.
The value-equality assertion alone passed even with ParsedManagedNameCache removed, so it only pinned correctness, not reuse. Namespace and TypeName are partial Substring results of ManagedTypeName, so re-running the parse hands back fresh string instances; asserting reference equality on them fails when the cache is bypassed and therefore guards the caching itself. Verified by temporarily calling ParsedManagedName.Parse directly: the test fails, and passes again once the cache lookup is restored. Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com> Copilot-Session: 01fd9297-53cd-4532-b294-a8da5723825d
Amaury Levé (Evangelink)
commented
Aug 1, 2026
Both suppressed comments from the latest review were valid. Acted on both in e8e0600.
Correct, and my miss: when I moved from caching the property to caching the parse, the old
Agreed. The description was already rewritten for the parse-only cache, and I have now updated the title to match. The accounting claims only what is actually saved per repeated conversion: one |
🧪 Test quality grade — PR #10366
This advisory comment was generated automatically. Grades are heuristic and informational — they do not block merging. Re-run with
|
Uh oh!
There was an error while loading. Please reload this page.
Fixes#10363
Problem
MSTestTestNodeConverter.AddTestMethodIdentifiercalledManagedNameParser.ParseManagedMethodNameand re-derived the namespace/type substrings on every node conversion — even though the parse 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 parsed pieces — namespace, type name, method name, arity and parameter types — in a static
ConditionalWeakTable<TestMethod, ParsedManagedName>keyed on theTestMethodinstance. Per repeated conversion this removes:ParseManagedMethodNamecall (scans the managed signature, allocates the parameter-type list/array)stringallocations (thenamespaceandtypeNamesubstrings)Each node still builds its own
TestMethodIdentifierProperty, with its own parameter array. That type publicly exposesParameterTypeFullNames, andTestNode.Propertiesis handed to arbitrary platform extensions, so caching and sharing the property instance itself would let any consumer that writes to the array corrupt every other node built from the same test method. An empty array cannot be mutated, so the (overwhelmingly common) parameterless case still allocates no array.The 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.TestNodegets a freshly built property and a non-aliased parameter array.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
Four tests added to
MSTestTestNodeConverterTests:ToResultTestNode_ProducesEquivalentTestMethodIdentifier_AsInProgressNode— the cached parse must yield an identifier equal on every field across nodes.ToResultTestNode_DoesNotShareParameterTypeArray_WithInProgressNode— pins the non-aliasing guarantee using a parameterized managed name.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 68/68 green on bothnet9.0andnet462.