Skip to content

[Perf Improver] perf: eliminate LINQ iterator allocations in GetTestCategories and GetTestFromMethod#7927

Merged
Evangelink merged 2 commits intomainfrom
perf-assist/reduce-linq-iterators-get-test-categories-d392d71fd502f8cc
Apr 30, 2026
Merged

[Perf Improver] perf: eliminate LINQ iterator allocations in GetTestCategories and GetTestFromMethod#7927
Evangelink merged 2 commits intomainfrom
perf-assist/reduce-linq-iterators-get-test-categories-d392d71fd502f8cc

Conversation

@Evangelink
Copy link
Copy Markdown
Member

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

…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>
Copilot AI review requested due to automatic review settings April 29, 2026 05:41
Copy link
Copy Markdown
Contributor

Copilot AI left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.GetTestCategories to iterate cached attribute arrays directly rather than using chained LINQ (Concat/SelectMany).
  • Updated TypeEnumerator.GetTestFromMethod to avoid LINQ allocations for parameter-type string building and to collect WorkItemAttribute IDs without OfType() 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

Copy link
Copy Markdown
Member Author

@Evangelink Evangelink left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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) and GetTestFromMethod (TypeEnumerator.cs), replacing LINQ chains with direct foreach loops.
  • Existing test coverage is adequateReflectHelperTests.cs has 7 test methods exercising GetTestCategories across method/type/assembly levels. TypeEnumeratorTests.cs covers GetTestFromMethod including GetTestFromMethodShouldSetWorkItemIds (two WorkItemAttribute instances) and GetTestFromMethodShouldSetWorkItemIdsToNullIfNotAny.
  • Behavior is unchanged — The refactoring preserves identical semantics: empty arrays return [], WorkItemIds is null when absent, and BeEquivalentTo assertions 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 🧪

Copy link
Copy Markdown
Member Author

@Evangelink Evangelink left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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 used OfType<WorkItemAttribute>() which scanned forward (index 0→n-1), so WorkItemIds was 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 uses BeEquivalentTo (order-insensitive). Worth noting for awareness, but not a bug.

  • GetTestCategories refactor: The three-pass iteration over cached Attribute[] arrays exactly mirrors the behavior of the previous GetAttributes<TestCategoryBaseAttribute> calls (which also called GetCustomAttributesCached internally). 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 original string.Join(",", GetParameters().Select(...))string.Join on an empty sequence returns "", same as string.Empty. The short-circuit also avoids the Array.ConvertAll allocation 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 🧠

Copy link
Copy Markdown
Contributor

Copilot AI commented Apr 29, 2026

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:

  • c50vsblobprodcus330.vsblob.vsassets.io
    • Triggering command: /usr/bin/dotnet dotnet test test/UnitTests/MSTestAdapter.UnitTests/MSTestAdapter.UnitTests.csproj -v q --filter TypeEnumerator (dns block)
  • jd4vsblobprodcus366.vsblob.vsassets.io
    • Triggering command: /usr/bin/dotnet dotnet test test/UnitTests/MSTestAdapter.UnitTests/MSTestAdapter.UnitTests.csproj -v q --filter TypeEnumerator (dns block)

If you need me to access, download, or install something from one of these locations, you can either:

@Evangelink Evangelink merged commit 35cdd6b into main Apr 30, 2026
24 checks passed
@Evangelink Evangelink deleted the perf-assist/reduce-linq-iterators-get-test-categories-d392d71fd502f8cc branch April 30, 2026 14:16
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] perf: eliminate LINQ iterator allocations in GetTestCategories and GetTestFromMethod

3 participants