From d6283bfd572640e5088271377ed325271c651422 Mon Sep 17 00:00:00 2001
From: Tom Longhurst <30480171+thomhurst@users.noreply.github.com>
Date: Thu, 11 Jun 2026 20:31:29 +0100
Subject: [PATCH 1/2] perf: reduce allocations in source-gen test building hot
 paths

- TestBuilder streaming path: replace LINQ Where/Cast/FirstOrDefault
  attribute lookup with a foreach, matching the non-streaming path
- TestBuilderPipeline: hoist ToAttributeDictionary() and the
  TestClassInstanceFactory delegate out of the repeat loops in both
  dynamic-test paths so iterations share one dictionary and closure
- DataSourceHelpers.UnwrapTupleWithTypes: write into a pre-sized array
  instead of List<T> + ToArray (both former branches were identical)
- DataSourceHelpers.InvokeIfFunc: fetch GetType() once
---
 TUnit.Core/Helpers/DataSourceHelpers.cs      | 29 ++++++--------------
 TUnit.Engine/Building/TestBuilder.cs         | 13 ++++++---
 TUnit.Engine/Building/TestBuilderPipeline.cs | 20 +++++++++-----
 3 files changed, 30 insertions(+), 32 deletions(-)

diff --git a/TUnit.Core/Helpers/DataSourceHelpers.cs b/TUnit.Core/Helpers/DataSourceHelpers.cs
index 2c11963bb9..cc79436c4f 100644
--- a/TUnit.Core/Helpers/DataSourceHelpers.cs
+++ b/TUnit.Core/Helpers/DataSourceHelpers.cs
@@ -23,7 +23,8 @@ public static class DataSourceHelpers
             return null;
         }
 
-        if(value.GetType().IsGenericType && value.GetType().GetGenericTypeDefinition() == typeof(Func<>))
+        var type = value.GetType();
+        if(type.IsGenericType && type.GetGenericTypeDefinition() == typeof(Func<>))
         {
             return ((Delegate)value).DynamicInvoke();
         }
@@ -426,30 +427,16 @@ public static T InvokeIfFunc<T>(object? value)
         // Try to use ITuple interface first for any ValueTuple type
         if (value is ITuple tuple)
         {
-            var result = new List<object?>();
-            var typeIndex = 0;
+            // Elements map 1:1 to expected parameters; nested tuples are kept as-is
+            var count = Math.Min(tuple.Length, expectedTypes.Length);
+            var result = new object?[count];
 
-            for (var i = 0; i < tuple.Length && typeIndex < expectedTypes.Length; i++)
+            for (var i = 0; i < count; i++)
             {
-                var element = tuple[i];
-                var expectedType = expectedTypes[typeIndex];
-
-                // Check if the expected type is a tuple type
-                if (TupleHelper.IsTupleType(expectedType) && IsTuple(element))
-                {
-                    // Keep nested tuple as-is
-                    result.Add(element);
-                    typeIndex++;
-                }
-                else
-                {
-                    // Add element normally
-                    result.Add(element);
-                    typeIndex++;
-                }
+                result[i] = tuple[i];
             }
 
-            return result.ToArray();
+            return result;
         }
 #endif
 
diff --git a/TUnit.Engine/Building/TestBuilder.cs b/TUnit.Engine/Building/TestBuilder.cs
index b41cff79e4..ae8a22bb70 100644
--- a/TUnit.Engine/Building/TestBuilder.cs
+++ b/TUnit.Engine/Building/TestBuilder.cs
@@ -1605,10 +1605,15 @@ public async IAsyncEnumerable<AbstractExecutableTest> BuildTestsStreamingAsync(
         // Check for ClassConstructor attribute and set it early if present
         // Look for any attribute that inherits from ClassConstructorAttribute
         // This handles both ClassConstructorAttribute and ClassConstructorAttribute<T>
-        var classConstructorAttribute = attributes
-            .Where(a => a is ClassConstructorAttribute)
-            .Cast<ClassConstructorAttribute>()
-            .FirstOrDefault();
+        ClassConstructorAttribute? classConstructorAttribute = null;
+        foreach (var attribute in attributes)
+        {
+            if (attribute is ClassConstructorAttribute cca)
+            {
+                classConstructorAttribute = cca;
+                break;
+            }
+        }
 
         if (classConstructorAttribute != null)
         {
diff --git a/TUnit.Engine/Building/TestBuilderPipeline.cs b/TUnit.Engine/Building/TestBuilderPipeline.cs
index 1879ed045e..929fb30f5c 100644
--- a/TUnit.Engine/Building/TestBuilderPipeline.cs
+++ b/TUnit.Engine/Building/TestBuilderPipeline.cs
@@ -210,6 +210,11 @@ private async Task<AbstractExecutableTest[]> GenerateDynamicTests(TestMetadata m
         // Get dynamic test metadata for DisplayName support
         var dynamicTestMetadata = metadata as IDynamicTestMetadata;
 
+        // Hoist loop-invariant state so repeat iterations share one attribute dictionary and factory delegate
+        var attributes = metadata.GetOrCreateAttributes();
+        var attributesByType = attributes.ToAttributeDictionary();
+        Func<Task<object>> instanceFactory = () => Task.FromResult(metadata.InstanceFactory(Type.EmptyTypes, []));
+
         return await Enumerable.Range(0, repeatCount + 1)
             .SelectAsync(async repeatIndex =>
         {
@@ -218,7 +223,7 @@ private async Task<AbstractExecutableTest[]> GenerateDynamicTests(TestMetadata m
             var dynamicTestIndex = dynamicTestMetadata?.DynamicTestIndex ?? 0;
             var testData = new TestBuilder.TestData
             {
-                TestClassInstanceFactory = () => Task.FromResult(metadata.InstanceFactory(Type.EmptyTypes, [])),
+                TestClassInstanceFactory = instanceFactory,
                 ClassDataSourceAttributeIndex = 0,
                 ClassDataLoopIndex = 0,
                 ClassData = [],
@@ -239,9 +244,6 @@ private async Task<AbstractExecutableTest[]> GenerateDynamicTests(TestMetadata m
                 ? $"{baseDisplayName} (Repeat {repeatIndex + 1}/{repeatCount + 1})"
                 : baseDisplayName;
 
-            // Get attributes first
-            var attributes = metadata.GetOrCreateAttributes();
-
             // Create TestDetails for dynamic tests
             var testDetails = new TestDetails(attributes)
             {
@@ -259,7 +261,7 @@ private async Task<AbstractExecutableTest[]> GenerateDynamicTests(TestMetadata m
                 TestEndColumnNumber = metadata.EndColumnNumber,
                 ReturnType = typeof(Task),
                 MethodMetadata = metadata.MethodMetadata,
-                AttributesByType = attributes.ToAttributeDictionary()
+                AttributesByType = attributesByType
             };
 
             var testBuilderContext = CreateTestBuilderContext(metadata);
@@ -339,6 +341,10 @@ private async IAsyncEnumerable<AbstractExecutableTest> BuildTestsFromSingleMetad
                 // Get attributes for test details
                 var attributes = resolvedMetadata.GetOrCreateAttributes();
 
+                // Hoist loop-invariant state so repeat iterations share one attribute dictionary and factory delegate
+                var attributesByType = attributes.ToAttributeDictionary();
+                Func<Task<object>> instanceFactory = () => Task.FromResult(resolvedMetadata.InstanceFactory(Type.EmptyTypes, []));
+
                 // Dynamic tests need to honor attributes like RepeatCount, RetryCount, etc.
                 // We'll create multiple test instances based on RepeatCount
                 // Use DynamicTestIndex from the metadata to ensure unique test IDs for multiple dynamic tests
@@ -348,7 +354,7 @@ private async IAsyncEnumerable<AbstractExecutableTest> BuildTestsFromSingleMetad
                     // Create a simple TestData for ID generation
                     var testData = new TestBuilder.TestData
                     {
-                        TestClassInstanceFactory = () => Task.FromResult(resolvedMetadata.InstanceFactory(Type.EmptyTypes, [])),
+                        TestClassInstanceFactory = instanceFactory,
                         ClassDataSourceAttributeIndex = 0,
                         ClassDataLoopIndex = 0,
                         ClassData = [],
@@ -385,7 +391,7 @@ private async IAsyncEnumerable<AbstractExecutableTest> BuildTestsFromSingleMetad
                         TestEndColumnNumber = resolvedMetadata.EndColumnNumber,
                         ReturnType = typeof(Task),
                         MethodMetadata = resolvedMetadata.MethodMetadata,
-                        AttributesByType = attributes.ToAttributeDictionary()
+                        AttributesByType = attributesByType
                     };
 
                     var context = _contextProvider.CreateTestContext(

From dd027b48fdeb522843c69e62876d5cbce9fb73b2 Mon Sep 17 00:00:00 2001
From: Tom Longhurst <30480171+thomhurst@users.noreply.github.com>
Date: Thu, 11 Jun 2026 20:52:29 +0100
Subject: [PATCH 2/2] refactor: deduplicate attribute lookup and tuple copy
 loops

- Add internal FirstOfType<T> extension and use it at the three
  identical first-ClassConstructorAttribute foreach sites
- Share the ITuple element copy loop between UnwrapTupleAot and
  UnwrapTupleWithTypes via a private CopyTupleElements helper
---
 TUnit.Core/Helpers/DataSourceHelpers.cs       | 32 +++++++++----------
 TUnit.Engine/Building/TestBuilder.cs          | 20 ++----------
 TUnit.Engine/Building/TestBuilderPipeline.cs  | 11 ++-----
 .../Extensions/AttributeArrayExtensions.cs    | 21 ++++++++++++
 4 files changed, 40 insertions(+), 44 deletions(-)
 create mode 100644 TUnit.Engine/Extensions/AttributeArrayExtensions.cs

diff --git a/TUnit.Core/Helpers/DataSourceHelpers.cs b/TUnit.Core/Helpers/DataSourceHelpers.cs
index cc79436c4f..11ee4cf52d 100644
--- a/TUnit.Core/Helpers/DataSourceHelpers.cs
+++ b/TUnit.Core/Helpers/DataSourceHelpers.cs
@@ -75,6 +75,18 @@ public static T InvokeIfFunc<T>(object? value)
         return [() => Task.FromResult<object?>(value)];
     }
 
+#if NET5_0_OR_GREATER || NETCOREAPP3_0_OR_GREATER
+    private static object?[] CopyTupleElements(ITuple tuple, int count)
+    {
+        var result = new object?[count];
+        for (var i = 0; i < count; i++)
+        {
+            result[i] = tuple[i];
+        }
+        return result;
+    }
+#endif
+
     /// <summary>
     /// AOT-compatible tuple unwrapping that handles common tuple types without reflection
     /// </summary>
@@ -89,13 +101,7 @@ public static T InvokeIfFunc<T>(object? value)
         // Try to use ITuple interface first for any ValueTuple type (available in .NET Core 3.0+)
         if (value is ITuple tuple)
         {
-            var length = tuple.Length;
-            var result = new object?[length];
-            for (var i = 0; i < length; i++)
-            {
-                result[i] = tuple[i];
-            }
-            return result;
+            return CopyTupleElements(tuple, tuple.Length);
         }
 #endif
 
@@ -427,16 +433,8 @@ public static T InvokeIfFunc<T>(object? value)
         // Try to use ITuple interface first for any ValueTuple type
         if (value is ITuple tuple)
         {
-            // Elements map 1:1 to expected parameters; nested tuples are kept as-is
-            var count = Math.Min(tuple.Length, expectedTypes.Length);
-            var result = new object?[count];
-
-            for (var i = 0; i < count; i++)
-            {
-                result[i] = tuple[i];
-            }
-
-            return result;
+            // Elements are copied 1:1, capped at the expected parameter count
+            return CopyTupleElements(tuple, Math.Min(tuple.Length, expectedTypes.Length));
         }
 #endif
 
diff --git a/TUnit.Engine/Building/TestBuilder.cs b/TUnit.Engine/Building/TestBuilder.cs
index ae8a22bb70..aa6f1e9496 100644
--- a/TUnit.Engine/Building/TestBuilder.cs
+++ b/TUnit.Engine/Building/TestBuilder.cs
@@ -173,15 +173,7 @@ public async Task<IEnumerable<AbstractExecutableTest>> BuildTestsFromMetadataAsy
             TestBuilderContext.Current = testBuilderContext;
 
             // Check for ClassConstructor attribute and set it early if present (reuse already created attributes)
-            ClassConstructorAttribute? classConstructorAttribute = null;
-            foreach (var attr in attributes)
-            {
-                if (attr is ClassConstructorAttribute cca)
-                {
-                    classConstructorAttribute = cca;
-                    break;
-                }
-            }
+            var classConstructorAttribute = attributes.FirstOfType<ClassConstructorAttribute>();
             if (classConstructorAttribute != null)
             {
                 testBuilderContext.ClassConstructor = (IClassConstructor)Activator.CreateInstance(classConstructorAttribute.ClassConstructorType)!;
@@ -1605,15 +1597,7 @@ public async IAsyncEnumerable<AbstractExecutableTest> BuildTestsStreamingAsync(
         // Check for ClassConstructor attribute and set it early if present
         // Look for any attribute that inherits from ClassConstructorAttribute
         // This handles both ClassConstructorAttribute and ClassConstructorAttribute<T>
-        ClassConstructorAttribute? classConstructorAttribute = null;
-        foreach (var attribute in attributes)
-        {
-            if (attribute is ClassConstructorAttribute cca)
-            {
-                classConstructorAttribute = cca;
-                break;
-            }
-        }
+        var classConstructorAttribute = attributes.FirstOfType<ClassConstructorAttribute>();
 
         if (classConstructorAttribute != null)
         {
diff --git a/TUnit.Engine/Building/TestBuilderPipeline.cs b/TUnit.Engine/Building/TestBuilderPipeline.cs
index 929fb30f5c..12d27b4ae6 100644
--- a/TUnit.Engine/Building/TestBuilderPipeline.cs
+++ b/TUnit.Engine/Building/TestBuilderPipeline.cs
@@ -7,6 +7,7 @@
 using TUnit.Core.Interfaces;
 using TUnit.Core.Services;
 using TUnit.Engine.Building.Interfaces;
+using TUnit.Engine.Extensions;
 using TUnit.Engine.Services;
 using TUnit.Engine.Utilities;
 
@@ -56,15 +57,7 @@ private TestBuilderContext CreateTestBuilderContext(TestMetadata metadata)
 
         // Look for any attribute that inherits from ClassConstructorAttribute
         // This handles both ClassConstructorAttribute and ClassConstructorAttribute<T>
-        ClassConstructorAttribute? classConstructorAttribute = null;
-        foreach (var attr in attributes)
-        {
-            if (attr is ClassConstructorAttribute cca)
-            {
-                classConstructorAttribute = cca;
-                break;
-            }
-        }
+        var classConstructorAttribute = attributes.FirstOfType<ClassConstructorAttribute>();
 
         if (classConstructorAttribute != null)
         {
diff --git a/TUnit.Engine/Extensions/AttributeArrayExtensions.cs b/TUnit.Engine/Extensions/AttributeArrayExtensions.cs
new file mode 100644
index 0000000000..ee4c73a0c9
--- /dev/null
+++ b/TUnit.Engine/Extensions/AttributeArrayExtensions.cs
@@ -0,0 +1,21 @@
+namespace TUnit.Engine.Extensions;
+
+internal static class AttributeArrayExtensions
+{
+    /// <summary>
+    /// Returns the first attribute assignable to <typeparamref name="T"/>, or null.
+    /// Allocation-free alternative to OfType&lt;T&gt;().FirstOrDefault() for hot paths.
+    /// </summary>
+    public static T? FirstOfType<T>(this Attribute[] attributes) where T : Attribute
+    {
+        foreach (var attribute in attributes)
+        {
+            if (attribute is T typed)
+            {
+                return typed;
+            }
+        }
+
+        return null;
+    }
+}
