-
-
Notifications
You must be signed in to change notification settings - Fork 108
perf: pre-compute and cache typed event receivers #4308
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
SummaryPre-computes and caches typed event receiver arrays to eliminate repeated filtering and sorting on every event invocation. Critical IssuesNone found ✅ SuggestionsPerformance Enhancement Opportunity (Optional)The current implementation still applies stage filtering after caching (lines EventReceiverOrchestrator.cs:106-108, 155-159). While this is correct and simpler, consider whether pre-computing stage-specific caches would provide additional benefit: Current approach:
Alternative:
Given that stage filtering is only used in a few places and the overhead is minimal (simple enum comparison), the current approach is reasonable. Only consider the alternative if profiling shows stage filtering is a measurable cost. Minor: Cache Invalidation ClarityThe TUnit Rule Compliance✅ Performance First - Minimizes allocations via caching and single-pass categorization Verdict✅ APPROVE - Solid performance optimization with proper cache invalidation handling. |
SummaryPre-computes and caches typed event receiver arrays to eliminate repeated filtering and sorting on every event invocation. Critical Issues1. Race Condition in Cache Initialization
|
Instead of filtering eligible objects by interface type on every event invocation, pre-compute and cache typed receiver arrays. Changes: - Add cached receiver arrays to TestContext for each receiver type - Single-pass categorization: iterate eligible objects once, classify by interface type, sort by Order, apply scoped-attribute filtering - Track class instance to invalidate caches on retry (new instance) - Update EventReceiverOrchestrator to use cached receivers directly - Update TestBuilder and TestFilterService to use cached receivers Benefits: - Eliminates repeated interface type checks (is ITestStartReceiver) - Eliminates repeated list building and sorting per invocation - Eliminates repeated scoped-attribute filtering - Single iteration through eligible objects populates all caches Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
a27d40e to
3ab7652
Compare
SummaryPre-computes and caches typed event receiver arrays to eliminate repeated filtering and sorting on every event invocation. Critical IssuesRace Condition in Cache Initialization
|
The engine doesn't call these methods from multiple threads, so locking overhead isn't worth it. Simplified to: - Single cache initialization path through EnsureEventReceiversCached - Cache validation checks class instance hasn't changed - GetEligibleEventObjects delegates to EnsureEventReceiversCached Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
SummaryPre-computes and caches typed event receiver arrays to eliminate repetitive filtering, sorting, and scoped-attribute processing on each event invocation, with cache invalidation handling for test retry scenarios. Critical Issues1. Stage filtering breaks the caching optimizationLocation: Problem: The caching strategy doesn't account for stage filtering, which happens AFTER retrieving cached receivers. Each test invokes event receivers twice (Early + Late stages), but the cache stores ALL receivers together regardless of stage. Current flow:
Impact: The primary benefit of caching (avoiding iteration) is lost since you still iterate through the cached array to filter by stage on every invocation. Looking at Suggested fix: Either:
This doesn't cause incorrect behavior, but significantly reduces the performance benefit described in the PR. SuggestionsNone - the implementation is otherwise sound. The cache invalidation logic for class instance changes is well-thought-out and handles test retry scenarios correctly. Verdict |
|
Regarding the race condition concerns: The same test won't have multiple threads accessing those methods at the same time - the engine scopes tests properly. Each TestContext is accessed by a single test execution thread, so the locking overhead isn't necessary. The cache validation still handles the retry scenario correctly (when class instance changes), but we don't need thread synchronization since concurrent access to the same TestContext doesn't occur in practice. |
On .NET 8+, pre-compute separate caches for Early and Late stage receivers to eliminate runtime stage filtering. This ensures the caching optimization delivers full performance benefits. - TestContext: Add stage-specific cache properties for .NET 8+ - TestContextExtensions: Categorize receivers by stage during cache build - EventReceiverOrchestrator: Use stage-specific caches directly When stage is null (e.g., skipped tests), both Early and Late receivers are invoked in order for backward compatibility. Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
SummaryPre-computes and caches typed event receiver arrays to eliminate repeated filtering on every event invocation, with class instance tracking for retry invalidation. Critical IssuesNone found Suggestions1. Consider AOT annotations for reflection usageWhile the code does not directly call reflection APIs, BuildEligibleEventObjects accesses details.ClassInstance and metadata that may involve reflection internally. Consider verifying whether DynamicallyAccessedMembers annotations are needed on any reflected types, as per Rule 5 (AOT compatibility). 2. Performance validation looks goodThe PR correctly addresses hot path optimization (Rule 4):
The PR description mentions 60/60 performance benchmark tests pass which validates the performance improvements. Rule Compliance Check
VerdictAPPROVE - No critical issues. This is a solid performance optimization that follows TUnit patterns. The suggestion about AOT annotations is precautionary rather than blocking. |
Summary
Before
Each event invocation:
is ITestStartEventReceiverAfter
First access:
Subsequent accesses:
Class instance invalidation
The test class instance can change on retries. We track which instance was used when building caches and invalidate all caches if it changes.
Test plan
🤖 Generated with Claude Code