Skip to content

Commit b3f06cf

Browse files
authored
refactor: optimize task handling by replacing Task with ValueTask for improved performance (#3528)
1 parent 80931a6 commit b3f06cf

File tree

4 files changed

+174
-73
lines changed

4 files changed

+174
-73
lines changed

TUnit.Engine/Events/EventReceiverRegistry.cs

Lines changed: 49 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -26,6 +26,7 @@ private enum EventTypes
2626

2727
private volatile EventTypes _registeredEvents = EventTypes.None;
2828
private readonly Dictionary<Type, object[]> _receiversByType = new();
29+
private readonly Dictionary<Type, Array> _cachedTypedReceivers = new();
2930
private readonly ReaderWriterLockSlim _lock = new();
3031

3132
/// <summary>
@@ -66,7 +67,7 @@ public void RegisterReceiver(object receiver)
6667
private void RegisterReceiverInternal(object receiver)
6768
{
6869
UpdateEventFlags(receiver);
69-
70+
7071
// Register for each interface type the object implements
7172
// We use a simpler approach that doesn't require reflection
7273
RegisterIfImplements<ITestStartEventReceiver>(receiver);
@@ -79,6 +80,8 @@ private void RegisterReceiverInternal(object receiver)
7980
RegisterIfImplements<ILastTestInAssemblyEventReceiver>(receiver);
8081
RegisterIfImplements<IFirstTestInClassEventReceiver>(receiver);
8182
RegisterIfImplements<ILastTestInClassEventReceiver>(receiver);
83+
84+
_cachedTypedReceivers.Clear();
8285
}
8386

8487
private void RegisterIfImplements<T>(object receiver) where T : class
@@ -136,30 +139,67 @@ private void RegisterIfImplements<T>(object receiver) where T : class
136139
[MethodImpl(MethodImplOptions.AggressiveInlining)]
137140
public bool HasAnyReceivers() => _registeredEvents != EventTypes.None;
138141

139-
/// <summary>
140-
/// Get receivers of specific type (for invocation)
141-
/// </summary>
142142
public T[] GetReceiversOfType<T>() where T : class
143143
{
144+
var typeKey = typeof(T);
145+
144146
_lock.EnterReadLock();
145147
try
146148
{
147-
if (_receiversByType.TryGetValue(typeof(T), out var receivers))
149+
if (_cachedTypedReceivers.TryGetValue(typeKey, out var cached))
150+
{
151+
return (T[])cached;
152+
}
153+
}
154+
finally
155+
{
156+
_lock.ExitReadLock();
157+
}
158+
159+
_lock.EnterUpgradeableReadLock();
160+
try
161+
{
162+
if (_cachedTypedReceivers.TryGetValue(typeKey, out var cached))
163+
{
164+
return (T[])cached;
165+
}
166+
167+
if (_receiversByType.TryGetValue(typeKey, out var receivers))
148168
{
149-
// Cast array to specific type
150169
var typedArray = new T[receivers.Length];
151170
for (var i = 0; i < receivers.Length; i++)
152171
{
153172
typedArray[i] = (T)receivers[i];
154173
}
174+
175+
_lock.EnterWriteLock();
176+
try
177+
{
178+
_cachedTypedReceivers[typeKey] = typedArray;
179+
}
180+
finally
181+
{
182+
_lock.ExitWriteLock();
183+
}
184+
155185
return typedArray;
156186
}
157-
return [
158-
];
187+
188+
T[] emptyArray = [];
189+
_lock.EnterWriteLock();
190+
try
191+
{
192+
_cachedTypedReceivers[typeKey] = emptyArray;
193+
}
194+
finally
195+
{
196+
_lock.ExitWriteLock();
197+
}
198+
return emptyArray;
159199
}
160200
finally
161201
{
162-
_lock.ExitReadLock();
202+
_lock.ExitUpgradeableReadLock();
163203
}
164204
}
165205

TUnit.Engine/Services/BeforeHookTaskCache.cs

Lines changed: 11 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -17,33 +17,34 @@ internal sealed class BeforeHookTaskCache
1717
private Task? _beforeTestSessionTask;
1818
private readonly object _testSessionLock = new();
1919

20-
public Task GetOrCreateBeforeTestSessionTask(Func<Task> taskFactory)
20+
public ValueTask GetOrCreateBeforeTestSessionTask(Func<ValueTask> taskFactory)
2121
{
2222
if (_beforeTestSessionTask != null)
2323
{
24-
return _beforeTestSessionTask;
24+
return new ValueTask(_beforeTestSessionTask);
2525
}
2626

2727
lock (_testSessionLock)
2828
{
29-
// Double-check after acquiring lock
3029
if (_beforeTestSessionTask == null)
3130
{
32-
_beforeTestSessionTask = taskFactory();
31+
_beforeTestSessionTask = taskFactory().AsTask();
3332
}
34-
return _beforeTestSessionTask;
33+
return new ValueTask(_beforeTestSessionTask);
3534
}
3635
}
3736

38-
public Task GetOrCreateBeforeAssemblyTask(Assembly assembly, Func<Assembly, Task> taskFactory)
37+
public ValueTask GetOrCreateBeforeAssemblyTask(Assembly assembly, Func<Assembly, ValueTask> taskFactory)
3938
{
40-
return _beforeAssemblyTasks.GetOrAdd(assembly, taskFactory);
39+
var task = _beforeAssemblyTasks.GetOrAdd(assembly, a => taskFactory(a).AsTask());
40+
return new ValueTask(task);
4141
}
4242

43-
public Task GetOrCreateBeforeClassTask(
43+
public ValueTask GetOrCreateBeforeClassTask(
4444
[DynamicallyAccessedMembers(DynamicallyAccessedMemberTypes.PublicConstructors | DynamicallyAccessedMemberTypes.PublicProperties | DynamicallyAccessedMemberTypes.PublicMethods)]
45-
Type testClass, Func<Type, Task> taskFactory)
45+
Type testClass, Func<Type, ValueTask> taskFactory)
4646
{
47-
return _beforeClassTasks.GetOrAdd(testClass, taskFactory);
47+
var task = _beforeClassTasks.GetOrAdd(testClass, t => taskFactory(t).AsTask());
48+
return new ValueTask(task);
4849
}
4950
}

TUnit.Engine/Services/EventReceiverOrchestrator.cs

Lines changed: 9 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -214,20 +214,19 @@ public async ValueTask InvokeHookRegistrationEventReceiversAsync(HookRegisteredC
214214

215215
// First/Last event methods with fast-path checks
216216
[MethodImpl(MethodImplOptions.AggressiveInlining)]
217-
public async ValueTask InvokeFirstTestInSessionEventReceiversAsync(
217+
public ValueTask InvokeFirstTestInSessionEventReceiversAsync(
218218
TestContext context,
219219
TestSessionContext sessionContext,
220220
CancellationToken cancellationToken)
221221
{
222222
if (!_registry.HasFirstTestInSessionReceivers())
223223
{
224-
return;
224+
return default;
225225
}
226226

227-
// Use GetOrAdd to ensure exactly one task is created per session and all tests await it
228227
var task = _firstTestInSessionTasks.GetOrAdd("session",
229228
_ => InvokeFirstTestInSessionEventReceiversCoreAsync(context, sessionContext, cancellationToken));
230-
await task;
229+
return new ValueTask(task);
231230
}
232231

233232
private async Task InvokeFirstTestInSessionEventReceiversCoreAsync(
@@ -244,21 +243,20 @@ private async Task InvokeFirstTestInSessionEventReceiversCoreAsync(
244243
}
245244

246245
[MethodImpl(MethodImplOptions.AggressiveInlining)]
247-
public async ValueTask InvokeFirstTestInAssemblyEventReceiversAsync(
246+
public ValueTask InvokeFirstTestInAssemblyEventReceiversAsync(
248247
TestContext context,
249248
AssemblyHookContext assemblyContext,
250249
CancellationToken cancellationToken)
251250
{
252251
if (!_registry.HasFirstTestInAssemblyReceivers())
253252
{
254-
return;
253+
return default;
255254
}
256255

257256
var assemblyName = assemblyContext.Assembly.GetName().FullName ?? "";
258-
// Use GetOrAdd to ensure exactly one task is created per assembly and all tests await it
259257
var task = _firstTestInAssemblyTasks.GetOrAdd(assemblyName,
260258
_ => InvokeFirstTestInAssemblyEventReceiversCoreAsync(context, assemblyContext, cancellationToken));
261-
await task;
259+
return new ValueTask(task);
262260
}
263261

264262
private async Task InvokeFirstTestInAssemblyEventReceiversCoreAsync(
@@ -275,21 +273,20 @@ private async Task InvokeFirstTestInAssemblyEventReceiversCoreAsync(
275273
}
276274

277275
[MethodImpl(MethodImplOptions.AggressiveInlining)]
278-
public async ValueTask InvokeFirstTestInClassEventReceiversAsync(
276+
public ValueTask InvokeFirstTestInClassEventReceiversAsync(
279277
TestContext context,
280278
ClassHookContext classContext,
281279
CancellationToken cancellationToken)
282280
{
283281
if (!_registry.HasFirstTestInClassReceivers())
284282
{
285-
return;
283+
return default;
286284
}
287285

288286
var classType = classContext.ClassType;
289-
// Use GetOrAdd to ensure exactly one task is created per class and all tests await it
290287
var task = _firstTestInClassTasks.GetOrAdd(classType,
291288
_ => InvokeFirstTestInClassEventReceiversCoreAsync(context, classContext, cancellationToken));
292-
await task;
289+
return new ValueTask(task);
293290
}
294291

295292
private async Task InvokeFirstTestInClassEventReceiversCoreAsync(

0 commit comments

Comments
 (0)