Skip to content

perf: TimeoutHelper creates unnecessary allocations when no timeout is configured #4287

@thomhurst

Description

@thomhurst

Summary

TimeoutHelper.ExecuteWithTimeoutAsync creates unnecessary allocations even when no timeout is configured. This was identified via dotnet-trace profiling of the speed-comparison ScaleTests, where TimeoutHelper consumed 5.16% of CPU time for tests with no timeout.

Profiling Evidence

From dotnet-trace on ScaleTests (150 simple sync tests):

TimeoutHelper+<ExecuteWithTimeoutAsync>d__1.MoveNext()  5.16% inclusive

Root Cause

In TUnit.Engine/Helpers/TimeoutHelper.cs lines 59-78, the "fast path" when timeout is null still allocates when cancellationToken.CanBeCanceled is true:

if (!timeout.HasValue)
{
    var task = taskFactory(cancellationToken);

    if (!cancellationToken.CanBeCanceled)  // This is rarely true
    {
        return await task.ConfigureAwait(false);
    }

    // ALLOCATES: TaskCompletionSource + Task.WhenAny for every test!
    var tcs = new TaskCompletionSource<T>(TaskCreationOptions.RunContinuationsAsynchronously);
    using var reg = cancellationToken.Register(...);
    return await await Task.WhenAny(task, tcs.Task).ConfigureAwait(false);
}

Since cancellationToken.CanBeCanceled is typically true (tests run with cancellation support), every test creates:

  • A TaskCompletionSource
  • A cancellation registration
  • A Task.WhenAny operation

Suggested Fix

When timeout is null, simply await the task directly. Cancellation is cooperative - the test code is already passed the token and should check it:

if (!timeout.HasValue)
{
    return await taskFactory(cancellationToken).ConfigureAwait(false);
}

The Task.WhenAny pattern is only needed when we want to return control immediately on timeout. Cancellation doesn't need this - we just need to propagate the exception when it occurs.

Impact

  • Reduces per-test overhead by ~5% for tests without timeouts (majority of tests)
  • Reduces allocations by removing TCS + registration + WhenAny per test
  • Will improve TUnit's performance in DataDrivenTests and ScaleTests benchmarks where TUnit currently loses to other frameworks

Metadata

Metadata

Assignees

No one assigned

    Labels

    enhancementNew feature or request

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions