[Perf Improver] perf: eliminate LINQ iterator allocations in GetTestCategories and GetTestFromMethod#7927
Conversation
…tTestFromMethod In the test discovery hot path, eliminate several LINQ iterator allocations per test method: 1. ReflectHelper.GetTestCategories: replace 3x GetAttributes<T>() yield iterators + 2x Concat + 1x SelectMany (6 iterator objects) with direct iteration over GetCustomAttributesCached() arrays. Uses the same allocation-free pattern already applied to GetTestPropertiesAsTraits. Most test methods have no categories so this avoids all allocations in the common case. 2. TypeEnumerator.GetTestFromMethod (WorkItemAttribute): fold the WorkItemAttribute scan into the existing attribute loop, eliminating the double-pass OfType().Any() + OfType().Select() pattern (2-3 iterator objects per method with WorkItem attrs; 1 OfType iterator even without). 3. TypeEnumerator.GetTestFromMethod (parameter types string): short-circuit the LINQ Select iterator for the common case of parameterless test methods by checking parameters.Length == 0 first. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
There was a problem hiding this comment.
Pull request overview
This PR optimizes MSTest adapter discovery hot paths by removing several LINQ-based enumerations and iterators during per-method metadata collection, reducing allocations and GC pressure during test discovery.
Changes:
- Reworked
ReflectHelper.GetTestCategoriesto iterate cached attribute arrays directly rather than using chained LINQ (Concat/SelectMany). - Updated
TypeEnumerator.GetTestFromMethodto avoid LINQ allocations for parameter-type string building and to collectWorkItemAttributeIDs withoutOfType()passes.
Show a summary per file
| File | Description |
|---|---|
| src/Adapter/MSTestAdapter.PlatformServices/Helpers/ReflectHelper.cs | Replaces LINQ category aggregation with direct iteration over cached attribute arrays. |
| src/Adapter/MSTestAdapter.PlatformServices/Discovery/TypeEnumerator.cs | Avoids iterator allocations for parameter type formatting and folds WorkItem ID collection into the attribute scan loop. |
Copilot's findings
- Files reviewed: 2/2 changed files
- Comments generated: 1
Evangelink
left a comment
There was a problem hiding this comment.
Summary
Workflow: Test Expert Reviewer 🧪
Date: 2026-04-29
Repository: microsoft/testfx
Key Findings
- No test files changed — This PR is a pure performance refactoring of
GetTestCategories(ReflectHelper.cs) andGetTestFromMethod(TypeEnumerator.cs), replacing LINQ chains with directforeachloops. - Existing test coverage is adequate —
ReflectHelperTests.cshas 7 test methods exercisingGetTestCategoriesacross method/type/assembly levels.TypeEnumeratorTests.cscoversGetTestFromMethodincludingGetTestFromMethodShouldSetWorkItemIds(twoWorkItemAttributeinstances) andGetTestFromMethodShouldSetWorkItemIdsToNullIfNotAny. - Behavior is unchanged — The refactoring preserves identical semantics: empty arrays return
[],WorkItemIdsisnullwhen absent, andBeEquivalentToassertions in the existing WorkItemIds tests confirm order-independence is acceptable. - No isolation or flakiness concerns — No shared mutable state introduced; no timing dependencies.
Recommendations
No changes required. The optimization is clean, well-scoped, and backed by existing test coverage.
Generated by Test Expert Reviewer
🧪 Test quality reviewed by Test Expert Reviewer 🧪
Evangelink
left a comment
There was a problem hiding this comment.
Summary
Workflow: Expert Code Reviewer 🧠
Date: 2026-04-29
Repository: microsoft/testfx
Key Findings
No blocking issues found. The changes are correct performance improvements.
Observations (no action needed):
-
WorkItemIds ordering (
TypeEnumerator.cs): The original code usedOfType<WorkItemAttribute>()which scanned forward (index 0→n-1), soWorkItemIdswas in declaration order. The new code collects work items inside the existing backward loop (index n-1→0), meaning the array is in reversed declaration order. This is inconsequential in practice — work item IDs are used for membership-based test filtering, and the existing test usesBeEquivalentTo(order-insensitive). Worth noting for awareness, but not a bug. -
GetTestCategoriesrefactor: The three-pass iteration over cachedAttribute[]arrays exactly mirrors the behavior of the previousGetAttributes<TestCategoryBaseAttribute>calls (which also calledGetCustomAttributesCachedinternally). Semantics are preserved, and the hot-path LINQ iterator + delegate allocations are eliminated. -
Parameter type join:
parameters.Length == 0 ? string.Empty : string.Join(",", Array.ConvertAll(...))is semantically equivalent to the originalstring.Join(",", GetParameters().Select(...))—string.Joinon an empty sequence returns"", same asstring.Empty. The short-circuit also avoids theArray.ConvertAllallocation for parameterless methods (the common case in MSTest).
Recommendations
None — the changes are a clean, well-scoped performance optimization consistent with the allocation-free pattern already established in GetTestPropertiesAsTraits.
Generated by Expert Code Reviewer
🧠 Reviewed by Expert Code Reviewer 🧠
Agent-Logs-Url: https://github.com/microsoft/testfx/sessions/2295393d-641f-4a1e-b849-be7bbf9ba3d1 Co-authored-by: Evangelink <11340282+Evangelink@users.noreply.github.com>
|
Just as a heads up, I was blocked by some firewall rules while working on your feedback. Expand below for details. Warning Firewall rules blocked me from connecting to one or more addresses (expand for details)I tried to connect to the following addresses, but was blocked by firewall rules:
If you need me to access, download, or install something from one of these locations, you can either:
|
This pull request optimizes the way test categories and work item IDs are collected during test discovery in the MSTest adapter. The changes focus on reducing memory allocations by replacing LINQ-based code with direct iteration, improving performance and efficiency.
Fixes #7868