Skip to content

perf: implement object pooling for frequent allocations #4161

@thomhurst

Description

@thomhurst

Parent Epic

Part of #4159 - Performance Optimization: Hot Path Improvements

Problem

Several objects are allocated per-test and immediately discarded. Pooling these reduces GC pressure and improves cache locality.

Evidence

File Lines Object Frequency
TUnit.Engine/TestExecutor.cs 162 List<Exception> Per-test
TUnit.Engine/Services/HookExecutor.cs 67, 129, 196, 288 List<Exception> Per-hook-level (4x per test)
TUnit.Engine/Building/TestBuilderPipeline.cs 39 ConcurrentDictionary<string, object?> state bags Per-test
TUnit.Assertions/Sources/ValueAssertion.cs 18-20 StringBuilder for expressions Per-assertion
TUnit.Engine/Discovery/ReflectionTestDataCollector.cs 338 List<TestMetadata>(100) Per-assembly

Suggested Approach

Use ObjectPool<T> from Microsoft.Extensions.ObjectPool or a simple thread-static pattern:

// Thread-static pool for List<Exception>
internal static class ExceptionListPool
{
    [ThreadStatic] private static List<Exception>? _cached;
    
    public static List<Exception> Rent() 
    {
        var list = _cached ?? new List<Exception>(4);
        _cached = null;
        return list;
    }
    
    public static void Return(List<Exception> list)
    {
        list.Clear();
        _cached = list;
    }
}

For StringBuilder in assertions, consider lazy initialization - only create when assertion fails:

// Defer StringBuilder creation until failure
private StringBuilder? _expressionBuilder;
private StringBuilder ExpressionBuilder => _expressionBuilder ??= new StringBuilder();

Verification

  1. Profile memory allocations before/after using dotnet-trace with gc-collect events
  2. Compare object allocation counts at 10k test scale
  3. Verify no memory leaks (pooled objects properly returned)

Risks

  • Medium risk - must ensure pooled objects are always returned
  • Thread-static pools have no cross-thread sharing (may need larger pools for high parallelism)
  • StringBuilder lazy init changes behavior slightly - ensure tests still pass

Priority

P1 - Medium complexity, high memory impact

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