From 14273824dcce96bf5c0e59a344a10305fc2bf253 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Tue, 4 Feb 2025 19:37:00 +0900 Subject: [PATCH 1/4] Fix `Carousel.FilterAsync` not working when called from a non-update thread I was trying to be smart about things and make use of our `SynchronisationContext` setup, but it turns out to not work in all cases due to the context being missing depending on where you are calling the method from. For now let's prefer the "works everywhere" method of scheduling the final work back to update. --- .../SongSelect/BeatmapCarouselV2TestScene.cs | 2 +- osu.Game/Screens/SelectV2/Carousel.cs | 19 ++++++++++--------- 2 files changed, 11 insertions(+), 10 deletions(-) diff --git a/osu.Game.Tests/Visual/SongSelect/BeatmapCarouselV2TestScene.cs b/osu.Game.Tests/Visual/SongSelect/BeatmapCarouselV2TestScene.cs index 72c9611fdb..b29394c55d 100644 --- a/osu.Game.Tests/Visual/SongSelect/BeatmapCarouselV2TestScene.cs +++ b/osu.Game.Tests/Visual/SongSelect/BeatmapCarouselV2TestScene.cs @@ -130,7 +130,7 @@ namespace osu.Game.Tests.Visual.SongSelect }); } - protected void SortBy(FilterCriteria criteria) => AddStep($"sort {criteria.Sort} group {criteria.Group}", () => Carousel.Filter(criteria)); + protected void SortBy(FilterCriteria criteria) => AddStep($"sort:{criteria.Sort} group:{criteria.Group}", () => Carousel.Filter(criteria)); protected void WaitForDrawablePanels() => AddUntilStep("drawable panels loaded", () => Carousel.ChildrenOfType().Count(), () => Is.GreaterThan(0)); protected void WaitForSorting() => AddUntilStep("sorting finished", () => Carousel.IsFiltering, () => Is.False); diff --git a/osu.Game/Screens/SelectV2/Carousel.cs b/osu.Game/Screens/SelectV2/Carousel.cs index 608ef207d9..78c2c99d99 100644 --- a/osu.Game/Screens/SelectV2/Carousel.cs +++ b/osu.Game/Screens/SelectV2/Carousel.cs @@ -228,8 +228,6 @@ namespace osu.Game.Screens.SelectV2 private async Task performFilter() { - Debug.Assert(SynchronizationContext.Current != null); - Stopwatch stopwatch = Stopwatch.StartNew(); var cts = new CancellationTokenSource(); @@ -266,19 +264,22 @@ namespace osu.Game.Screens.SelectV2 { log("Cancelled due to newer request arriving"); } - }, cts.Token).ConfigureAwait(true); + }, cts.Token).ConfigureAwait(false); if (cts.Token.IsCancellationRequested) return; - log("Items ready for display"); - carouselItems = items.ToList(); - displayedRange = null; + Schedule(() => + { + log("Items ready for display"); + carouselItems = items.ToList(); + displayedRange = null; - // Need to call this to ensure correct post-selection logic is handled on the new items list. - HandleItemSelected(currentSelection.Model); + // Need to call this to ensure correct post-selection logic is handled on the new items list. + HandleItemSelected(currentSelection.Model); - refreshAfterSelection(); + refreshAfterSelection(); + }); void log(string text) => Logger.Log($"Carousel[op {cts.GetHashCode().ToString()}] {stopwatch.ElapsedMilliseconds} ms: {text}"); } From 5c9e84caf0350760c1f7d78cbe80024aed7661de Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Wed, 5 Feb 2025 17:31:54 +0900 Subject: [PATCH 2/4] Add lock object --- osu.Game/Screens/SelectV2/Carousel.cs | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/osu.Game/Screens/SelectV2/Carousel.cs b/osu.Game/Screens/SelectV2/Carousel.cs index 78c2c99d99..681da84390 100644 --- a/osu.Game/Screens/SelectV2/Carousel.cs +++ b/osu.Game/Screens/SelectV2/Carousel.cs @@ -226,12 +226,14 @@ namespace osu.Game.Screens.SelectV2 private Task filterTask = Task.CompletedTask; private CancellationTokenSource cancellationSource = new CancellationTokenSource(); + private readonly object cancellationLock = new object(); + private async Task performFilter() { Stopwatch stopwatch = Stopwatch.StartNew(); var cts = new CancellationTokenSource(); - lock (this) + lock (cancellationLock) { cancellationSource.Cancel(); cancellationSource = cts; From c5deb9f36b067f03bd9d597967ac17f8502ade27 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bart=C5=82omiej=20Dach?= Date: Wed, 5 Feb 2025 10:28:09 +0100 Subject: [PATCH 3/4] Use alternative lockless solution for atomic cancellation token recreation --- osu.Game/Screens/SelectV2/Carousel.cs | 9 ++------- 1 file changed, 2 insertions(+), 7 deletions(-) diff --git a/osu.Game/Screens/SelectV2/Carousel.cs b/osu.Game/Screens/SelectV2/Carousel.cs index 681da84390..0b706b4bb8 100644 --- a/osu.Game/Screens/SelectV2/Carousel.cs +++ b/osu.Game/Screens/SelectV2/Carousel.cs @@ -226,18 +226,13 @@ namespace osu.Game.Screens.SelectV2 private Task filterTask = Task.CompletedTask; private CancellationTokenSource cancellationSource = new CancellationTokenSource(); - private readonly object cancellationLock = new object(); - private async Task performFilter() { Stopwatch stopwatch = Stopwatch.StartNew(); var cts = new CancellationTokenSource(); - lock (cancellationLock) - { - cancellationSource.Cancel(); - cancellationSource = cts; - } + var previousCancellationSource = Interlocked.Exchange(ref cancellationSource, cts); + await previousCancellationSource.CancelAsync().ConfigureAwait(false); if (DebounceDelay > 0) { From e5943e460d657cb12545078d10b89ca58f6456f8 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bart=C5=82omiej=20Dach?= Date: Wed, 5 Feb 2025 10:28:23 +0100 Subject: [PATCH 4/4] Unify `ConfigureAwait()` calls across method --- osu.Game/Screens/SelectV2/Carousel.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/osu.Game/Screens/SelectV2/Carousel.cs b/osu.Game/Screens/SelectV2/Carousel.cs index 0b706b4bb8..3371e45453 100644 --- a/osu.Game/Screens/SelectV2/Carousel.cs +++ b/osu.Game/Screens/SelectV2/Carousel.cs @@ -237,7 +237,7 @@ namespace osu.Game.Screens.SelectV2 if (DebounceDelay > 0) { log($"Filter operation queued, waiting for {DebounceDelay} ms debounce"); - await Task.Delay(DebounceDelay, cts.Token).ConfigureAwait(true); + await Task.Delay(DebounceDelay, cts.Token).ConfigureAwait(false); } // Copy must be performed on update thread for now (see ConfigureAwait above).