-
-
Notifications
You must be signed in to change notification settings - Fork 108
perf: cache TestNode properties to reduce allocations #4293
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
Co-Authored-By: Claude Opus 4.5 <[email protected]>
ToTestNode is called 3+ times per test (discovered, in-progress, passed/failed). Before this change, it was creating dozens of objects per call including: - Assembly.GetName().FullName which allocates AssemblyName - TestFileLocationProperty - TestMethodIdentifierProperty - TestMetadataProperty[] for categories and custom properties - TrxCategoriesProperty This change: - Caches AssemblyName.FullName per assembly using ConcurrentDictionary - Caches all static TestNode properties per test ID that never change between state transitions - Uses StringBuilderPool in GetClassTypeName instead of new StringBuilder Profiling shows ToTestNode dropped from 3.06% exclusive CPU time to effectively negligible (not in top 50 hotspots). Async overhead also improved from ~4.13% to ~3.28% due to reduced GC pressure. For a 1000 test suite, this eliminates approximately 6000+ object allocations per run. Co-Authored-By: Claude Opus 4.5 <[email protected]>
SummaryPerformance optimization that caches TestNode properties and assembly names to reduce allocations in the hot path. Critical Issues1. Thread Safety Concern with Cache Invalidation
|
Co-Authored-By: Claude Opus 4.5 <[email protected]>
SummaryThis PR caches static TestNode properties and assembly names to eliminate ~6000+ allocations per 1000-test run, reducing ToTestNode from 3% of execution time to not appearing in the top 50 hot paths. Critical IssuesNone found ✅ Suggestions1. Thread-Safety Documentation for Caching StrategyThe PR introduces two static ConcurrentDictionary caches (AssemblyFullNameCache and TestNodePropertiesCache) that are shared across all test executions. While ConcurrentDictionary is thread-safe, consider documenting:
Location: TUnit.Engine/Extensions/TestExtensions.cs:19-20 2. Potential Over-Caching of Test IDsThe TestNodePropertiesCache keys on testDetails.TestId. If test IDs are globally unique across all test runs, this is fine. However, if dynamic tests can have colliding IDs across different test sessions or test IDs can be reused in watch mode, the cache might return stale data. Question: Are test IDs guaranteed to be unique within the lifetime of these static caches? 3. StringBuilder Pool UsageThe change correctly uses StringBuilderPool.Get() and returns it in a finally block. This is a good performance improvement. ✅ Performance ValidationThe PR claims significant improvements based on profiling:
Recommendation: Run the full test suite to verify no regressions, especially in concurrent execution scenarios. Verdict✅ APPROVE - No critical issues. The performance improvements are substantial and the implementation is sound. Suggestions above are optional enhancements for documentation and edge cases. |
Add ClearCaches() method to TestExtensions and call it during TUnitServiceProvider.DisposeAsync() to prevent unbounded cache growth in long-running scenarios. Co-Authored-By: Claude Opus 4.5 <[email protected]>
SummaryCaches static TestNode properties (file location, method identifier, categories, TRX metadata) to eliminate 6000+ allocations per 1000-test run. Critical IssuesNone found ✅ Suggestions1. Consider memory growth in long-running scenariosThe Context: The caches are cleared in Recommendation: Consider adding metrics or documentation about cache behavior, or explore size-bounded caching if dynamic test generation is common. Not a blocker since 2. StringBuilderPool usage is excellentThe migration from manual StringBuilder to 3. Cache key uniqueness assumptionThe code assumes Performance NotesThe profiling data shows impressive results:
This aligns well with TUnit's Performance First principle from CLAUDE.md. Verdict✅ APPROVE - Solid performance optimization with proper thread-safety and cleanup. Suggestions are optional improvements for edge cases. |
Summary
AssemblyName.FullNameper assembly usingConcurrentDictionaryto avoid repeatedGetName()allocationsTestNodeproperties per test ID that never change between state transitions (discovered → in-progress → passed/failed)StringBuilderPoolinGetClassTypeNameinstead of allocating newStringBuilderProblem
ToTestNodeis called 3+ times per test (discovered, in-progress, passed/failed). Before this change, it was creating dozens of objects per call including:Assembly.GetName().FullNamewhich allocates a newAssemblyNameeach timeTestFileLocationPropertyTestMethodIdentifierPropertyTestMetadataProperty[]for categories and custom propertiesTrxCategoriesPropertySolution
Properties that never change between test state transitions are now cached on first access:
TestFileLocationPropertyTestMethodIdentifierPropertyResults
Profiling with
dotnet-traceshows:TestExtensions.ToTestNodeAsyncMethodBuilderCore.StartMonitor.Enter_SlowpathFor a 1000 test suite, this eliminates approximately 6000+ object allocations per run.
Test plan
🤖 Generated with Claude Code