You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
π€ This PR was created by Perf Improver, an automated AI assistant focused on performance improvements.
Goal and Rationale
Reduce heap allocations in AttributeExtensions.IsIgnored, which is called twice per test execution β once for the test class (ClassType.IsIgnored) and once for the test method (MethodInfo.IsIgnored). Allocations here scale linearly with test count.
A yield iterator state machine from GetAttributes<T> (even when there are no ConditionBaseAttribute present)
A LINQ GroupBy operator object
After:
Attribute[]allAttributes=ReflectHelper.Instance.GetCustomAttributesCached(type);// Fast path: no ConditionBaseAttribute (common case) β zero allocationsboolhasConditionAttr=false;foreach(AttributeattrinallAttributes){if(attrisConditionBaseAttribute){hasConditionAttr=true;break;}}if(!hasConditionAttr){ignoreMessage=null;returnfalse;}// Uncommon path: manual grouping with Dictionary (lazy allocation)Dictionary<string,List<ConditionBaseAttribute>>groups=[];
...
Direct iteration over the cached Attribute[] β no iterator, no LINQ operator. The Dictionary is allocated only when a ConditionBaseAttribute is present (the uncommon case).
Performance Evidence
Methodology: Object allocation count per IsIgnored call.
Scenario
Before (objects)
After
No ConditionBaseAttribute (common)
1 yield iterator + 1 GroupBy operator
0
[Ignore] / condition attr present
1 yield iterator + LINQ Lookup internals
1 Dictionary + lists
Per test execution (class + method)
~4 iterator/LINQ objects
0 (common case)
For a test suite with 1,000 tests: eliminates ~4,000 short-lived LINQ objects per run in the typical case.
Trade-offs
Manual grouping logic is slightly more verbose, but follows the established allocation-free pattern used by GetTestPropertiesAsTraits and GetTestCategories in the same codebase.
The Dictionary allocated in the uncommon case replaces a LINQ Lookup<> β comparable or better allocation profile.
Semantics are identical: OR within group, AND across groups, first non-null ignore message reported.
β MSTestAdapter.PlatformServices builds with 0 warnings, 0 errors on net8.0.
Note: MSTestAdapter.PlatformServices.UnitTests has a pre-existing build failure in this environment due to AwesomeAssertions only shipping netstandard2.1 (no net8.0 folder). This is unrelated to these changes. CI will run the full test suite.
The patch file is available in the agent artifact in the workflow run linked above.
To create a pull request with the changes:
# Download the artifact from the workflow run
gh run download 25378671157 -n agent -D /tmp/agent-25378671157
# Create a new branch
git checkout -b perf-assist/eliminate-linq-allocations-isignored-a44bead3bb99114d
# Apply the patch (--3way handles cross-repo patches where files may already exist)
git am --3way /tmp/agent-25378671157/aw-perf-assist-eliminate-linq-allocations-isignored.patch
# Push the branch to origin
git push origin perf-assist/eliminate-linq-allocations-isignored-a44bead3bb99114d
# Create the pull request
gh pr create --title '[Perf Improver] perf: eliminate LINQ allocations in IsIgnored hot path' --base main --head perf-assist/eliminate-linq-allocations-isignored-a44bead3bb99114d --repo microsoft/testfx
Show patch preview (88 of 88 lines)
From b401892b6a5b7797eb1d13ea45192e8441fe9768 Mon Sep 17 00:00:00 2001
From: "github-actions[bot]" <github-actions[bot]@users.noreply.github.com>
Date: Tue, 5 May 2026 13:22:39 +0000
Subject: [PATCH] perf: eliminate LINQ allocations in IsIgnored hot path
Replace GetAttributes<ConditionBaseAttribute> (yield iterator) + GroupBy
(LINQ operator) with direct iteration of GetCustomAttributesCached().
IsIgnored() is called twice per test execution (class + method), so
allocations here scale linearly with test count.
Fast path: when no ConditionBaseAttribute is present (common case),
exits with zero allocations. Uncommon path uses a Dictionary instead
of a LINQ Lookup.
Preserves semantics: OR within group, AND across groups, first ignore
message reported per unsatisfied group.
Follows the allocation-free pattern from GetTestPropertiesAsTraits and
GetTestCategories in the same codebase.
Closes #7992
Closes #7993
Closes #8000
Closes #8016
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
---
.../Helpers/AttributeHelpers.cs | 42 +++++++++++++++++--
1 file changed, 39 insertions(+), 3 deletions(-)
diff --git a/src/Adapter/MSTestAdapter.PlatformServices/Helpers/AttributeHelpers.cs b/src/Adapter/MSTestAdapter.PlatformServices/Helpers/AttributeHelpers.cs
index a2bbcbe..b45ce77 100644
--- a/src/Adapter/MSTestAdapter.PlatformServices/Helpers/AttributeHelpers.cs+++ b/src/Adapter/MSTestAdapter.PlatformServices/Helpers/AttributeHelpers.cs@@ -9,9 +9,45 @@ internal static class AttributeExtensions
{
public static bool IsIgnored(this ICustomAttributeProvider type, out string? ignoreMessage)
{
- IEnumerable<ConditionBaseAttribute> attributes = ReflectHelper.Instance.GetAttributes<ConditionBaseAttribute>(type);- IEnumerable<IGrouping<string, ConditionBaseAttribute>> groups = attributes.GroupBy(attr => attr.GroupName);- foreach (IGrouping<string, ConditionBaseAttribute>? group in groups)+ Attribute[] allAt
... (truncated)
π€ This PR was created by Perf Improver, an automated AI assistant focused on performance improvements.
Goal and Rationale
Reduce heap allocations in
AttributeExtensions.IsIgnored, which is called twice per test execution β once for the test class (ClassType.IsIgnored) and once for the test method (MethodInfo.IsIgnored). Allocations here scale linearly with test count.Approach
Before:
Every call allocates:
GetAttributes<T>(even when there are noConditionBaseAttributepresent)GroupByoperator objectAfter:
Direct iteration over the cached
Attribute[]β no iterator, no LINQ operator. TheDictionaryis allocated only when aConditionBaseAttributeis present (the uncommon case).Performance Evidence
Methodology: Object allocation count per
IsIgnoredcall.ConditionBaseAttribute(common)[Ignore]/ condition attr presentFor a test suite with 1,000 tests: eliminates ~4,000 short-lived LINQ objects per run in the typical case.
Trade-offs
GetTestPropertiesAsTraitsandGetTestCategoriesin the same codebase.Dictionaryallocated in the uncommon case replaces a LINQLookup<>β comparable or better allocation profile.Reproducibility
Test Status
β
MSTestAdapter.PlatformServicesbuilds with 0 warnings, 0 errors on net8.0.Closes #7992
Closes #7993
Closes #8000
Closes #8016
Note
This was originally intended as a pull request, but the git push operation failed.
Workflow Run: View run details and download patch artifact
The patch file is available in the
agentartifact in the workflow run linked above.To create a pull request with the changes:
Show patch preview (88 of 88 lines)