Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
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
3 changes: 2 additions & 1 deletion TUnit.Engine/Framework/TUnitServiceProvider.cs
Original file line number Diff line number Diff line change
Expand Up @@ -256,7 +256,8 @@ public TUnitServiceProvider(IExtension extension,

var constraintKeyScheduler = Register<IConstraintKeyScheduler>(new ConstraintKeyScheduler(
testRunner,
Logger));
Logger,
hashSetPool));

var staticPropertyHandler = Register(new StaticPropertyHandler(Logger, objectTracker, trackableObjectGraphProvider, disposer, lazyPropertyInjector, objectGraphDiscoveryService));

Expand Down
80 changes: 47 additions & 33 deletions TUnit.Engine/Scheduling/ConstraintKeyScheduler.cs
Original file line number Diff line number Diff line change
Expand Up @@ -9,13 +9,16 @@ internal sealed class ConstraintKeyScheduler : IConstraintKeyScheduler
{
private readonly TestRunner _testRunner;
private readonly TUnitFrameworkLogger _logger;
private readonly HashSetPool _hashSetPool;

public ConstraintKeyScheduler(
TestRunner testRunner,
TUnitFrameworkLogger logger)
TUnitFrameworkLogger logger,
HashSetPool hashSetPool)
{
_testRunner = testRunner;
_logger = logger;
_hashSetPool = hashSetPool;
}

#if NET8_0_OR_GREATER
Expand Down Expand Up @@ -152,52 +155,63 @@ private async Task ExecuteTestAndReleaseKeysAsync(
// Release the constraint keys and check if any waiting tests can now run
var testsToStart = new List<WaitingTest>();

lock (lockObject)
// Rent a pooled HashSet to collect candidates; returned immediately after we
// copy out the (priority-sorted) elements so we avoid allocating a fresh set
// per constraint-key release on hot scheduling paths.
var candidates = _hashSetPool.Rent<WaitingTest>();
try
{
// Release all constraint keys for this test
foreach (var key in constraintKeys)
lock (lockObject)
{
lockedKeys.Remove(key);
}
// Release all constraint keys for this test
foreach (var key in constraintKeys)
{
lockedKeys.Remove(key);
}

// Only examine tests that are waiting on the keys we just released (O(k) lookup)
var candidates = waitingTestIndex.GetCandidatesForReleasedKeys(constraintKeys);
// Only examine tests that are waiting on the keys we just released (O(k) lookup)
waitingTestIndex.GetCandidatesForReleasedKeys(constraintKeys, candidates);

// Sort candidates by priority to respect ordering
// Use a simple list + sort rather than a SortedSet to avoid per-element allocation
var sortedCandidates = new List<WaitingTest>(candidates.Count);
sortedCandidates.AddRange(candidates);
sortedCandidates.Sort(static (a, b) => a.Priority.CompareTo(b.Priority));
// Sort candidates by priority to respect ordering
// Use a simple list + sort rather than a SortedSet to avoid per-element allocation
var sortedCandidates = new List<WaitingTest>(candidates.Count);
sortedCandidates.AddRange(candidates);
sortedCandidates.Sort(static (a, b) => a.Priority.CompareTo(b.Priority));

foreach (var candidate in sortedCandidates)
{
// Check if all constraint keys are available for this candidate
var canStart = true;
var waitingKeyCount = candidate.ConstraintKeys.Count;
for (var i = 0; i < waitingKeyCount; i++)
foreach (var candidate in sortedCandidates)
{
if (lockedKeys.Contains(candidate.ConstraintKeys[i]))
// Check if all constraint keys are available for this candidate
var canStart = true;
var waitingKeyCount = candidate.ConstraintKeys.Count;
for (var i = 0; i < waitingKeyCount; i++)
{
canStart = false;
break;
if (lockedKeys.Contains(candidate.ConstraintKeys[i]))
{
canStart = false;
break;
}
}
}

if (canStart)
{
// Lock the keys for this test
for (var i = 0; i < waitingKeyCount; i++)
if (canStart)
{
lockedKeys.Add(candidate.ConstraintKeys[i]);
// Lock the keys for this test
for (var i = 0; i < waitingKeyCount; i++)
{
lockedKeys.Add(candidate.ConstraintKeys[i]);
}

// Remove from the index and mark for starting
waitingTestIndex.Remove(candidate);
testsToStart.Add(candidate);
}

// Remove from the index and mark for starting
waitingTestIndex.Remove(candidate);
testsToStart.Add(candidate);
// If can't start, leave it in the index for future key releases
}
// If can't start, leave it in the index for future key releases
}
}
finally
{
_hashSetPool.Return(candidates);
}

// Log and signal tests to start outside the lock
if (_logger.IsTraceEnabled)
Expand Down
13 changes: 6 additions & 7 deletions TUnit.Engine/Scheduling/WaitingTestIndex.cs
Original file line number Diff line number Diff line change
Expand Up @@ -77,13 +77,14 @@ public void Remove(WaitingTest waitingTest)
}

/// <summary>
/// Returns a deduplicated set of waiting tests that are associated with any of the released keys.
/// These are candidates that might be unblocked (but still need to be checked against locked keys).
/// Populates the supplied set with a deduplicated collection of waiting tests
/// that are associated with any of the released keys. These are candidates that
/// might be unblocked (but still need to be checked against locked keys).
/// The caller owns the lifetime of <paramref name="candidates"/> (typically rented
/// from a pool) so this method avoids allocating a fresh set per key release.
/// </summary>
public HashSet<WaitingTest> GetCandidatesForReleasedKeys(IReadOnlyList<string> releasedKeys)
public void GetCandidatesForReleasedKeys(IReadOnlyList<string> releasedKeys, HashSet<WaitingTest> candidates)
{
var candidates = new HashSet<WaitingTest>();

var keyCount = releasedKeys.Count;
for (var i = 0; i < keyCount; i++)
{
Expand All @@ -93,7 +94,5 @@ public HashSet<WaitingTest> GetCandidatesForReleasedKeys(IReadOnlyList<string> r
candidates.UnionWith(tests);
}
}

return candidates;
}
}
Loading