Skip to content

Cache TestMethodIdentifierProperty per TestMethod to avoid redundant parsing - #10366

Open
Evangelink wants to merge 1 commit into
mainfrom
dev/amauryleve/cache-test-method-identifier-property
Open

Cache TestMethodIdentifierProperty per TestMethod to avoid redundant parsing#10366
Evangelink wants to merge 1 commit into
mainfrom
dev/amauryleve/cache-test-method-identifier-property

Conversation

@Evangelink

Copy link
Copy Markdown
Member

Fixes #10363

Problem

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, both of which are immutable for the lifetime of the instance.

The same TestMethod instance is converted several times per executed test: TestExecutionManager.ExecuteTestsWithTestRunnerAsync reports the original currentTest element to RecordStartAsync (in-progress node) and then to SendTestResultsAsync, which loops over the results and calls RecordResultAsync once 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 the TestMethod instance. Per repeated conversion this removes:

  • ParseManagedMethodName call (scans the managed signature, allocates the parameter-type list/array)
  • string allocations (the namespace and typeName substrings)
  • TestMethodIdentifierProperty allocation

The cache lives in the converter rather than as a lazy property on TestMethod (the way TestMethod.ManagedTypeName caches itself) because TestMethodIdentifierProperty is a Microsoft.Testing.Platform type and MSTestAdapter.PlatformServices does not reference the platform; within MSTest.TestAdapter the platform is only reachable from the TestingPlatformAdapter folder via the file-level RS0030 suppression.

Behavior is unchanged: the null/empty guards are equivalent (HasManagedMethodAndTypeProperties already subsumes the removed IsNullOrEmpty(ManagedMethodName) check), and InvalidManagedNameException still propagates out of the factory with nothing cached, so it is re-thrown identically on the next call.

Safety notes

  • Key stabilityManagedMethodName and FullClassName are assigned only in the TestMethod constructor; 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.
  • Sharing — every consumer of TestMethodIdentifierProperty (including ParameterTypeFullNames) in src/Platform/** is read-only. That invariant is called out in the XML doc, since the type is not deeply immutable.
  • Lifetime — weak keys mean entries disappear as soon as the TestMethod becomes unreachable, so there is no unbounded growth.
  • Thread safetyConditionalWeakTable.GetValue is thread-safe; concurrent misses may each run the factory, but a single value is published.
  • The #pragma warning disable IDE0028 is required: a collection expression on ConditionalWeakTable is CS9174 on net462, which lacks IEnumerable<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-ManagedTypeName guard that moved into the fast path.

Full repo build.cmd is clean (0 warnings, 0 errors); MSTestAdapter.UnitTests is 67/67 green on both net9.0 and net462.

Note: the perf claim is based on code inspection (call-path analysis + removed allocations), not a benchmark.

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
Copilot AI review requested due to automatic review settings July 31, 2026 18:19

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

Caches parsed MSTest method identifiers during MTP node conversion to reduce repeated parsing and allocations.

Changes:

  • Adds a weak per-TestMethod identifier 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

Comment on lines +196 to +197
TestMethodIdentifierProperty testMethodIdentifier = TestMethodIdentifierCache.GetValue(testMethod, BuildTestMethodIdentifier);
testNode.Properties.Add(testMethodIdentifier);

@github-actions github-actions Bot 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.

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.

@github-actions

Copy link
Copy Markdown
Contributor

🧪 Test quality grade — PR #10366

GradeTestMutationNotesHow to improve
A (90–100) new MSTestTestNodeConverterTests.
ToDiscoveredTestNode_
DoesNotAddTestMethodIdentifier_
WhenManagedTypeNameIsEmpty
2/2 killed Precisely targets the new guard condition; absence assertion kills both the missing-clause and wrong-operator mutations.
A (90–100) new MSTestTestNodeConverterTests.
ToResultTestNode_
ReusesTestMethodIdentifier_
FromInProgressNode
3/3 killed BeSameAs reference-identity check directly validates the caching contract and kills any value-equal-but-distinct-instance mutation.
A (90–100) new MSTestTestNodeConverterTests.
ToDiscoveredTestNode_
DoesNotShareTestMethodIdentifier_
AcrossDistinctTestMethods
3/3 killed Five meaningful assertions (NotBeNull ×2, NotBeSameAs, MethodName ×2) collectively guard against cache-key conflation and value-swapping mutations.

This advisory comment was generated automatically. Grades are heuristic and informational — they do not block merging. Re-run with /grade-tests.

🤖 Automated content by GitHub Copilot. Generated by the Grade Tests on PR (on open / sync) workflow. · sonnet46 42.7 AIC · ⌖ 4.87 AIC · ⊞ 11.8K · [◷]( · )

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

[perf-improver] Cache TestMethodIdentifierProperty per TestMethod to avoid redundant parsing

2 participants