diff --git a/TUnit.Engine/Interfaces/ITestCoordinator.cs b/TUnit.Engine/Interfaces/ITestCoordinator.cs index fe91169564..608fee019b 100644 --- a/TUnit.Engine/Interfaces/ITestCoordinator.cs +++ b/TUnit.Engine/Interfaces/ITestCoordinator.cs @@ -9,5 +9,5 @@ namespace TUnit.Engine.Interfaces; /// internal interface ITestCoordinator { - Task ExecuteTestAsync(AbstractExecutableTest test, CancellationToken cancellationToken); + ValueTask ExecuteTestAsync(AbstractExecutableTest test, CancellationToken cancellationToken); } diff --git a/TUnit.Engine/Scheduling/TestRunner.cs b/TUnit.Engine/Scheduling/TestRunner.cs index 5dde86ed23..f07240df07 100644 --- a/TUnit.Engine/Scheduling/TestRunner.cs +++ b/TUnit.Engine/Scheduling/TestRunner.cs @@ -39,14 +39,14 @@ internal TestRunner( private readonly ThreadSafeDictionary _executingTests = new(); private Exception? _firstFailFastException; - public async Task ExecuteTestAsync(AbstractExecutableTest test, CancellationToken cancellationToken) + public async ValueTask ExecuteTestAsync(AbstractExecutableTest test, CancellationToken cancellationToken) { // Prevent double execution with a simple lock - var executionTask = _executingTests.GetOrAdd(test.TestId, _ => ExecuteTestInternalAsync(test, cancellationToken)); + var executionTask = _executingTests.GetOrAdd(test.TestId, _ => ExecuteTestInternalAsync(test, cancellationToken).AsTask()); await executionTask.ConfigureAwait(false); } - private async Task ExecuteTestInternalAsync(AbstractExecutableTest test, CancellationToken cancellationToken) + private async ValueTask ExecuteTestInternalAsync(AbstractExecutableTest test, CancellationToken cancellationToken) { try { diff --git a/TUnit.Engine/Scheduling/TestScheduler.cs b/TUnit.Engine/Scheduling/TestScheduler.cs index 08802fe337..286d514565 100644 --- a/TUnit.Engine/Scheduling/TestScheduler.cs +++ b/TUnit.Engine/Scheduling/TestScheduler.cs @@ -420,7 +420,7 @@ private async Task ExecuteWithGlobalLimitAsync( try { - test.ExecutionTask ??= _testRunner.ExecuteTestAsync(test, cancellationToken); + test.ExecutionTask ??= _testRunner.ExecuteTestAsync(test, cancellationToken).AsTask(); await test.ExecutionTask.ConfigureAwait(false); } finally @@ -458,9 +458,7 @@ await Parallel.ForEachAsync( try { -#pragma warning disable IL2026 // ExecuteTestAsync uses reflection, but caller (ExecuteWithGlobalLimitAsync) is already marked with RequiresUnreferencedCode - test.ExecutionTask ??= _testRunner.ExecuteTestAsync(test, ct); -#pragma warning restore IL2026 + test.ExecutionTask ??= _testRunner.ExecuteTestAsync(test, ct).AsTask(); await test.ExecutionTask.ConfigureAwait(false); } finally @@ -485,9 +483,7 @@ await Parallel.ForEachAsync( }, async (test, ct) => { -#pragma warning disable IL2026 // ExecuteTestAsync uses reflection, but caller (ExecuteWithGlobalLimitAsync) is already marked with RequiresUnreferencedCode - test.ExecutionTask ??= _testRunner.ExecuteTestAsync(test, ct); -#pragma warning restore IL2026 + test.ExecutionTask ??= _testRunner.ExecuteTestAsync(test, ct).AsTask(); await test.ExecutionTask.ConfigureAwait(false); } ).ConfigureAwait(false); diff --git a/TUnit.Engine/Services/DataSourceInitializer.cs b/TUnit.Engine/Services/DataSourceInitializer.cs index 5fa59eba0e..4711af5a32 100644 --- a/TUnit.Engine/Services/DataSourceInitializer.cs +++ b/TUnit.Engine/Services/DataSourceInitializer.cs @@ -12,8 +12,7 @@ namespace TUnit.Engine.Services; /// internal sealed class DataSourceInitializer { - private readonly Dictionary _initializationTasks = new(); - private readonly object _lock = new(); + private readonly ConcurrentDictionary> _initializationTasks = new(); private PropertyInjectionService? _propertyInjectionService; /// @@ -42,30 +41,22 @@ public async Task EnsureInitializedAsync( } // Check if already initialized or being initialized - Task? existingTask; - lock (_lock) - { - if (_initializationTasks.TryGetValue(dataSource, out existingTask)) - { - // Already initialized or being initialized - } - else - { - // Start initialization - existingTask = InitializeDataSourceAsync(dataSource, objectBag, methodMetadata, events, cancellationToken); - _initializationTasks[dataSource] = existingTask; - } - } + // Use Lazy to ensure only one initialization task is created per data source (thread-safe) + var lazyTask = _initializationTasks.GetOrAdd( + dataSource, + _ => new Lazy(() => InitializeDataSourceAsync(dataSource, objectBag, methodMetadata, events, cancellationToken))); + + var task = lazyTask.Value; // Wait for initialization with cancellation support if (cancellationToken.CanBeCanceled) { - await existingTask.ConfigureAwait(false); + await task.ConfigureAwait(false); cancellationToken.ThrowIfCancellationRequested(); } else { - await existingTask.ConfigureAwait(false); + await task.ConfigureAwait(false); } return dataSource; @@ -221,9 +212,6 @@ private void CollectNestedObjects( /// public void ClearCache() { - lock (_lock) - { - _initializationTasks.Clear(); - } + _initializationTasks.Clear(); } } diff --git a/TUnit.Engine/Services/PropertyInjectionService.cs b/TUnit.Engine/Services/PropertyInjectionService.cs index b329bd4d03..dc3e3882cb 100644 --- a/TUnit.Engine/Services/PropertyInjectionService.cs +++ b/TUnit.Engine/Services/PropertyInjectionService.cs @@ -40,7 +40,6 @@ public async Task InjectPropertiesIntoArgumentsAsync(object?[] arguments, Concur return; } - // Fast path: check if any arguments need injection var injectableArgs = arguments .Where(argument => argument != null && PropertyInjectionCache.HasInjectableProperties(argument.GetType())) .ToArray(); @@ -50,7 +49,6 @@ public async Task InjectPropertiesIntoArgumentsAsync(object?[] arguments, Concur return; } - // Process arguments in parallel var argumentTasks = injectableArgs .Select(argument => InjectPropertiesIntoObjectAsync(argument!, objectBag, methodMetadata, events)) .ToArray(); @@ -80,7 +78,6 @@ public Task InjectPropertiesIntoObjectAsync(object instance, ConcurrentDictionar throw new ArgumentNullException(nameof(events), "TestContextEvents must not be null. Each test permutation must have a unique TestContextEvents instance for proper disposal tracking."); } - // Start with an empty visited set for cycle detection #if NETSTANDARD2_0 var visitedObjects = new ConcurrentDictionary(); #else @@ -96,8 +93,7 @@ internal async Task InjectPropertiesIntoObjectAsyncCore(object instance, Concurr return; } - // Prevent cycles - if we're already processing this object, skip it - // TryAdd returns false if the key already exists (thread-safe) + // Prevent cycles if (!visitedObjects.TryAdd(instance, 0)) { return; @@ -117,13 +113,11 @@ await PropertyInjectionCache.GetOrAddInjectionTask(instance, async _ => { var plan = PropertyInjectionCache.GetOrCreatePlan(instance.GetType()); - // Use the orchestrator for property initialization await _orchestrator.InitializeObjectWithPropertiesAsync( instance, plan, objectBag, methodMetadata, events, visitedObjects); }); } - // After properties are initialized, recursively inject nested properties await RecurseIntoNestedPropertiesAsync(instance, objectBag, methodMetadata, events, visitedObjects); } catch (Exception ex) diff --git a/TUnit.Engine/Services/TestExecution/HashSetPool.cs b/TUnit.Engine/Services/TestExecution/HashSetPool.cs index dba17daaf6..d9bf999713 100644 --- a/TUnit.Engine/Services/TestExecution/HashSetPool.cs +++ b/TUnit.Engine/Services/TestExecution/HashSetPool.cs @@ -1,34 +1,26 @@ +using System.Collections.Concurrent; + namespace TUnit.Engine.Services.TestExecution; /// /// Thread-safe object pool for HashSet instances used during test execution. /// Single Responsibility: Managing pooled HashSet objects to reduce allocations. +/// Uses lock-free concurrent collections for high-performance parallel test execution. /// internal sealed class HashSetPool { - private readonly Dictionary _pools = new(); - private readonly object _lock = new(); + private readonly ConcurrentDictionary> _pools = new(); public HashSet Rent() { var type = typeof(T); + var bag = _pools.GetOrAdd(type, _ => new ConcurrentBag()); - lock (_lock) + if (bag.TryTake(out var pooledSet)) { - if (!_pools.TryGetValue(type, out var poolObj)) - { - poolObj = new Stack>(); - _pools[type] = poolObj; - } - - var pool = (Stack>)poolObj; - - if (pool.Count > 0) - { - var set = pool.Pop(); - set.Clear(); - return set; - } + var set = (HashSet)pooledSet; + set.Clear(); + return set; } return []; @@ -37,19 +29,9 @@ public HashSet Rent() public void Return(HashSet set) { var type = typeof(T); + set.Clear(); - lock (_lock) - { - set.Clear(); - - if (!_pools.TryGetValue(type, out var poolObj)) - { - poolObj = new Stack>(); - _pools[type] = poolObj; - } - - var pool = (Stack>)poolObj; - pool.Push(set); - } + var bag = _pools.GetOrAdd(type, _ => new ConcurrentBag()); + bag.Add(set); } } diff --git a/TUnit.Engine/Services/TestExecution/TestCoordinator.cs b/TUnit.Engine/Services/TestExecution/TestCoordinator.cs index 13f2ac7df4..bae008f9bc 100644 --- a/TUnit.Engine/Services/TestExecution/TestCoordinator.cs +++ b/TUnit.Engine/Services/TestExecution/TestCoordinator.cs @@ -50,13 +50,13 @@ public TestCoordinator( _hashSetPool = hashSetPool; } - public async Task ExecuteTestAsync(AbstractExecutableTest test, CancellationToken cancellationToken) + public async ValueTask ExecuteTestAsync(AbstractExecutableTest test, CancellationToken cancellationToken) { await _executionGuard.TryStartExecutionAsync(test.TestId, - () => ExecuteTestInternalAsync(test, cancellationToken)); + () => ExecuteTestInternalAsync(test, cancellationToken).AsTask()); } - private async Task ExecuteTestInternalAsync(AbstractExecutableTest test, CancellationToken cancellationToken) + private async ValueTask ExecuteTestInternalAsync(AbstractExecutableTest test, CancellationToken cancellationToken) { try { diff --git a/TUnit.Engine/TestExecutor.cs b/TUnit.Engine/TestExecutor.cs index dc5562796d..bc1ab31272 100644 --- a/TUnit.Engine/TestExecutor.cs +++ b/TUnit.Engine/TestExecutor.cs @@ -51,7 +51,7 @@ await _beforeHookTaskCache.GetOrCreateBeforeTestSessionTask(() => /// Creates a test executor delegate that wraps the provided executor with hook orchestration. /// Uses focused services that follow SRP to manage lifecycle and execution. /// - public async Task ExecuteAsync(AbstractExecutableTest executableTest, CancellationToken cancellationToken) + public async ValueTask ExecuteAsync(AbstractExecutableTest executableTest, CancellationToken cancellationToken) { var testClass = executableTest.Metadata.TestClassType; @@ -148,7 +148,7 @@ await _eventReceiverOrchestrator.InvokeFirstTestInClassEventReceiversAsync( } } - private static async Task ExecuteTestAsync(AbstractExecutableTest executableTest, CancellationToken cancellationToken) + private static async ValueTask ExecuteTestAsync(AbstractExecutableTest executableTest, CancellationToken cancellationToken) { // Skip the actual test invocation for skipped tests if (executableTest.Context.Metadata.TestDetails.ClassInstance is SkippedTestInstance || diff --git a/TUnit.Engine/TestInitializer.cs b/TUnit.Engine/TestInitializer.cs index 00297f354f..6c3f1a6f55 100644 --- a/TUnit.Engine/TestInitializer.cs +++ b/TUnit.Engine/TestInitializer.cs @@ -21,7 +21,7 @@ public TestInitializer(EventReceiverOrchestrator eventReceiverOrchestrator, _objectTracker = objectTracker; } - public async Task InitializeTest(AbstractExecutableTest test, CancellationToken cancellationToken) + public async ValueTask InitializeTest(AbstractExecutableTest test, CancellationToken cancellationToken) { var testClassInstance = test.Context.Metadata.TestDetails.ClassInstance;