From 2eaf0d786bc27c00a50978556d6d00603ae85a3b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Amaury=20Lev=C3=A9?= Date: Sat, 27 Jun 2026 00:16:30 +0200 Subject: [PATCH] perf: eliminate LINQ iterator allocations in DerivesFrom interface check Replace the OfType() + optional Select() + Contains() chain with a direct foreach over ImmutableArray. Before: IEnumerable allInterfaces = symbol.AllInterfaces.OfType(); if (useOrigDef) allInterfaces = allInterfaces.Select(i => i.OriginalDefinition); allInterfaces.Contains(candidateBaseType, SymbolEqualityComparer.Default); After: bool useOrigDef = ...; foreach (INamedTypeSymbol iface in symbol.AllInterfaces) { ITypeSymbol candidate = useOrigDef ? iface.OriginalDefinition : iface; if (Equals(candidate, candidateBaseType)) return true; } Each LINQ operator (OfType, Select, Contains) allocates a heap-based iterator state machine. ImmutableArray.GetEnumerator() returns a struct, so the foreach loop above is zero-allocation. DerivesFrom() is called from Inherits(), which is invoked 36+ times across the analyzer suite (per-symbol, per-method). On a solution with many test classes this fires thousands of times per analysis pass, so eliminating 1-2 iterator allocations per call measurably reduces GC pressure. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- .../ITypeSymbolExtensions.cs | 21 +++++++++++-------- 1 file changed, 12 insertions(+), 9 deletions(-) diff --git a/src/Analyzers/MSTest.Analyzers/RoslynAnalyzerHelpers/ITypeSymbolExtensions.cs b/src/Analyzers/MSTest.Analyzers/RoslynAnalyzerHelpers/ITypeSymbolExtensions.cs index a76b4c0dc4..12401c104c 100644 --- a/src/Analyzers/MSTest.Analyzers/RoslynAnalyzerHelpers/ITypeSymbolExtensions.cs +++ b/src/Analyzers/MSTest.Analyzers/RoslynAnalyzerHelpers/ITypeSymbolExtensions.cs @@ -48,16 +48,19 @@ public static bool DerivesFrom([NotNullWhen(returnValue: true)] this ITypeSymbol if (!baseTypesOnly && candidateBaseType.TypeKind == TypeKind.Interface) { - IEnumerable allInterfaces = symbol.AllInterfaces.OfType(); - if (SymbolEqualityComparer.Default.Equals(candidateBaseType.OriginalDefinition, candidateBaseType)) + // Avoid OfType() + optional Select() + Contains() chain — each creates a heap-allocated + // LINQ iterator state machine. AllInterfaces is ImmutableArray; iterating it + // directly uses the struct enumerator (zero heap allocation). + bool useOriginalDefinition = SymbolEqualityComparer.Default.Equals(candidateBaseType.OriginalDefinition, candidateBaseType); + foreach (INamedTypeSymbol iface in symbol.AllInterfaces) { - // Candidate base type is not a constructed generic type, so use original definition for interfaces. - allInterfaces = allInterfaces.Select(i => i.OriginalDefinition); - } - - if (allInterfaces.Contains(candidateBaseType, SymbolEqualityComparer.Default)) - { - return true; + // When the candidate is not a constructed generic type, compare via OriginalDefinition + // (mirrors the original Select(i => i.OriginalDefinition) projection). + ITypeSymbol candidate = useOriginalDefinition ? iface.OriginalDefinition : iface; + if (SymbolEqualityComparer.Default.Equals(candidate, candidateBaseType)) + { + return true; + } } }