perf: Add caching to AttributeHelpers.GetCustomAttributesIncludingBaseInterfaces#1730
Conversation
…eInterfaces Fixes #1532 Add a ConcurrentDictionary cache to store attribute lookup results per (Type, AttributeType) combination to avoid repeated reflection calls on each invocation. 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
There was a problem hiding this comment.
Pull request overview
This PR adds caching to AttributeHelpers.GetCustomAttributesIncludingBaseInterfaces to improve performance by eliminating redundant reflection calls for repeated attribute queries on the same type.
Key Changes:
- Introduced a
ConcurrentDictionarycache with composite keys of(Type, AttributeType) - Refactored the method from using
yield returnto materializing results into an array for caching - Created a new internal helper method
GetAttributesInternalto separate attribute collection from caching logic
| if (Cache.TryGetValue(key, out var cachedResult)) | ||
| { | ||
| return cachedResult.Cast<T>(); | ||
| } | ||
|
|
||
| var attributes = GetAttributesInternal<T>(type); | ||
| Cache[key] = attributes; |
There was a problem hiding this comment.
Using the indexer assignment Cache[key] = attributes can lead to redundant computation if multiple threads race to populate the same cache entry. Consider using GetOrAdd to ensure the computation happens only once per key, or use TryAdd to avoid overwriting an existing entry that another thread may have added.
| if (Cache.TryGetValue(key, out var cachedResult)) | |
| { | |
| return cachedResult.Cast<T>(); | |
| } | |
| var attributes = GetAttributesInternal<T>(type); | |
| Cache[key] = attributes; | |
| var attributes = Cache.GetOrAdd(key, _ => GetAttributesInternal<T>(type)); |
|
|
||
| internal static class AttributeHelpers | ||
| { | ||
| private static readonly ConcurrentDictionary<(Type Type, Type AttributeType), object[]> Cache = new(); |
There was a problem hiding this comment.
The cache grows unbounded and will never evict entries, which could lead to memory issues in long-running applications that work with many different types. Consider implementing a cache eviction policy (e.g., LRU, size limit) or documenting this limitation if the unbounded growth is acceptable for the expected usage patterns.
SummaryAdds caching to attribute lookups using ConcurrentDictionary to improve performance during module dependency resolution. Critical IssuesNone found ✅ SuggestionsConsider memory lifecycle: The cache is static and lives for the entire application lifetime. While this is likely fine for typical pipeline scenarios (single execution per process), consider if there are any long-running scenarios where modules could be dynamically loaded/unloaded. Minor optimization: The current implementation creates a List and converts to array. You could use ToArray() directly on the LINQ expressions to avoid the intermediate list. However, the current implementation is clearer and the performance difference is negligible. Verdict✅ APPROVE - No critical issues This follows the same performance optimization pattern as PR #1726. The caching is appropriate since attributes are compile-time metadata that doesn't change at runtime. The use of ConcurrentDictionary ensures thread-safety for parallel module resolution. |
Summary
ConcurrentDictionary<(Type, Type), object[]>cache to store attribute lookup results(Type, AttributeType)for efficient lookupsFixes #1532
Test plan
🤖 Generated with Claude Code