From 6d864cb47e2e5efb0b283d078526b833362d252e Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Mon, 24 Jun 2019 12:42:21 +0900 Subject: [PATCH 1/5] Load beatmap content asynchronously in the background --- osu.Game/Beatmaps/WorkingBeatmap.cs | 14 +++++++++----- 1 file changed, 9 insertions(+), 5 deletions(-) diff --git a/osu.Game/Beatmaps/WorkingBeatmap.cs b/osu.Game/Beatmaps/WorkingBeatmap.cs index 328763fc9f..3ef7b4feca 100644 --- a/osu.Game/Beatmaps/WorkingBeatmap.cs +++ b/osu.Game/Beatmaps/WorkingBeatmap.cs @@ -11,6 +11,7 @@ using osu.Framework.IO.File; using System.IO; using System.Linq; using System.Threading; +using System.Threading.Tasks; using osu.Framework.Audio; using osu.Game.IO.Serialization; using osu.Game.Rulesets; @@ -38,7 +39,7 @@ namespace osu.Game.Beatmaps BeatmapSetInfo = beatmapInfo.BeatmapSet; Metadata = beatmapInfo.Metadata ?? BeatmapSetInfo?.Metadata ?? new BeatmapMetadata(); - beatmap = new RecyclableLazy(() => + beatmapLoadTask = Task.Factory.StartNew(() => { var b = GetBeatmap() ?? new Beatmap(); @@ -49,7 +50,7 @@ namespace osu.Game.Beatmaps b.BeatmapInfo = BeatmapInfo; return b; - }); + }, beatmapCancellation.Token, TaskCreationOptions.LongRunning, TaskScheduler.Default); track = new RecyclableLazy(() => GetTrack() ?? GetVirtualTrack()); background = new RecyclableLazy(GetBackground, BackgroundStillValid); @@ -153,10 +154,11 @@ namespace osu.Game.Beatmaps public override string ToString() => BeatmapInfo.ToString(); - public bool BeatmapLoaded => beatmap.IsResultAvailable; - public IBeatmap Beatmap => beatmap.Value; + public bool BeatmapLoaded => beatmapLoadTask.IsCompleted; + public IBeatmap Beatmap => beatmapLoadTask.Result; + private readonly CancellationTokenSource beatmapCancellation = new CancellationTokenSource(); protected abstract IBeatmap GetBeatmap(); - private readonly RecyclableLazy beatmap; + private readonly Task beatmapLoadTask; public bool BackgroundLoaded => background.IsResultAvailable; public Texture Background => background.Value; @@ -201,6 +203,8 @@ namespace osu.Game.Beatmaps waveform.Recycle(); storyboard.Recycle(); skin.Recycle(); + + beatmapCancellation.Cancel(); } /// From 18303623371ab1544d6e26d8458813fa05afff3e Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Mon, 24 Jun 2019 17:10:50 +0900 Subject: [PATCH 2/5] Move task out of ctor to avoid initialisation ordering issues --- osu.Game/Beatmaps/WorkingBeatmap.cs | 34 +++++++++++++++-------------- osu.Game/OsuGame.cs | 2 ++ 2 files changed, 20 insertions(+), 16 deletions(-) diff --git a/osu.Game/Beatmaps/WorkingBeatmap.cs b/osu.Game/Beatmaps/WorkingBeatmap.cs index 3ef7b4feca..a1864526d1 100644 --- a/osu.Game/Beatmaps/WorkingBeatmap.cs +++ b/osu.Game/Beatmaps/WorkingBeatmap.cs @@ -39,19 +39,6 @@ namespace osu.Game.Beatmaps BeatmapSetInfo = beatmapInfo.BeatmapSet; Metadata = beatmapInfo.Metadata ?? BeatmapSetInfo?.Metadata ?? new BeatmapMetadata(); - beatmapLoadTask = Task.Factory.StartNew(() => - { - var b = GetBeatmap() ?? new Beatmap(); - - // The original beatmap version needs to be preserved as the database doesn't contain it - BeatmapInfo.BeatmapVersion = b.BeatmapInfo.BeatmapVersion; - - // Use the database-backed info for more up-to-date values (beatmap id, ranked status, etc) - b.BeatmapInfo = BeatmapInfo; - - return b; - }, beatmapCancellation.Token, TaskCreationOptions.LongRunning, TaskScheduler.Default); - track = new RecyclableLazy(() => GetTrack() ?? GetVirtualTrack()); background = new RecyclableLazy(GetBackground, BackgroundStillValid); waveform = new RecyclableLazy(GetWaveform); @@ -154,11 +141,26 @@ namespace osu.Game.Beatmaps public override string ToString() => BeatmapInfo.ToString(); - public bool BeatmapLoaded => beatmapLoadTask.IsCompleted; - public IBeatmap Beatmap => beatmapLoadTask.Result; + public bool BeatmapLoaded => beatmapLoadTask?.IsCompleted ?? false; + + public Task LoadBeatmapAsync() => (beatmapLoadTask ?? (beatmapLoadTask = Task.Factory.StartNew(() => + { + var b = GetBeatmap() ?? new Beatmap(); + + // The original beatmap version needs to be preserved as the database doesn't contain it + BeatmapInfo.BeatmapVersion = b.BeatmapInfo.BeatmapVersion; + + // Use the database-backed info for more up-to-date values (beatmap id, ranked status, etc) + b.BeatmapInfo = BeatmapInfo; + + return b; + }, beatmapCancellation.Token, TaskCreationOptions.LongRunning, TaskScheduler.Default))); + + public IBeatmap Beatmap => LoadBeatmapAsync().Result; + private readonly CancellationTokenSource beatmapCancellation = new CancellationTokenSource(); protected abstract IBeatmap GetBeatmap(); - private readonly Task beatmapLoadTask; + private Task beatmapLoadTask; public bool BackgroundLoaded => background.IsResultAvailable; public Texture Background => background.Value; diff --git a/osu.Game/OsuGame.cs b/osu.Game/OsuGame.cs index 35684849a3..b018c2b64a 100644 --- a/osu.Game/OsuGame.cs +++ b/osu.Game/OsuGame.cs @@ -293,6 +293,8 @@ namespace osu.Game var nextBeatmap = beatmap.NewValue; if (nextBeatmap?.Track != null) nextBeatmap.Track.Completed += currentTrackCompleted; + + nextBeatmap?.LoadBeatmapAsync(); } private void currentTrackCompleted() From 8b0aaccfe618dd9fd92e43718458e7315594ee40 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Thu, 27 Jun 2019 13:56:36 +0900 Subject: [PATCH 3/5] Add finaliser to WorkingBeatmap --- osu.Game.Tests/WaveformTestBeatmap.cs | 2 +- osu.Game/Beatmaps/WorkingBeatmap.cs | 36 +++++++++++++++++++-------- osu.Game/Tests/Visual/OsuTestScene.cs | 2 +- 3 files changed, 28 insertions(+), 12 deletions(-) diff --git a/osu.Game.Tests/WaveformTestBeatmap.cs b/osu.Game.Tests/WaveformTestBeatmap.cs index fdb91b7c5b..c5a2f5be51 100644 --- a/osu.Game.Tests/WaveformTestBeatmap.cs +++ b/osu.Game.Tests/WaveformTestBeatmap.cs @@ -30,7 +30,7 @@ namespace osu.Game.Tests trackStore = audioManager.GetTrackStore(reader); } - public override void Dispose() + protected override void Dispose(bool isDisposing) { base.Dispose(); stream?.Dispose(); diff --git a/osu.Game/Beatmaps/WorkingBeatmap.cs b/osu.Game/Beatmaps/WorkingBeatmap.cs index a1864526d1..61390fe51b 100644 --- a/osu.Game/Beatmaps/WorkingBeatmap.cs +++ b/osu.Game/Beatmaps/WorkingBeatmap.cs @@ -46,6 +46,11 @@ namespace osu.Game.Beatmaps skin = new RecyclableLazy(GetSkin); } + ~WorkingBeatmap() + { + Dispose(false); + } + protected virtual Track GetVirtualTrack() { const double excess_length = 1000; @@ -199,22 +204,33 @@ namespace osu.Game.Beatmaps other.track = track; } - public virtual void Dispose() - { - background.Recycle(); - waveform.Recycle(); - storyboard.Recycle(); - skin.Recycle(); - - beatmapCancellation.Cancel(); - } - /// /// Eagerly dispose of the audio track associated with this (if any). /// Accessing track again will load a fresh instance. /// public virtual void RecycleTrack() => track.Recycle(); + #region Disposal + + public void Dispose() + { + Dispose(true); + GC.SuppressFinalize(this); + } + + protected virtual void Dispose(bool isDisposing) + { + // recycling logic is not here for the time being, as components which use + // retrieved objects from WorkingBeatmap may not hold a reference to the WorkingBeatmap itself. + // this should be fine as each retrieved comopnent do have their own finalizers. + + // cancelling the beatmap load is safe for now since the retrieval is a synchronous + // operation. if we add an async retrieval method this may need to be reconsidered. + beatmapCancellation.Cancel(); + } + + #endregion + public class RecyclableLazy { private Lazy lazy; diff --git a/osu.Game/Tests/Visual/OsuTestScene.cs b/osu.Game/Tests/Visual/OsuTestScene.cs index c8798448ae..9f4532513f 100644 --- a/osu.Game/Tests/Visual/OsuTestScene.cs +++ b/osu.Game/Tests/Visual/OsuTestScene.cs @@ -137,7 +137,7 @@ namespace osu.Game.Tests.Visual track = audio?.Tracks.GetVirtual(length); } - public override void Dispose() + protected override void Dispose(bool isDisposing) { base.Dispose(); store?.Dispose(); From 1072431fbbc17373f367eb06c8b6de0156f16cb7 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Thu, 27 Jun 2019 14:08:58 +0900 Subject: [PATCH 4/5] Fix test StackOverflows --- osu.Game.Tests/WaveformTestBeatmap.cs | 2 +- osu.Game/Tests/Visual/OsuTestScene.cs | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/osu.Game.Tests/WaveformTestBeatmap.cs b/osu.Game.Tests/WaveformTestBeatmap.cs index c5a2f5be51..3e0df8d45e 100644 --- a/osu.Game.Tests/WaveformTestBeatmap.cs +++ b/osu.Game.Tests/WaveformTestBeatmap.cs @@ -32,7 +32,7 @@ namespace osu.Game.Tests protected override void Dispose(bool isDisposing) { - base.Dispose(); + base.Dispose(isDisposing); stream?.Dispose(); reader?.Dispose(); trackStore?.Dispose(); diff --git a/osu.Game/Tests/Visual/OsuTestScene.cs b/osu.Game/Tests/Visual/OsuTestScene.cs index 9f4532513f..9b3c15aa91 100644 --- a/osu.Game/Tests/Visual/OsuTestScene.cs +++ b/osu.Game/Tests/Visual/OsuTestScene.cs @@ -139,7 +139,7 @@ namespace osu.Game.Tests.Visual protected override void Dispose(bool isDisposing) { - base.Dispose(); + base.Dispose(isDisposing); store?.Dispose(); } From ef384b86676510f045b70da9068e222136ed9eb2 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Wed, 26 Jun 2019 14:08:19 +0900 Subject: [PATCH 5/5] Add simple (weak) WorkingBeatmap cache --- osu.Game/Beatmaps/BeatmapManager.cs | 9 +++++++++ 1 file changed, 9 insertions(+) diff --git a/osu.Game/Beatmaps/BeatmapManager.cs b/osu.Game/Beatmaps/BeatmapManager.cs index d5b19485de..6c7929d193 100644 --- a/osu.Game/Beatmaps/BeatmapManager.cs +++ b/osu.Game/Beatmaps/BeatmapManager.cs @@ -13,6 +13,7 @@ using osu.Framework.Audio; using osu.Framework.Audio.Track; using osu.Framework.Extensions; using osu.Framework.Graphics.Textures; +using osu.Framework.Lists; using osu.Framework.Logging; using osu.Framework.Platform; using osu.Framework.Threading; @@ -157,6 +158,8 @@ namespace osu.Game.Beatmaps /// The beatmap difficulty to restore. public void Restore(BeatmapInfo beatmap) => beatmaps.Restore(beatmap); + private readonly WeakList workingCache = new WeakList(); + /// /// Retrieve a instance for the provided /// @@ -171,12 +174,18 @@ namespace osu.Game.Beatmaps if (beatmapInfo?.BeatmapSet == null || beatmapInfo == DefaultBeatmap?.BeatmapInfo) return DefaultBeatmap; + var cached = workingCache.FirstOrDefault(w => w.BeatmapInfo?.ID == beatmapInfo.ID); + + if (cached != null) + return cached; + if (beatmapInfo.Metadata == null) beatmapInfo.Metadata = beatmapInfo.BeatmapSet.Metadata; WorkingBeatmap working = new BeatmapManagerWorkingBeatmap(Files.Store, new LargeTextureStore(host?.CreateTextureLoaderStore(Files.Store)), beatmapInfo, audioManager); previous?.TransferTo(working); + workingCache.Add(working); return working; }