Skip to content
Merged
Show file tree
Hide file tree
Changes from 1 commit
Commits
Show all changes
44 commits
Select commit Hold shift + click to select a range
0d5d220
Perf Improvements
thomhurst Aug 8, 2025
dd2cc3b
chore: remove obsolete test result files and reflection log
thomhurst Aug 8, 2025
4216d5c
Remove redundant execution plan
thomhurst Aug 8, 2025
08e0de6
Optimize task handling and property injection for improved performance
thomhurst Aug 8, 2025
cd44cb8
Merge main branch and resolve conflicts
thomhurst Aug 8, 2025
3da8363
Update EnumerableAsyncProcessor to version 3.2.0 and modify GetOnlyDi…
thomhurst Aug 8, 2025
919b7e5
Remove 5 minute timeout from DedicatedThreadExecutor
thomhurst Aug 8, 2025
a0e8a25
Revert "Remove 5 minute timeout from DedicatedThreadExecutor"
thomhurst Aug 8, 2025
b311a46
fix: Resolve async data source deadlock in DedicatedThreadExecutor
thomhurst Aug 8, 2025
48bb732
Merge branch 'main' into feature/perf-08082025
thomhurst Aug 10, 2025
e5eb020
Fix build error: Remove TestMethodParameterTypes property that doesn'…
thomhurst Aug 10, 2025
32795dd
Remove maximum-parallel-tests argument from RunEngineTestsModule
thomhurst Aug 10, 2025
5360509
Merge branch 'main' into feature/perf-08082025
thomhurst Aug 10, 2025
a308c07
chore(deps): update EnumerableAsyncProcessor to version 3.6.0
thomhurst Aug 10, 2025
c99fd5d
refactor: convert synchronous methods to asynchronous and improve tim…
thomhurst Aug 10, 2025
9bae603
fix: handle unexpected test states and ensure TaskCompletionSource is…
thomhurst Aug 10, 2025
729a9d8
chore(deps): update EnumerableAsyncProcessor to version 3.6.3
thomhurst Aug 10, 2025
59ce527
refactor: remove obsolete TryCreateWithInitializer method and impleme…
thomhurst Aug 10, 2025
d0c2340
refactor: implement KeyedConstraintManager to manage test execution w…
thomhurst Aug 10, 2025
29d7884
feat: add hang dump configuration to all test runner modules
thomhurst Aug 10, 2025
3613cbf
feat: add Microsoft.Testing.Extensions.HangDump package to multiple t…
thomhurst Aug 10, 2025
2a0550b
feat: add Microsoft.Testing.Extensions.HangDump package reference to …
thomhurst Aug 10, 2025
9e51ecc
feat: add Microsoft.Testing.Extensions.HangDump package reference to …
thomhurst Aug 10, 2025
a035a98
fix: eliminate deadlock in Context output handling using lock-free de…
thomhurst Aug 10, 2025
2af6f2d
fix: resolve potential deadlock in dependency resolution by setting T…
thomhurst Aug 10, 2025
2ba8871
Merge remote-tracking branch 'origin/main' into feature/perf-08082025
thomhurst Aug 10, 2025
c1bde0d
Fix hanging/deadlock issues in test execution
thomhurst Aug 10, 2025
4b183b3
Fix duplicate test IDs for inherited tests and remove debug logs
thomhurst Aug 11, 2025
94acbaf
Add InheritanceDepth property to API change verification files
thomhurst Aug 11, 2025
b97dc9a
Fix TestContext.Dependencies population to include transitive depende…
thomhurst Aug 11, 2025
d16d6d2
Remove redundant comments from dependency resolution code
thomhurst Aug 11, 2025
7795b68
Fix test scheduler deadlock by proactively starting dependencies
thomhurst Aug 11, 2025
e14930b
Fix test scheduler deadlocks in parallel and semaphore-constrained ex…
thomhurst Aug 11, 2025
d0fb98f
Fix test scheduler deadlocks and ensure unique test IDs
thomhurst Aug 11, 2025
3a64efb
Add InheritanceDepth property to test metadata for various test cases
thomhurst Aug 11, 2025
c44c7f9
Optimize parallel processing and add ConfigureAwait(false) to async c…
thomhurst Aug 11, 2025
bc03969
Update hangdump filename format and timeout for improved diagnostics
thomhurst Aug 11, 2025
8610772
Fix build errors with missing FilePath and LineNumber in TestMetadata
thomhurst Aug 11, 2025
a4b59cf
Prevent unobserved task exceptions by ensuring all background tasks a…
thomhurst Aug 11, 2025
9c930ec
Add FilePath and LineNumber properties to test metadata for improved …
thomhurst Aug 11, 2025
970a9af
Add FilePath and LineNumber to test metadata for enhanced diagnostics
thomhurst Aug 11, 2025
59ef8e3
Update hangdump timeout and test timeout for improved performance
thomhurst Aug 11, 2025
8b21d2b
Refactor circular dependency detection to return detailed dependency …
thomhurst Aug 11, 2025
48faea1
Add Microsoft.Testing.Extensions.HangDump package reference and refac…
thomhurst Aug 11, 2025
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Prev Previous commit
Next Next commit
chore: remove obsolete test result files and reflection log
  • Loading branch information
thomhurst committed Aug 8, 2025
commit dd2cc3b028d15411b2fb1a2837f4c4a6c8424e80
16 changes: 11 additions & 5 deletions TUnit.Engine/Models/GroupedTests.cs
Original file line number Diff line number Diff line change
@@ -1,15 +1,21 @@
using TUnit.Core;
using TUnit.Engine.Scheduling;

namespace TUnit.Engine.Models;

internal record GroupedTests
{
public required IList<AbstractExecutableTest> Parallel { get; init; }
// Use arrays for blazingly fast iteration and zero allocation enumeration
public required AbstractExecutableTest[] Parallel { get; init; }

public required PriorityQueue<AbstractExecutableTest, TestPriority> NotInParallel { get; init; }
// Pre-sorted array by priority for ultra-fast iteration
// Tests are already sorted, no need to store priority
public required AbstractExecutableTest[] NotInParallel { get; init; }

public required IDictionary<string, PriorityQueue<AbstractExecutableTest, TestPriority>> KeyedNotInParallel { get; init; }
// Array of key-value pairs since we only iterate, never lookup by key
// Tests within each key are pre-sorted by priority
public required (string Key, AbstractExecutableTest[] Tests)[] KeyedNotInParallel { get; init; }

public required IDictionary<string, SortedDictionary<int, List<AbstractExecutableTest>>> ParallelGroups { get; init; }
// Array of groups with nested arrays for maximum iteration performance
// Tests are grouped by order, ready for parallel execution
public required (string Group, (int Order, AbstractExecutableTest[] Tests)[] OrderedTests)[] ParallelGroups { get; init; }
}
95 changes: 28 additions & 67 deletions TUnit.Engine/Scheduling/TestScheduler.cs
Original file line number Diff line number Diff line change
Expand Up @@ -38,24 +38,21 @@ public async Task ScheduleAndExecuteAsync(
if (tests == null) throw new ArgumentNullException(nameof(tests));
if (executor == null) throw new ArgumentNullException(nameof(executor));

// Create execution plan upfront
var plan = ExecutionPlan.Create(tests);

if (plan.ExecutableTests.Count == 0)
var testList = tests as IList<AbstractExecutableTest> ?? tests.ToList();
if (testList.Count == 0)
{
await _logger.LogDebugAsync("No executable tests found");
return;
}

// Group tests by constraints
var groupedTests = await _groupingService.GroupTestsByConstraintsAsync(plan.ExecutableTests);
var groupedTests = await _groupingService.GroupTestsByConstraintsAsync(testList);

// Execute tests
await ExecuteGroupedTestsAsync(plan, groupedTests, executor, cancellationToken);
await ExecuteGroupedTestsAsync(groupedTests, executor, cancellationToken);
}

private async Task ExecuteGroupedTestsAsync(
ExecutionPlan plan,
GroupedTests groupedTests,
ITestExecutor executor,
CancellationToken cancellationToken)
Expand Down Expand Up @@ -85,10 +82,9 @@ private async Task ExecuteGroupedTestsAsync(
var allTestTasks = new List<Task>();

// 1. NotInParallel tests (global) - must run one at a time
if (groupedTests.NotInParallel.Count > 0)
if (groupedTests.NotInParallel.Length > 0)
{
var globalNotInParallelTask = ExecuteNotInParallelTestsAsync(
plan,
groupedTests.NotInParallel,
executor,
runningTasks,
Expand All @@ -98,11 +94,10 @@ private async Task ExecuteGroupedTestsAsync(
}

// 2. Keyed NotInParallel tests - can run in parallel with other keys
foreach (var kvp in groupedTests.KeyedNotInParallel)
foreach (var (key, tests) in groupedTests.KeyedNotInParallel)
{
var keyedTask = ExecuteKeyedNotInParallelTestsAsync(
plan,
kvp.Value,
tests,
executor,
runningTasks,
completedTests,
Expand All @@ -111,9 +106,9 @@ private async Task ExecuteGroupedTestsAsync(
}

// 3. Parallel groups - can run in parallel within constraints
foreach (var group in groupedTests.ParallelGroups)
foreach (var (groupName, orderedTests) in groupedTests.ParallelGroups)
{
var groupTask = ExecuteParallelGroupAsync(group.Value,
var groupTask = ExecuteParallelGroupAsync(orderedTests,
executor,
runningTasks,
completedTests,
Expand All @@ -136,40 +131,23 @@ private async Task ExecuteGroupedTestsAsync(
}

private async Task ExecuteNotInParallelTestsAsync(
ExecutionPlan plan,
PriorityQueue<AbstractExecutableTest, TestPriority> queue,
AbstractExecutableTest[] tests,
ITestExecutor executor,
ConcurrentDictionary<AbstractExecutableTest, Task> runningTasks,
ConcurrentDictionary<AbstractExecutableTest, bool> completedTests,
CancellationToken cancellationToken)
{
var testsWithPriority = new List<(AbstractExecutableTest Test, TestPriority Priority)>();
while (queue.TryDequeue(out var test, out var priority))
{
testsWithPriority.Add((test, priority));
}

// Group tests by class
var testsByClass = testsWithPriority
.GroupBy(t => t.Test.Context.TestDetails.ClassType)
// Tests are already sorted by priority from TestGroupingService
// Group tests by class for execution
var testsByClass = tests
.GroupBy(t => t.Context.TestDetails.ClassType)
.ToList();

// Sort classes by their minimum test Order
testsByClass.Sort((a, b) =>
{
var aMinOrder = a.Min(t => t.Priority.Order);
var bMinOrder = b.Min(t => t.Priority.Order);
return aMinOrder.CompareTo(bMinOrder);
});

// Execute class by class
foreach (var classGroup in testsByClass)
{
// Sort tests within the class by Order, then by execution plan order
var classTests = classGroup.OrderBy(t => t.Priority.Order)
.ThenBy(t => plan.ExecutionOrder.TryGetValue(t.Test, out var order) ? order : int.MaxValue)
.Select(t => t.Test)
.ToList();
// Tests are already in priority order, just execute them
var classTests = classGroup.ToList();

// Execute all tests from this class sequentially
foreach (var test in classTests)
Expand All @@ -180,40 +158,23 @@ private async Task ExecuteNotInParallelTestsAsync(
}

private async Task ExecuteKeyedNotInParallelTestsAsync(
ExecutionPlan plan,
PriorityQueue<AbstractExecutableTest, TestPriority> queue,
AbstractExecutableTest[] tests,
ITestExecutor executor,
ConcurrentDictionary<AbstractExecutableTest, Task> runningTasks,
ConcurrentDictionary<AbstractExecutableTest, bool> completedTests,
CancellationToken cancellationToken)
{
var testsWithPriority = new List<(AbstractExecutableTest Test, TestPriority Priority)>();
while (queue.TryDequeue(out var test, out var priority))
{
testsWithPriority.Add((test, priority));
}

// Group tests by class
var testsByClass = testsWithPriority
.GroupBy(t => t.Test.Context.TestDetails.ClassType)
// Tests are already sorted by priority from TestGroupingService
// Group tests by class for execution
var testsByClass = tests
.GroupBy(t => t.Context.TestDetails.ClassType)
.ToList();

// Sort classes by their minimum test Order
testsByClass.Sort((a, b) =>
{
var aMinOrder = a.Min(t => t.Priority.Order);
var bMinOrder = b.Min(t => t.Priority.Order);
return aMinOrder.CompareTo(bMinOrder);
});

// Execute class by class within this key
foreach (var classGroup in testsByClass)
{
// Sort tests within the class by Order, then by execution plan order
var classTests = classGroup.OrderBy(t => t.Priority.Order)
.ThenBy(t => plan.ExecutionOrder.TryGetValue(t.Test, out var order) ? order : int.MaxValue)
.Select(t => t.Test)
.ToList();
// Tests are already in priority order, just execute them
var classTests = classGroup.ToList();

// Execute all tests from this class sequentially
foreach (var test in classTests)
Expand All @@ -223,17 +184,17 @@ private async Task ExecuteKeyedNotInParallelTestsAsync(
}
}

private async Task ExecuteParallelGroupAsync(SortedDictionary<int, List<AbstractExecutableTest>> orderGroups,
private async Task ExecuteParallelGroupAsync((int Order, AbstractExecutableTest[] Tests)[] orderedTests,
ITestExecutor executor,
ConcurrentDictionary<AbstractExecutableTest, Task> runningTasks,
ConcurrentDictionary<AbstractExecutableTest, bool> completedTests,
int? maxParallelism,
CancellationToken cancellationToken)
{
// Execute order groups sequentially
foreach (var orderGroup in orderGroups.OrderBy(og => og.Key))
// Execute order groups sequentially (already sorted by order)
foreach (var (order, tests) in orderedTests)
{
var processor = orderGroup.Value
var processor = tests
.ForEachAsync(async test => await ExecuteTestWhenReadyAsync(test, executor, runningTasks, completedTests, cancellationToken));

if (maxParallelism is > 0)
Expand All @@ -247,7 +208,7 @@ private async Task ExecuteParallelGroupAsync(SortedDictionary<int, List<Abstract
}
}

private async Task ExecuteParallelTestsAsync(IEnumerable<AbstractExecutableTest> tests,
private async Task ExecuteParallelTestsAsync(AbstractExecutableTest[] tests,
ITestExecutor executor,
ConcurrentDictionary<AbstractExecutableTest, Task> runningTasks,
ConcurrentDictionary<AbstractExecutableTest, bool> completedTests,
Expand Down
48 changes: 34 additions & 14 deletions TUnit.Engine/Services/TestGroupingService.cs
Original file line number Diff line number Diff line change
Expand Up @@ -18,8 +18,8 @@ public ValueTask<GroupedTests> GroupTestsByConstraintsAsync(IEnumerable<Abstract
{
// Use collection directly if already materialized, otherwise create efficient list
var allTests = tests as IReadOnlyList<AbstractExecutableTest> ?? tests.ToList();
var notInParallelQueue = new PriorityQueue<AbstractExecutableTest, TestPriority>();
var keyedNotInParallelQueues = new Dictionary<string, PriorityQueue<AbstractExecutableTest, TestPriority>>();
var notInParallelList = new List<(AbstractExecutableTest Test, TestPriority Priority)>();
var keyedNotInParallelLists = new Dictionary<string, List<(AbstractExecutableTest Test, TestPriority Priority)>>();
var parallelTests = new List<AbstractExecutableTest>();
var parallelGroups = new Dictionary<string, SortedDictionary<int, List<AbstractExecutableTest>>>();

Expand All @@ -30,7 +30,7 @@ public ValueTask<GroupedTests> GroupTestsByConstraintsAsync(IEnumerable<Abstract
switch (constraint)
{
case NotInParallelConstraint notInParallel:
ProcessNotInParallelConstraint(test, notInParallel, notInParallelQueue, keyedNotInParallelQueues);
ProcessNotInParallelConstraint(test, notInParallel, notInParallelList, keyedNotInParallelLists);
break;

case ParallelGroupConstraint parallelGroup:
Expand All @@ -43,12 +43,32 @@ public ValueTask<GroupedTests> GroupTestsByConstraintsAsync(IEnumerable<Abstract
}
}

// Sort the NotInParallel tests by priority and extract just the tests
notInParallelList.Sort((a, b) => a.Priority.CompareTo(b.Priority));
var sortedNotInParallel = notInParallelList.Select(t => t.Test).ToArray();

// Sort keyed lists by priority and convert to array of tuples
var keyedArrays = keyedNotInParallelLists.Select(kvp =>
{
kvp.Value.Sort((a, b) => a.Priority.CompareTo(b.Priority));
return (kvp.Key, kvp.Value.Select(t => t.Test).ToArray());
}).ToArray();

// Convert parallel groups to array of tuples
var parallelGroupArrays = parallelGroups.Select(kvp =>
{
var orderedTests = kvp.Value.Select(orderKvp =>
(orderKvp.Key, orderKvp.Value.ToArray())
).ToArray();
return (kvp.Key, orderedTests);
}).ToArray();

var result = new GroupedTests
{
Parallel = parallelTests,
NotInParallel = notInParallelQueue,
KeyedNotInParallel = keyedNotInParallelQueues,
ParallelGroups = parallelGroups
Parallel = parallelTests.ToArray(),
NotInParallel = sortedNotInParallel,
KeyedNotInParallel = keyedArrays,
ParallelGroups = parallelGroupArrays
};

return new ValueTask<GroupedTests>(result);
Expand All @@ -57,8 +77,8 @@ public ValueTask<GroupedTests> GroupTestsByConstraintsAsync(IEnumerable<Abstract
private static void ProcessNotInParallelConstraint(
AbstractExecutableTest test,
NotInParallelConstraint constraint,
PriorityQueue<AbstractExecutableTest, TestPriority> notInParallelQueue,
Dictionary<string, PriorityQueue<AbstractExecutableTest, TestPriority>> keyedQueues)
List<(AbstractExecutableTest Test, TestPriority Priority)> notInParallelList,
Dictionary<string, List<(AbstractExecutableTest Test, TestPriority Priority)>> keyedLists)
{
var order = constraint.Order;
var priority = test.Context.ExecutionPriority;
Expand All @@ -67,18 +87,18 @@ private static void ProcessNotInParallelConstraint(

if (constraint.NotInParallelConstraintKeys.Count == 0)
{
notInParallelQueue.Enqueue(test, testPriority);
notInParallelList.Add((test, testPriority));
}
else
{
foreach (var key in constraint.NotInParallelConstraintKeys)
{
if (!keyedQueues.TryGetValue(key, out var queue))
if (!keyedLists.TryGetValue(key, out var list))
{
queue = new PriorityQueue<AbstractExecutableTest, TestPriority>();
keyedQueues[key] = queue;
list = new List<(AbstractExecutableTest Test, TestPriority Priority)>();
keyedLists[key] = list;
}
queue.Enqueue(test, testPriority);
list.Add((test, testPriority));
}
}
}
Expand Down
4 changes: 0 additions & 4 deletions TUnit.Engine/TestDiscoveryService.cs
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,6 @@
using TUnit.Engine.Building;
using TUnit.Engine.Configuration;
using TUnit.Engine.Services;
using TUnit.Engine.Scheduling;

namespace TUnit.Engine;

Expand Down Expand Up @@ -78,9 +77,6 @@ public async Task<TestDiscoveryResult> DiscoverTests(string testSessionId, ITest
// Check for circular dependencies and mark failed tests
_dependencyResolver.CheckForCircularDependencies();

// Create execution plan for ordering
var executionPlan = ExecutionPlan.Create(tests);

// Apply filter first to get the tests we want to run
var filteredTests = _testFilterService.FilterTests(filter, tests);

Expand Down

This file was deleted.

This file was deleted.

This file was deleted.

This file was deleted.

This file was deleted.

This file was deleted.

This file was deleted.

This file was deleted.

This file was deleted.

This file was deleted.

This file was deleted.

This file was deleted.

This file was deleted.

This file was deleted.

This file was deleted.

This file was deleted.

This file was deleted.

This file was deleted.

This file was deleted.

This file was deleted.

This file was deleted.

Binary file removed TUnit.TestProject/reflection_all.txt
Binary file not shown.
Binary file removed TUnit.TestProject/reflection_filtered.txt
Binary file not shown.
Loading