Skip to content

perf: reduce allocations via Span<T>, ArrayPool, and avoiding defensive copies #4163

@thomhurst

Description

@thomhurst

Parent Epic

Part of #4159 - Performance Optimization: Hot Path Improvements

Problem

Various hot paths allocate intermediate arrays, create defensive copies, or use heap allocations where stack/pool alternatives exist.

Evidence

File Lines Pattern Suggested Fix
TUnit.Engine/Discovery/ReflectionTestDataCollector.cs 112, 125-126 Task<List<T>>[] + merge allocations Use ArrayPool<Task>
TUnit.Engine/Discovery/ReflectionTestDataCollector.cs 140 new List<TestMetadata>(_discoveredTests) Return IReadOnlyList
TUnit.Engine/Helpers/DataUnwrapper.cs 41 .Select(p => p.Type).ToArray() Pre-sized array + loop
TUnit.Engine/Extensions/TestExtensions.cs 37, 46, 57, 208 Multiple .Select().ToArray() chains Manual loops
TUnit.Engine/Building/ReflectionMetadataBuilder.cs 30-31, 78-79 .Select().ToArray() for parameters Pre-sized array + loop
TUnit.Engine/Discovery/ReflectionAttributeExtractor.cs 125, 140, 164, 179, 192, 221 Multiple .ToArray() calls Reuse lists or use ArrayPool

Suggested Approach

Replace LINQ with pre-sized arrays:

// Before: LINQ allocation
var paramTypes = expectedParameters.Select(p => p.Type).ToArray();

// After: Pre-sized array
var paramTypes = new Type[expectedParameters.Length];
for (int i = 0; i < expectedParameters.Length; i++)
{
    paramTypes[i] = expectedParameters[i].Type;
}

Use ArrayPool for temporary buffers:

// Before
var tasks = new Task<List<TestMetadata>>[assemblies.Count];

// After
var tasks = ArrayPool<Task<List<TestMetadata>>>.Shared.Rent(assemblies.Count);
try
{
    // ... use tasks
}
finally
{
    ArrayPool<Task<List<TestMetadata>>>.Shared.Return(tasks);
}

Avoid defensive copies:

// Before: Creates copy
return new List<TestMetadata>(_discoveredTests);

// After: Return read-only view
return _discoveredTests.AsReadOnly();

Verification

  1. Use dotnet-trace with allocation tracking
  2. Compare object allocation counts before/after at 10k scale
  3. Profile with SpeedScope to verify no new hot spots introduced

Risks

  • Low-medium risk
  • ArrayPool requires careful return (use try/finally)
  • Removing defensive copies requires verifying callers don't mutate

Priority

P2 - Mixed complexity, broad impact across many files

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions