From b84105d93b5ae67657937cf7f1bac605a2ff8d02 Mon Sep 17 00:00:00 2001 From: Salman Alshamrani Date: Thu, 1 May 2025 08:37:33 +0300 Subject: [PATCH 1/5] Add test stressing title wedge performance with a heavy beatmap --- .../TestSceneBeatmapTitleWedge.cs | 62 +++++++++++++++++++ 1 file changed, 62 insertions(+) diff --git a/osu.Game.Tests/Visual/SongSelectV2/TestSceneBeatmapTitleWedge.cs b/osu.Game.Tests/Visual/SongSelectV2/TestSceneBeatmapTitleWedge.cs index df334736e2..85d82e536d 100644 --- a/osu.Game.Tests/Visual/SongSelectV2/TestSceneBeatmapTitleWedge.cs +++ b/osu.Game.Tests/Visual/SongSelectV2/TestSceneBeatmapTitleWedge.cs @@ -2,11 +2,16 @@ // See the LICENCE file in the repository root for full licence text. using System; +using System.Collections.Generic; +using System.IO; using System.Linq; using NUnit.Framework; using osu.Framework.Allocation; +using osu.Framework.Audio; +using osu.Framework.Audio.Track; using osu.Framework.Graphics; using osu.Framework.Graphics.Containers; +using osu.Framework.Graphics.Textures; using osu.Framework.Testing; using osu.Game.Beatmaps; using osu.Game.Beatmaps.ControlPoints; @@ -16,9 +21,12 @@ using osu.Game.Online.API.Requests; using osu.Game.Online.API.Requests.Responses; using osu.Game.Resources.Localisation.Web; using osu.Game.Rulesets; +using osu.Game.Rulesets.Objects; +using osu.Game.Rulesets.Objects.Legacy; using osu.Game.Rulesets.Osu; using osu.Game.Rulesets.Osu.Mods; using osu.Game.Screens.SelectV2; +using osu.Game.Skinning; using osu.Game.Tests.Visual.SongSelect; namespace osu.Game.Tests.Visual.SongSelectV2 @@ -193,6 +201,16 @@ namespace osu.Game.Tests.Visual.SongSelectV2 checkDisplayedBPM(expectedDisplay); } + [Test] + [Explicit] + public void TestPerformanceWithLongBeatmap() + { + AddStep("select heavy beatmap", () => Beatmap.Value = new HeavyWorkingBeatmap(Audio)); + + foreach (var rulesetInfo in rulesets.AvailableRulesets) + setRuleset(rulesetInfo); + } + private void setRuleset(RulesetInfo rulesetInfo) { AddStep("set ruleset", () => Ruleset.Value = rulesetInfo); @@ -238,5 +256,49 @@ namespace osu.Game.Tests.Visual.SongSelectV2 working.BeatmapSetInfo.DateRanked = DateTimeOffset.Now; return (working, onlineSet); } + + private class TestHitObject : ConvertHitObject; + + private class HeavyWorkingBeatmap : WorkingBeatmap + { + private static readonly BeatmapInfo beatmap_info = new BeatmapInfo + { + Metadata = new BeatmapMetadata + { + Author = { Username = "osuAuthor" }, + Artist = "osuArtist", + Source = "osuSource", + Title = "osuTitle" + }, + Ruleset = new OsuRuleset().RulesetInfo, + StarRating = 6, + DifficultyName = "osuVersion", + Difficulty = new BeatmapDifficulty() + }; + + public HeavyWorkingBeatmap(AudioManager audioManager) + : base(beatmap_info, audioManager) + { + } + + protected override IBeatmap GetBeatmap() + { + List objects = new List(); + + for (int i = 0; i < 200_000; i++) + objects.Add(new TestHitObject { StartTime = i * 1000 }); + + return new Beatmap + { + BeatmapInfo = beatmap_info, + HitObjects = objects + }; + } + + public override Texture? GetBackground() => null; + public override Stream? GetStream(string storagePath) => null; + protected override Track? GetBeatmapTrack() => null; + protected internal override ISkin? GetSkin() => null; + } } } From 4f79dcb41135587a92d91e1b01f17a5a965a6959 Mon Sep 17 00:00:00 2001 From: Salman Alshamrani Date: Thu, 1 May 2025 08:54:50 +0300 Subject: [PATCH 2/5] Fix length & BPM statistics computation causing direct beatmap load --- .../Screens/SelectV2/BeatmapTitleWedge.cs | 42 +++++++++++++------ 1 file changed, 30 insertions(+), 12 deletions(-) diff --git a/osu.Game/Screens/SelectV2/BeatmapTitleWedge.cs b/osu.Game/Screens/SelectV2/BeatmapTitleWedge.cs index 154374cbcb..65ea89e96b 100644 --- a/osu.Game/Screens/SelectV2/BeatmapTitleWedge.cs +++ b/osu.Game/Screens/SelectV2/BeatmapTitleWedge.cs @@ -4,6 +4,8 @@ using System; using System.Collections.Generic; using System.Linq; +using System.Threading; +using System.Threading.Tasks; using osu.Framework.Allocation; using osu.Framework.Bindables; using osu.Framework.Extensions.LocalisationExtensions; @@ -246,25 +248,41 @@ namespace osu.Game.Screens.SelectV2 updateOnlineDisplay(); } + private CancellationTokenSource? lengthBpmCancellationSource; + private void updateLengthAndBpmStatistics() { - var beatmapInfo = beatmap.Value.BeatmapInfo; + lengthBpmCancellationSource?.Cancel(); + lengthBpmCancellationSource = new CancellationTokenSource(); - double rate = ModUtils.CalculateRateWithMods(mods.Value); + var token = lengthBpmCancellationSource.Token; - int bpmMax = FormatUtils.RoundBPM(beatmap.Value.Beatmap.ControlPointInfo.BPMMaximum, rate); - int bpmMin = FormatUtils.RoundBPM(beatmap.Value.Beatmap.ControlPointInfo.BPMMinimum, rate); - int mostCommonBPM = FormatUtils.RoundBPM(60000 / beatmap.Value.Beatmap.GetMostCommonBeatLength(), rate); + Task.Run(() => + { + var beatmapInfo = beatmap.Value.BeatmapInfo; - double drainLength = Math.Round(beatmap.Value.Beatmap.CalculateDrainLength() / rate); - double hitLength = Math.Round(beatmapInfo.Length / rate); + double rate = ModUtils.CalculateRateWithMods(mods.Value); - lengthStatistic.Text = hitLength.ToFormattedDuration(); - lengthStatistic.TooltipText = BeatmapsetsStrings.ShowStatsTotalLength(drainLength.ToFormattedDuration()); + int bpmMax = FormatUtils.RoundBPM(beatmap.Value.Beatmap.ControlPointInfo.BPMMaximum, rate); + int bpmMin = FormatUtils.RoundBPM(beatmap.Value.Beatmap.ControlPointInfo.BPMMinimum, rate); + int mostCommonBPM = FormatUtils.RoundBPM(60000 / beatmap.Value.Beatmap.GetMostCommonBeatLength(), rate); - bpmStatistic.Text = bpmMin == bpmMax - ? $"{bpmMin}" - : $"{bpmMin}-{bpmMax} (mostly {mostCommonBPM})"; + double drainLength = Math.Round(beatmap.Value.Beatmap.CalculateDrainLength() / rate); + double hitLength = Math.Round(beatmapInfo.Length / rate); + + Schedule(() => + { + if (token.IsCancellationRequested) + return; + + lengthStatistic.Text = hitLength.ToFormattedDuration(); + lengthStatistic.TooltipText = BeatmapsetsStrings.ShowStatsTotalLength(drainLength.ToFormattedDuration()); + + bpmStatistic.Text = bpmMin == bpmMax + ? $"{bpmMin}" + : $"{bpmMin}-{bpmMax} (mostly {mostCommonBPM})"; + }); + }, token); } private void refetchBeatmapSet() From e8161778b98b9f080bb44f7f45d9cc0bb3c1c793 Mon Sep 17 00:00:00 2001 From: Salman Alshamrani Date: Thu, 1 May 2025 08:54:59 +0300 Subject: [PATCH 3/5] Fix count statistics causing direct beatmap load --- .../BeatmapTitleWedge_DifficultyDisplay.cs | 66 ++++++++++++------- 1 file changed, 43 insertions(+), 23 deletions(-) diff --git a/osu.Game/Screens/SelectV2/BeatmapTitleWedge_DifficultyDisplay.cs b/osu.Game/Screens/SelectV2/BeatmapTitleWedge_DifficultyDisplay.cs index 7e3589b001..ca714964a8 100644 --- a/osu.Game/Screens/SelectV2/BeatmapTitleWedge_DifficultyDisplay.cs +++ b/osu.Game/Screens/SelectV2/BeatmapTitleWedge_DifficultyDisplay.cs @@ -5,6 +5,7 @@ using System; using System.Collections.Generic; using System.Linq; using System.Threading; +using System.Threading.Tasks; using osu.Framework.Allocation; using osu.Framework.Bindables; using osu.Framework.Extensions; @@ -241,8 +242,6 @@ namespace osu.Game.Screens.SelectV2 cancellationSource?.Cancel(); cancellationSource = new CancellationTokenSource(); - computeStarDifficulty(cancellationSource.Token); - if (beatmap.IsDefault) { ratingAndNameContainer.FadeOut(300, Easing.OutQuint); @@ -254,17 +253,53 @@ namespace osu.Game.Screens.SelectV2 difficultyText.Text = beatmap.Value.BeatmapInfo.DifficultyName; mapperLink.Action = () => linkHandler?.HandleLink(new LinkDetails(LinkAction.OpenUserProfile, beatmap.Value.Metadata.Author)); mapperText.Text = beatmap.Value.Metadata.Author.Username; - - var playableBeatmap = beatmap.Value.GetPlayableBeatmap(ruleset.Value); - - countStatisticsDisplay.Statistics = playableBeatmap.GetStatistics() - .Select(s => new StatisticDifficulty.Data(s.Name, s.BarDisplayLength ?? 0, s.BarDisplayLength ?? 0, 1, s.Content)) - .ToList(); } + updateStarDifficulty(cancellationSource.Token); + updateCountStatistics(cancellationSource.Token); updateDifficultyStatistics(); } + private void updateStarDifficulty(CancellationToken cancellationToken) + { + difficultyCache.GetDifficultyAsync(beatmap.Value.BeatmapInfo, ruleset.Value, mods.Value, cancellationToken) + .ContinueWith(task => + { + Schedule(() => + { + if (cancellationToken.IsCancellationRequested) + return; + + starRatingDisplay.Current.Value = task.GetResultSafely() ?? default; + }); + }, cancellationToken); + } + + private void updateCountStatistics(CancellationToken cancellationToken) + { + if (beatmap.IsDefault) + { + countStatisticsDisplay.Statistics = Array.Empty(); + return; + } + + Task.Run(() => + { + var playableBeatmap = beatmap.Value.GetPlayableBeatmap(ruleset.Value); + var statistics = playableBeatmap.GetStatistics() + .Select(s => new StatisticDifficulty.Data(s.Name, s.BarDisplayLength ?? 0, s.BarDisplayLength ?? 0, 1, s.Content)) + .ToList(); + + Schedule(() => + { + if (cancellationToken.IsCancellationRequested) + return; + + countStatisticsDisplay.Statistics = statistics; + }); + }, cancellationToken); + } + private void updateDifficultyStatistics() => Scheduler.AddOnce(() => { if (beatmap.IsDefault) @@ -321,21 +356,6 @@ namespace osu.Game.Screens.SelectV2 }; }); - private void computeStarDifficulty(CancellationToken cancellationToken) - { - difficultyCache.GetDifficultyAsync(beatmap.Value.BeatmapInfo, ruleset.Value, mods.Value, cancellationToken) - .ContinueWith(task => - { - Schedule(() => - { - if (cancellationToken.IsCancellationRequested) - return; - - starRatingDisplay.Current.Value = task.GetResultSafely() ?? default; - }); - }, cancellationToken); - } - protected override void Update() { base.Update(); From 512460e9f7774f2c00f3345cd045e2c8105def1d Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Thu, 1 May 2025 17:02:18 +0900 Subject: [PATCH 4/5] Extract beatmap variable and comment to better show why async is required --- .../Screens/SelectV2/BeatmapTitleWedge.cs | 26 ++++++++++--------- 1 file changed, 14 insertions(+), 12 deletions(-) diff --git a/osu.Game/Screens/SelectV2/BeatmapTitleWedge.cs b/osu.Game/Screens/SelectV2/BeatmapTitleWedge.cs index 65ea89e96b..a73fc78771 100644 --- a/osu.Game/Screens/SelectV2/BeatmapTitleWedge.cs +++ b/osu.Game/Screens/SelectV2/BeatmapTitleWedge.cs @@ -35,7 +35,7 @@ namespace osu.Game.Screens.SelectV2 private const float corner_radius = 10; [Resolved] - private IBindable beatmap { get; set; } = null!; + private IBindable working { get; set; } = null!; [Resolved] private IBindable ruleset { get; set; } = null!; @@ -186,7 +186,7 @@ namespace osu.Game.Screens.SelectV2 { base.LoadComplete(); - beatmap.BindValueChanged(_ => updateDisplay()); + working.BindValueChanged(_ => updateDisplay()); ruleset.BindValueChanged(_ => updateDisplay()); mods.BindValueChanged(m => @@ -226,9 +226,9 @@ namespace osu.Game.Screens.SelectV2 private void updateDisplay() { - var metadata = beatmap.Value.Metadata; - var beatmapInfo = beatmap.Value.BeatmapInfo; - var beatmapSetInfo = beatmap.Value.BeatmapSetInfo; + var metadata = working.Value.Metadata; + var beatmapInfo = working.Value.BeatmapInfo; + var beatmapSetInfo = working.Value.BeatmapSetInfo; statusPill.Status = beatmapInfo.Status; @@ -259,15 +259,17 @@ namespace osu.Game.Screens.SelectV2 Task.Run(() => { - var beatmapInfo = beatmap.Value.BeatmapInfo; + var beatmapInfo = working.Value.BeatmapInfo; + // This can take time as it is a synchronous task. + var beatmap = working.Value.Beatmap; double rate = ModUtils.CalculateRateWithMods(mods.Value); - int bpmMax = FormatUtils.RoundBPM(beatmap.Value.Beatmap.ControlPointInfo.BPMMaximum, rate); - int bpmMin = FormatUtils.RoundBPM(beatmap.Value.Beatmap.ControlPointInfo.BPMMinimum, rate); - int mostCommonBPM = FormatUtils.RoundBPM(60000 / beatmap.Value.Beatmap.GetMostCommonBeatLength(), rate); + int bpmMax = FormatUtils.RoundBPM(beatmap.ControlPointInfo.BPMMaximum, rate); + int bpmMin = FormatUtils.RoundBPM(beatmap.ControlPointInfo.BPMMinimum, rate); + int mostCommonBPM = FormatUtils.RoundBPM(60000 / beatmap.GetMostCommonBeatLength(), rate); - double drainLength = Math.Round(beatmap.Value.Beatmap.CalculateDrainLength() / rate); + double drainLength = Math.Round(beatmap.CalculateDrainLength() / rate); double hitLength = Math.Round(beatmapInfo.Length / rate); Schedule(() => @@ -287,7 +289,7 @@ namespace osu.Game.Screens.SelectV2 private void refetchBeatmapSet() { - var beatmapSetInfo = beatmap.Value.BeatmapSetInfo; + var beatmapSetInfo = working.Value.BeatmapSetInfo; currentRequest?.Cancel(); currentRequest = null; @@ -323,7 +325,7 @@ namespace osu.Game.Screens.SelectV2 else { var onlineBeatmapSet = currentOnlineBeatmapSet; - var onlineBeatmap = currentOnlineBeatmapSet.Beatmaps.SingleOrDefault(b => b.OnlineID == beatmap.Value.BeatmapInfo.OnlineID); + var onlineBeatmap = currentOnlineBeatmapSet.Beatmaps.SingleOrDefault(b => b.OnlineID == working.Value.BeatmapInfo.OnlineID); playCount.Value = new StatisticPlayCount.Data(onlineBeatmap?.PlayCount ?? -1, onlineBeatmap?.UserPlayCount ?? -1); favouritesStatistic.Text = onlineBeatmapSet.FavouriteCount.ToLocalisableString(@"N0"); From e46434731ebaf1351f173366c51d195ea6dee7a2 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Fri, 2 May 2025 19:55:45 +0900 Subject: [PATCH 5/5] Add note about multiple usage of `GetPlayableBeatmap` --- .../Screens/SelectV2/BeatmapTitleWedge_DifficultyDisplay.cs | 2 ++ 1 file changed, 2 insertions(+) diff --git a/osu.Game/Screens/SelectV2/BeatmapTitleWedge_DifficultyDisplay.cs b/osu.Game/Screens/SelectV2/BeatmapTitleWedge_DifficultyDisplay.cs index ca714964a8..9aaf317cb0 100644 --- a/osu.Game/Screens/SelectV2/BeatmapTitleWedge_DifficultyDisplay.cs +++ b/osu.Game/Screens/SelectV2/BeatmapTitleWedge_DifficultyDisplay.cs @@ -285,6 +285,8 @@ namespace osu.Game.Screens.SelectV2 Task.Run(() => { + // This can take time as it is a synchronous task. + // TODO: We're calling `GetPlayableBeatmap` multiple times every map load at song select. var playableBeatmap = beatmap.Value.GetPlayableBeatmap(ruleset.Value); var statistics = playableBeatmap.GetStatistics() .Select(s => new StatisticDifficulty.Data(s.Name, s.BarDisplayLength ?? 0, s.BarDisplayLength ?? 0, 1, s.Content))