From 6e1a7239bdd157ffdaa384deb48229854ea7a2c3 Mon Sep 17 00:00:00 2001 From: Tom Longhurst <30480171+thomhurst@users.noreply.github.com> Date: Tue, 4 Nov 2025 22:40:20 +0000 Subject: [PATCH] refactor: adjust parallelism handling in TestScheduler for better resource management --- TUnit.Engine/Scheduling/TestScheduler.cs | 14 +++-- TUnit.Profile/Program.cs | 75 ++++++++++++++++++++++++ 2 files changed, 83 insertions(+), 6 deletions(-) diff --git a/TUnit.Engine/Scheduling/TestScheduler.cs b/TUnit.Engine/Scheduling/TestScheduler.cs index 286d5145659..b7e7a16c1f4 100644 --- a/TUnit.Engine/Scheduling/TestScheduler.cs +++ b/TUnit.Engine/Scheduling/TestScheduler.cs @@ -473,13 +473,14 @@ private async Task ExecuteUnlimitedAsync( List tests, CancellationToken cancellationToken) { - // Execute tests without limiters with unlimited parallelism (no global semaphore overhead) + // Execute tests without limiters - still apply global parallelism limit to prevent thread pool saturation + // "Unlimited" means no per-test constraints (ParallelLimiter), not unlimited concurrency await Parallel.ForEachAsync( tests, new ParallelOptions { + MaxDegreeOfParallelism = _maxParallelism, CancellationToken = cancellationToken - // No MaxDegreeOfParallelism = unlimited parallelism }, async (test, ct) => { @@ -550,10 +551,11 @@ private static int GetMaxParallelism(ILogger logger, ICommandLineOptions command } } - // Default: 8x CPU cores (optimized for I/O-bound and async tests) - // Allows higher concurrency for tests with async/await patterns while preventing resource exhaustion - var defaultLimit = Environment.ProcessorCount * 8; - logger.LogDebug($"Maximum parallel tests limit defaulting to {defaultLimit} ({Environment.ProcessorCount} processors * 8)"); + // Default: 4x CPU cores (balanced for mixed CPU/I/O workloads) + // Higher values can cause thread pool saturation with many async tests + // Users can override via --maximum-parallel-tests or TUNIT_MAX_PARALLEL_TESTS + var defaultLimit = Environment.ProcessorCount * 4; + logger.LogDebug($"Maximum parallel tests limit defaulting to {defaultLimit} ({Environment.ProcessorCount} processors * 4)"); return defaultLimit; } } diff --git a/TUnit.Profile/Program.cs b/TUnit.Profile/Program.cs index efbf0950697..145548d2c91 100644 --- a/TUnit.Profile/Program.cs +++ b/TUnit.Profile/Program.cs @@ -156,3 +156,78 @@ public class MixedWorkloadTests [Repeat(30)] public async Task QuickAsyncTest3() { await Task.CompletedTask; _ = 3; } } + +/// +/// Massive parallel tests similar to benchmark suite - stress tests parallelism +/// with large numbers of CPU-bound, IO-bound, and mixed workload tests +/// +public class MassiveParallelTests +{ + [Test] + [Arguments(1)] [Arguments(2)] [Arguments(3)] [Arguments(4)] [Arguments(5)] + [Arguments(6)] [Arguments(7)] [Arguments(8)] [Arguments(9)] [Arguments(10)] + [Arguments(11)] [Arguments(12)] [Arguments(13)] [Arguments(14)] [Arguments(15)] + [Arguments(16)] [Arguments(17)] [Arguments(18)] [Arguments(19)] [Arguments(20)] + [Arguments(21)] [Arguments(22)] [Arguments(23)] [Arguments(24)] [Arguments(25)] + [Arguments(26)] [Arguments(27)] [Arguments(28)] [Arguments(29)] [Arguments(30)] + [Arguments(31)] [Arguments(32)] [Arguments(33)] [Arguments(34)] [Arguments(35)] + [Arguments(36)] [Arguments(37)] [Arguments(38)] [Arguments(39)] [Arguments(40)] + [Arguments(41)] [Arguments(42)] [Arguments(43)] [Arguments(44)] [Arguments(45)] + [Arguments(46)] [Arguments(47)] [Arguments(48)] [Arguments(49)] + public void Parallel_CPUBound_Test(int taskId) + { + var result = PerformCPUWork(taskId); + var doubled = result * 2; + _ = doubled; + } + + [Test] + [Arguments(1)] [Arguments(2)] [Arguments(3)] [Arguments(4)] [Arguments(5)] + [Arguments(6)] [Arguments(7)] [Arguments(8)] [Arguments(9)] [Arguments(10)] + [Arguments(11)] [Arguments(12)] [Arguments(13)] [Arguments(14)] [Arguments(15)] + [Arguments(16)] [Arguments(17)] [Arguments(18)] [Arguments(19)] [Arguments(20)] + [Arguments(21)] [Arguments(22)] [Arguments(23)] [Arguments(24)] [Arguments(25)] + [Arguments(26)] [Arguments(27)] [Arguments(28)] [Arguments(29)] [Arguments(30)] + [Arguments(31)] [Arguments(32)] [Arguments(33)] [Arguments(34)] [Arguments(35)] + [Arguments(36)] [Arguments(37)] [Arguments(38)] [Arguments(39)] [Arguments(40)] + [Arguments(41)] [Arguments(42)] [Arguments(43)] [Arguments(44)] [Arguments(45)] + [Arguments(46)] [Arguments(47)] [Arguments(48)] [Arguments(49)] + public async Task Parallel_IOBound_Test(int taskId) + { + await Task.Delay(50); + var result = await PerformIOWorkAsync(taskId); + var length = result.Length; + _ = length; + } + + [Test] + [Arguments(1, 101)] [Arguments(2, 102)] [Arguments(3, 103)] [Arguments(4, 104)] + [Arguments(5, 105)] [Arguments(6, 106)] [Arguments(7, 107)] [Arguments(8, 108)] + [Arguments(9, 109)] [Arguments(10, 110)] [Arguments(11, 111)] [Arguments(12, 112)] + [Arguments(13, 113)] [Arguments(14, 114)] [Arguments(15, 115)] [Arguments(16, 116)] + [Arguments(17, 117)] [Arguments(18, 118)] [Arguments(19, 119)] + public async Task Parallel_Mixed_Test(int cpuValue, int ioValue) + { + var cpuResult = PerformCPUWork(cpuValue); + await Task.Yield(); + var ioResult = await PerformIOWorkAsync(ioValue); + var combined = $"{cpuResult}_{ioResult}"; + _ = combined; + } + + private static int PerformCPUWork(int taskId) + { + var sum = 0; + for (var i = 1; i <= 100; i++) + { + sum += (i * taskId) % 1000; + } + return sum; + } + + private static async Task PerformIOWorkAsync(int taskId) + { + await Task.Yield(); + return $"Task_{taskId}_Result_{Guid.NewGuid().ToString()[..8]}"; + } +}