-
-
Notifications
You must be signed in to change notification settings - Fork 109
perf: reduce lock contention in test discovery and scheduling #4171
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Conversation
Adds a synthetic benchmark suite for profiling TUnit's performance: - 1000 tests with mixed realistic patterns (60% simple, 30% data-driven, 10% lifecycle) - Scalable via generate-tests.ps1 -Scale <N> for different test counts - Profiling scripts for dotnet-trace + SpeedScope workflow - Baseline runner for all scale tiers (100, 500, 1k, 5k, 10k) This is a prerequisite for the performance optimization work tracked in: - #4159 (Epic) - #4160 (LINQ Elimination) - #4161 (Object Pooling) - #4162 (Lock Contention) - #4163 (Allocation Reduction) 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
- Add CancellationTokenSource with 30-second timeout to ConstraintKeySchedulerConcurrencyTests - Use RunOptions.WithForcefulCancellationToken() to protect against potential deadlocks - Simplify ConstraintKeyStressTests by removing unnecessary execution tracking - Remove redundant verification hooks that were not part of baseline spec Addresses spec compliance issues: 1. Missing timeout protection (CRITICAL) 2. Extra complexity beyond baseline requirements 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
CRITICAL Issues Fixed: - C1: Removed confusing delay and assertion in ConstraintKeySchedulerConcurrencyTests Added comment explaining that timeout itself proves no deadlock - C2: Added verification of constraint key semantics in ConstraintKeyStressTests Tests now verify that tests with same constraint key don't overlap in execution Tracks execution windows and validates mutual exclusion IMPORTANT Issues Fixed: - I1: Reduced code duplication by extracting common logic into ExecuteConstraintKeyStressTest helper All 10 test methods now call shared implementation - I2: Added explanatory comment for timeout in ConstraintKeySchedulerConcurrencyTests - I3: Added tracking of test invocations to verify all tests actually execute MINOR Issues Fixed: - M1: Extracted magic numbers to named constants (WorkDurationMilliseconds, TimeoutMilliseconds) - M2: Added Timeout attributes to all stress test methods to prevent CI hangs Required adding CancellationToken parameter to comply with TUnit analyzer Tests passing: - All 24 ConstraintKeySchedulerConcurrencyTests pass (5 repetitions × multiple modes) 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
…cking Replace LINQ .Any() calls with manual for loops to eliminate allocations in lock-critical paths. This optimization applies to: 1. Initial constraint key availability checking (line 56-69) 2. Waiting test key availability checking (line 165+) Manual loops provide: - Zero allocations (no delegate/enumerator objects) - Early break optimization - Better performance in hot path Part of lock contention optimization (issue #4162). 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
Move testsToRequeue list allocation outside lock in ExecuteTestAndReleaseKeysAsync. Reduces lock duration by eliminating allocations within critical section. - Pre-allocate testsToRequeue alongside testsToStart before entering lock - Rename tempQueue to testsToRequeue for clarity - No behavioral changes, only performance optimization Part of lock contention optimization (issue #4162, Task 4) 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
Replaces List<T> + lock with ImmutableList<T> + atomic operations: - CollectTestsAsync uses CAS loop for atomic swap - CollectTestsStreamingAsync uses ImmutableInterlocked.Update - ClearCaches uses Interlocked.Exchange This eliminates defensive copies under lock and enables lock-free reads. 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
Code Review: Performance Optimization - Lock Contention ReductionThank you for this well-documented performance optimization PR! This is a thoughtful approach to reducing lock contention in critical code paths. Here's my detailed review: ✅ Strengths1. Excellent Documentation
2. Solid Testing Strategy
3. Performance-Focused Changes
🔍 Issues & RecommendationsCRITICAL: Potential Race Condition in ConstraintKeyStressTestsLocation: Problem: The verification logic has a subtle race condition. The test stores its execution window and then immediately checks all other windows for overlaps. However, tests with the same constraint key execute serially, so when test A completes and stores its window, test B (which shares a key) hasn't started yet. The overlap check won't detect violations because test B's window isn't recorded yet. Fix: Move verification to a separate cleanup phase that runs after ALL tests complete, or use a IMPORTANT: ImmutableList Performance CharacteristicsLocation: Issue: While Considerations:
MINOR: Manual Loop Optimization Could Be CleanerLocation: Issue: The manual loops are correct but verbose. Consider extracting to a helper method for reusability for DRY principle. 📋 Code Quality Checklist✅ Dual-Mode Implementation: N/A (engine-only changes) 🎯 TUnit-Specific CompliancePer
🔧 Action ItemsBefore Merge:
Nice to Have:
🏆 Overall AssessmentThis is a well-executed performance optimization with solid engineering practices:
The core optimizations are sound. Once the test race condition is addressed and benchmarks confirm the improvement, this will be a valuable contribution. Recommendation: Request changes to address the CRITICAL race condition, then approve after verification. Great work on this optimization! The attention to detail in documentation and testing is particularly appreciated. 🚀 |
Add stress tests that exercise the ConstraintKeyScheduler under high contention scenarios: - Single-key tests (5 keys × 50 repeats = 250 tests) - Overlapping two-key tests (5 combos × 30 repeats = 150 tests) - Triple-key tests (2 combos × 20 repeats = 40 tests) Total: 440 constraint key tests for profiling lock contention. 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
Benchmark ResultsAdded constraint key stress tests to Wall Clock Time Comparison
Result: Wall clock time is essentially identical (within variance). Why No Visible Improvement?This is expected behavior because:
Actual Benefits (Not Visible in Wall Clock)These optimizations provide benefits that require profiling (dotnet trace) to measure:
Profiling RecommendationTo verify the improvements, use
|
PR Review: Lock Contention OptimizationI've reviewed this PR against TUnit's development guidelines and best practices. Overall, this is a well-executed performance optimization with solid engineering practices. Here's my detailed feedback: ✅ Strengths1. Excellent Adherence to CLAUDE.md Guidelines
2. Smart Performance OptimizationsReflectionTestDataCollector (lines 32-141)
ConstraintKeyScheduler (lines 58-209)
3. Comprehensive Testing Strategy
🔍 Issues & SuggestionsCRITICAL: Potential Fairness Issue in ConstraintKeySchedulerLocation: Problem: The current implementation dequeues ALL waiting tests and processes them in queue order. This can lead to starvation for tests with highly-contested keys. Scenario: When Test C completes:
Test A will never be reconsidered until another test releases "X", even though "Y" is now free. Current Code: while (waitingTests.TryDequeue(out var waitingTest))
{
var canStart = true;
for (var i = 0; i < waitingKeyCount; i++)
{
if (lockedKeys.Contains(waitingTest.ConstraintKeys[i]))
{
canStart = false;
break; // ⚠️ Early exit means we never reconsider this test
}
}
// ...
}Recommendation: Consider a more sophisticated scheduling algorithm:
For now, the current approach is acceptable if queue depth stays low, but add a comment documenting this limitation. MINOR: Variable Naming ClarityLocation: canStart = true;
var keyCount = constraintKeys.Count;Suggestion: SUGGESTION: Add Benchmark BaselineThe PR description mentions:
Recommendation: Complete this checklist item before merge: cd TUnit.PerformanceBenchmarks
dotnet run -c Release --framework net9.0Capture baseline metrics for:
SUGGESTION: Test Coverage GapMissing Test Case: The stress tests verify mutual exclusion, but don't verify parallelism for independent keys. Add a test like: [Test]
public async Task IndependentConstraintKeys_RunInParallel()
{
// Verify tests with keys ["A"], ["B"], ["C"] run concurrently
// Expected: All start before any completes
var results = await RunTestsWithFilter("/*/*/IndependentKeyTests/*");
// Assert: Max concurrency > 1 (tests ran in parallel)
await Assert.That(results.MaxConcurrency).IsGreaterThan(1);
}This ensures the optimization doesn't accidentally serialize everything. 🎯 CLAUDE.md Compliance Check
📊 Performance Impact AssessmentExpected Improvements:
Trade-offs:
🚀 Pre-Merge Checklist
🎖️ Final RecommendationAPPROVE with minor suggestions This PR demonstrates strong understanding of:
The fairness issue is minor and unlikely to cause problems in practice (queue depth should stay low). Consider documenting it and monitoring in production. Suggested Commit Message Enhancement: Great work! 🎉 |
Response to Code ReviewI've analyzed the review feedback against the actual code. Here's my technical evaluation: Issue 1: "Race condition in ConstraintKeyStressTests" — No issue foundThe reviewer claims the verification logic misses overlaps because "test B's window isn't recorded yet when test A checks." Actual behavior (lines 136-178 of Verification happens at completion time. If tests A and B overlap:
The key insight: overlap is detected when either test completes and checks against the other. We don't need both complete simultaneously. When the second overlapping test finishes, it finds the first test's window already recorded and detects the violation. Issue 2: "Fairness/Starvation in ConstraintKeyScheduler" — No issue foundThe reviewer's scenario doesn't demonstrate starvation. Let me trace through
Result: No starvation. The FIFO queue (ConcurrentQueue) ensures tests waiting longest get priority. B runs as soon as its keys are available. Issue 3: ImmutableList O(n) writes — Acknowledged trade-offThis is correct but acceptable:
Issues 4-5: Variable naming / Helper methods — Style preferences
Not implementing these changes. Issue 6: Parallel verification test — Already implicitly verifiedThe benchmarks demonstrate parallelism is working:
An explicit test could be added but isn't critical. ConclusionThe CRITICAL issues identified in the review are based on misunderstanding the code flow. The verification logic and scheduling algorithm are correct as implemented. No changes required. |
Summary
Fixes #4162 - Reduces lock contention in two performance-critical code paths:
.Any()with manual loops, pre-allocated lists outside lock scopeList<T>+ lock toImmutableList<T>with atomic CAS operationsChanges
ConstraintKeyScheduler.csReflectionTestDataCollector.csPerformance Impact
Test Plan
dotnet trace🤖 Generated with Claude Code