mirror of
https://github.com/ppy/osu.git
synced 2026-06-03 16:46:05 +08:00
Merge pull request #35262 from bdach/song-select-move-realm-fetches-off-thread
Move realm refetches of beatmap in song select wedges off of update thread
This commit is contained in:
@@ -109,6 +109,8 @@ namespace osu.Game.Database
|
||||
/// </summary>
|
||||
private readonly SemaphoreSlim realmRetrievalLock = new SemaphoreSlim(1);
|
||||
|
||||
private readonly CountdownEvent pendingAsyncOperations = new CountdownEvent(0);
|
||||
|
||||
/// <summary>
|
||||
/// <c>true</c> when the current thread has already entered the <see cref="realmRetrievalLock"/>.
|
||||
/// </summary>
|
||||
@@ -467,6 +469,30 @@ namespace osu.Game.Database
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Run work on realm on a TPL thread, in a way that ensures that the realm isn't disposed before the work is done.
|
||||
/// </summary>
|
||||
public Task<T> RunAsync<T>(Func<Realm, T> 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);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Write changes to realm.
|
||||
/// </summary>
|
||||
@@ -507,8 +533,6 @@ namespace osu.Game.Database
|
||||
}
|
||||
}
|
||||
|
||||
private readonly CountdownEvent pendingAsyncWrites = new CountdownEvent(0);
|
||||
|
||||
/// <summary>
|
||||
/// Write changes to realm asynchronously, guaranteeing order of execution.
|
||||
/// </summary>
|
||||
@@ -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();
|
||||
|
||||
@@ -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<BeatmapInfo>(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);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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<BeatmapInfo>(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);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user