Skip to content

Commit 6fb1790

Browse files
thomhurstclaude
andcommitted
perf: optimize GetEligibleEventObjects with single-allocation approach
Replaces the previous implementation that used: - List allocation + ToArray() + OfType<object>().ToArray() - Three separate allocations for the same data New implementation: - Counts non-null items first to determine exact array size - Single array allocation with exact size needed - Direct population without intermediate collections - Eliminates LINQ overhead from OfType<object>() Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
1 parent 0db4047 commit 6fb1790

1 file changed

Lines changed: 103 additions & 28 deletions

File tree

TUnit.Engine/Extensions/TestContextExtensions.cs

Lines changed: 103 additions & 28 deletions
Original file line numberDiff line numberDiff line change
@@ -4,44 +4,119 @@ namespace TUnit.Engine.Extensions;
44

55
internal static class TestContextExtensions
66
{
7-
private static object?[] GetInternal(TestContext testContext)
7+
public static IEnumerable<object> GetEligibleEventObjects(this TestContext testContext)
88
{
9-
var testClassArgs = testContext.Metadata.TestDetails.TestClassArguments;
10-
var attributes = testContext.Metadata.TestDetails.GetAllAttributes();
11-
var testMethodArgs = testContext.Metadata.TestDetails.TestMethodArguments;
12-
var injectedProps = testContext.Metadata.TestDetails.TestClassInjectedPropertyArguments;
13-
14-
// Pre-calculate capacity to avoid reallocations
15-
var capacity = 3 + testClassArgs.Length + attributes.Count + testMethodArgs.Length + injectedProps.Count;
16-
var result = new List<object?>(capacity);
17-
18-
result.Add(testContext.ClassConstructor);
19-
result.Add(testContext.Events);
20-
result.AddRange(testClassArgs);
21-
result.Add(testContext.Metadata.TestDetails.ClassInstance);
22-
result.AddRange(attributes);
23-
result.AddRange(testMethodArgs);
24-
25-
// Manual loop instead of .Select() to avoid LINQ allocation
26-
foreach (var prop in injectedProps)
9+
// Return cached result if available
10+
if (testContext.CachedEligibleEventObjects != null)
2711
{
28-
result.Add(prop.Value);
12+
return testContext.CachedEligibleEventObjects;
2913
}
3014

31-
return result.ToArray();
15+
// Build result directly with single allocation
16+
var result = BuildEligibleEventObjects(testContext);
17+
testContext.CachedEligibleEventObjects = result;
18+
return result;
3219
}
3320

34-
public static IEnumerable<object> GetEligibleEventObjects(this TestContext testContext)
21+
private static object[] BuildEligibleEventObjects(TestContext testContext)
3522
{
36-
// Return cached result if available
37-
if (testContext.CachedEligibleEventObjects != null)
23+
var details = testContext.Metadata.TestDetails;
24+
var testClassArgs = details.TestClassArguments;
25+
var attributes = details.GetAllAttributes();
26+
var testMethodArgs = details.TestMethodArguments;
27+
var injectedProps = details.TestClassInjectedPropertyArguments;
28+
29+
// Count non-null items first to allocate exact size
30+
var count = CountNonNull(testContext.ClassConstructor)
31+
+ CountNonNull(testContext.Events)
32+
+ CountNonNullInArray(testClassArgs)
33+
+ CountNonNull(details.ClassInstance)
34+
+ attributes.Count // Attributes are never null
35+
+ CountNonNullInArray(testMethodArgs)
36+
+ CountNonNullValues(injectedProps);
37+
38+
if (count == 0)
3839
{
39-
return testContext.CachedEligibleEventObjects;
40+
return [];
41+
}
42+
43+
// Single allocation with exact size
44+
var result = new object[count];
45+
var index = 0;
46+
47+
// Add items, skipping nulls
48+
if (testContext.ClassConstructor is { } constructor)
49+
{
50+
result[index++] = constructor;
51+
}
52+
53+
if (testContext.Events is { } events)
54+
{
55+
result[index++] = events;
56+
}
57+
58+
foreach (var arg in testClassArgs)
59+
{
60+
if (arg is { } nonNullArg)
61+
{
62+
result[index++] = nonNullArg;
63+
}
64+
}
65+
66+
if (details.ClassInstance is { } classInstance)
67+
{
68+
result[index++] = classInstance;
69+
}
70+
71+
foreach (var attr in attributes)
72+
{
73+
result[index++] = attr;
74+
}
75+
76+
foreach (var arg in testMethodArgs)
77+
{
78+
if (arg is { } nonNullArg)
79+
{
80+
result[index++] = nonNullArg;
81+
}
82+
}
83+
84+
foreach (var prop in injectedProps)
85+
{
86+
if (prop.Value is { } value)
87+
{
88+
result[index++] = value;
89+
}
4090
}
4191

42-
// Materialize and cache the result
43-
var result = GetInternal(testContext).OfType<object>().ToArray();
44-
testContext.CachedEligibleEventObjects = result;
4592
return result;
4693
}
94+
95+
private static int CountNonNull(object? obj) => obj != null ? 1 : 0;
96+
97+
private static int CountNonNullInArray(object?[] array)
98+
{
99+
var count = 0;
100+
foreach (var item in array)
101+
{
102+
if (item != null)
103+
{
104+
count++;
105+
}
106+
}
107+
return count;
108+
}
109+
110+
private static int CountNonNullValues(IDictionary<string, object?> props)
111+
{
112+
var count = 0;
113+
foreach (var prop in props)
114+
{
115+
if (prop.Value != null)
116+
{
117+
count++;
118+
}
119+
}
120+
return count;
121+
}
47122
}

0 commit comments

Comments
 (0)