Skip to content

Commit

Permalink
Merge pull request #343 from hangy/perf-experiments
Browse files Browse the repository at this point in the history
perf: Minor optimizations
  • Loading branch information
valdisiljuconoks authored Dec 5, 2024
2 parents e1e9f67 + 2cbdd43 commit 45ee7b1
Show file tree
Hide file tree
Showing 3 changed files with 114 additions and 47 deletions.
30 changes: 11 additions & 19 deletions common/src/DbLocalizationProvider/FallbackLanguagesCollection.cs
Original file line number Diff line number Diff line change
Expand Up @@ -12,14 +12,16 @@ namespace DbLocalizationProvider;
/// </summary>
public class FallbackLanguagesCollection
{
private readonly Dictionary<string, FallbackLanguages> _collection = new();
private readonly Dictionary<string, FallbackLanguages> _collection = [];

private readonly FallbackLanguages _defaultFallbackLanguages;

/// <summary>
/// Creates new instance of this collection.
/// </summary>
public FallbackLanguagesCollection()
{
_collection.Add("default", new FallbackLanguages(this));
_defaultFallbackLanguages = new FallbackLanguages(this);
}

/// <summary>
Expand All @@ -28,8 +30,7 @@ public FallbackLanguagesCollection()
/// <param name="fallbackCulture">Specifies default fallback language.</param>
public FallbackLanguagesCollection(CultureInfo fallbackCulture)
{
var fallbackLanguages = new FallbackLanguages(this) { fallbackCulture };
_collection.Add("default", fallbackLanguages);
_defaultFallbackLanguages = new FallbackLanguages(this) { fallbackCulture };
}

/// <summary>
Expand All @@ -39,10 +40,7 @@ public FallbackLanguagesCollection(CultureInfo fallbackCulture)
/// <returns>The list of registered fallback languages for given <paramref name="language" />.</returns>
public FallbackLanguages GetFallbackLanguages(CultureInfo language)
{
if (language == null)
{
throw new ArgumentNullException(nameof(language));
}
ArgumentNullException.ThrowIfNull(language);

return GetFallbackLanguages(language.Name);
}
Expand All @@ -54,14 +52,11 @@ public FallbackLanguages GetFallbackLanguages(CultureInfo language)
/// <returns>The list of registered fallback languages for given <paramref name="language" />.</returns>
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;
}

/// <summary>
Expand All @@ -71,10 +66,7 @@ public FallbackLanguages GetFallbackLanguages(string language)
/// <returns>List of fallback languages on which you can call extension methods to get list configured.</returns>
public FallbackLanguages Add(CultureInfo notFoundCulture)
{
if (notFoundCulture == null)
{
throw new ArgumentNullException(nameof(notFoundCulture));
}
ArgumentNullException.ThrowIfNull(notFoundCulture);

if (_collection.ContainsKey(notFoundCulture.Name))
{
Expand Down
40 changes: 24 additions & 16 deletions common/src/DbLocalizationProvider/Json/JsonConverter.cs
Original file line number Diff line number Diff line change
Expand Up @@ -76,7 +76,7 @@ public JObject GetJson(
}

internal JObject Convert(
ICollection<LocalizationResource> resources,
List<LocalizationResource> resources,
string language,
CultureInfo fallbackCulture,
bool camelCase)
Expand All @@ -89,8 +89,8 @@ internal JObject Convert(
}

internal JObject Convert(
ICollection<LocalizationResource> resources,
ICollection<LocalizationResource> allResources,
List<LocalizationResource> resources,
List<LocalizationResource> allResources,
string language,
FallbackLanguagesCollection fallbackCollection,
bool camelCase)
Expand All @@ -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)
Expand Down Expand Up @@ -152,29 +154,35 @@ internal JObject Convert(

private static void Aggregate(
JObject seed,
ICollection<string> segments,
string[] segments,
Func<JObject, string, JObject> act,
Action<JObject, string> last)
{
if (segments == null || !segments.Any())
if (segments == null || segments.Length == 0)
{
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);
}

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 that.Substring(0, 1).ToLower() + that.Substring(1);
return text;
}

return that.ToLower();
return string.Create(text.Length, text, (chars, state) =>
{
state.AsSpan().CopyTo(chars);
chars[0] = char.ToLower(chars[0]);
});
}
}
91 changes: 79 additions & 12 deletions common/src/DbLocalizationProvider/LocalizationProvider.cs
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,7 @@ namespace DbLocalizationProvider;
/// <summary>
/// Main class to use when resource translation is needed.
/// </summary>
public class LocalizationProvider : ILocalizationProvider
public partial class LocalizationProvider : ILocalizationProvider
{
private readonly ExpressionHelper _expressionHelper;
private readonly FallbackLanguagesCollection _fallbackCollection;
Expand Down Expand Up @@ -51,6 +51,12 @@ public LocalizationProvider(
_queryExecutor = queryExecutor;
_scanState = scanState;

// _converter = new JsonConverter(_queryExecutor, _scanState);
// _serializer = new JsonSerializer
// {
// ContractResolver = new StaticPropertyContractResolver()
// };

_reflectionConverter = new ReflectionConverter(_queryExecutor, _scanState, _keyBuilder);
}

Expand Down Expand Up @@ -418,28 +424,89 @@ private static string FormatWithAnonymousObject(string message, object model)
return string.Format(message, model);
}

var placeHolders = Regex.Matches(message, "{.*?}").Select(m => m.Value).ToList();

if (!placeHolders.Any())
const char Prefix = '{';
const char Postfix = '}';
if (!message.Contains(Prefix) || !message.Contains(Postfix))
{
return message;
}

var placeholderMap = new Dictionary<string, object>();
var properties = type.GetProperties();

foreach (var placeHolder in placeHolders)
ReadOnlySpan<char> tmp = message;

// 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;

int prefixIndex;
while ((prefixIndex = tmp.IndexOf(Prefix)) != -1)
{
var propertyInfo = properties.FirstOrDefault(p => p.Name == placeHolder.Trim('{', '}'));
(int at, int task) next = (prefixIndex, -1);
for (var i = 0; i < properties.Length; i++)
{
// 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;
}
}

// property found - extract value and add to the map
var val = propertyInfo?.GetValue(model);
if (val != null && !placeholderMap.ContainsKey(placeHolder))
if (next.task == -1)
{
placeholderMap.Add(placeHolder, val);
// this delimiter character is just part of the string, so skip it
tmp = tmp[(prefixIndex + 1)..];
offset += prefixIndex + 1;
continue;
}

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;
}

return placeholderMap.Aggregate(message, (current, pair) => current.Replace(pair.Key, pair.Value.ToString()));
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;

ReadOnlySpan<char> origin = message;
var lastStart = 0;

while (occurrences.Count != 0)
{
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;
}

origin[lastStart..].CopyTo(chars[position..]);
});

return result;
}
}

0 comments on commit 45ee7b1

Please sign in to comment.