Skip to content

perf: Add caching to AttributeHelpers.GetCustomAttributesIncludingBaseInterfaces#1730

Merged
thomhurst merged 1 commit into
mainfrom
fix/1532-attribute-caching
Jan 1, 2026
Merged

perf: Add caching to AttributeHelpers.GetCustomAttributesIncludingBaseInterfaces#1730
thomhurst merged 1 commit into
mainfrom
fix/1532-attribute-caching

Conversation

@thomhurst
Copy link
Copy Markdown
Owner

Summary

  • Added a ConcurrentDictionary<(Type, Type), object[]> cache to store attribute lookup results
  • Cache key is a tuple of (Type, AttributeType) for efficient lookups
  • Eliminates redundant reflection calls for repeated attribute queries on the same type

Fixes #1532

Test plan

  • Verify the project builds successfully
  • Existing unit tests should pass unchanged (behavior is identical, only performance improves)
  • Verify caching behavior in scenarios with repeated attribute lookups

🤖 Generated with Claude Code

…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>
Copilot AI review requested due to automatic review settings January 1, 2026 21:41
Copy link
Copy Markdown

Copilot AI left a comment

Choose a reason for hiding this comment

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

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 ConcurrentDictionary cache with composite keys of (Type, AttributeType)
  • Refactored the method from using yield return to materializing results into an array for caching
  • Created a new internal helper method GetAttributesInternal to separate attribute collection from caching logic

Comment on lines +15 to +21
if (Cache.TryGetValue(key, out var cachedResult))
{
return cachedResult.Cast<T>();
}

var attributes = GetAttributesInternal<T>(type);
Cache[key] = attributes;
Copy link

Copilot AI Jan 1, 2026

Choose a reason for hiding this comment

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

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.

Suggested change
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));

Copilot uses AI. Check for mistakes.

internal static class AttributeHelpers
{
private static readonly ConcurrentDictionary<(Type Type, Type AttributeType), object[]> Cache = new();
Copy link

Copilot AI Jan 1, 2026

Choose a reason for hiding this comment

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

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.

Copilot uses AI. Check for mistakes.
@thomhurst
Copy link
Copy Markdown
Owner Author

Summary

Adds caching to attribute lookups using ConcurrentDictionary to improve performance during module dependency resolution.

Critical Issues

None found ✅

Suggestions

Consider 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.

@thomhurst thomhurst merged commit fac3579 into main Jan 1, 2026
18 of 19 checks passed
@thomhurst thomhurst deleted the fix/1532-attribute-caching branch January 1, 2026 21:57
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.

Performance: AttributeHelpers.GetCustomAttributesIncludingBaseInterfaces lacks caching

2 participants