Skip to content

Conversation

@thomhurst
Copy link
Owner

Summary

  • Defer List<object> allocation in RegisterReceivers until actually needed
  • Use single TryAdd operation instead of Contains + Add pattern (reduces dictionary lookups)
  • Defer List<Exception> allocations in after-hook methods until an exception actually occurs
  • Return empty collection literal [] instead of pre-allocated list when no items

Rationale

CPU profiling with dotnet trace identified EventReceiverOrchestrator.RegisterReceivers as taking 0.36% exclusive time. The changes reduce GC pressure by avoiding unnecessary allocations in the common case where:

  • No exceptions occur during hook execution
  • Objects are already registered (most common path after first test)

Test plan

  • Build succeeds with 0 errors
  • Performance benchmark tests pass (60/60)
  • Test execution timing is consistent with baseline

🤖 Generated with Claude Code

- Defer List<object> allocation in RegisterReceivers until needed
- Use single TryAdd operation instead of Contains + Add pattern
- Defer List<Exception> allocations in after-hook methods until exception occurs
- Return empty collection literal [] instead of pre-allocated list when no items

These changes reduce GC pressure by avoiding unnecessary allocations in the
common case where no exceptions occur and objects are already registered.

Co-Authored-By: Claude Opus 4.5 <[email protected]>
@thomhurst
Copy link
Owner Author

Summary

Performance optimization that defers allocations in event receivers and hook executors until actually needed.

Critical Issues

None found ✅

Suggestions

None - the changes are well-implemented and follow TUnit's performance-first principles.

Analysis

This PR optimizes hot paths in the TUnit engine by applying lazy allocation patterns:

EventReceiverOrchestrator.cs (TUnit.Engine/Services/EventReceiverOrchestrator.cs:41-82)

  1. RegisterReceivers optimization: Defers List<object> allocation until objects need registration, eliminating allocations when all objects are already registered (common case after first test)
  2. Dictionary lookup optimization: Replaces Contains + Add pattern (2 lookups) with single TryAdd operation using HashSet's Add return value

Both files - Exception collection pattern (TUnit.Engine/Services/EventReceiverOrchestrator.cs:148-196, TUnit.Engine/Services/HookExecutor.cs:65-330)

  1. Deferred exception lists: Changes from unconditional new List<Exception>() to List<Exception>? with null-coalescing allocation (exceptions ??= []) only when exceptions occur
  2. Empty return optimization: Returns collection literal [] instead of pre-allocated empty list when no exceptions occur

TUnit Rules Compliance

  • Performance First: Minimizes allocations in hot paths (test execution)
  • AOT Compatible: No reflection changes, existing annotations preserved
  • Modern C#: Uses collection expressions [] and null-coalescing patterns
  • Dual-mode: N/A - engine runtime code only
  • Snapshot tests: N/A - no source generator or public API changes
  • No VSTest: N/A - no test platform changes

The changes are well-justified by CPU profiling data (0.36% exclusive time in RegisterReceivers) and target the common case where hooks execute successfully without exceptions.

Verdict

APPROVE - No critical issues. Solid performance optimization following TUnit's engineering principles.

@thomhurst thomhurst merged commit 0db4047 into main Jan 11, 2026
12 of 13 checks passed
@thomhurst thomhurst deleted the perf/defer-allocations branch January 11, 2026 23:01
This was referenced Jan 12, 2026
{
var exceptions = new List<Exception>();
// Defer exception list allocation until actually needed
List<Exception>? exceptions = null;
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Would be good to move this down after receivers == null check as well. This was done everywhere else

This was referenced Jan 12, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants