From 811dab0d57c615b98bf18a1a789740059511ff4c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Amaury=20Lev=C3=A9?= Date: Sun, 28 Jun 2026 00:17:46 +0200 Subject: [PATCH] perf: eliminate LINQ iterator allocations in SourceGeneratedReflectionOperations attribute helpers MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Replace OfType().Any(), OfType().FirstOrDefault(), and OfType().Take(2) LINQ chains in IsAttributeDefined, GetFirstAttributeOrDefault, and GetSingleAttributeOrDefault with direct foreach loops over the cached Attribute[] array. Each LINQ operator (OfType, Any/FirstOrDefault/Take) allocates a heap-based iterator state machine. With direct foreach, the array's struct enumerator is used — zero heap allocations. These methods are called per test method and per test class during attribute discovery (e.g. GetSingleAttributeOrDefault in TestMethodInfo, IsAttributeDefined in TypeEnumerator). In a suite with source-generated reflection enabled, eliminating these allocations reduces Gen-0 GC pressure across the entire test run. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- .../SourceGeneratedReflectionOperations.cs | 48 +++++++++++++++---- 1 file changed, 38 insertions(+), 10 deletions(-) diff --git a/src/Adapter/MSTestAdapter.PlatformServices/SourceGeneration/SourceGeneratedReflectionOperations.cs b/src/Adapter/MSTestAdapter.PlatformServices/SourceGeneration/SourceGeneratedReflectionOperations.cs index a925434152..cf6ed80411 100644 --- a/src/Adapter/MSTestAdapter.PlatformServices/SourceGeneration/SourceGeneratedReflectionOperations.cs +++ b/src/Adapter/MSTestAdapter.PlatformServices/SourceGeneration/SourceGeneratedReflectionOperations.cs @@ -287,24 +287,52 @@ private static bool TryInvokeMatchingConstructor( public bool IsAttributeDefined(ICustomAttributeProvider attributeProvider) where TAttribute : Attribute - => attributeProvider is null - ? throw new ArgumentNullException(nameof(attributeProvider)) - : GetCustomAttributesCached(attributeProvider).OfType().Any(); + { + foreach (Attribute attr in GetCustomAttributesCached(attributeProvider)) + { + if (attr is TAttribute) + { + return true; + } + } + + return false; + } public TAttribute? GetFirstAttributeOrDefault(ICustomAttributeProvider attributeProvider) where TAttribute : Attribute - => GetCustomAttributesCached(attributeProvider).OfType().FirstOrDefault(); + { + foreach (Attribute attr in GetCustomAttributesCached(attributeProvider)) + { + if (attr is TAttribute match) + { + return match; + } + } + + return null; + } public TAttribute? GetSingleAttributeOrDefault(ICustomAttributeProvider attributeProvider) where TAttribute : Attribute { - TAttribute[] matches = [.. GetCustomAttributesCached(attributeProvider).OfType().Take(2)]; - return matches.Length switch + TAttribute? first = null; + foreach (Attribute attr in GetCustomAttributesCached(attributeProvider)) { - 0 => null, - 1 => matches[0], - _ => throw new InvalidOperationException(string.Format(CultureInfo.InvariantCulture, "Found multiple attributes of type '{0}' when only one was expected.", typeof(TAttribute))), - }; + if (attr is not TAttribute match) + { + continue; + } + + if (first is not null) + { + throw new InvalidOperationException(string.Format(CultureInfo.InvariantCulture, "Found multiple attributes of type '{0}' when only one was expected.", typeof(TAttribute))); + } + + first = match; + } + + return first; } public IEnumerable GetAttributes(ICustomAttributeProvider attributeProvider)