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
Summary
TimeoutHelper.ExecuteWithTimeoutAsynccreates 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):
Root Cause
In
TUnit.Engine/Helpers/TimeoutHelper.cslines 59-78, the "fast path" whentimeoutis null still allocates whencancellationToken.CanBeCanceledis true:Since
cancellationToken.CanBeCanceledis typically true (tests run with cancellation support), every test creates:TaskCompletionSourceTask.WhenAnyoperationSuggested Fix
When
timeoutis null, simply await the task directly. Cancellation is cooperative - the test code is already passed the token and should check it:The
Task.WhenAnypattern 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