From 27ea37c6907cbfe67946bcc62f3f4796b94e8011 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Mon, 17 Jan 2022 18:48:11 +0900 Subject: [PATCH] Rewrite `TopLocalRank` to use realm subscriptions This addresses the number one performance concern with realm (when entering song select). Previous logic was causing instantiation and property reads of every score in the database due to the `AsEnumerable` specfication. --- .../Screens/Select/Carousel/TopLocalRank.cs | 75 ++++++------------- 1 file changed, 24 insertions(+), 51 deletions(-) diff --git a/osu.Game/Screens/Select/Carousel/TopLocalRank.cs b/osu.Game/Screens/Select/Carousel/TopLocalRank.cs index fb1fa09253..f12de8562d 100644 --- a/osu.Game/Screens/Select/Carousel/TopLocalRank.cs +++ b/osu.Game/Screens/Select/Carousel/TopLocalRank.cs @@ -6,9 +6,9 @@ using System.Linq; using osu.Framework.Allocation; using osu.Framework.Bindables; using osu.Framework.Graphics; -using osu.Framework.Threading; using osu.Game.Beatmaps; using osu.Game.Database; +using osu.Game.Models; using osu.Game.Online.API; using osu.Game.Online.Leaderboards; using osu.Game.Rulesets; @@ -30,72 +30,45 @@ namespace osu.Game.Screens.Select.Carousel [Resolved] private IAPIProvider api { get; set; } + private IDisposable scoreSubscription; + + private bool rankUpdatePending; + public TopLocalRank(BeatmapInfo beatmapInfo) : base(null) { this.beatmapInfo = beatmapInfo; } - [BackgroundDependencyLoader] - private void load() - { - ruleset.ValueChanged += _ => fetchAndLoadTopScore(); - - fetchAndLoadTopScore(); - } - protected override void LoadComplete() { base.LoadComplete(); - scoreSubscription = realmFactory.Context.All() - .Filter($"{nameof(ScoreInfo.BeatmapInfo)}.{nameof(BeatmapInfo.ID)} = $0", beatmapInfo.ID) - .QueryAsyncWithNotifications((_, changes, ___) => - { - if (changes == null) - return; - - fetchTopScoreRank(); - }); - } - - private IDisposable scoreSubscription; - - private ScheduledDelegate scheduledRankUpdate; - - private void fetchAndLoadTopScore() - { - // TODO: this lookup likely isn't required, we can use the results of the subscription directly. - var rank = fetchTopScoreRank(); - - scheduledRankUpdate = Scheduler.Add(() => + ruleset.BindValueChanged(_ => { - Rank = rank; - + rankUpdatePending = true; // Required since presence is changed via IsPresent override Invalidate(Invalidation.Presence); - }); + + scoreSubscription?.Dispose(); + scoreSubscription = realmFactory.Context.All() + .Filter($"{nameof(ScoreInfo.User)}.{nameof(RealmUser.OnlineID)} == $0" + + $"&& {nameof(ScoreInfo.BeatmapInfo)}.{nameof(BeatmapInfo.ID)} == $1" + + $"&& {nameof(ScoreInfo.Ruleset)}.{nameof(RulesetInfo.ShortName)} == $2" + + $"&& {nameof(ScoreInfo.DeletePending)} == false", api.LocalUser.Value.Id, beatmapInfo.ID, ruleset.Value.ShortName) + .OrderByDescending(s => s.TotalScore) + .QueryAsyncWithNotifications((items, changes, ___) => + { + if (changes == null) + rankUpdatePending = false; + + Rank = items.FirstOrDefault()?.Rank; + }); + }, true); } // We're present if a rank is set, or if there is a pending rank update (IsPresent = true is required for the scheduler to run). - public override bool IsPresent => base.IsPresent && (Rank != null || scheduledRankUpdate?.Completed == false); - - private ScoreRank? fetchTopScoreRank() - { - if (realmFactory == null || beatmapInfo == null || ruleset?.Value == null || api?.LocalUser.Value == null) - return null; - - using (var realm = realmFactory.CreateContext()) - { - return realm.All() - .AsEnumerable() - // TODO: update to use a realm filter directly (or at least figure out the beatmap part to reduce scope). - .Where(s => s.UserID == api.LocalUser.Value.Id && s.BeatmapInfoID == beatmapInfo.ID && s.RulesetID == ruleset.Value.ID && !s.DeletePending) - .OrderByDescending(s => s.TotalScore) - .FirstOrDefault() - ?.Rank; - } - } + public override bool IsPresent => base.IsPresent && (Rank != null || rankUpdatePending); protected override void Dispose(bool isDisposing) {