Cache reflection lookups in DynamicDataOperations#7535
Cache reflection lookups in DynamicDataOperations#7535Evangelink wants to merge 2 commits intomainfrom
Conversation
There was a problem hiding this comment.
Pull request overview
This PR improves DynamicData reflection performance in MSTest’s DynamicDataOperations by caching inheritance-walking member lookups (fields/properties/methods) so repeated calls don’t traverse the full type hierarchy every time.
Changes:
- Added per-member-kind
ConcurrentDictionarycaches keyed by(Type, memberName)(via a customTypeMemberKeystruct). - Refactored the existing hierarchy-walk implementations into
Lookup*InHierarchyhelpers and routedGet*ConsideringInheritancethrough the caches.
You can also share your feedback on Copilot code review. Take the survey.
Youssef1313
left a comment
There was a problem hiding this comment.
- Have we seen real perf issues here that warrants adding a cache?
- I feel like walking the inheritance hierarchy is already not the right approach. Instead, we should avoid
BindingFlags.DeclaredOnlyand let the .NET runtime do its thing. The .NET reflection implementation has some caching already, so if we are able to avoid walking the hierarchy ourselves and let the caching behavior of reflection do its job, I think that's the best.
…l hierarchy walk Replace the manual type hierarchy walk with DeclaredOnly + loop with a single reflection call using BindingFlags.FlattenHierarchy. This lets the .NET runtime handle inheritance search for static members with its own built-in caching, removing the need for custom ConcurrentDictionary caches and the TypeMemberKey struct.
|
@Youssef1313 Great suggestion — replaced the manual hierarchy walk + caching with a single reflection call using |
96f1ad1 to
28dac23
Compare
src/TestFramework/TestFramework/Attributes/DataSource/DynamicDataOperations.cs
Show resolved
Hide resolved
src/TestFramework/TestFramework/Attributes/DataSource/DynamicDataOperations.cs
Outdated
Show resolved
Hide resolved
Youssef1313
left a comment
There was a problem hiding this comment.
LGTM, with two small comments.
There was a problem hiding this comment.
Pull request overview
Copilot reviewed 1 out of 1 changed files in this pull request and generated 2 comments.
You can also share your feedback on Copilot code review. Take the survey.
| internal static class DynamicDataOperations | ||
| { | ||
| private const BindingFlags DeclaredOnlyLookup = BindingFlags.Public | BindingFlags.NonPublic | BindingFlags.Instance | BindingFlags.Static | BindingFlags.DeclaredOnly; | ||
| private const BindingFlags MemberLookup = BindingFlags.Public | BindingFlags.NonPublic | BindingFlags.Instance | BindingFlags.Static | BindingFlags.FlattenHierarchy; |
There was a problem hiding this comment.
This change switches from a most-derived-first, per-type lookup (via DeclaredOnly + manual BaseType walk) to a single reflection lookup using FlattenHierarchy. That is not behavior-equivalent: (1) BindingFlags.FlattenHierarchy will not return private static members declared on base types, whereas the previous implementation would find them; and (2) the single-call lookup can surface multiple matches across the inheritance chain and throw AmbiguousMatchException in cases where the previous implementation would have returned the derived member. If the intent is purely perf, consider keeping the previous resolution semantics (derived-first, include non-public base members) and introducing caching as described in the PR, rather than changing the BindingFlags behavior.
| internal static class DynamicDataOperations | ||
| { | ||
| private const BindingFlags DeclaredOnlyLookup = BindingFlags.Public | BindingFlags.NonPublic | BindingFlags.Instance | BindingFlags.Static | BindingFlags.DeclaredOnly; | ||
| private const BindingFlags MemberLookup = BindingFlags.Public | BindingFlags.NonPublic | BindingFlags.Instance | BindingFlags.Static | BindingFlags.FlattenHierarchy; |
There was a problem hiding this comment.
The PR title/description mention adding ConcurrentDictionary caches (and a TypeMemberKey struct for net462), but this diff instead removes the custom hierarchy-walk helpers and performs direct reflection lookups with FlattenHierarchy—no caching is introduced. Please update the PR description/title to reflect the actual approach, or include the intended caching changes.
There was a problem hiding this comment.
I'm fine with the behavior change. Let's ensure that the analyzer that validates dynamic data handles it correctly though.
There was a problem hiding this comment.
In DynamicDataShouldBeValidAnalyzer, we have TryGetMemberCore local function. We can possibly add bool disallowPrivate there, and we pass false in the first iteration, and true on subsequent iterations.
The Get*ConsideringInheritance methods (for fields, properties, and methods) walk the full type hierarchy via reflection on every call. This adds a ConcurrentDictionary cache per member kind, keyed by a (Type, memberName) struct, so the hierarchy walk happens only once per unique pair.
Uses a custom TypeMemberKey struct instead of ValueTuple for net462 compatibility.
The gain is most of the time neglictible but for some code bases where the same datasource is reused many times (e.g. MTP and MSTest acceptance tests) there is some gain.
I think it's not hurting readability and maintainance so it's probably worth it to keep it but I am fine if we prefer to close.