1
0
mirror of https://github.com/ppy/osu.git synced 2024-12-05 09:42:54 +08:00

Merge pull request #30868 from peppy/ur-perf-fix

Improve performance of UR calculations
This commit is contained in:
Bartłomiej Dach 2024-11-26 13:54:17 +01:00 committed by GitHub
commit 3e373ae85e
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
8 changed files with 142 additions and 31 deletions

View File

@ -4,28 +4,54 @@
using System.Collections.Generic; using System.Collections.Generic;
using BenchmarkDotNet.Attributes; using BenchmarkDotNet.Attributes;
using osu.Framework.Utils; using osu.Framework.Utils;
using osu.Game.Rulesets.Objects; using osu.Game.Beatmaps;
using osu.Game.Beatmaps.ControlPoints;
using osu.Game.Rulesets.Osu.Objects;
using osu.Game.Rulesets.Scoring; using osu.Game.Rulesets.Scoring;
namespace osu.Game.Benchmarks namespace osu.Game.Benchmarks
{ {
public class BenchmarkUnstableRate : BenchmarkTest public class BenchmarkUnstableRate : BenchmarkTest
{ {
private List<HitEvent> events = null!; private readonly List<List<HitEvent>> incrementalEventLists = new List<List<HitEvent>>();
public override void SetUp() public override void SetUp()
{ {
base.SetUp(); base.SetUp();
events = new List<HitEvent>();
for (int i = 0; i < 1000; i++) var events = new List<HitEvent>();
events.Add(new HitEvent(RNG.NextDouble(-200.0, 200.0), RNG.NextDouble(1.0, 2.0), HitResult.Great, new HitObject(), null, null));
for (int i = 0; i < 2048; i++)
{
// Ensure the object has hit windows populated.
var hitObject = new HitCircle();
hitObject.ApplyDefaults(new ControlPointInfo(), new BeatmapDifficulty());
events.Add(new HitEvent(RNG.NextDouble(-200.0, 200.0), RNG.NextDouble(1.0, 2.0), HitResult.Great, hitObject, null, null));
incrementalEventLists.Add(new List<HitEvent>(events));
}
} }
[Benchmark] [Benchmark]
public void CalculateUnstableRate() public void CalculateUnstableRate()
{ {
_ = events.CalculateUnstableRate(); for (int i = 0; i < 2048; i++)
{
var events = incrementalEventLists[i];
_ = events.CalculateUnstableRate();
}
}
[Benchmark]
public void CalculateUnstableRateUsingIncrementalCalculation()
{
HitEventExtensions.UnstableRateCalculationResult? last = null;
for (int i = 0; i < 2048; i++)
{
var events = incrementalEventLists[i];
last = events.CalculateUnstableRate(last);
}
} }
} }
} }

View File

@ -20,12 +20,53 @@ namespace osu.Game.Tests.NonVisual.Ranking
public void TestDistributedHits() public void TestDistributedHits()
{ {
var events = Enumerable.Range(-5, 11) var events = Enumerable.Range(-5, 11)
.Select(t => new HitEvent(t - 5, 1.0, HitResult.Great, new HitObject(), null, null)); .Select(t => new HitEvent(t - 5, 1.0, HitResult.Great, new HitObject(), null, null))
.ToList();
var unstableRate = new UnstableRate(events); var unstableRate = new UnstableRate(events);
Assert.IsNotNull(unstableRate.Value); Assert.IsNotNull(unstableRate.Value);
Assert.IsTrue(Precision.AlmostEquals(unstableRate.Value.Value, 10 * Math.Sqrt(10))); Assert.AreEqual(unstableRate.Value.Value, 10 * Math.Sqrt(10), Precision.DOUBLE_EPSILON);
}
[Test]
public void TestDistributedHitsIncrementalRewind()
{
var events = Enumerable.Range(-5, 11)
.Select(t => new HitEvent(t - 5, 1.0, HitResult.Great, new HitObject(), null, null))
.ToList();
HitEventExtensions.UnstableRateCalculationResult result = null;
for (int i = 0; i < events.Count; i++)
{
result = events.GetRange(0, i + 1)
.CalculateUnstableRate(result);
}
result = events.GetRange(0, 2).CalculateUnstableRate(result);
Assert.IsNotNull(result!.Result);
Assert.AreEqual(5, result.Result, Precision.DOUBLE_EPSILON);
}
[Test]
public void TestDistributedHitsIncremental()
{
var events = Enumerable.Range(-5, 11)
.Select(t => new HitEvent(t - 5, 1.0, HitResult.Great, new HitObject(), null, null))
.ToList();
HitEventExtensions.UnstableRateCalculationResult result = null;
for (int i = 0; i < events.Count; i++)
{
result = events.GetRange(0, i + 1)
.CalculateUnstableRate(result);
}
Assert.IsNotNull(result!.Result);
Assert.AreEqual(10 * Math.Sqrt(10), result.Result, Precision.DOUBLE_EPSILON);
} }
[Test] [Test]

View File

@ -205,7 +205,7 @@ namespace osu.Game.Rulesets.Mods
{ {
foreach (var hitObject in hitObjects) foreach (var hitObject in hitObjects)
{ {
if (!(hitObject.HitWindows is HitWindows.EmptyHitWindows)) if (hitObject.HitWindows != HitWindows.Empty)
yield return hitObject; yield return hitObject;
foreach (HitObject nested in getAllApplicableHitObjects(hitObject.NestedHitObjects)) foreach (HitObject nested in getAllApplicableHitObjects(hitObject.NestedHitObjects))

View File

@ -5,6 +5,7 @@ using System;
using System.Collections.Generic; using System.Collections.Generic;
using System.Diagnostics; using System.Diagnostics;
using System.Linq; using System.Linq;
using osu.Game.Rulesets.Objects;
namespace osu.Game.Rulesets.Scoring namespace osu.Game.Rulesets.Scoring
{ {
@ -20,32 +21,36 @@ namespace osu.Game.Rulesets.Scoring
/// A non-null <see langword="double"/> value if unstable rate could be calculated, /// A non-null <see langword="double"/> value if unstable rate could be calculated,
/// and <see langword="null"/> if unstable rate cannot be calculated due to <paramref name="hitEvents"/> being empty. /// and <see langword="null"/> if unstable rate cannot be calculated due to <paramref name="hitEvents"/> being empty.
/// </returns> /// </returns>
public static double? CalculateUnstableRate(this IEnumerable<HitEvent> hitEvents) public static UnstableRateCalculationResult? CalculateUnstableRate(this IReadOnlyList<HitEvent> hitEvents, UnstableRateCalculationResult? result = null)
{ {
Debug.Assert(hitEvents.All(ev => ev.GameplayRate != null)); Debug.Assert(hitEvents.All(ev => ev.GameplayRate != null));
int count = 0; result ??= new UnstableRateCalculationResult();
double mean = 0;
double sumOfSquares = 0;
foreach (var e in hitEvents) // Handle rewinding in the simplest way possible.
if (hitEvents.Count < result.EventCount + 1)
result = new UnstableRateCalculationResult();
for (int i = result.EventCount; i < hitEvents.Count; i++)
{ {
HitEvent e = hitEvents[i];
if (!AffectsUnstableRate(e)) if (!AffectsUnstableRate(e))
continue; continue;
count++; result.EventCount++;
// Division by gameplay rate is to account for TimeOffset scaling with gameplay rate. // Division by gameplay rate is to account for TimeOffset scaling with gameplay rate.
double currentValue = e.TimeOffset / e.GameplayRate!.Value; double currentValue = e.TimeOffset / e.GameplayRate!.Value;
double nextMean = mean + (currentValue - mean) / count; double nextMean = result.Mean + (currentValue - result.Mean) / result.EventCount;
sumOfSquares += (currentValue - mean) * (currentValue - nextMean); result.SumOfSquares += (currentValue - result.Mean) * (currentValue - nextMean);
mean = nextMean; result.Mean = nextMean;
} }
if (count == 0) if (result.EventCount == 0)
return null; return null;
return 10.0 * Math.Sqrt(sumOfSquares / count); return result;
} }
/// <summary> /// <summary>
@ -65,6 +70,39 @@ namespace osu.Game.Rulesets.Scoring
return timeOffsets.Average(); return timeOffsets.Average();
} }
public static bool AffectsUnstableRate(HitEvent e) => !(e.HitObject.HitWindows is HitWindows.EmptyHitWindows) && e.Result.IsHit(); public static bool AffectsUnstableRate(HitEvent e) => AffectsUnstableRate(e.HitObject, e.Result);
public static bool AffectsUnstableRate(HitObject hitObject, HitResult result) => hitObject.HitWindows != HitWindows.Empty && result.IsHit();
/// <summary>
/// Data type returned by <see cref="HitEventExtensions.CalculateUnstableRate"/> which allows efficient incremental processing.
/// </summary>
/// <remarks>
/// This should be passed back into future <see cref="HitEventExtensions.CalculateUnstableRate"/> calls as a parameter.
///
/// The optimisations used here rely on hit events being a consecutive sequence from a single gameplay session.
/// When a new gameplay session is started, any existing results should be disposed.
/// </remarks>
public class UnstableRateCalculationResult
{
/// <summary>
/// Total events processed. For internal incremental calculation use.
/// </summary>
public int EventCount;
/// <summary>
/// Last sum-of-squares value. For internal incremental calculation use.
/// </summary>
public double SumOfSquares;
/// <summary>
/// Last mean value. For internal incremental calculation use.
/// </summary>
public double Mean;
/// <summary>
/// The unstable rate.
/// </summary>
public double Result => EventCount == 0 ? 0 : 10.0 * Math.Sqrt(SumOfSquares / EventCount);
}
} }
} }

View File

@ -36,7 +36,7 @@ namespace osu.Game.Rulesets.Scoring
/// An empty <see cref="HitWindows"/> with only <see cref="HitResult.Miss"/> and <see cref="HitResult.Perfect"/>. /// An empty <see cref="HitWindows"/> with only <see cref="HitResult.Miss"/> and <see cref="HitResult.Perfect"/>.
/// No time values are provided (meaning instantaneous hit or miss). /// No time values are provided (meaning instantaneous hit or miss).
/// </summary> /// </summary>
public static HitWindows Empty => new EmptyHitWindows(); public static HitWindows Empty { get; } = new EmptyHitWindows();
public HitWindows() public HitWindows()
{ {
@ -182,7 +182,7 @@ namespace osu.Game.Rulesets.Scoring
/// </summary> /// </summary>
protected virtual DifficultyRange[] GetRanges() => base_ranges; protected virtual DifficultyRange[] GetRanges() => base_ranges;
public class EmptyHitWindows : HitWindows private class EmptyHitWindows : HitWindows
{ {
private static readonly DifficultyRange[] ranges = private static readonly DifficultyRange[] ranges =
{ {

View File

@ -28,6 +28,8 @@ namespace osu.Game.Screens.Play.HUD
private const float alpha_when_invalid = 0.3f; private const float alpha_when_invalid = 0.3f;
private readonly Bindable<bool> valid = new Bindable<bool>(); private readonly Bindable<bool> valid = new Bindable<bool>();
private HitEventExtensions.UnstableRateCalculationResult? unstableRateResult;
[Resolved] [Resolved]
private ScoreProcessor scoreProcessor { get; set; } = null!; private ScoreProcessor scoreProcessor { get; set; } = null!;
@ -44,9 +46,6 @@ namespace osu.Game.Screens.Play.HUD
DrawableCount.FadeTo(e.NewValue ? 1 : alpha_when_invalid, 1000, Easing.OutQuint)); DrawableCount.FadeTo(e.NewValue ? 1 : alpha_when_invalid, 1000, Easing.OutQuint));
} }
private bool changesUnstableRate(JudgementResult judgement)
=> !(judgement.HitObject.HitWindows is HitWindows.EmptyHitWindows) && judgement.IsHit;
protected override void LoadComplete() protected override void LoadComplete()
{ {
base.LoadComplete(); base.LoadComplete();
@ -56,13 +55,20 @@ namespace osu.Game.Screens.Play.HUD
updateDisplay(); updateDisplay();
} }
private void updateDisplay(JudgementResult _) => Scheduler.AddOnce(updateDisplay); private void updateDisplay(JudgementResult result)
{
if (HitEventExtensions.AffectsUnstableRate(result.HitObject, result.Type))
Scheduler.AddOnce(updateDisplay);
}
private void updateDisplay() private void updateDisplay()
{ {
double? unstableRate = scoreProcessor.HitEvents.CalculateUnstableRate(); unstableRateResult = scoreProcessor.HitEvents.CalculateUnstableRate(unstableRateResult);
double? unstableRate = unstableRateResult?.Result;
valid.Value = unstableRate != null; valid.Value = unstableRate != null;
if (unstableRate != null) if (unstableRate != null)
Current.Value = (int)Math.Round(unstableRate.Value); Current.Value = (int)Math.Round(unstableRate.Value);
} }

View File

@ -58,7 +58,7 @@ namespace osu.Game.Screens.Ranking.Statistics
/// <param name="hitEvents">The <see cref="HitEvent"/>s to display the timing distribution of.</param> /// <param name="hitEvents">The <see cref="HitEvent"/>s to display the timing distribution of.</param>
public HitEventTimingDistributionGraph(IReadOnlyList<HitEvent> hitEvents) public HitEventTimingDistributionGraph(IReadOnlyList<HitEvent> hitEvents)
{ {
this.hitEvents = hitEvents.Where(e => !(e.HitObject.HitWindows is HitWindows.EmptyHitWindows) && e.Result.IsBasic() && e.Result.IsHit()).ToList(); this.hitEvents = hitEvents.Where(e => e.HitObject.HitWindows != HitWindows.Empty && e.Result.IsBasic() && e.Result.IsHit()).ToList();
bins = Enumerable.Range(0, total_timing_distribution_bins).Select(_ => new Dictionary<HitResult, int>()).ToArray<IDictionary<HitResult, int>>(); bins = Enumerable.Range(0, total_timing_distribution_bins).Select(_ => new Dictionary<HitResult, int>()).ToArray<IDictionary<HitResult, int>>();
} }

View File

@ -15,10 +15,10 @@ namespace osu.Game.Screens.Ranking.Statistics
/// Creates and computes an <see cref="UnstableRate"/> statistic. /// Creates and computes an <see cref="UnstableRate"/> statistic.
/// </summary> /// </summary>
/// <param name="hitEvents">Sequence of <see cref="HitEvent"/>s to calculate the unstable rate based on.</param> /// <param name="hitEvents">Sequence of <see cref="HitEvent"/>s to calculate the unstable rate based on.</param>
public UnstableRate(IEnumerable<HitEvent> hitEvents) public UnstableRate(IReadOnlyList<HitEvent> hitEvents)
: base("Unstable Rate") : base("Unstable Rate")
{ {
Value = hitEvents.CalculateUnstableRate(); Value = hitEvents.CalculateUnstableRate()?.Result;
} }
protected override string DisplayValue(double? value) => value == null ? "(not available)" : value.Value.ToString(@"N2"); protected override string DisplayValue(double? value) => value == null ? "(not available)" : value.Value.ToString(@"N2");