From 63fcde55387a0782e26e91ae99102f1d7b0c8f66 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Wed, 19 Mar 2025 17:14:32 +0900 Subject: [PATCH] Fix `DifficultySpectrumDisplay` churning drawables Was causing so much GC that song select (v2) was grinding to a halt. --- .../TestSceneDifficultySpectrumDisplay.cs | 43 ++--- .../Drawables/DifficultySpectrumDisplay.cs | 149 +++++++++++++----- 2 files changed, 134 insertions(+), 58 deletions(-) diff --git a/osu.Game.Tests/Visual/Beatmaps/TestSceneDifficultySpectrumDisplay.cs b/osu.Game.Tests/Visual/Beatmaps/TestSceneDifficultySpectrumDisplay.cs index d4e5c1d966..39de2b7bc9 100644 --- a/osu.Game.Tests/Visual/Beatmaps/TestSceneDifficultySpectrumDisplay.cs +++ b/osu.Game.Tests/Visual/Beatmaps/TestSceneDifficultySpectrumDisplay.cs @@ -1,12 +1,10 @@ // Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. // See the LICENCE file in the repository root for full licence text. -#nullable disable - using System.Linq; using NUnit.Framework; using osu.Framework.Graphics; -using osu.Game.Beatmaps; +using osu.Framework.Testing; using osu.Game.Beatmaps.Drawables; using osu.Game.Online.API.Requests.Responses; using osuTK; @@ -15,14 +13,18 @@ namespace osu.Game.Tests.Visual.Beatmaps { public partial class TestSceneDifficultySpectrumDisplay : OsuTestScene { - private static APIBeatmapSet createBeatmapSetWith(params (int rulesetId, double stars)[] difficulties) => new APIBeatmapSet + private DifficultySpectrumDisplay display = null!; + + [SetUpSteps] + public void SetUpSteps() { - Beatmaps = difficulties.Select(difficulty => new APIBeatmap + AddStep("create spectrum display", () => Child = display = new DifficultySpectrumDisplay { - RulesetID = difficulty.rulesetId, - StarRating = difficulty.stars - }).ToArray() - }; + Anchor = Anchor.Centre, + Origin = Anchor.Centre, + Scale = new Vector2(3) + }); + } [Test] public void TestSingleRuleset() @@ -32,7 +34,7 @@ namespace osu.Game.Tests.Visual.Beatmaps (rulesetId: 0, stars: 3.2), (rulesetId: 0, stars: 5.6)); - createDisplay(beatmapSet); + AddStep("set beatmap to display", () => display.BeatmapSet = beatmapSet); } [Test] @@ -45,7 +47,7 @@ namespace osu.Game.Tests.Visual.Beatmaps (rulesetId: 1, stars: 4.3), (rulesetId: 0, stars: 5.6)); - createDisplay(beatmapSet); + AddStep("set beatmap to display", () => display.BeatmapSet = beatmapSet); } [Test] @@ -59,29 +61,30 @@ namespace osu.Game.Tests.Visual.Beatmaps (rulesetId: 0, stars: 5.6), (rulesetId: 15, stars: 7.8)); - createDisplay(beatmapSet); + AddStep("set beatmap to display", () => display.BeatmapSet = beatmapSet); } [Test] public void TestMaximumUncollapsed() { var beatmapSet = createBeatmapSetWith(Enumerable.Range(0, 12).Select(i => (rulesetId: i % 4, stars: 2.5 + i * 0.25)).ToArray()); - createDisplay(beatmapSet); + AddStep("set beatmap to display", () => display.BeatmapSet = beatmapSet); } [Test] public void TestMinimumCollapsed() { var beatmapSet = createBeatmapSetWith(Enumerable.Range(0, 13).Select(i => (rulesetId: i % 4, stars: 2.5 + i * 0.25)).ToArray()); - createDisplay(beatmapSet); + AddStep("set beatmap to display", () => display.BeatmapSet = beatmapSet); } - private void createDisplay(IBeatmapSetInfo beatmapSetInfo) => AddStep("create spectrum display", () => Child = new DifficultySpectrumDisplay + private static APIBeatmapSet createBeatmapSetWith(params (int rulesetId, double stars)[] difficulties) => new APIBeatmapSet { - BeatmapSet = beatmapSetInfo, - Anchor = Anchor.Centre, - Origin = Anchor.Centre, - Scale = new Vector2(3) - }); + Beatmaps = difficulties.Select(difficulty => new APIBeatmap + { + RulesetID = difficulty.rulesetId, + StarRating = difficulty.stars + }).ToArray() + }; } } diff --git a/osu.Game/Beatmaps/Drawables/DifficultySpectrumDisplay.cs b/osu.Game/Beatmaps/Drawables/DifficultySpectrumDisplay.cs index 60685cd31d..347ad3101c 100644 --- a/osu.Game/Beatmaps/Drawables/DifficultySpectrumDisplay.cs +++ b/osu.Game/Beatmaps/Drawables/DifficultySpectrumDisplay.cs @@ -34,6 +34,8 @@ namespace osu.Game.Beatmaps.Drawables private FillFlowContainer flow = null!; + private const int max_difficulties_before_collapsing = 12; + [BackgroundDependencyLoader] private void load() { @@ -55,31 +57,71 @@ namespace osu.Game.Beatmaps.Drawables private void updateDisplay() { - flow.Clear(); + foreach (var group in flow) + group.Alpha = 0; if (beatmapSet == null) + { + foreach (var group in flow) + group.Beatmaps = []; return; + } // matching web: https://github.com/ppy/osu-web/blob/d06d8c5e735eb1f48799b1654b528e9a7afb0a35/resources/assets/lib/beatmapset-panel.tsx#L127 - bool collapsed = beatmapSet.Beatmaps.Count() > 12; + bool collapsed = beatmapSet.Beatmaps.Count() > max_difficulties_before_collapsing; foreach (var rulesetGrouping in beatmapSet.Beatmaps.GroupBy(beatmap => beatmap.Ruleset).OrderBy(group => group.Key)) { - flow.Add(new RulesetDifficultyGroup(rulesetGrouping.Key.OnlineID, rulesetGrouping, collapsed)); + int rulesetId = rulesetGrouping.Key.OnlineID; + + var group = flow.SingleOrDefault(rg => rg.RulesetId == rulesetId); + + if (group == null) + { + group = new RulesetDifficultyGroup(rulesetId); + flow.Add(group); + flow.SetLayoutPosition(group, rulesetId); + } + + group.Alpha = 1; + group.Beatmaps = rulesetGrouping; + group.Collapsed = collapsed; } } private partial class RulesetDifficultyGroup : FillFlowContainer { - private readonly int rulesetId; - private readonly IEnumerable beatmapInfos; - private readonly bool collapsed; + public readonly int RulesetId; - public RulesetDifficultyGroup(int rulesetId, IEnumerable beatmapInfos, bool collapsed) + private IEnumerable beatmaps = []; + + public IEnumerable Beatmaps { - this.rulesetId = rulesetId; - this.beatmapInfos = beatmapInfos; - this.collapsed = collapsed; + get => beatmaps; + set + { + beatmaps = value; + updateDisplay(); + } + } + + private bool collapsed; + + public bool Collapsed + { + get => collapsed; + set + { + collapsed = value; + updateDisplay(); + } + } + + private OsuSpriteText countText = null!; + + public RulesetDifficultyGroup(int rulesetId) + { + RulesetId = rulesetId; } [BackgroundDependencyLoader] @@ -89,53 +131,84 @@ namespace osu.Game.Beatmaps.Drawables Spacing = new Vector2(1, 0); Direction = FillDirection.Horizontal; - var icon = rulesets.GetRuleset(rulesetId)?.CreateInstance().CreateIcon() ?? new SpriteIcon { Icon = FontAwesome.Regular.QuestionCircle }; + var icon = rulesets.GetRuleset(RulesetId)?.CreateInstance().CreateIcon() ?? new SpriteIcon { Icon = FontAwesome.Regular.QuestionCircle }; Add(icon.With(i => { i.Size = new Vector2(14); i.Anchor = i.Origin = Anchor.Centre; })); - if (!collapsed) + for (int i = 0; i < max_difficulties_before_collapsing; i++) + Add(new DifficultyDot()); + + Add(countText = new OsuSpriteText { - foreach (var beatmapInfo in beatmapInfos.OrderBy(bi => bi.StarRating)) - Add(new DifficultyDot(beatmapInfo.StarRating)); - } - else + Font = OsuFont.Default.With(size: 12), + Anchor = Anchor.Centre, + Origin = Anchor.Centre, + Padding = new MarginPadding { Bottom = 1 } + }); + } + + protected override void LoadComplete() + { + base.LoadComplete(); + updateDisplay(); + } + + private void updateDisplay() + { + countText.Alpha = collapsed ? 1 : 0; + countText.Text = beatmaps.Count().ToLocalisableString(@"N0"); + + var orderedBeatmaps = beatmaps.OrderBy(bi => bi.StarRating).ToArray(); + var dots = this.OfType().ToArray(); + + for (int i = 0; i < max_difficulties_before_collapsing; i++) { - Add(new OsuSpriteText + var dot = dots[i]; + + if (collapsed || i >= orderedBeatmaps.Length) { - Text = beatmapInfos.Count().ToLocalisableString(@"N0"), - Font = OsuFont.Default.With(size: 12), - Anchor = Anchor.Centre, - Origin = Anchor.Centre, - Padding = new MarginPadding { Bottom = 1 } - }); + dot.Alpha = 0; + continue; + } + + dot.Alpha = 1; + dot.StarDifficulty = orderedBeatmaps[i].StarRating; } } } - private partial class DifficultyDot : CircularContainer + private partial class DifficultyDot : Circle { - private readonly double starDifficulty; + private double starDifficulty; - public DifficultyDot(double starDifficulty) + public double StarDifficulty { - this.starDifficulty = starDifficulty; - Size = new Vector2(5, 10); + get => starDifficulty; + set + { + starDifficulty = value; + updateColour(); + } } - [BackgroundDependencyLoader] - private void load(OsuColour colours) - { - Anchor = Origin = Anchor.Centre; - Masking = true; + [Resolved] + private OsuColour colours { get; set; } = null!; - Child = new Box - { - RelativeSizeAxes = Axes.Both, - Colour = colours.ForStarDifficulty(starDifficulty) - }; + [BackgroundDependencyLoader] + private void load() + { + Size = new Vector2(5, 10); + Anchor = Origin = Anchor.Centre; + + updateColour(); + } + + private void updateColour() + { + Colour = colours.ForStarDifficulty(starDifficulty); } } }