Skip to content

Fix ResourceManagerStringLocalizerFactory caching #33129

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

Merged
merged 7 commits into from
Feb 28, 2022
Merged

Conversation

sebastienros
Copy link
Member

The cache key was using reflection which was visible in allocation profiles since it's called for every view that is rendered.

Before:
image

After
image

Allocations

Type Bytes Before Bytes After
GetCustomAttributeRecords 912,000 0
String 2,196,946 2,017,262

The cache key was using reflection which was visible in allocation profiles since it's called for every view that is rendered.
@sebastienros sebastienros requested a review from Pilchie as a code owner May 29, 2021 00:19

var baseName = GetResourcePrefix(typeInfo);
// Get without Add to prevent unnecessary lambda allocation
if (_localizerCache.TryGetValue(resourceSource.AssemblyQualifiedName!, out var cache))
Copy link
Member

Choose a reason for hiding this comment

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

Can GetResourcePrefix return the same result for different Types? Basically, I'm wondering if this change will mean that additional localizers will end up in the cache because the key isn't shared between types.

Either way, add a comment to that effect explaining why AssemblyQualifiedName is preferable to baseName.

Copy link
Member Author

Choose a reason for hiding this comment

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

I had looked at it a while ago when I first worked on it. Will check and come back with an answer.

Copy link
Member Author

Choose a reason for hiding this comment

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

Can GetResourcePrefix return the same result for different Types

I don't think so, since it will have the type name in the result: return baseNamespace + "." + resourcesRelativePath + TrimPrefix(typeInfo.FullName, baseNamespace + ".");


var assembly = typeInfo.Assembly;
return _localizerCache.GetOrAdd(resourceSource.AssemblyQualifiedName!, _ =>
Copy link
Member

Choose a reason for hiding this comment

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

This lambda has a closure because it needs to get the type. Is there a reason why AssemblyQualifiedName is preferable as a key to the type itself? If the type is the key then it can be accessed in the lambda without a closure and it can be made static.

Copy link
Member Author

Choose a reason for hiding this comment

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

The cache is used by another method that is not using a Type

Copy link
Member

@davidfowl davidfowl May 29, 2021

Choose a reason for hiding this comment

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

This right?

return _localizerCache.GetOrAdd($"B={baseName},L={location}", _ =>
{
var assemblyName = new AssemblyName(location);
var assembly = Assembly.Load(assemblyName);
baseName = GetResourcePrefix(baseName, location);
return CreateResourceManagerStringLocalizer(assembly, baseName);
});

Copy link
Member

Choose a reason for hiding this comment

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

This change looked simple at first, but now I'm not so sure. The methods before were calling a virtual GetResourcePrefix to resolve the cache key, not that's no longer being called so this is a breaking change if you changed that method expecting to cache based on that key (I don't know if that happens in practice though). One workaround is to only do this change if the exact type is ResourceManagerStringLocalizerFactory

Copy link
Contributor

Choose a reason for hiding this comment

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

Yes. It also avoids the keys conflating in case the derived implementation returned AssemblyQualifiedName. Alternatively, you could cache these as ConcurrentDictionary<Type, ResourceManagerStringLocalizer>. There isn't much reason to have the keys be the type name.

Copy link
Member Author

Choose a reason for hiding this comment

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

@pranavkm The IStringLocalizer has two Create methods, one using a Type, the other using two strings. The current implementation uses a single CD to cache the result of both methods, hence it's using a string key. I am not changing that in this PR.

@davidfowl Before this PR the key was using GetResourcePrefix(TypeInfo) directly. Now it's still used in the lambda when the item is added in the cache. This value only differs by type, so by using the type name as the key we are having a direct mapping between the cache entry and the value of this method. The only "breaking change" is that this was called for each cache lookup, and now only once (the goal of this PR since it's reflection heavy).

@JamesNK about the lambda creating a closure, that's why I make a call to the dictionary without the lambda right before. And the type can't be used as the key since the CD is used by the other interface method (that is not using a type).

@Pilchie Pilchie added the area-mvc Includes: MVC, Actions and Controllers, Localization, CORS, most templates label May 31, 2021
@dougbu
Copy link
Contributor

dougbu commented Jul 23, 2021

Conflict is due to #18873 The file needs the new MIT license

@pranavkm pranavkm merged commit 472403e into main Feb 28, 2022
@pranavkm pranavkm deleted the sebros/localizercache branch February 28, 2022 17:42
@ghost ghost added this to the 7.0-preview3 milestone Feb 28, 2022
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
area-mvc Includes: MVC, Actions and Controllers, Localization, CORS, most templates
Projects
None yet
Development

Successfully merging this pull request may close these issues.

6 participants