From cca4a849db4323fcfdacdfc2368ce4f5f0be1476 Mon Sep 17 00:00:00 2001 From: hangy Date: Mon, 2 Dec 2024 22:47:51 +0100 Subject: [PATCH 1/9] perf: Avoid dictionary lookup for default value --- .../FallbackLanguagesCollection.cs | 30 +++++++------------ 1 file changed, 11 insertions(+), 19 deletions(-) diff --git a/common/src/DbLocalizationProvider/FallbackLanguagesCollection.cs b/common/src/DbLocalizationProvider/FallbackLanguagesCollection.cs index 964a00d4..ee418e9e 100644 --- a/common/src/DbLocalizationProvider/FallbackLanguagesCollection.cs +++ b/common/src/DbLocalizationProvider/FallbackLanguagesCollection.cs @@ -12,14 +12,16 @@ namespace DbLocalizationProvider; /// public class FallbackLanguagesCollection { - private readonly Dictionary _collection = new(); + private readonly Dictionary _collection = []; + + private readonly FallbackLanguages _defaultFallbackLanguages; /// /// Creates new instance of this collection. /// public FallbackLanguagesCollection() { - _collection.Add("default", new FallbackLanguages(this)); + _defaultFallbackLanguages = new FallbackLanguages(this); } /// @@ -28,8 +30,7 @@ public FallbackLanguagesCollection() /// Specifies default fallback language. public FallbackLanguagesCollection(CultureInfo fallbackCulture) { - var fallbackLanguages = new FallbackLanguages(this) { fallbackCulture }; - _collection.Add("default", fallbackLanguages); + _defaultFallbackLanguages = new FallbackLanguages(this) { fallbackCulture }; } /// @@ -39,10 +40,7 @@ public FallbackLanguagesCollection(CultureInfo fallbackCulture) /// The list of registered fallback languages for given . public FallbackLanguages GetFallbackLanguages(CultureInfo language) { - if (language == null) - { - throw new ArgumentNullException(nameof(language)); - } + ArgumentNullException.ThrowIfNull(language); return GetFallbackLanguages(language.Name); } @@ -54,14 +52,11 @@ public FallbackLanguages GetFallbackLanguages(CultureInfo language) /// The list of registered fallback languages for given . public FallbackLanguages GetFallbackLanguages(string language) { - if (language == null) - { - throw new ArgumentNullException(nameof(language)); - } + ArgumentNullException.ThrowIfNull(language); - return !_collection.ContainsKey(language) - ? _collection["default"] - : _collection[language]; + return _collection.TryGetValue(language, out var fallbackLanguages) + ? fallbackLanguages + : _defaultFallbackLanguages; } /// @@ -71,10 +66,7 @@ public FallbackLanguages GetFallbackLanguages(string language) /// List of fallback languages on which you can call extension methods to get list configured. public FallbackLanguages Add(CultureInfo notFoundCulture) { - if (notFoundCulture == null) - { - throw new ArgumentNullException(nameof(notFoundCulture)); - } + ArgumentNullException.ThrowIfNull(notFoundCulture); if (_collection.ContainsKey(notFoundCulture.Name)) { From b4c0088d2311eb195113b6b2d7dce354d48355c5 Mon Sep 17 00:00:00 2001 From: hangy Date: Mon, 2 Dec 2024 22:49:42 +0100 Subject: [PATCH 2/9] perf: Avoid virtcall for `ICollection` parameters --- .../src/DbLocalizationProvider/Json/JsonConverter.cs | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/common/src/DbLocalizationProvider/Json/JsonConverter.cs b/common/src/DbLocalizationProvider/Json/JsonConverter.cs index 95661ddf..b347665b 100644 --- a/common/src/DbLocalizationProvider/Json/JsonConverter.cs +++ b/common/src/DbLocalizationProvider/Json/JsonConverter.cs @@ -76,7 +76,7 @@ public JObject GetJson( } internal JObject Convert( - ICollection resources, + List resources, string language, CultureInfo fallbackCulture, bool camelCase) @@ -89,8 +89,8 @@ internal JObject Convert( } internal JObject Convert( - ICollection resources, - ICollection allResources, + List resources, + List allResources, string language, FallbackLanguagesCollection fallbackCollection, bool camelCase) @@ -152,11 +152,11 @@ internal JObject Convert( private static void Aggregate( JObject seed, - ICollection segments, + string[] segments, Func act, Action last) { - if (segments == null || !segments.Any()) + if (segments == null || segments.Length == 0) { return; } From 38f5954594dab976c3b311ee2810237710352a52 Mon Sep 17 00:00:00 2001 From: hangy Date: Mon, 2 Dec 2024 22:51:02 +0100 Subject: [PATCH 3/9] perf: Improve key processing and string manipulation in JsonConverter --- .../Json/JsonConverter.cs | 18 ++++++++++-------- 1 file changed, 10 insertions(+), 8 deletions(-) diff --git a/common/src/DbLocalizationProvider/Json/JsonConverter.cs b/common/src/DbLocalizationProvider/Json/JsonConverter.cs index b347665b..ed85ee66 100644 --- a/common/src/DbLocalizationProvider/Json/JsonConverter.cs +++ b/common/src/DbLocalizationProvider/Json/JsonConverter.cs @@ -101,15 +101,17 @@ internal JObject Convert( { // we need to process key names and supported nested classes with "+" symbols in keys // so we replace those with dots to have proper object nesting on client side - var key = resource.ResourceKey.Replace("+", "."); - if (!key.Contains(".")) + var key = resource.ResourceKey.Replace('+', '.'); + if (!key.Contains('.')) { continue; } - var segments = key.Split(new[] { "." }, StringSplitOptions.None) - .Select(k => camelCase ? CamelCase(k) : k) - .ToList(); + var segments = key.Split('.', StringSplitOptions.None); + if (segments.Length > 0 && camelCase) + { + segments = [.. segments.Select(CamelCase)]; + } // let's try to look for translation explicitly in requested language // if there is no translation in given language -> worth to look in fallback culture *and* invariant (if configured to do so) @@ -161,8 +163,8 @@ private static void Aggregate( return; } - var lastElement = segments.Last(); - var seqWithNoLast = segments.Take(segments.Count - 1); + var lastElement = segments[^1]; + var seqWithNoLast = segments.Take(..^1); var s = seqWithNoLast.Aggregate(seed, act); last(s, lastElement); @@ -172,7 +174,7 @@ private static string CamelCase(string that) { if (that.Length > 1) { - return that.Substring(0, 1).ToLower() + that.Substring(1); + return string.Concat(that[..1].ToLower(), that.AsSpan(1)); } return that.ToLower(); From 74ef0f9709bb463dbc285965e119e8c387cec8c1 Mon Sep 17 00:00:00 2001 From: hangy Date: Mon, 2 Dec 2024 23:20:52 +0100 Subject: [PATCH 4/9] perf: Use generated regex for placeholder matching in LocalizationProvider --- common/src/DbLocalizationProvider/LocalizationProvider.cs | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/common/src/DbLocalizationProvider/LocalizationProvider.cs b/common/src/DbLocalizationProvider/LocalizationProvider.cs index ecfcb57a..71730cd7 100644 --- a/common/src/DbLocalizationProvider/LocalizationProvider.cs +++ b/common/src/DbLocalizationProvider/LocalizationProvider.cs @@ -21,7 +21,7 @@ namespace DbLocalizationProvider; /// /// Main class to use when resource translation is needed. /// -public class LocalizationProvider : ILocalizationProvider +public partial class LocalizationProvider : ILocalizationProvider { private readonly ExpressionHelper _expressionHelper; private readonly FallbackLanguagesCollection _fallbackCollection; @@ -439,7 +439,7 @@ private static string FormatWithAnonymousObject(string message, object model) return string.Format(message, model); } - var placeHolders = Regex.Matches(message, "{.*?}").Select(m => m.Value).ToList(); + var placeHolders = PlaceHolderRegex().Matches(message).Select(m => m.Value).ToList(); if (!placeHolders.Any()) { @@ -463,4 +463,7 @@ private static string FormatWithAnonymousObject(string message, object model) return placeholderMap.Aggregate(message, (current, pair) => current.Replace(pair.Key, pair.Value.ToString())); } + + [GeneratedRegex("{.*?}")] + private static partial Regex PlaceHolderRegex(); } From f47cf776599af8575cf5860daf10e680499c15a8 Mon Sep 17 00:00:00 2001 From: hangy Date: Tue, 3 Dec 2024 00:14:32 +0100 Subject: [PATCH 5/9] perf: Initialize `placeholderMap` with the known number of `placeHolders` --- common/src/DbLocalizationProvider/LocalizationProvider.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/common/src/DbLocalizationProvider/LocalizationProvider.cs b/common/src/DbLocalizationProvider/LocalizationProvider.cs index 71730cd7..6284f9b2 100644 --- a/common/src/DbLocalizationProvider/LocalizationProvider.cs +++ b/common/src/DbLocalizationProvider/LocalizationProvider.cs @@ -446,8 +446,8 @@ private static string FormatWithAnonymousObject(string message, object model) return message; } - var placeholderMap = new Dictionary(); var properties = type.GetProperties(); + var placeholderMap = new Dictionary(placeHolders.Count); foreach (var placeHolder in placeHolders) { From 0f18d2e62bf4f47ce294d96376c794be87c33f12 Mon Sep 17 00:00:00 2001 From: hangy Date: Tue, 3 Dec 2024 19:48:04 +0100 Subject: [PATCH 6/9] perf: Use optimized `CamelCase` method by @stefanolsen Co-authored-by: stefanolsen --- .../DbLocalizationProvider/Json/JsonConverter.cs | 14 ++++++++++---- 1 file changed, 10 insertions(+), 4 deletions(-) diff --git a/common/src/DbLocalizationProvider/Json/JsonConverter.cs b/common/src/DbLocalizationProvider/Json/JsonConverter.cs index ed85ee66..3bc00757 100644 --- a/common/src/DbLocalizationProvider/Json/JsonConverter.cs +++ b/common/src/DbLocalizationProvider/Json/JsonConverter.cs @@ -170,13 +170,19 @@ private static void Aggregate( last(s, lastElement); } - private static string CamelCase(string that) + private static string CamelCase(string text) { - if (that.Length > 1) + ArgumentNullException.ThrowIfNull(text); + + if (text.Length == 0 || char.IsLower(text, 0)) { - return string.Concat(that[..1].ToLower(), that.AsSpan(1)); + return text; } - return that.ToLower(); + return string.Create(text.Length, text, (chars, state) => + { + state.AsSpan().CopyTo(chars); + chars[0] = char.ToLower(chars[0]); + }); } } From 56c01e876713ec8332c6f1a08ea73994ebd7d2f7 Mon Sep 17 00:00:00 2001 From: hangy Date: Tue, 3 Dec 2024 21:32:09 +0100 Subject: [PATCH 7/9] perf: Optimize placeholder matching and processing in LocalizationProvider --- .../LocalizationProvider.cs | 30 +++++++++++++------ 1 file changed, 21 insertions(+), 9 deletions(-) diff --git a/common/src/DbLocalizationProvider/LocalizationProvider.cs b/common/src/DbLocalizationProvider/LocalizationProvider.cs index 6284f9b2..e28dbcd1 100644 --- a/common/src/DbLocalizationProvider/LocalizationProvider.cs +++ b/common/src/DbLocalizationProvider/LocalizationProvider.cs @@ -54,7 +54,7 @@ public LocalizationProvider( _fallbackCollection = context.Value._fallbackCollection; _queryExecutor = queryExecutor; _scanState = scanState; - + _converter = new JsonConverter(_queryExecutor, _scanState); _serializer = new JsonSerializer { @@ -439,9 +439,9 @@ private static string FormatWithAnonymousObject(string message, object model) return string.Format(message, model); } - var placeHolders = PlaceHolderRegex().Matches(message).Select(m => m.Value).ToList(); + var placeHolders = PlaceHolderRegex().Matches(message); - if (!placeHolders.Any()) + if (placeHolders is not { Count: > 0 }) { return message; } @@ -449,21 +449,33 @@ private static string FormatWithAnonymousObject(string message, object model) var properties = type.GetProperties(); var placeholderMap = new Dictionary(placeHolders.Count); - foreach (var placeHolder in placeHolders) + foreach (Match placeHolderMatch in placeHolders) { - var propertyInfo = properties.FirstOrDefault(p => p.Name == placeHolder.Trim('{', '}')); + if (placeHolderMatch is not { Success: true, Groups: { Count: > 1 } groups }) + { + continue; + } + + if (placeholderMap.ContainsKey(placeHolderMatch.Value)) + { + // Don't process same placeholder twice + continue; + } + + var placeHolder = groups[1].Value; + var propertyInfo = properties.FirstOrDefault(p => p.Name == placeHolder); - // property found - extract value and add to the map + // property found - extract value and add to the map, if it's not null var val = propertyInfo?.GetValue(model); - if (val != null && !placeholderMap.ContainsKey(placeHolder)) + if (val != null) { - placeholderMap.Add(placeHolder, val); + placeholderMap.Add(placeHolderMatch.Value, val); } } return placeholderMap.Aggregate(message, (current, pair) => current.Replace(pair.Key, pair.Value.ToString())); } - [GeneratedRegex("{.*?}")] + [GeneratedRegex("{(.+?)}")] private static partial Regex PlaceHolderRegex(); } From 1165930b97c6d72add68618bdd1416f9d57bdb7a Mon Sep 17 00:00:00 2001 From: hangy Date: Wed, 4 Dec 2024 23:46:31 +0100 Subject: [PATCH 8/9] perf: Add checks for placeholder presence in LocalizationProvider --- common/src/DbLocalizationProvider/LocalizationProvider.cs | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/common/src/DbLocalizationProvider/LocalizationProvider.cs b/common/src/DbLocalizationProvider/LocalizationProvider.cs index e28dbcd1..03d72e5f 100644 --- a/common/src/DbLocalizationProvider/LocalizationProvider.cs +++ b/common/src/DbLocalizationProvider/LocalizationProvider.cs @@ -439,6 +439,13 @@ private static string FormatWithAnonymousObject(string message, object model) return string.Format(message, model); } + const char Prefix = '{'; + const char Postfix = '}'; + if (!message.Contains(Prefix) || !message.Contains(Postfix)) + { + return message; + } + var placeHolders = PlaceHolderRegex().Matches(message); if (placeHolders is not { Count: > 0 }) From 97b908bba763b6e250624e07b1d6b7eb219c77af Mon Sep 17 00:00:00 2001 From: hangy Date: Wed, 4 Dec 2024 23:49:15 +0100 Subject: [PATCH 9/9] perf: Refactor placeholder processing in LocalizationProvider for improved efficiency --- .../LocalizationProvider.cs | 85 ++++++++++++++----- 1 file changed, 62 insertions(+), 23 deletions(-) diff --git a/common/src/DbLocalizationProvider/LocalizationProvider.cs b/common/src/DbLocalizationProvider/LocalizationProvider.cs index 03d72e5f..6e99c531 100644 --- a/common/src/DbLocalizationProvider/LocalizationProvider.cs +++ b/common/src/DbLocalizationProvider/LocalizationProvider.cs @@ -446,43 +446,82 @@ private static string FormatWithAnonymousObject(string message, object model) return message; } - var placeHolders = PlaceHolderRegex().Matches(message); + var properties = type.GetProperties(); - if (placeHolders is not { Count: > 0 }) - { - return message; - } + ReadOnlySpan tmp = message; - var properties = type.GetProperties(); - var placeholderMap = new Dictionary(placeHolders.Count); + // The rest of the method is based on https://stackoverflow.com/a/74391485/11963 (Licensed under CC BY-SA 4.0) + // we store the occurrences in the queue while calculating the length of the final string + // so we don't have to search for them the 2nd time later + var occurrences = new Queue<(int at, int task)>(); + var offset = 0; + var resultLength = tmp.Length; - foreach (Match placeHolderMatch in placeHolders) + int prefixIndex; + while ((prefixIndex = tmp.IndexOf(Prefix)) != -1) { - if (placeHolderMatch is not { Success: true, Groups: { Count: > 1 } groups }) + (int at, int task) next = (prefixIndex, -1); + for (var i = 0; i < properties.Length; i++) { - continue; + // we expect the postfix to be at this place + var postfixIndex = prefixIndex + properties[i].Name.Length + 1; + if (tmp.Length > postfixIndex // check that we don't cross the bounds + && tmp[postfixIndex] == Postfix // check that the postfix IS were we expect it to be + && tmp.Slice(prefixIndex + 1, postfixIndex - prefixIndex - 1).SequenceEqual(properties[i].Name)) // compare all the characters in between the delimiters + { + next.task = i; + break; + } } - if (placeholderMap.ContainsKey(placeHolderMatch.Value)) + if (next.task == -1) { - // Don't process same placeholder twice + // this delimiter character is just part of the string, so skip it + tmp = tmp[(prefixIndex + 1)..]; + offset += prefixIndex + 1; continue; } - var placeHolder = groups[1].Value; - var propertyInfo = properties.FirstOrDefault(p => p.Name == placeHolder); + var newStart = next.at + properties[next.task].Name.Length + 2; + tmp = tmp[newStart..]; + + occurrences.Enqueue((next.at + offset, next.task)); + offset += newStart; + + resultLength += (properties[next.task].GetValue(model)?.ToString()?.Length ?? 0) - properties[next.task].Name.Length - 2; + } + + var result = string.Create(resultLength, (message, model, properties, occurrences), (chars, state) => + { + var message = state.message; + var model = state.model; + var replaceTasks = state.properties; + var occurrences = state.occurrences; + + var position = 0; - // property found - extract value and add to the map, if it's not null - var val = propertyInfo?.GetValue(model); - if (val != null) + ReadOnlySpan origin = message; + var lastStart = 0; + + while (occurrences.Count != 0) { - placeholderMap.Add(placeHolderMatch.Value, val); + var next = occurrences.Dequeue(); + + var value = replaceTasks[next.task].GetValue(model)?.ToString(); + if (value is null) + { + continue; + } + + origin[lastStart..next.at].CopyTo(chars[position..]); + value.CopyTo(chars[(position + next.at - lastStart)..]); + position += next.at - lastStart + value.Length; + lastStart = next.at + replaceTasks[next.task].Name.Length + 2; } - } - return placeholderMap.Aggregate(message, (current, pair) => current.Replace(pair.Key, pair.Value.ToString())); - } + origin[lastStart..].CopyTo(chars[position..]); + }); - [GeneratedRegex("{(.+?)}")] - private static partial Regex PlaceHolderRegex(); + return result; + } }