Skip to content

Commit 775732c

Browse files
committed
refactor: Refactor score processing and service utilities
- Improved StdDevScoreProcesser to utilize parallel processing and simplified logic for ranking records. - Enhanced StealScoreProcesser with a FrozenDictionary for control records and optimized record selection. - Updated BestsService and DfBestsService to use Task.WhenAll for asynchronous data preparation. - Refactored DfListService and ListService to streamline record conversion and data retrieval. - Introduced FrozenSet and FrozenDictionary in various services for improved performance and memory efficiency. - Simplified score filtering logic in ScoreFilterHelper and ScoreProcesserHelper. - Added new rating factors and utility methods in ConstantMap for better rank resolution. - Enhanced Union class with caching for JSON property names to optimize serialization performance.
1 parent 9c91d46 commit 775732c

35 files changed

+626
-667
lines changed

src/Prober/DivingFish/Models/Status.cs

Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,5 @@
1+
using System.Collections.Frozen;
2+
using System.Collections.Immutable;
13
using System.Text.Json.Serialization;
24

35
namespace Limekuma.Prober.DivingFish.Models;
@@ -12,6 +14,27 @@ public record Status
1214
[JsonPropertyName("diff_data")]
1315
public required Dictionary<string, LevelState> LevelStatus { get; init; }
1416

17+
public FrozenDictionary<string, ImmutableArray<ChartState>> FrozenChartStatus => field ??= ChartStatus.ToFrozenDictionary(pair => pair.Key, pair => pair.Value.ToImmutableArray(), StringComparer.Ordinal);
18+
19+
public FrozenDictionary<string, LevelState> FrozenLevelStatus => field ??= LevelStatus.ToFrozenDictionary(StringComparer.Ordinal);
20+
21+
public bool TryGetChartState(int songId, int difficultyIndex, out ChartState chartState)
22+
{
23+
chartState = null!;
24+
if (!FrozenChartStatus.TryGetValue(songId.ToString(), out ImmutableArray<ChartState> chartStates))
25+
{
26+
return false;
27+
}
28+
29+
if (difficultyIndex >= chartStates.Length)
30+
{
31+
return false;
32+
}
33+
34+
chartState = chartStates[difficultyIndex];
35+
return true;
36+
}
37+
1538
public static Status Shared
1639
{
1740
get

src/Prober/Lxns/LxnsResourceClient.cs

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -9,15 +9,15 @@ private async Task<T> GetListAsync<T>(string type, string includeType, int? vers
99
CancellationToken cancellationToken = default)
1010
{
1111
string url = $"/api/v0/maimai/{type}/list";
12-
List<string> queryParams = [];
12+
List<string> queryParts = [];
1313
if (version.HasValue)
1414
{
15-
queryParams.Add($"version={version}");
15+
queryParts.Add($"version={version.Value}");
1616
}
1717

1818
if (include.HasValue)
1919
{
20-
queryParams.Add($"{includeType}={include.Value.ToString().ToLower()}");
20+
queryParts.Add($"{includeType}={include.Value.ToString().ToLowerInvariant()}");
2121
}
2222

2323
if (queryParams.Count > 0)

src/Prober/Lxns/Models/Player.cs

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@ namespace Limekuma.Prober.Lxns.Models;
66

77
public record Player
88
{
9+
[JsonIgnore]
910
public LxnsDeveloperClient? Client { get; internal set; }
1011

1112
[JsonPropertyName("name")]

src/Render/AssetProvider.cs

Lines changed: 30 additions & 59 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,8 @@
22
using SixLabors.ImageSharp;
33
using SmartFormat;
44
using System.Collections.Concurrent;
5+
using System.Collections.Frozen;
6+
using System.Collections.Immutable;
57
using System.Xml.Linq;
68

79
namespace Limekuma.Render;
@@ -10,8 +12,9 @@ public sealed class AssetProvider
1012
{
1113
private readonly ConcurrentDictionary<string, Image> _assets;
1214
private readonly ConcurrentDictionary<string, LoadedFont> _fontCache;
13-
private readonly Dictionary<string, (string, List<string>?)> _fontRules;
14-
private readonly Dictionary<string, (string, string?)> _pathRules;
15+
private readonly ConcurrentDictionary<string, ImmutableArray<FontFamily>> _fontFallbackCache;
16+
private readonly FrozenDictionary<string, (string, ImmutableArray<string>)> _fontRules;
17+
private readonly FrozenDictionary<string, (string, string?)> _pathRules;
1518

1619
static AssetProvider() => Shared = new();
1720

@@ -21,11 +24,10 @@ public AssetProvider() : this("./Resources/Resources.xml")
2124

2225
public AssetProvider(string resourcePath)
2326
{
24-
_fontRules = [];
25-
_pathRules = [];
2627
_assets = [];
2728
_fontCache = [];
28-
LoadResources(resourcePath);
29+
_fontFallbackCache = [];
30+
(_pathRules, _fontRules) = LoadResources(resourcePath);
2931
}
3032

3133
public static AssetProvider Shared { get; }
@@ -36,21 +38,21 @@ public Image LoadImage(string ns, string key)
3638
return LoadImage(path);
3739
}
3840

39-
public (FontFamily, List<FontFamily>) ResolveFont(string key)
41+
public (FontFamily, ImmutableArray<FontFamily>) ResolveFont(string key)
4042
{
41-
if (!_fontRules.TryGetValue(key, out (string, List<string>?) font))
43+
if (!_fontRules.TryGetValue(key, out (string, ImmutableArray<string>) font))
4244
{
4345
throw new FontFamilyNotFoundException(key);
4446
}
4547

46-
(string mainFontName, List<string>? fallbackNames) = font;
48+
(string mainFontName, ImmutableArray<string> fallbackNames) = font;
4749
FontFamily mainFont = LoadFont(mainFontName);
48-
if (fallbackNames is null)
50+
if (fallbackNames.IsDefaultOrEmpty)
4951
{
5052
return (mainFont, []);
5153
}
5254

53-
List<FontFamily> fallbacks = [.. fallbackNames.Select(LoadFont)];
55+
ImmutableArray<FontFamily> fallbacks = _fontFallbackCache.GetOrAdd(key, _ => [.. fallbackNames.Select(LoadFont)]);
5456
return (mainFont, fallbacks);
5557
}
5658

@@ -65,67 +67,36 @@ public Image LoadImage(string ns, string key)
6567
return path;
6668
}
6769

68-
private void LoadResources(string resourcePath)
70+
private static (FrozenDictionary<string, (string, string?)>, FrozenDictionary<string, (string, ImmutableArray<string>)>) LoadResources(string resourcePath)
6971
{
7072
XDocument doc = XDocument.Load(resourcePath);
7173
XElement? resources = doc.Element("Resources");
7274
if (resources is null)
7375
{
74-
return;
76+
return (FrozenDictionary<string, (string, string?)>.Empty, FrozenDictionary<string, (string, ImmutableArray<string>)>.Empty);
7577
}
7678

77-
LoadPathRules(resources);
78-
LoadFontRules(resources);
79+
return (LoadPathRules(resources), LoadFontRules(resources));
7980
}
8081

81-
private void LoadPathRules(XElement resourcesNode)
82+
private static FrozenDictionary<string, (string, string?)> LoadPathRules(XElement resourcesNode) => resourcesNode.Elements("Path").Select(node => new
8283
{
83-
foreach (XElement node in resourcesNode.Elements("Path"))
84-
{
85-
string? ns = node.Attribute("namespace")?.Value;
86-
string path = node.Value;
87-
if (string.IsNullOrEmpty(ns))
88-
{
89-
continue;
90-
}
91-
92-
if (string.IsNullOrEmpty(path))
93-
{
94-
continue;
95-
}
96-
97-
_pathRules[ns] = new(path, node.Attribute("rule")?.Value);
98-
}
99-
}
100-
101-
private void LoadFontRules(XElement resourcesNode)
102-
{
103-
foreach (XElement node in resourcesNode.Elements("FontFamily"))
104-
{
105-
string? ns = node.Attribute("namespace")?.Value;
106-
string? fontPath = node.Element("Font")?.Value;
107-
if (string.IsNullOrEmpty(ns))
108-
{
109-
continue;
110-
}
111-
112-
if (string.IsNullOrEmpty(fontPath))
113-
{
114-
continue;
115-
}
84+
Namespace = node.Attribute("namespace")?.Value,
85+
Path = node.Value,
86+
Rule = node.Attribute("rule")?.Value
87+
}).Where(x => !string.IsNullOrEmpty(x.Namespace) && !string.IsNullOrEmpty(x.Path)).ToFrozenDictionary(x => x.Namespace!, x => (x.Path, x.Rule));
11688

117-
XElement? fallbacks = node.Element("Fallbacks");
118-
if (fallbacks is null)
89+
private static FrozenDictionary<string, (string, ImmutableArray<string>)> LoadFontRules(XElement resourcesNode) => resourcesNode.Elements("FontFamily").Select(node =>
11990
{
120-
_fontRules[ns] = new(fontPath, null);
121-
continue;
122-
}
123-
124-
List<string> fallbackPaths =
125-
[.. fallbacks.Elements("Font").Select(e => e.Value).Where(p => !string.IsNullOrEmpty(p))];
126-
_fontRules[ns] = new(fontPath, fallbackPaths);
127-
}
128-
}
91+
string? fontPath = node.Element("Font")?.Value;
92+
ImmutableArray<string> fallbackPaths = [.. node.Element("Fallbacks")?.Elements("Font").Select(e => e.Value).Where(p => !string.IsNullOrEmpty(p)) ?? []];
93+
return new
94+
{
95+
Namespace = node.Attribute("namespace")?.Value,
96+
FontPath = fontPath,
97+
Fallbacks = fallbackPaths
98+
};
99+
}).Where(x => !string.IsNullOrEmpty(x.Namespace) && !string.IsNullOrEmpty(x.FontPath)).ToFrozenDictionary(x => x.Namespace!, x => (x.FontPath!, x.Fallbacks));
129100

130101
private string ResolveResourcePath(string ns, string key)
131102
{

src/Render/Drawer.cs

Lines changed: 6 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,8 @@ namespace Limekuma.Render;
99

1010
public sealed class Drawer
1111
{
12+
private static readonly AsyncNCalcEngine ExpressionEngine = CreateExpressionEngine();
13+
1214
public async Task<Image> DrawBestsAsync(CommonUser user, IReadOnlyList<CommonRecord> ever,
1315
IReadOnlyList<CommonRecord> current, int everTotal, int currentTotal, string? condition, string prober,
1416
IEnumerable<string> tags) => await DrawBestsAsync(user, ever, current, everTotal, currentTotal, condition,
@@ -79,17 +81,17 @@ public async Task<Image> DrawListAsync(CommonUser user, IReadOnlyList<CommonReco
7981

8082
private static async Task<Image> DrawAsync(IDictionary<string, object?> scope, string xmlPath)
8183
{
82-
AsyncNCalcEngine expr = new();
83-
RegisterFunctions(expr);
84-
TemplateReader loader = new(expr);
84+
TemplateReader loader = new(ExpressionEngine);
8585
Node tree = await loader.LoadAsync(xmlPath, scope);
8686
AssetProvider assets = AssetProvider.Shared;
8787
return NodeRenderer.Render((CanvasNode)tree, assets, assets);
8888
}
8989

90-
private static void RegisterFunctions(AsyncNCalcEngine expr)
90+
private static AsyncNCalcEngine CreateExpressionEngine()
9191
{
92+
AsyncNCalcEngine expr = new();
9293
expr.RegisterFunction("ToString", (object x) => Convert.ToString(x));
9394
expr.RegisterFunction("Count", (IList x) => x.Count);
95+
return expr;
9496
}
9597
}

src/Render/ExpressionEngine/AsyncNCalcEngine.cs

Lines changed: 17 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -9,8 +9,13 @@ namespace Limekuma.Render.ExpressionEngine;
99
public sealed class AsyncNCalcEngine
1010
{
1111
private readonly ConcurrentDictionary<string, Delegate> _functions = new();
12+
private readonly ConcurrentDictionary<string, ParameterInfo[]> _functionParameters = new();
1213

13-
public void RegisterFunction(string name, Delegate func) => _functions[name] = func;
14+
public void RegisterFunction(string name, Delegate func)
15+
{
16+
_functions[name] = func;
17+
_functionParameters[name] = func.Method.GetParameters();
18+
}
1419

1520
public async Task<T?> EvalAsync<T>(string expr, object? scope)
1621
{
@@ -35,20 +40,14 @@ public sealed class AsyncNCalcEngine
3540
return (T?)ConvertValue(result, typeof(T));
3641
}
3742

38-
List<object> list = [];
39-
list.AddRange(enumerable.OfType<object>());
40-
41-
return (T)(object)list;
43+
return (T)(object)enumerable.Cast<object?>().Where(item => item is not null).Cast<object>().ToArray();
4244
}
4345

4446
public async Task<object?> EvalAsync(string expr, object? scope)
4547
{
4648
AsyncExpression expression = new(expr);
4749
Dictionary<string, object?> flattened = ScopeFlattener.Flatten(scope);
48-
foreach ((string key, object? value) in flattened)
49-
{
50-
expression.Parameters[key] = value is Enum e ? Convert.ToInt32(e, CultureInfo.InvariantCulture) : value;
51-
}
50+
AddScopeParameters(expression, flattened);
5251

5352
expression.EvaluateFunctionAsync += async (name, args) =>
5453
{
@@ -57,7 +56,7 @@ public sealed class AsyncNCalcEngine
5756
return;
5857
}
5958

60-
ParameterInfo[] parameters = func.Method.GetParameters();
59+
ParameterInfo[] parameters = _functionParameters.GetOrAdd(name, _ => func.Method.GetParameters());
6160
object?[] funcArgs = new object[args.Parameters.Length];
6261
for (int i = 0; i < args.Parameters.Length; ++i)
6362
{
@@ -73,6 +72,14 @@ public sealed class AsyncNCalcEngine
7372
return await expression.EvaluateAsync();
7473
}
7574

75+
private static void AddScopeParameters(AsyncExpression expression, Dictionary<string, object?> flattened)
76+
{
77+
foreach ((string key, object? value) in flattened)
78+
{
79+
expression.Parameters[key] = value is Enum e ? Convert.ToInt32(e, CultureInfo.InvariantCulture) : value;
80+
}
81+
}
82+
7683
private static object? ConvertValue(object? value, Type targetType)
7784
{
7885
if (value is null)

src/Render/ExpressionEngine/ScopeFlattener.cs

Lines changed: 18 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -1,11 +1,16 @@
11
using System.Collections;
2+
using System.Collections.Concurrent;
3+
using System.Collections.Immutable;
24
using System.Globalization;
35
using System.Reflection;
46

57
namespace Limekuma.Render.ExpressionEngine;
68

79
internal static class ScopeFlattener
810
{
11+
private static readonly ConcurrentDictionary<Type, ImmutableArray<PropertyInfo>> PropertyCache = new();
12+
private static readonly ConcurrentDictionary<Type, bool> LeafTypeCache = new();
13+
914
public static Dictionary<string, object?> Flatten(object? scope)
1015
{
1116
Dictionary<string, object?> result = new(StringComparer.OrdinalIgnoreCase);
@@ -41,13 +46,19 @@ private static void FlattenObject(IDictionary<string, object?> output, string pr
4146
}
4247

4348
Type type = value.GetType();
44-
if (type.IsPrimitive)
49+
if (IsLeafType(type))
4550
{
4651
return;
4752
}
4853

49-
if (value is string or decimal or DateTime or DateTimeOffset or TimeSpan or Guid or Enum)
54+
if (value is IDictionary<string, object?> genericDictionary)
5055
{
56+
foreach ((string key, object? entryValue) in genericDictionary)
57+
{
58+
string childPrefix = prefix.Length is 0 ? key : string.Concat(prefix, ".", key);
59+
FlattenObject(output, childPrefix, entryValue, depth + 1);
60+
}
61+
5162
return;
5263
}
5364

@@ -56,7 +67,7 @@ private static void FlattenObject(IDictionary<string, object?> output, string pr
5667
foreach (DictionaryEntry entry in dictionary)
5768
{
5869
string key = Convert.ToString(entry.Key, CultureInfo.InvariantCulture) ?? string.Empty;
59-
string childPrefix = string.IsNullOrEmpty(prefix) ? key : $"{prefix}.{key}";
70+
string childPrefix = prefix.Length is 0 ? key : string.Concat(prefix, ".", key);
6071
FlattenObject(output, childPrefix, entry.Value, depth + 1);
6172
}
6273

@@ -68,22 +79,14 @@ private static void FlattenObject(IDictionary<string, object?> output, string pr
6879
return;
6980
}
7081

71-
PropertyInfo[] properties = type.GetProperties(BindingFlags.Instance | BindingFlags.Public);
82+
ImmutableArray<PropertyInfo> properties = PropertyCache.GetOrAdd(type, static t => [.. t.GetProperties(BindingFlags.Instance | BindingFlags.Public).Where(property => property.CanRead && property.GetIndexParameters().Length is 0)]);
7283
foreach (PropertyInfo property in properties)
7384
{
74-
if (!property.CanRead)
75-
{
76-
continue;
77-
}
78-
79-
if (property.GetIndexParameters().Length > 0)
80-
{
81-
continue;
82-
}
83-
8485
object? propertyValue = property.GetValue(value);
85-
string childName = string.IsNullOrEmpty(prefix) ? property.Name : $"{prefix}.{property.Name}";
86+
string childName = prefix.Length is 0 ? property.Name : string.Concat(prefix, ".", property.Name);
8687
FlattenObject(output, childName, propertyValue, depth + 1);
8788
}
8889
}
90+
91+
private static bool IsLeafType(Type type) => LeafTypeCache.GetOrAdd(type, static t => t.IsPrimitive || t.IsEnum || t == typeof(string) || t == typeof(decimal) || t == typeof(DateTime) || t == typeof(DateTimeOffset) || t == typeof(TimeSpan) || t == typeof(Guid));
8992
}

0 commit comments

Comments
 (0)