diff --git a/osu.Game/Database/RealmAccess.cs b/osu.Game/Database/RealmAccess.cs
index 17f4068fc4..fa54ed538a 100644
--- a/osu.Game/Database/RealmAccess.cs
+++ b/osu.Game/Database/RealmAccess.cs
@@ -109,6 +109,8 @@ namespace osu.Game.Database
///
private readonly SemaphoreSlim realmRetrievalLock = new SemaphoreSlim(1);
+ private readonly CountdownEvent pendingAsyncOperations = new CountdownEvent(0);
+
///
/// true when the current thread has already entered the .
///
@@ -467,6 +469,30 @@ namespace osu.Game.Database
}
}
+ ///
+ /// Run work on realm on a TPL thread, in a way that ensures that the realm isn't disposed before the work is done.
+ ///
+ public Task RunAsync(Func action, CancellationToken token = default)
+ {
+ ObjectDisposedException.ThrowIf(isDisposed, this);
+
+ // Required to ensure the read is tracked and accounted for before disposal.
+ // Can potentially be avoided if we have a need to do so in the future.
+ if (!ThreadSafety.IsUpdateThread)
+ throw new InvalidOperationException($@"{nameof(RunAsync)} must be called from the update thread.");
+
+ // CountdownEvent will fail if already at zero.
+ if (!pendingAsyncOperations.TryAddCount())
+ pendingAsyncOperations.Reset(1);
+
+ return Task.Run(() =>
+ {
+ var result = Run(action);
+ pendingAsyncOperations.Signal();
+ return result;
+ }, token);
+ }
+
///
/// Write changes to realm.
///
@@ -507,8 +533,6 @@ namespace osu.Game.Database
}
}
- private readonly CountdownEvent pendingAsyncWrites = new CountdownEvent(0);
-
///
/// Write changes to realm asynchronously, guaranteeing order of execution.
///
@@ -523,8 +547,8 @@ namespace osu.Game.Database
throw new InvalidOperationException(@$"{nameof(WriteAsync)} must be called from the update thread.");
// CountdownEvent will fail if already at zero.
- if (!pendingAsyncWrites.TryAddCount())
- pendingAsyncWrites.Reset(1);
+ if (!pendingAsyncOperations.TryAddCount())
+ pendingAsyncOperations.Reset(1);
// Regardless of calling Realm.GetInstance or Realm.GetInstanceAsync, there is a blocking overhead on retrieval.
// Adding a forced Task.Run resolves this.
@@ -539,7 +563,7 @@ namespace osu.Game.Database
// ReSharper disable once AccessToDisposedClosure (WriteAsync should be marked as [InstantHandle]).
await realm.WriteAsync(() => action(realm)).ConfigureAwait(false);
- pendingAsyncWrites.Signal();
+ pendingAsyncOperations.Signal();
});
return writeTask;
@@ -559,8 +583,8 @@ namespace osu.Game.Database
throw new InvalidOperationException(@$"{nameof(WriteAsync)} must be called from the update thread.");
// CountdownEvent will fail if already at zero.
- if (!pendingAsyncWrites.TryAddCount())
- pendingAsyncWrites.Reset(1);
+ if (!pendingAsyncOperations.TryAddCount())
+ pendingAsyncOperations.Reset(1);
// Regardless of calling Realm.GetInstance or Realm.GetInstanceAsync, there is a blocking overhead on retrieval.
// Adding a forced Task.Run resolves this.
@@ -576,7 +600,7 @@ namespace osu.Game.Database
// ReSharper disable once AccessToDisposedClosure (WriteAsync should be marked as [InstantHandle]).
result = await realm.WriteAsync(() => action(realm)).ConfigureAwait(false);
- pendingAsyncWrites.Signal();
+ pendingAsyncOperations.Signal();
return result;
});
@@ -1494,7 +1518,7 @@ namespace osu.Game.Database
public void Dispose()
{
- if (!pendingAsyncWrites.Wait(10000))
+ if (!pendingAsyncOperations.Wait(10000))
Logger.Log("Realm took too long waiting on pending async writes", level: LogLevel.Error);
updateRealm?.Dispose();
diff --git a/osu.Game/Screens/SelectV2/BeatmapMetadataWedge.cs b/osu.Game/Screens/SelectV2/BeatmapMetadataWedge.cs
index 818176b3c4..d516f4b846 100644
--- a/osu.Game/Screens/SelectV2/BeatmapMetadataWedge.cs
+++ b/osu.Game/Screens/SelectV2/BeatmapMetadataWedge.cs
@@ -3,10 +3,12 @@
using System;
using System.Linq;
+using System.Threading;
using osu.Framework.Allocation;
using osu.Framework.Audio;
using osu.Framework.Audio.Sample;
using osu.Framework.Bindables;
+using osu.Framework.Extensions;
using osu.Framework.Graphics;
using osu.Framework.Graphics.Containers;
using osu.Framework.Utils;
@@ -402,24 +404,46 @@ namespace osu.Game.Screens.SelectV2
updateSubWedgeVisibility();
}
+ private CancellationTokenSource? userTagsCancellationSource;
+
private void updateUserTags()
{
- string[] tags = realm.Run(r =>
+ userTagsCancellationSource?.Cancel();
+ userTagsCancellationSource = new CancellationTokenSource();
+
+ var token = userTagsCancellationSource.Token;
+
+ realm.RunAsync(r =>
{
// need to refetch because `beatmap.Value.BeatmapInfo` is not going to have the latest tags
- r.Refresh();
var refetchedBeatmap = r.Find(beatmap.Value.BeatmapInfo.ID);
return refetchedBeatmap?.Metadata.UserTags.ToArray() ?? [];
- });
-
- if (tags.Length == 0)
+ }, token).ContinueWith(t =>
{
- userTags.FadeOut(transition_duration, Easing.OutQuint);
- return;
- }
+ string[] tags = t.GetResultSafely();
- userTags.FadeIn(transition_duration, Easing.OutQuint);
- userTags.Tags = (tags, t => songSelect?.Search($@"tag=""{t}""!"));
+ Schedule(() =>
+ {
+ if (token.IsCancellationRequested)
+ return;
+
+ if (tags.Length == 0)
+ {
+ userTags.FadeOut(transition_duration, Easing.OutQuint);
+ return;
+ }
+
+ userTags.FadeIn(transition_duration, Easing.OutQuint);
+ userTags.Tags = (tags, tag => songSelect?.Search($@"tag=""{tag}""!"));
+ });
+ }, token);
+ }
+
+ protected override void Dispose(bool isDisposing)
+ {
+ userTagsCancellationSource?.Cancel();
+ userTagsCancellationSource = null;
+ base.Dispose(isDisposing);
}
}
}
diff --git a/osu.Game/Screens/SelectV2/BeatmapTitleWedge.cs b/osu.Game/Screens/SelectV2/BeatmapTitleWedge.cs
index 21ac04b18a..530b1348dd 100644
--- a/osu.Game/Screens/SelectV2/BeatmapTitleWedge.cs
+++ b/osu.Game/Screens/SelectV2/BeatmapTitleWedge.cs
@@ -8,6 +8,7 @@ using System.Threading;
using System.Threading.Tasks;
using osu.Framework.Allocation;
using osu.Framework.Bindables;
+using osu.Framework.Extensions;
using osu.Framework.Graphics;
using osu.Framework.Graphics.Containers;
using osu.Framework.Localisation;
@@ -278,8 +279,13 @@ namespace osu.Game.Screens.SelectV2
}, token);
}
+ private CancellationTokenSource? onlineDisplayCancellationSource;
+
private void updateOnlineDisplay()
{
+ onlineDisplayCancellationSource?.Cancel();
+ onlineDisplayCancellationSource = null;
+
if (onlineLookupResult.Value?.Status != SongSelect.BeatmapSetLookupStatus.Completed)
{
playCount.Value = null;
@@ -291,21 +297,42 @@ namespace osu.Game.Screens.SelectV2
playCount.Value = new StatisticPlayCount.Data(onlineBeatmap?.PlayCount ?? -1, onlineBeatmap?.UserPlayCount ?? -1);
favouriteButton.SetBeatmapSet(onlineLookupResult.Value.Result);
+ onlineDisplayCancellationSource = new CancellationTokenSource();
+ var token = onlineDisplayCancellationSource.Token;
+
// the online fetch may have also updated the beatmap's status.
// this needs to be checked against the *local* beatmap model rather than the online one, because it's not known here whether the status change has occurred or not
// (think scenarios like the beatmap being locally modified).
// it also has to be handled explicitly like this because the working beatmap's `BeatmapInfo` will not receive these updates due to being detached
// (and because of https://github.com/ppy/osu/blob/4b73afd1957a9161e2956fc4191c8114d9958372/osu.Game/Screens/SelectV2/SongSelect.cs#L487-L488
// which prevents working beatmap refetches caused by changes to the realm model of perceived low importance).
- var status = realm.Run(r =>
+ realm.RunAsync(r =>
{
- r.Refresh();
var refetchedBeatmap = r.Find(working.Value.BeatmapInfo.ID);
return refetchedBeatmap?.Status;
- });
- if (status != null)
- statusPill.Status = status.Value;
+ }, token).ContinueWith(t =>
+ {
+ var status = t.GetResultSafely();
+
+ if (status != null)
+ {
+ Schedule(() =>
+ {
+ if (token.IsCancellationRequested)
+ return;
+
+ statusPill.Status = status.Value;
+ });
+ }
+ }, token);
}
}
+
+ protected override void Dispose(bool isDisposing)
+ {
+ onlineDisplayCancellationSource?.Dispose();
+ onlineDisplayCancellationSource = null;
+ base.Dispose(isDisposing);
+ }
}
}