From 9384848cc7c0a4faf92c3d2157fcd7621ac3fe57 Mon Sep 17 00:00:00 2001 From: damianedwards Date: Wed, 20 May 2015 14:41:36 -0700 Subject: [PATCH] Cache walk of the culture tree when building resource name list: - #15 --- Localization.sln | 9 ++ NuGet.Config | 1 + .../Properties/AssemblyInfo.cs | 6 + .../ResourceManagerStringLocalizer.cs | 80 +++++----- ...icrosoft.Framework.Localization.Test.xproj | 21 +++ .../ResourceManagerStringLocalizerTest.cs | 139 ++++++++++++++++++ .../project.json | 16 ++ 7 files changed, 227 insertions(+), 45 deletions(-) create mode 100644 src/Microsoft.Framework.Localization/Properties/AssemblyInfo.cs create mode 100644 test/Microsoft.Framework.Localization.Test/Microsoft.Framework.Localization.Test.xproj create mode 100644 test/Microsoft.Framework.Localization.Test/ResourceManagerStringLocalizerTest.cs create mode 100644 test/Microsoft.Framework.Localization.Test/project.json diff --git a/Localization.sln b/Localization.sln index 841e5c453f..f5fe65e09e 100644 --- a/Localization.sln +++ b/Localization.sln @@ -24,6 +24,10 @@ Project("{8BB2217D-0F2D-49D1-97BC-3654ED321F3B}") = "CultureInfoGenerator", "src EndProject Project("{8BB2217D-0F2D-49D1-97BC-3654ED321F3B}") = "Microsoft.Framework.Globalization.CultureInfoCache", "src\Microsoft.Framework.Globalization.CultureInfoCache\Microsoft.Framework.Globalization.CultureInfoCache.xproj", "{F3988D3A-A4C8-4FD7-BAFE-13E0D0A1659A}" EndProject +Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "test", "test", "{B723DB83-A670-4BCB-95FB-195361331AD2}" +EndProject +Project("{8BB2217D-0F2D-49D1-97BC-3654ED321F3B}") = "Microsoft.Framework.Localization.Test", "test\Microsoft.Framework.Localization.Test\Microsoft.Framework.Localization.Test.xproj", "{287AD58D-DF34-4F16-8616-FD78FA1CADF9}" +EndProject Global GlobalSection(SolutionConfigurationPlatforms) = preSolution Debug|Any CPU = Debug|Any CPU @@ -54,6 +58,10 @@ Global {F3988D3A-A4C8-4FD7-BAFE-13E0D0A1659A}.Debug|Any CPU.Build.0 = Debug|Any CPU {F3988D3A-A4C8-4FD7-BAFE-13E0D0A1659A}.Release|Any CPU.ActiveCfg = Release|Any CPU {F3988D3A-A4C8-4FD7-BAFE-13E0D0A1659A}.Release|Any CPU.Build.0 = Release|Any CPU + {287AD58D-DF34-4F16-8616-FD78FA1CADF9}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {287AD58D-DF34-4F16-8616-FD78FA1CADF9}.Debug|Any CPU.Build.0 = Debug|Any CPU + {287AD58D-DF34-4F16-8616-FD78FA1CADF9}.Release|Any CPU.ActiveCfg = Release|Any CPU + {287AD58D-DF34-4F16-8616-FD78FA1CADF9}.Release|Any CPU.Build.0 = Release|Any CPU EndGlobalSection GlobalSection(SolutionProperties) = preSolution HideSolutionNode = FALSE @@ -65,5 +73,6 @@ Global {55D9501F-15B9-4339-A0AB-6082850E5FCE} = {79878809-8D1C-4BD4-BA99-F1F13FF96FD8} {BD22AE1C-6631-4DA6-874D-0DC0F803CEAB} = {FB313677-BAB3-4E49-8CDB-4FA4A9564767} {F3988D3A-A4C8-4FD7-BAFE-13E0D0A1659A} = {FB313677-BAB3-4E49-8CDB-4FA4A9564767} + {287AD58D-DF34-4F16-8616-FD78FA1CADF9} = {B723DB83-A670-4BCB-95FB-195361331AD2} EndGlobalSection EndGlobal diff --git a/NuGet.Config b/NuGet.Config index da57d47267..da5e353ffb 100644 --- a/NuGet.Config +++ b/NuGet.Config @@ -2,6 +2,7 @@ + \ No newline at end of file diff --git a/src/Microsoft.Framework.Localization/Properties/AssemblyInfo.cs b/src/Microsoft.Framework.Localization/Properties/AssemblyInfo.cs new file mode 100644 index 0000000000..e3c2061307 --- /dev/null +++ b/src/Microsoft.Framework.Localization/Properties/AssemblyInfo.cs @@ -0,0 +1,6 @@ +// Copyright (c) .NET Foundation. All rights reserved. +// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. + +using System.Runtime.CompilerServices; + +[assembly: InternalsVisibleTo("Microsoft.Framework.Localization.Test")] diff --git a/src/Microsoft.Framework.Localization/ResourceManagerStringLocalizer.cs b/src/Microsoft.Framework.Localization/ResourceManagerStringLocalizer.cs index ae0772ac64..1ff97dfb8d 100644 --- a/src/Microsoft.Framework.Localization/ResourceManagerStringLocalizer.cs +++ b/src/Microsoft.Framework.Localization/ResourceManagerStringLocalizer.cs @@ -18,9 +18,12 @@ namespace Microsoft.Framework.Localization /// public class ResourceManagerStringLocalizer : IStringLocalizer { - private readonly ConcurrentDictionary _missingManifestCache = - new ConcurrentDictionary(); + private readonly ConcurrentDictionary _missingManifestCache = + new ConcurrentDictionary(); + private static readonly ConcurrentDictionary> _resourceNamesCache = + new ConcurrentDictionary>(); + /// /// Creates a new . /// @@ -96,9 +99,10 @@ namespace Microsoft.Framework.Localization /// The name of the string resource. /// The to get the string for. /// The resource string, or null if none was found. - protected string GetStringSafely([NotNull] string name, [NotNull] CultureInfo culture) + protected string GetStringSafely([NotNull] string name, CultureInfo culture) { - var cacheKey = new MissingManifestCacheKey(name, culture ?? CultureInfo.CurrentUICulture); + var cacheKey = $"name={name}&culture={(culture ?? CultureInfo.CurrentUICulture).Name}"; + if (_missingManifestCache.ContainsKey(cacheKey)) { return null; @@ -136,7 +140,6 @@ namespace Microsoft.Framework.Localization /// The . protected IEnumerator GetEnumerator([NotNull] CultureInfo culture) { - // TODO: I'm sure something here should be cached, probably the whole result var resourceNames = GetResourceNamesFromCultureHierarchy(culture); foreach (var name in resourceNames) @@ -146,6 +149,12 @@ namespace Microsoft.Framework.Localization } } + // Internal to allow testing + internal static void ClearResourceNamesCache() + { + _resourceNamesCache.Clear(); + } + private IEnumerable GetResourceNamesFromCultureHierarchy(CultureInfo startingCulture) { var currentCulture = startingCulture; @@ -155,20 +164,10 @@ namespace Microsoft.Framework.Localization { try { - var resourceStreamName = ResourceBaseName; - if (!string.IsNullOrEmpty(currentCulture.Name)) + var cultureResourceNames = GetResourceNamesForCulture(currentCulture); + foreach (var resourceName in cultureResourceNames) { - resourceStreamName += "." + currentCulture.Name; - } - resourceStreamName += ".resources"; - using (var cultureResourceStream = ResourceAssembly.GetManifestResourceStream(resourceStreamName)) - using (var resources = new ResourceReader(cultureResourceStream)) - { - foreach (DictionaryEntry entry in resources) - { - var resourceName = (string)entry.Key; - resourceNames.Add(resourceName); - } + resourceNames.Add(resourceName); } } catch (MissingManifestResourceException) { } @@ -185,43 +184,34 @@ namespace Microsoft.Framework.Localization return resourceNames; } - private class MissingManifestCacheKey : IEquatable + private IList GetResourceNamesForCulture(CultureInfo culture) { - private readonly int _hashCode; - - public MissingManifestCacheKey(string name, CultureInfo culture) + var resourceStreamName = ResourceBaseName; + if (!string.IsNullOrEmpty(culture.Name)) { - Name = name; - CultureInfo = culture; - _hashCode = new { Name, CultureInfo }.GetHashCode(); + resourceStreamName += "." + culture.Name; } + resourceStreamName += ".resources"; - public string Name { get; } + var cacheKey = $"assembly={ResourceAssembly.FullName};resourceStreamName={resourceStreamName}"; - public CultureInfo CultureInfo { get; } - - public bool Equals(MissingManifestCacheKey other) + var cultureResourceNames = _resourceNamesCache.GetOrAdd(cacheKey, key => { - return string.Equals(Name, other.Name, StringComparison.Ordinal) - && CultureInfo == other.CultureInfo; - } - - public override bool Equals(object obj) - { - var other = obj as MissingManifestCacheKey; - - if (other != null) + var names = new List(); + using (var cultureResourceStream = ResourceAssembly.GetManifestResourceStream(key)) + using (var resources = new ResourceReader(cultureResourceStream)) { - return Equals(other); + foreach (DictionaryEntry entry in resources) + { + var resourceName = (string)entry.Key; + names.Add(resourceName); + } } - return base.Equals(obj); - } + return names; + }); - public override int GetHashCode() - { - return _hashCode; - } + return cultureResourceNames; } } } \ No newline at end of file diff --git a/test/Microsoft.Framework.Localization.Test/Microsoft.Framework.Localization.Test.xproj b/test/Microsoft.Framework.Localization.Test/Microsoft.Framework.Localization.Test.xproj new file mode 100644 index 0000000000..f307c04f02 --- /dev/null +++ b/test/Microsoft.Framework.Localization.Test/Microsoft.Framework.Localization.Test.xproj @@ -0,0 +1,21 @@ + + + + 14.0 + $(MSBuildExtensionsPath32)\Microsoft\VisualStudio\v$(VisualStudioVersion) + + + + 287ad58d-df34-4f16-8616-fd78fa1cadf9 + Microsoft.Framework.Localization.Test + ..\..\artifacts\obj\$(MSBuildProjectName) + ..\..\artifacts\bin\$(MSBuildProjectName)\ + + + 2.0 + + + + + + \ No newline at end of file diff --git a/test/Microsoft.Framework.Localization.Test/ResourceManagerStringLocalizerTest.cs b/test/Microsoft.Framework.Localization.Test/ResourceManagerStringLocalizerTest.cs new file mode 100644 index 0000000000..ce88695724 --- /dev/null +++ b/test/Microsoft.Framework.Localization.Test/ResourceManagerStringLocalizerTest.cs @@ -0,0 +1,139 @@ +// Copyright (c) .NET Foundation. All rights reserved. +// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. + +using System.Globalization; +using System.IO; +using System.Linq; +using System.Reflection; +using System.Resources; +using Moq; +using Xunit; + +namespace Microsoft.Framework.Localization.Test +{ + public class ResourceManagerStringLocalizerTest + { + [Fact] + public void EnumeratorCachesCultureWalkForSameAssembly() + { + // Arrange + ResourceManagerStringLocalizer.ClearResourceNamesCache(); + var resourceManager = new Mock(); + var resourceAssembly = new Mock(); + resourceAssembly.Setup(rm => rm.GetManifestResourceStream(It.IsAny())) + .Returns(() => MakeResourceStream()); + var baseName = "test"; + var localizer1 = new ResourceManagerStringLocalizer( + resourceManager.Object, + resourceAssembly.Object, + baseName); + var localizer2 = new ResourceManagerStringLocalizer( + resourceManager.Object, + resourceAssembly.Object, + baseName); + + // Act + for (int i = 0; i < 5; i++) + { + localizer1.ToList(); + localizer2.ToList(); + } + + // Assert + var expectedCallCount = GetCultureInfoDepth(CultureInfo.CurrentUICulture); + resourceAssembly.Verify( + rm => rm.GetManifestResourceStream(It.IsAny()), + Times.Exactly(expectedCallCount)); + } + + [Fact] + public void EnumeratorCacheIsScopedByAssembly() + { + // Arrange + ResourceManagerStringLocalizer.ClearResourceNamesCache(); + var resourceManager = new Mock(); + var resourceAssembly1 = new Mock(); + resourceAssembly1.CallBase = true; + var resourceAssembly2 = new Mock(); + resourceAssembly2.CallBase = true; + resourceAssembly1.Setup(rm => rm.GetManifestResourceStream(It.IsAny())) + .Returns(() => MakeResourceStream()); + resourceAssembly2.Setup(rm => rm.GetManifestResourceStream(It.IsAny())) + .Returns(() => MakeResourceStream()); + var baseName = "test"; + var localizer1 = new ResourceManagerStringLocalizer( + resourceManager.Object, + resourceAssembly1.Object, + baseName); + var localizer2 = new ResourceManagerStringLocalizer( + resourceManager.Object, + resourceAssembly2.Object, + baseName); + + // Act + localizer1.ToList(); + localizer2.ToList(); + + // Assert + var expectedCallCount = GetCultureInfoDepth(CultureInfo.CurrentUICulture); + resourceAssembly1.Verify( + rm => rm.GetManifestResourceStream(It.IsAny()), + Times.Exactly(expectedCallCount)); + resourceAssembly2.Verify( + rm => rm.GetManifestResourceStream(It.IsAny()), + Times.Exactly(expectedCallCount)); + } + + private static Stream MakeResourceStream() + { + var stream = new MemoryStream(); + var resourceWriter = new ResourceWriter(stream); + resourceWriter.AddResource("TestName", "value"); + resourceWriter.Generate(); + stream.Position = 0; + return stream; + } + + private static int GetCultureInfoDepth(CultureInfo culture) + { + var result = 0; + var currentCulture = culture; + + while (true) + { + result++; + + if (currentCulture == currentCulture.Parent) + { + break; + } + + currentCulture = currentCulture.Parent; + } + + return result; + } + + public class TestAssembly1 : Assembly + { + public override string FullName + { + get + { + return nameof(TestAssembly1); + } + } + } + + public class TestAssembly2 : Assembly + { + public override string FullName + { + get + { + return nameof(TestAssembly2); + } + } + } + } +} diff --git a/test/Microsoft.Framework.Localization.Test/project.json b/test/Microsoft.Framework.Localization.Test/project.json new file mode 100644 index 0000000000..45f8dd778c --- /dev/null +++ b/test/Microsoft.Framework.Localization.Test/project.json @@ -0,0 +1,16 @@ +{ + "dependencies": { + "Moq": "4.2.1502.911", + "xunit": "2.1.0-*", + "xunit.runner.dnx": "2.1.0-*", + "Microsoft.Framework.Localization": "1.0.0-*" + }, + + "commands": { + "test": "xunit.runner.dnx" + }, + + "frameworks": { + "dnx451": { } + } +}