Skip to content

Commit e3e38f3

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 e3e38f3

34 files changed

+664
-667
lines changed

src/Prober/DivingFish/Models/Status.cs

Lines changed: 25 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,12 @@ 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 ??=
18+
ChartStatus.ToFrozenDictionary(pair => pair.Key, pair => pair.Value.ToImmutableArray(), StringComparer.Ordinal);
19+
20+
public FrozenDictionary<string, LevelState> FrozenLevelStatus =>
21+
field ??= LevelStatus.ToFrozenDictionary(StringComparer.Ordinal);
22+
1523
public static Status Shared
1624
{
1725
get
@@ -27,4 +35,21 @@ public static Status Shared
2735
return field;
2836
}
2937
}
38+
39+
public bool TryGetChartState(int songId, int difficultyIndex, out ChartState chartState)
40+
{
41+
chartState = null!;
42+
if (!FrozenChartStatus.TryGetValue(songId.ToString(), out ImmutableArray<ChartState> chartStates))
43+
{
44+
return false;
45+
}
46+
47+
if (difficultyIndex >= chartStates.Length)
48+
{
49+
return false;
50+
}
51+
52+
chartState = chartStates[difficultyIndex];
53+
return true;
54+
}
3055
}

src/Prober/Lxns/LxnsResourceClient.cs

Lines changed: 5 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -9,20 +9,20 @@ 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

23-
if (queryParams.Count > 0)
23+
if (queryParts.Count > 0)
2424
{
25-
url += $"?{string.Join("&", queryParams)}";
25+
url += $"?{string.Join("&", queryParts)}";
2626
}
2727

2828
T response = await GetAsync<T>(url, cancellationToken);

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: 42 additions & 60 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,22 @@ 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 =
56+
_fontFallbackCache.GetOrAdd(key, _ => [.. fallbackNames.Select(LoadFont)]);
5457
return (mainFont, fallbacks);
5558
}
5659

@@ -65,67 +68,46 @@ public Image LoadImage(string ns, string key)
6568
return path;
6669
}
6770

68-
private void LoadResources(string resourcePath)
71+
private static (FrozenDictionary<string, (string, string?)>,
72+
FrozenDictionary<string, (string, ImmutableArray<string>)>) LoadResources(string resourcePath)
6973
{
7074
XDocument doc = XDocument.Load(resourcePath);
7175
XElement? resources = doc.Element("Resources");
7276
if (resources is null)
7377
{
74-
return;
78+
return (FrozenDictionary<string, (string, string?)>.Empty,
79+
FrozenDictionary<string, (string, ImmutableArray<string>)>.Empty);
7580
}
7681

77-
LoadPathRules(resources);
78-
LoadFontRules(resources);
82+
return (LoadPathRules(resources), LoadFontRules(resources));
7983
}
8084

81-
private void LoadPathRules(XElement resourcesNode)
82-
{
83-
foreach (XElement node in resourcesNode.Elements("Path"))
85+
private static FrozenDictionary<string, (string, string?)> LoadPathRules(XElement resourcesNode) => resourcesNode
86+
.Elements("Path").Select(node => new
8487
{
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))
88+
Namespace = node.Attribute("namespace")?.Value,
89+
Path = node.Value,
90+
Rule = node.Attribute("rule")?.Value
91+
}).Where(x => !string.IsNullOrEmpty(x.Namespace) && !string.IsNullOrEmpty(x.Path))
92+
.ToFrozenDictionary(x => x.Namespace!, x => (x.Path, x.Rule));
93+
94+
private static FrozenDictionary<string, (string, ImmutableArray<string>)> LoadFontRules(XElement resourcesNode) =>
95+
resourcesNode.Elements("FontFamily").Select(node =>
9396
{
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-
}
116-
117-
XElement? fallbacks = node.Element("Fallbacks");
118-
if (fallbacks is null)
119-
{
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-
}
97+
string? fontPath = node.Element("Font")?.Value;
98+
ImmutableArray<string> fallbackPaths =
99+
[
100+
.. node.Element("Fallbacks")?.Elements("Font").Select(e => e.Value)
101+
.Where(p => !string.IsNullOrEmpty(p)) ?? []
102+
];
103+
return new
104+
{
105+
Namespace = node.Attribute("namespace")?.Value,
106+
FontPath = fontPath,
107+
Fallbacks = fallbackPaths
108+
};
109+
}).Where(x => !string.IsNullOrEmpty(x.Namespace) && !string.IsNullOrEmpty(x.FontPath))
110+
.ToFrozenDictionary(x => x.Namespace!, x => (x.FontPath!, x.Fallbacks));
129111

130112
private string ResolveResourcePath(string ns, string key)
131113
{

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
@@ -8,9 +8,14 @@ namespace Limekuma.Render.ExpressionEngine;
88

99
public sealed class AsyncNCalcEngine
1010
{
11+
private readonly ConcurrentDictionary<string, ParameterInfo[]> _functionParameters = new();
1112
private readonly ConcurrentDictionary<string, Delegate> _functions = 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)

0 commit comments

Comments
 (0)