Skip to content

Commit ee6597b

Browse files
authored
Merge pull request #24 from Syriiin/better-batching
Implement better batching
2 parents ba655a1 + aaf7059 commit ee6597b

17 files changed

+123
-38
lines changed

Difficalcy.Catch.Tests/Difficalcy.Catch.Tests.csproj

+1-1
Original file line numberDiff line numberDiff line change
@@ -8,7 +8,7 @@
88
</PropertyGroup>
99

1010
<ItemGroup>
11-
<PackageReference Include="Microsoft.NET.Test.Sdk" Version="17.9.0" />
11+
<PackageReference Include="Microsoft.NET.Test.Sdk" Version="17.10.0" />
1212
<PackageReference Include="xunit" Version="2.8.0" />
1313
<PackageReference Include="xunit.runner.visualstudio" Version="2.8.0">
1414
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>

Difficalcy.Catch/Difficalcy.Catch.csproj

+2-2
Original file line numberDiff line numberDiff line change
@@ -5,8 +5,8 @@
55
</PropertyGroup>
66

77
<ItemGroup>
8-
<PackageReference Include="ppy.osu.Game.Rulesets.Catch" Version="2024.412.1" />
9-
<PackageReference Include="ppy.osu.Game.Rulesets.Osu" Version="2024.412.1" /> <!-- required for convert support -->
8+
<PackageReference Include="ppy.osu.Game.Rulesets.Catch" Version="2024.523.0" />
9+
<PackageReference Include="ppy.osu.Game.Rulesets.Osu" Version="2024.523.0" /> <!-- required for convert support -->
1010
</ItemGroup>
1111

1212
<ItemGroup>

Difficalcy.Catch/Services/CatchCalculatorService.cs

+3-3
Original file line numberDiff line numberDiff line change
@@ -43,10 +43,10 @@ protected override async Task EnsureBeatmap(string beatmapId)
4343
await beatmapProvider.EnsureBeatmap(beatmapId);
4444
}
4545

46-
protected override (object, string) CalculateDifficultyAttributes(CatchScore score)
46+
protected override (object, string) CalculateDifficultyAttributes(string beatmapId, int bitMods)
4747
{
48-
var workingBeatmap = GetWorkingBeatmap(score.BeatmapId);
49-
var mods = CatchRuleset.ConvertFromLegacyMods((LegacyMods)score.Mods).ToArray();
48+
var workingBeatmap = GetWorkingBeatmap(beatmapId);
49+
var mods = CatchRuleset.ConvertFromLegacyMods((LegacyMods)bitMods).ToArray();
5050

5151
var difficultyCalculator = CatchRuleset.CreateDifficultyCalculator(workingBeatmap);
5252
var difficultyAttributes = difficultyCalculator.Calculate(mods) as CatchDifficultyAttributes;

Difficalcy.Mania.Tests/Difficalcy.Mania.Tests.csproj

+1-1
Original file line numberDiff line numberDiff line change
@@ -8,7 +8,7 @@
88
</PropertyGroup>
99

1010
<ItemGroup>
11-
<PackageReference Include="Microsoft.NET.Test.Sdk" Version="17.9.0" />
11+
<PackageReference Include="Microsoft.NET.Test.Sdk" Version="17.10.0" />
1212
<PackageReference Include="xunit" Version="2.8.0" />
1313
<PackageReference Include="xunit.runner.visualstudio" Version="2.8.0">
1414
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>

Difficalcy.Mania/Difficalcy.Mania.csproj

+2-2
Original file line numberDiff line numberDiff line change
@@ -5,8 +5,8 @@
55
</PropertyGroup>
66

77
<ItemGroup>
8-
<PackageReference Include="ppy.osu.Game.Rulesets.Mania" Version="2024.412.1" />
9-
<PackageReference Include="ppy.osu.Game.Rulesets.Osu" Version="2024.412.1" /> <!-- required for convert support -->
8+
<PackageReference Include="ppy.osu.Game.Rulesets.Mania" Version="2024.523.0" />
9+
<PackageReference Include="ppy.osu.Game.Rulesets.Osu" Version="2024.523.0" /> <!-- required for convert support -->
1010
</ItemGroup>
1111

1212
<ItemGroup>

Difficalcy.Mania/Services/ManiaCalculatorService.cs

+3-3
Original file line numberDiff line numberDiff line change
@@ -43,10 +43,10 @@ protected override async Task EnsureBeatmap(string beatmapId)
4343
await _beatmapProvider.EnsureBeatmap(beatmapId);
4444
}
4545

46-
protected override (object, string) CalculateDifficultyAttributes(ManiaScore score)
46+
protected override (object, string) CalculateDifficultyAttributes(string beatmapId, int bitMods)
4747
{
48-
var workingBeatmap = GetWorkingBeatmap(score.BeatmapId);
49-
var mods = ManiaRuleset.ConvertFromLegacyMods((LegacyMods)score.Mods).ToArray();
48+
var workingBeatmap = GetWorkingBeatmap(beatmapId);
49+
var mods = ManiaRuleset.ConvertFromLegacyMods((LegacyMods)bitMods).ToArray();
5050

5151
var difficultyCalculator = ManiaRuleset.CreateDifficultyCalculator(workingBeatmap);
5252
var difficultyAttributes = difficultyCalculator.Calculate(mods) as ManiaDifficultyAttributes;

Difficalcy.Osu.Tests/Difficalcy.Osu.Tests.csproj

+1-1
Original file line numberDiff line numberDiff line change
@@ -8,7 +8,7 @@
88
</PropertyGroup>
99

1010
<ItemGroup>
11-
<PackageReference Include="Microsoft.NET.Test.Sdk" Version="17.9.0" />
11+
<PackageReference Include="Microsoft.NET.Test.Sdk" Version="17.10.0" />
1212
<PackageReference Include="xunit" Version="2.8.0" />
1313
<PackageReference Include="xunit.runner.visualstudio" Version="2.8.0">
1414
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>

Difficalcy.Osu/Difficalcy.Osu.csproj

+1-1
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,7 @@
55
</PropertyGroup>
66

77
<ItemGroup>
8-
<PackageReference Include="ppy.osu.Game.Rulesets.Osu" Version="2024.412.1" />
8+
<PackageReference Include="ppy.osu.Game.Rulesets.Osu" Version="2024.523.0" />
99
</ItemGroup>
1010

1111
<ItemGroup>

Difficalcy.Osu/Services/OsuCalculatorService.cs

+3-3
Original file line numberDiff line numberDiff line change
@@ -41,10 +41,10 @@ protected override async Task EnsureBeatmap(string beatmapId)
4141
await beatmapProvider.EnsureBeatmap(beatmapId);
4242
}
4343

44-
protected override (object, string) CalculateDifficultyAttributes(OsuScore score)
44+
protected override (object, string) CalculateDifficultyAttributes(string beatmapId, int bitMods)
4545
{
46-
var workingBeatmap = GetWorkingBeatmap(score.BeatmapId);
47-
var mods = OsuRuleset.ConvertFromLegacyMods((LegacyMods)score.Mods).ToArray();
46+
var workingBeatmap = GetWorkingBeatmap(beatmapId);
47+
var mods = OsuRuleset.ConvertFromLegacyMods((LegacyMods)bitMods).ToArray();
4848

4949
var difficultyCalculator = OsuRuleset.CreateDifficultyCalculator(workingBeatmap);
5050
var difficultyAttributes = difficultyCalculator.Calculate(mods) as OsuDifficultyAttributes;

Difficalcy.Taiko.Tests/Difficalcy.Taiko.Tests.csproj

+1-1
Original file line numberDiff line numberDiff line change
@@ -8,7 +8,7 @@
88
</PropertyGroup>
99

1010
<ItemGroup>
11-
<PackageReference Include="Microsoft.NET.Test.Sdk" Version="17.9.0" />
11+
<PackageReference Include="Microsoft.NET.Test.Sdk" Version="17.10.0" />
1212
<PackageReference Include="xunit" Version="2.8.0" />
1313
<PackageReference Include="xunit.runner.visualstudio" Version="2.8.0">
1414
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>

Difficalcy.Taiko/Difficalcy.Taiko.csproj

+2-2
Original file line numberDiff line numberDiff line change
@@ -5,8 +5,8 @@
55
</PropertyGroup>
66

77
<ItemGroup>
8-
<PackageReference Include="ppy.osu.Game.Rulesets.Taiko" Version="2024.412.1" />
9-
<PackageReference Include="ppy.osu.Game.Rulesets.Osu" Version="2024.412.1" /> <!-- required for convert support -->
8+
<PackageReference Include="ppy.osu.Game.Rulesets.Taiko" Version="2024.523.0" />
9+
<PackageReference Include="ppy.osu.Game.Rulesets.Osu" Version="2024.523.0" /> <!-- required for convert support -->
1010
</ItemGroup>
1111

1212
<ItemGroup>

Difficalcy.Taiko/Services/TaikoCalculatorService.cs

+3-3
Original file line numberDiff line numberDiff line change
@@ -43,10 +43,10 @@ protected override async Task EnsureBeatmap(string beatmapId)
4343
await _beatmapProvider.EnsureBeatmap(beatmapId);
4444
}
4545

46-
protected override (object, string) CalculateDifficultyAttributes(TaikoScore score)
46+
protected override (object, string) CalculateDifficultyAttributes(string beatmapId, int bitMods)
4747
{
48-
var workingBeatmap = GetWorkingBeatmap(score.BeatmapId);
49-
var mods = TaikoRuleset.ConvertFromLegacyMods((LegacyMods)score.Mods).ToArray();
48+
var workingBeatmap = GetWorkingBeatmap(beatmapId);
49+
var mods = TaikoRuleset.ConvertFromLegacyMods((LegacyMods)bitMods).ToArray();
5050

5151
var difficultyCalculator = TaikoRuleset.CreateDifficultyCalculator(workingBeatmap);
5252
var difficultyAttributes = difficultyCalculator.Calculate(mods) as TaikoDifficultyAttributes;

Difficalcy.Tests/Difficalcy.Tests.csproj

+1-1
Original file line numberDiff line numberDiff line change
@@ -8,7 +8,7 @@
88
</PropertyGroup>
99

1010
<ItemGroup>
11-
<PackageReference Include="Microsoft.NET.Test.Sdk" Version="17.9.0" />
11+
<PackageReference Include="Microsoft.NET.Test.Sdk" Version="17.10.0" />
1212
<PackageReference Include="xunit" Version="2.8.0" />
1313
<PackageReference Include="xunit.runner.visualstudio" Version="2.8.0">
1414
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>

Difficalcy.Tests/DummyCalculatorServiceTest.cs

+69-5
Original file line numberDiff line numberDiff line change
@@ -11,7 +11,68 @@ public class DummyCalculatorServiceTest : CalculatorServiceTest<DummyScore, Dumm
1111
[InlineData(15, 1500, "test 1", 150)]
1212
[InlineData(10, 1000, "test 2", 100)]
1313
public void Test(double expectedDifficultyTotal, double expectedPerformanceTotal, string beatmapId, int mods)
14-
=> TestGetCalculationReturnsCorrectValues(expectedDifficultyTotal, expectedPerformanceTotal, new DummyScore { BeatmapId = beatmapId, Mods = mods });
14+
=> TestGetCalculationReturnsCorrectValues(expectedDifficultyTotal, expectedPerformanceTotal, new DummyScore { BeatmapId = beatmapId, Mods = mods, Points = 100 });
15+
16+
[Fact]
17+
public async Task TestGetCalculationBatchReturnsCorrectValuesInOrder()
18+
{
19+
// values are intentionally in a random order to ensure unique beatmap grouping doesnt break return ordering
20+
var scores = new[]
21+
{
22+
new DummyScore { BeatmapId = "test 1", Mods = 200, Points = 200 }, // 3
23+
new DummyScore { BeatmapId = "test 2", Mods = 300, Points = 100 }, // 4
24+
new DummyScore { BeatmapId = "test 2", Mods = 300, Points = 200 }, // 5
25+
new DummyScore { BeatmapId = "test 3", Mods = 500, Points = 200 }, // 9
26+
new DummyScore { BeatmapId = "test 2", Mods = 400, Points = 200 }, // 7
27+
new DummyScore { BeatmapId = "test 1", Mods = 200, Points = 100 }, // 2
28+
new DummyScore { BeatmapId = "test 3", Mods = 600, Points = 100 }, // 10
29+
new DummyScore { BeatmapId = "test 2", Mods = 400, Points = 100 }, // 6
30+
new DummyScore { BeatmapId = "test 3", Mods = 500, Points = 100 }, // 8
31+
new DummyScore { BeatmapId = "test 1", Mods = 100, Points = 200 }, // 1
32+
new DummyScore { BeatmapId = "test 3", Mods = 600, Points = 200 }, // 11
33+
new DummyScore { BeatmapId = "test 1", Mods = 100, Points = 100 }, // 0
34+
};
35+
36+
var calculations = (await CalculatorService.GetCalculationBatch(scores)).ToArray();
37+
38+
Assert.Equal(12, calculations.Length);
39+
40+
Assert.Equal(20, calculations[0].Difficulty.Total); // 3
41+
Assert.Equal(4000, calculations[0].Performance.Total);
42+
43+
Assert.Equal(30, calculations[1].Difficulty.Total); // 4
44+
Assert.Equal(3000, calculations[1].Performance.Total);
45+
46+
Assert.Equal(30, calculations[2].Difficulty.Total); // 5
47+
Assert.Equal(6000, calculations[2].Performance.Total);
48+
49+
Assert.Equal(50, calculations[3].Difficulty.Total); // 9
50+
Assert.Equal(10000, calculations[3].Performance.Total);
51+
52+
Assert.Equal(40, calculations[4].Difficulty.Total); // 7
53+
Assert.Equal(8000, calculations[4].Performance.Total);
54+
55+
Assert.Equal(20, calculations[5].Difficulty.Total); // 2
56+
Assert.Equal(2000, calculations[5].Performance.Total);
57+
58+
Assert.Equal(60, calculations[6].Difficulty.Total); // 10
59+
Assert.Equal(6000, calculations[6].Performance.Total);
60+
61+
Assert.Equal(40, calculations[7].Difficulty.Total); // 6
62+
Assert.Equal(4000, calculations[7].Performance.Total);
63+
64+
Assert.Equal(50, calculations[8].Difficulty.Total); // 8
65+
Assert.Equal(5000, calculations[8].Performance.Total);
66+
67+
Assert.Equal(10, calculations[9].Difficulty.Total); // 1
68+
Assert.Equal(2000, calculations[9].Performance.Total);
69+
70+
Assert.Equal(60, calculations[10].Difficulty.Total); // 11
71+
Assert.Equal(12000, calculations[10].Performance.Total);
72+
73+
Assert.Equal(10, calculations[11].Difficulty.Total); // 0
74+
Assert.Equal(1000, calculations[11].Performance.Total);
75+
}
1576
}
1677

1778
/// <summary>
@@ -29,17 +90,17 @@ public class DummyCalculatorService(ICache cache) : CalculatorService<DummyScore
2990
CalculatorUrl = $"not.a.real.url"
3091
};
3192

32-
protected override (object, string) CalculateDifficultyAttributes(DummyScore score)
93+
protected override (object, string) CalculateDifficultyAttributes(string beatmapId, int mods)
3394
{
34-
var difficulty = score.Mods / 10.0;
95+
var difficulty = mods / 10.0;
3596
return (difficulty, difficulty.ToString());
3697
}
3798

3899
protected override DummyCalculation CalculatePerformance(DummyScore score, object difficultyAttributes) =>
39100
new()
40101
{
41102
Difficulty = new DummyDifficulty() { Total = (double)difficultyAttributes },
42-
Performance = new DummyPerformance() { Total = (double)difficultyAttributes * 100 }
103+
Performance = new DummyPerformance() { Total = (double)difficultyAttributes * score.Points }
43104
};
44105

45106
protected override object DeserialiseDifficultyAttributes(string difficultyAttributesJson) =>
@@ -49,7 +110,10 @@ protected override Task EnsureBeatmap(string beatmapId) =>
49110
Task.FromResult(true);
50111
}
51112

52-
public record DummyScore : Score { }
113+
public record DummyScore : Score
114+
{
115+
public int Points { get; init; }
116+
}
53117
public record DummyDifficulty : Difficulty { }
54118
public record DummyPerformance : Performance { }
55119
public record DummyCalculation : Calculation<DummyDifficulty, DummyPerformance> { }

Difficalcy/Controllers/CalculatorController.cs

+1-2
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,3 @@
1-
using System.Linq;
21
using System.Threading.Tasks;
32
using Difficalcy.Models;
43
using Difficalcy.Services;
@@ -52,7 +51,7 @@ public async Task<ActionResult<TCalculation[]>> GetCalculationBatch([FromBody] T
5251
{
5352
try
5453
{
55-
return Ok(await Task.WhenAll(scores.Select(calculatorService.GetCalculation)));
54+
return Ok(await calculatorService.GetCalculationBatch(scores));
5655
}
5756
catch (BeatmapNotFoundException e)
5857
{

Difficalcy/Difficalcy.csproj

+1-1
Original file line numberDiff line numberDiff line change
@@ -9,7 +9,7 @@
99
</ItemGroup>
1010

1111
<ItemGroup>
12-
<PackageReference Include="Swashbuckle.AspNetCore" Version="6.6.1" />
12+
<PackageReference Include="Swashbuckle.AspNetCore" Version="6.6.2" />
1313
<PackageReference Include="StackExchange.Redis" Version="2.7.33" />
1414
</ItemGroup>
1515

Difficalcy/Services/CalculatorService.cs

+28-6
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,5 @@
1+
using System.Collections.Generic;
2+
using System.Linq;
13
using System.Threading.Tasks;
24
using Difficalcy.Models;
35

@@ -28,7 +30,7 @@ public abstract class CalculatorService<TScore, TDifficulty, TPerformance, TCalc
2830
/// <summary>
2931
/// Runs the difficulty calculator and returns the difficulty attributes as both an object and JSON serialised string.
3032
/// </summary>
31-
protected abstract (object, string) CalculateDifficultyAttributes(TScore score);
33+
protected abstract (object, string) CalculateDifficultyAttributes(string beatmapId, int mods);
3234

3335
/// <summary>
3436
/// Returns the deserialised object for a given JSON serialised difficulty attributes object.
@@ -45,22 +47,42 @@ public abstract class CalculatorService<TScore, TDifficulty, TPerformance, TCalc
4547
/// </summary>
4648
public async Task<TCalculation> GetCalculation(TScore score)
4749
{
48-
var difficultyAttributes = await GetDifficultyAttributes(score);
50+
var difficultyAttributes = await GetDifficultyAttributes(score.BeatmapId, score.Mods);
4951
return CalculatePerformance(score, difficultyAttributes);
5052
}
5153

52-
private async Task<object> GetDifficultyAttributes(TScore score)
54+
public async Task<IEnumerable<TCalculation>> GetCalculationBatch(TScore[] scores)
5355
{
54-
await EnsureBeatmap(score.BeatmapId);
56+
var scoresWithIndex = scores.Select((score, index) => (score, index));
57+
var uniqueBeatmapGroups = scoresWithIndex.GroupBy(scoreWithIndex => (scoreWithIndex.score.BeatmapId, scoreWithIndex.score.Mods));
58+
59+
var calculationGroups = await Task.WhenAll(uniqueBeatmapGroups.Select(async group =>
60+
{
61+
var scores = group.Select(scoreWithIndex => scoreWithIndex.score);
62+
return group.Select(scoreWithIndex => scoreWithIndex.index).Zip(await GetUniqueBeatmapCalculationBatch(group.Key.BeatmapId, group.Key.Mods, scores));
63+
}));
64+
65+
return calculationGroups.SelectMany(group => group).OrderBy(group => group.First).Select(group => group.Second);
66+
}
67+
68+
private async Task<IEnumerable<TCalculation>> GetUniqueBeatmapCalculationBatch(string beatmapId, int mods, IEnumerable<TScore> scores)
69+
{
70+
var difficultyAttributes = await GetDifficultyAttributes(beatmapId, mods);
71+
return scores.AsParallel().AsOrdered().Select(score => CalculatePerformance(score, difficultyAttributes));
72+
}
73+
74+
private async Task<object> GetDifficultyAttributes(string beatmapId, int mods)
75+
{
76+
await EnsureBeatmap(beatmapId);
5577

5678
var db = cache.GetDatabase();
57-
var redisKey = $"difficalcy:{CalculatorDiscriminator}:{score.BeatmapId}:{score.Mods}";
79+
var redisKey = $"difficalcy:{CalculatorDiscriminator}:{beatmapId}:{mods}";
5880
var difficultyAttributesJson = await db.GetAsync(redisKey);
5981

6082
object difficultyAttributes;
6183
if (difficultyAttributesJson == null)
6284
{
63-
(difficultyAttributes, difficultyAttributesJson) = CalculateDifficultyAttributes(score);
85+
(difficultyAttributes, difficultyAttributesJson) = CalculateDifficultyAttributes(beatmapId, mods);
6486
db.Set(redisKey, difficultyAttributesJson);
6587
}
6688
else

0 commit comments

Comments
 (0)