From 079b2dfc42ac22168dbc4f436653a648b51f9ad8 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Fri, 21 Jan 2022 00:01:34 +0900 Subject: [PATCH 1/5] Create backup of databases before opening contexts Attempt to avoid file IO issues. Closes #16531. --- osu.Game/Database/EFToRealmMigrator.cs | 30 ++++++++------------------ 1 file changed, 9 insertions(+), 21 deletions(-) diff --git a/osu.Game/Database/EFToRealmMigrator.cs b/osu.Game/Database/EFToRealmMigrator.cs index 0f726f8ee5..727815cc4d 100644 --- a/osu.Game/Database/EFToRealmMigrator.cs +++ b/osu.Game/Database/EFToRealmMigrator.cs @@ -26,8 +26,6 @@ namespace osu.Game.Database private readonly OsuConfigManager config; private readonly Storage storage; - private bool hasTakenBackup; - public EFToRealmMigrator(DatabaseContextFactory efContextFactory, RealmContextFactory realmContextFactory, OsuConfigManager config, Storage storage) { this.efContextFactory = efContextFactory; @@ -38,6 +36,8 @@ namespace osu.Game.Database public void Run() { + createBackup(); + using (var ef = efContextFactory.Get()) { migrateSettings(ef); @@ -77,8 +77,6 @@ namespace osu.Game.Database { Logger.Log($"Found {count} beatmaps in EF", LoggingTarget.Database); - ensureBackup(); - // only migrate data if the realm database is empty. // note that this cannot be written as: `realm.All().All(s => s.Protected)`, because realm does not support `.All()`. if (realm.All().Any(s => !s.Protected)) @@ -210,8 +208,6 @@ namespace osu.Game.Database { Logger.Log($"Found {count} scores in EF", LoggingTarget.Database); - ensureBackup(); - // only migrate data if the realm database is empty. if (realm.All().Any()) { @@ -291,8 +287,6 @@ namespace osu.Game.Database if (!existingSkins.Any()) return; - ensureBackup(); - var userSkinChoice = config.GetBindable(OsuSetting.Skin); int.TryParse(userSkinChoice.Value, out int userSkinInt); @@ -363,7 +357,6 @@ namespace osu.Game.Database return; Logger.Log("Beginning settings migration to realm", LoggingTarget.Database); - ensureBackup(); using (var realm = realmContextFactory.CreateContext()) using (var transaction = realm.BeginWrite()) @@ -400,21 +393,16 @@ namespace osu.Game.Database private string? getRulesetShortNameFromLegacyID(long rulesetId) => efContextFactory.Get().RulesetInfo.FirstOrDefault(r => r.ID == rulesetId)?.ShortName; - private void ensureBackup() + private void createBackup() { - if (!hasTakenBackup) - { - string migration = $"before_final_migration_{DateTimeOffset.UtcNow.ToUnixTimeSeconds()}"; + string migration = $"before_final_migration_{DateTimeOffset.UtcNow.ToUnixTimeSeconds()}"; - efContextFactory.CreateBackup($"client.{migration}.db"); - realmContextFactory.CreateBackup($"client.{migration}.realm"); + efContextFactory.CreateBackup($"client.{migration}.db"); + realmContextFactory.CreateBackup($"client.{migration}.realm"); - using (var source = storage.GetStream("collection.db")) - using (var destination = storage.GetStream($"collection.{migration}.db", FileAccess.Write, FileMode.CreateNew)) - source.CopyTo(destination); - - hasTakenBackup = true; - } + using (var source = storage.GetStream("collection.db")) + using (var destination = storage.GetStream($"collection.{migration}.db", FileAccess.Write, FileMode.CreateNew)) + source.CopyTo(destination); } } } From 7aad2780b1fae24d9fd6213f0e44c519fc87cc89 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Fri, 21 Jan 2022 00:46:47 +0900 Subject: [PATCH 2/5] Add retry logic for realm backup creation --- osu.Game/Database/RealmContextFactory.cs | 21 ++++++++++++++++++--- 1 file changed, 18 insertions(+), 3 deletions(-) diff --git a/osu.Game/Database/RealmContextFactory.cs b/osu.Game/Database/RealmContextFactory.cs index 31dbb0c6c4..ffadf8258d 100644 --- a/osu.Game/Database/RealmContextFactory.cs +++ b/osu.Game/Database/RealmContextFactory.cs @@ -367,9 +367,24 @@ namespace osu.Game.Database using (BlockAllOperations()) { Logger.Log($"Creating full realm database backup at {backupFilename}", LoggingTarget.Database); - using (var source = storage.GetStream(Filename)) - using (var destination = storage.GetStream(backupFilename, FileAccess.Write, FileMode.CreateNew)) - source.CopyTo(destination); + + int attempts = 10; + + while (attempts-- > 0) + { + try + { + using (var source = storage.GetStream(Filename)) + using (var destination = storage.GetStream(backupFilename, FileAccess.Write, FileMode.CreateNew)) + source.CopyTo(destination); + return; + } + catch (IOException) + { + // file may be locked during use. + Thread.Sleep(500); + } + } } } From 45bf35c42532d4051166df6a8df955d41a8b7e49 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Fri, 21 Jan 2022 12:26:24 +0900 Subject: [PATCH 3/5] Avoid performing keyword filtering at song select unless keywords are specified --- osu.Game/Screens/Select/Carousel/CarouselBeatmap.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/osu.Game/Screens/Select/Carousel/CarouselBeatmap.cs b/osu.Game/Screens/Select/Carousel/CarouselBeatmap.cs index d54a3bb54e..6b198ab505 100644 --- a/osu.Game/Screens/Select/Carousel/CarouselBeatmap.cs +++ b/osu.Game/Screens/Select/Carousel/CarouselBeatmap.cs @@ -55,7 +55,7 @@ namespace osu.Game.Screens.Select.Carousel match &= !criteria.UserStarDifficulty.HasFilter || criteria.UserStarDifficulty.IsInRange(BeatmapInfo.StarRating); - if (match) + if (match && criteria.SearchTerms.Length > 0) { string[] terms = BeatmapInfo.GetSearchableTerms(); From 5b24800b0e9c88af8747f9e33a3c14b97ca1f4d8 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Fri, 21 Jan 2022 12:37:17 +0900 Subject: [PATCH 4/5] Avoid applying filter in `UpdateBeatmapSet` flow --- osu.Game/Screens/Select/BeatmapCarousel.cs | 12 +++++---- .../Screens/Select/Carousel/CarouselGroup.cs | 27 +++++++++++++++++-- 2 files changed, 32 insertions(+), 7 deletions(-) diff --git a/osu.Game/Screens/Select/BeatmapCarousel.cs b/osu.Game/Screens/Select/BeatmapCarousel.cs index 75ad0511e6..6d3d7aa185 100644 --- a/osu.Game/Screens/Select/BeatmapCarousel.cs +++ b/osu.Game/Screens/Select/BeatmapCarousel.cs @@ -300,16 +300,18 @@ namespace osu.Game.Screens.Select root.AddChild(newSet); - // only reset scroll position if already near the scroll target. - // without this, during a large beatmap import it is impossible to navigate the carousel. - applyActiveCriteria(false, alwaysResetScrollPosition: false); - // check if we can/need to maintain our current selection. if (previouslySelectedID != null) select((CarouselItem)newSet.Beatmaps.FirstOrDefault(b => b.BeatmapInfo.ID == previouslySelectedID) ?? newSet); itemsCache.Invalidate(); - Schedule(() => BeatmapSetsChanged?.Invoke()); + Schedule(() => + { + if (!Scroll.UserScrolling) + ScrollToSelected(true); + + BeatmapSetsChanged?.Invoke(); + }); }); /// diff --git a/osu.Game/Screens/Select/Carousel/CarouselGroup.cs b/osu.Game/Screens/Select/Carousel/CarouselGroup.cs index b85e868b89..7e4c5fad72 100644 --- a/osu.Game/Screens/Select/Carousel/CarouselGroup.cs +++ b/osu.Game/Screens/Select/Carousel/CarouselGroup.cs @@ -3,6 +3,7 @@ using System.Collections.Generic; using System.Linq; +using JetBrains.Annotations; namespace osu.Game.Screens.Select.Carousel { @@ -36,7 +37,21 @@ namespace osu.Game.Screens.Select.Carousel { i.State.ValueChanged += state => ChildItemStateChanged(i, state.NewValue); i.ChildID = ++currentChildID; - InternalChildren.Add(i); + + if (lastCriteria != null) + { + i.Filter(lastCriteria); + + int index = InternalChildren.BinarySearch(i, criteriaComparer); + if (index < 0) index = ~index; // BinarySearch hacks multiple return values with 2's complement. + + InternalChildren.Insert(index, i); + } + else + { + // criteria may be null for initial population. the filtering will be applied post-add. + InternalChildren.Add(i); + } } public CarouselGroup(List items = null) @@ -62,14 +77,22 @@ namespace osu.Game.Screens.Select.Carousel }; } + private Comparer criteriaComparer; + + [CanBeNull] + private FilterCriteria lastCriteria; + public override void Filter(FilterCriteria criteria) { base.Filter(criteria); InternalChildren.ForEach(c => c.Filter(criteria)); + // IEnumerable.OrderBy() is used instead of List.Sort() to ensure sorting stability - var criteriaComparer = Comparer.Create((x, y) => x.CompareTo(criteria, y)); + criteriaComparer = Comparer.Create((x, y) => x.CompareTo(criteria, y)); InternalChildren = InternalChildren.OrderBy(c => c, criteriaComparer).ToList(); + + lastCriteria = criteria; } protected virtual void ChildItemStateChanged(CarouselItem item, CarouselItemState value) From bed7b69464ed1b6971ae84ee81885d6ce90c5fed Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Fri, 21 Jan 2022 13:09:03 +0900 Subject: [PATCH 5/5] Apply NRT to `CarouselGroup` --- .../Screens/Select/Carousel/CarouselGroup.cs | 16 ++++++++-------- 1 file changed, 8 insertions(+), 8 deletions(-) diff --git a/osu.Game/Screens/Select/Carousel/CarouselGroup.cs b/osu.Game/Screens/Select/Carousel/CarouselGroup.cs index 7e4c5fad72..6ebe314072 100644 --- a/osu.Game/Screens/Select/Carousel/CarouselGroup.cs +++ b/osu.Game/Screens/Select/Carousel/CarouselGroup.cs @@ -3,7 +3,8 @@ using System.Collections.Generic; using System.Linq; -using JetBrains.Annotations; + +#nullable enable namespace osu.Game.Screens.Select.Carousel { @@ -12,7 +13,7 @@ namespace osu.Game.Screens.Select.Carousel /// public class CarouselGroup : CarouselItem { - public override DrawableCarouselItem CreateDrawableRepresentation() => null; + public override DrawableCarouselItem? CreateDrawableRepresentation() => null; public IReadOnlyList Children => InternalChildren; @@ -24,6 +25,10 @@ namespace osu.Game.Screens.Select.Carousel /// private ulong currentChildID; + private Comparer? criteriaComparer; + + private FilterCriteria? lastCriteria; + public virtual void RemoveChild(CarouselItem i) { InternalChildren.Remove(i); @@ -54,7 +59,7 @@ namespace osu.Game.Screens.Select.Carousel } } - public CarouselGroup(List items = null) + public CarouselGroup(List? items = null) { if (items != null) InternalChildren = items; @@ -77,11 +82,6 @@ namespace osu.Game.Screens.Select.Carousel }; } - private Comparer criteriaComparer; - - [CanBeNull] - private FilterCriteria lastCriteria; - public override void Filter(FilterCriteria criteria) { base.Filter(criteria);