1
0
mirror of https://github.com/ppy/osu.git synced 2025-01-19 13:02:54 +08:00

Merge branch 'master' into realm-migration-ui

This commit is contained in:
Bartłomiej Dach 2022-01-22 13:52:19 +01:00
commit 1b2cca4a0d
No known key found for this signature in database
GPG Key ID: BCECCD4FA41F6497
32 changed files with 605 additions and 361 deletions

View File

@ -0,0 +1,141 @@
// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence.
// See the LICENCE file in the repository root for full licence text.
using System.Linq;
using System.Threading;
using BenchmarkDotNet.Attributes;
using osu.Framework.Testing;
using osu.Framework.Threading;
using osu.Game.Beatmaps;
using osu.Game.Database;
using osu.Game.Rulesets.Osu;
using osu.Game.Tests.Resources;
namespace osu.Game.Benchmarks
{
public class BenchmarkRealmReads : BenchmarkTest
{
private TemporaryNativeStorage storage;
private RealmContextFactory realmFactory;
private UpdateThread updateThread;
[Params(1, 100, 1000)]
public int ReadsPerFetch { get; set; }
public override void SetUp()
{
storage = new TemporaryNativeStorage("realm-benchmark");
storage.DeleteDirectory(string.Empty);
realmFactory = new RealmContextFactory(storage, "client");
realmFactory.Run(realm =>
{
realm.Write(c => c.Add(TestResources.CreateTestBeatmapSetInfo(rulesets: new[] { new OsuRuleset().RulesetInfo })));
});
updateThread = new UpdateThread(() => { }, null);
updateThread.Start();
}
[Benchmark]
public void BenchmarkDirectPropertyRead()
{
realmFactory.Run(realm =>
{
var beatmapSet = realm.All<BeatmapSetInfo>().First();
for (int i = 0; i < ReadsPerFetch; i++)
{
string _ = beatmapSet.Beatmaps.First().Hash;
}
});
}
[Benchmark]
public void BenchmarkDirectPropertyReadUpdateThread()
{
var done = new ManualResetEventSlim();
updateThread.Scheduler.Add(() =>
{
try
{
var beatmapSet = realmFactory.Context.All<BeatmapSetInfo>().First();
for (int i = 0; i < ReadsPerFetch; i++)
{
string _ = beatmapSet.Beatmaps.First().Hash;
}
}
finally
{
done.Set();
}
});
done.Wait();
}
[Benchmark]
public void BenchmarkRealmLivePropertyRead()
{
realmFactory.Run(realm =>
{
var beatmapSet = realm.All<BeatmapSetInfo>().First().ToLive(realmFactory);
for (int i = 0; i < ReadsPerFetch; i++)
{
string _ = beatmapSet.PerformRead(b => b.Beatmaps.First().Hash);
}
});
}
[Benchmark]
public void BenchmarkRealmLivePropertyReadUpdateThread()
{
var done = new ManualResetEventSlim();
updateThread.Scheduler.Add(() =>
{
try
{
var beatmapSet = realmFactory.Context.All<BeatmapSetInfo>().First().ToLive(realmFactory);
for (int i = 0; i < ReadsPerFetch; i++)
{
string _ = beatmapSet.PerformRead(b => b.Beatmaps.First().Hash);
}
}
finally
{
done.Set();
}
});
done.Wait();
}
[Benchmark]
public void BenchmarkDetachedPropertyRead()
{
realmFactory.Run(realm =>
{
var beatmapSet = realm.All<BeatmapSetInfo>().First().Detach();
for (int i = 0; i < ReadsPerFetch; i++)
{
string _ = beatmapSet.Beatmaps.First().Hash;
}
});
}
[GlobalCleanup]
public void Cleanup()
{
realmFactory?.Dispose();
storage?.Dispose();
updateThread?.Exit();
}
}
}

View File

@ -55,8 +55,7 @@ namespace osu.Game.Tests.Beatmaps.IO
{ {
var realmContextFactory = osu.Dependencies.Get<RealmContextFactory>(); var realmContextFactory = osu.Dependencies.Get<RealmContextFactory>();
using (var realm = realmContextFactory.CreateContext()) realmContextFactory.Run(realm => BeatmapImporterTests.EnsureLoaded(realm, timeout));
BeatmapImporterTests.EnsureLoaded(realm, timeout);
// TODO: add back some extra checks outside of the realm ones? // TODO: add back some extra checks outside of the realm ones?
// var set = queryBeatmapSets().First(); // var set = queryBeatmapSets().First();

View File

@ -21,7 +21,7 @@ namespace osu.Game.Tests.Database
[Test] [Test]
public void TestConstructRealm() public void TestConstructRealm()
{ {
RunTestWithRealm((realmFactory, _) => { realmFactory.CreateContext().Refresh(); }); RunTestWithRealm((realmFactory, _) => { realmFactory.Run(realm => realm.Refresh()); });
} }
[Test] [Test]
@ -46,23 +46,21 @@ namespace osu.Game.Tests.Database
{ {
bool callbackRan = false; bool callbackRan = false;
using (var context = realmFactory.CreateContext()) realmFactory.Run(realm =>
{ {
var subscription = context.All<BeatmapInfo>().QueryAsyncWithNotifications((sender, changes, error) => var subscription = realm.All<BeatmapInfo>().QueryAsyncWithNotifications((sender, changes, error) =>
{ {
using (realmFactory.CreateContext()) realmFactory.Run(_ =>
{ {
callbackRan = true; callbackRan = true;
} });
}); });
// Force the callback above to run. // Force the callback above to run.
using (realmFactory.CreateContext()) realmFactory.Run(r => r.Refresh());
{
}
subscription?.Dispose(); subscription?.Dispose();
} });
Assert.IsTrue(callbackRan); Assert.IsTrue(callbackRan);
}); });
@ -78,12 +76,12 @@ namespace osu.Game.Tests.Database
Task.Factory.StartNew(() => Task.Factory.StartNew(() =>
{ {
using (realmFactory.CreateContext()) realmFactory.Run(_ =>
{ {
hasThreadedUsage.Set(); hasThreadedUsage.Set();
stopThreadedUsage.Wait(); stopThreadedUsage.Wait();
} });
}, TaskCreationOptions.LongRunning | TaskCreationOptions.HideScheduler); }, TaskCreationOptions.LongRunning | TaskCreationOptions.HideScheduler);
hasThreadedUsage.Wait(); hasThreadedUsage.Wait();

View File

@ -23,9 +23,9 @@ namespace osu.Game.Tests.Database
{ {
RunTestWithRealm((realmFactory, _) => RunTestWithRealm((realmFactory, _) =>
{ {
ILive<BeatmapInfo> beatmap = realmFactory.CreateContext().Write(r => r.Add(new BeatmapInfo(CreateRuleset(), new BeatmapDifficulty(), new BeatmapMetadata()))).ToLive(realmFactory); ILive<BeatmapInfo> beatmap = realmFactory.Run(realm => realm.Write(r => r.Add(new BeatmapInfo(CreateRuleset(), new BeatmapDifficulty(), new BeatmapMetadata()))).ToLive(realmFactory));
ILive<BeatmapInfo> beatmap2 = realmFactory.CreateContext().All<BeatmapInfo>().First().ToLive(realmFactory); ILive<BeatmapInfo> beatmap2 = realmFactory.Run(realm => realm.All<BeatmapInfo>().First().ToLive(realmFactory));
Assert.AreEqual(beatmap, beatmap2); Assert.AreEqual(beatmap, beatmap2);
}); });
@ -38,13 +38,18 @@ namespace osu.Game.Tests.Database
{ {
var beatmap = new BeatmapInfo(CreateRuleset(), new BeatmapDifficulty(), new BeatmapMetadata()); var beatmap = new BeatmapInfo(CreateRuleset(), new BeatmapDifficulty(), new BeatmapMetadata());
ILive<BeatmapInfo> liveBeatmap; ILive<BeatmapInfo>? liveBeatmap = null;
using (var context = realmFactory.CreateContext()) realmFactory.Run(realm =>
{ {
context.Write(r => r.Add(beatmap)); realm.Write(r => r.Add(beatmap));
liveBeatmap = beatmap.ToLive(realmFactory); liveBeatmap = beatmap.ToLive(realmFactory);
});
using (realmFactory.BlockAllOperations())
{
// recycle realm before migrating
} }
using (var migratedStorage = new TemporaryNativeStorage("realm-test-migration-target")) using (var migratedStorage = new TemporaryNativeStorage("realm-test-migration-target"))
@ -53,7 +58,7 @@ namespace osu.Game.Tests.Database
storage.Migrate(migratedStorage); storage.Migrate(migratedStorage);
Assert.IsFalse(liveBeatmap.PerformRead(l => l.Hidden)); Assert.IsFalse(liveBeatmap?.PerformRead(l => l.Hidden));
} }
}); });
} }
@ -67,8 +72,7 @@ namespace osu.Game.Tests.Database
var liveBeatmap = beatmap.ToLive(realmFactory); var liveBeatmap = beatmap.ToLive(realmFactory);
using (var context = realmFactory.CreateContext()) realmFactory.Run(realm => realm.Write(r => r.Add(beatmap)));
context.Write(r => r.Add(beatmap));
Assert.IsFalse(liveBeatmap.PerformRead(l => l.Hidden)); Assert.IsFalse(liveBeatmap.PerformRead(l => l.Hidden));
}); });
@ -99,12 +103,12 @@ namespace osu.Game.Tests.Database
ILive<BeatmapInfo>? liveBeatmap = null; ILive<BeatmapInfo>? liveBeatmap = null;
Task.Factory.StartNew(() => Task.Factory.StartNew(() =>
{ {
using (var threadContext = realmFactory.CreateContext()) realmFactory.Run(threadContext =>
{ {
var beatmap = threadContext.Write(r => r.Add(new BeatmapInfo(CreateRuleset(), new BeatmapDifficulty(), new BeatmapMetadata()))); var beatmap = threadContext.Write(r => r.Add(new BeatmapInfo(CreateRuleset(), new BeatmapDifficulty(), new BeatmapMetadata())));
liveBeatmap = beatmap.ToLive(realmFactory); liveBeatmap = beatmap.ToLive(realmFactory);
} });
}, TaskCreationOptions.LongRunning | TaskCreationOptions.HideScheduler).WaitSafely(); }, TaskCreationOptions.LongRunning | TaskCreationOptions.HideScheduler).WaitSafely();
Debug.Assert(liveBeatmap != null); Debug.Assert(liveBeatmap != null);
@ -128,12 +132,12 @@ namespace osu.Game.Tests.Database
ILive<BeatmapInfo>? liveBeatmap = null; ILive<BeatmapInfo>? liveBeatmap = null;
Task.Factory.StartNew(() => Task.Factory.StartNew(() =>
{ {
using (var threadContext = realmFactory.CreateContext()) realmFactory.Run(threadContext =>
{ {
var beatmap = threadContext.Write(r => r.Add(new BeatmapInfo(CreateRuleset(), new BeatmapDifficulty(), new BeatmapMetadata()))); var beatmap = threadContext.Write(r => r.Add(new BeatmapInfo(CreateRuleset(), new BeatmapDifficulty(), new BeatmapMetadata())));
liveBeatmap = beatmap.ToLive(realmFactory); liveBeatmap = beatmap.ToLive(realmFactory);
} });
}, TaskCreationOptions.LongRunning | TaskCreationOptions.HideScheduler).WaitSafely(); }, TaskCreationOptions.LongRunning | TaskCreationOptions.HideScheduler).WaitSafely();
Debug.Assert(liveBeatmap != null); Debug.Assert(liveBeatmap != null);
@ -170,12 +174,12 @@ namespace osu.Game.Tests.Database
Task.Factory.StartNew(() => Task.Factory.StartNew(() =>
{ {
using (var threadContext = realmFactory.CreateContext()) realmFactory.Run(threadContext =>
{ {
var beatmap = threadContext.Write(r => r.Add(new BeatmapInfo(CreateRuleset(), new BeatmapDifficulty(), new BeatmapMetadata()))); var beatmap = threadContext.Write(r => r.Add(new BeatmapInfo(CreateRuleset(), new BeatmapDifficulty(), new BeatmapMetadata())));
liveBeatmap = beatmap.ToLive(realmFactory); liveBeatmap = beatmap.ToLive(realmFactory);
} });
}, TaskCreationOptions.LongRunning | TaskCreationOptions.HideScheduler).WaitSafely(); }, TaskCreationOptions.LongRunning | TaskCreationOptions.HideScheduler).WaitSafely();
Debug.Assert(liveBeatmap != null); Debug.Assert(liveBeatmap != null);
@ -189,13 +193,13 @@ namespace osu.Game.Tests.Database
}); });
// Can't be used, even from within a valid context. // Can't be used, even from within a valid context.
using (realmFactory.CreateContext()) realmFactory.Run(threadContext =>
{ {
Assert.Throws<InvalidOperationException>(() => Assert.Throws<InvalidOperationException>(() =>
{ {
var __ = liveBeatmap.Value; var __ = liveBeatmap.Value;
}); });
} });
}, TaskCreationOptions.LongRunning | TaskCreationOptions.HideScheduler).WaitSafely(); }, TaskCreationOptions.LongRunning | TaskCreationOptions.HideScheduler).WaitSafely();
}); });
} }
@ -208,12 +212,12 @@ namespace osu.Game.Tests.Database
ILive<BeatmapInfo>? liveBeatmap = null; ILive<BeatmapInfo>? liveBeatmap = null;
Task.Factory.StartNew(() => Task.Factory.StartNew(() =>
{ {
using (var threadContext = realmFactory.CreateContext()) realmFactory.Run(threadContext =>
{ {
var beatmap = threadContext.Write(r => r.Add(new BeatmapInfo(CreateRuleset(), new BeatmapDifficulty(), new BeatmapMetadata()))); var beatmap = threadContext.Write(r => r.Add(new BeatmapInfo(CreateRuleset(), new BeatmapDifficulty(), new BeatmapMetadata())));
liveBeatmap = beatmap.ToLive(realmFactory); liveBeatmap = beatmap.ToLive(realmFactory);
} });
}, TaskCreationOptions.LongRunning | TaskCreationOptions.HideScheduler).WaitSafely(); }, TaskCreationOptions.LongRunning | TaskCreationOptions.HideScheduler).WaitSafely();
Debug.Assert(liveBeatmap != null); Debug.Assert(liveBeatmap != null);
@ -235,50 +239,50 @@ namespace osu.Game.Tests.Database
{ {
int changesTriggered = 0; int changesTriggered = 0;
using (var updateThreadContext = realmFactory.CreateContext()) realmFactory.Run(outerRealm =>
{ {
updateThreadContext.All<BeatmapInfo>().QueryAsyncWithNotifications(gotChange); outerRealm.All<BeatmapInfo>().QueryAsyncWithNotifications(gotChange);
ILive<BeatmapInfo>? liveBeatmap = null; ILive<BeatmapInfo>? liveBeatmap = null;
Task.Factory.StartNew(() => Task.Factory.StartNew(() =>
{ {
using (var threadContext = realmFactory.CreateContext()) realmFactory.Run(innerRealm =>
{ {
var ruleset = CreateRuleset(); var ruleset = CreateRuleset();
var beatmap = threadContext.Write(r => r.Add(new BeatmapInfo(ruleset, new BeatmapDifficulty(), new BeatmapMetadata()))); var beatmap = innerRealm.Write(r => r.Add(new BeatmapInfo(ruleset, new BeatmapDifficulty(), new BeatmapMetadata())));
// add a second beatmap to ensure that a full refresh occurs below. // add a second beatmap to ensure that a full refresh occurs below.
// not just a refresh from the resolved Live. // not just a refresh from the resolved Live.
threadContext.Write(r => r.Add(new BeatmapInfo(ruleset, new BeatmapDifficulty(), new BeatmapMetadata()))); innerRealm.Write(r => r.Add(new BeatmapInfo(ruleset, new BeatmapDifficulty(), new BeatmapMetadata())));
liveBeatmap = beatmap.ToLive(realmFactory); liveBeatmap = beatmap.ToLive(realmFactory);
} });
}, TaskCreationOptions.LongRunning | TaskCreationOptions.HideScheduler).WaitSafely(); }, TaskCreationOptions.LongRunning | TaskCreationOptions.HideScheduler).WaitSafely();
Debug.Assert(liveBeatmap != null); Debug.Assert(liveBeatmap != null);
// not yet seen by main context // not yet seen by main context
Assert.AreEqual(0, updateThreadContext.All<BeatmapInfo>().Count()); Assert.AreEqual(0, outerRealm.All<BeatmapInfo>().Count());
Assert.AreEqual(0, changesTriggered); Assert.AreEqual(0, changesTriggered);
liveBeatmap.PerformRead(resolved => liveBeatmap.PerformRead(resolved =>
{ {
// retrieval causes an implicit refresh. even changes that aren't related to the retrieval are fired at this point. // retrieval causes an implicit refresh. even changes that aren't related to the retrieval are fired at this point.
// ReSharper disable once AccessToDisposedClosure // ReSharper disable once AccessToDisposedClosure
Assert.AreEqual(2, updateThreadContext.All<BeatmapInfo>().Count()); Assert.AreEqual(2, outerRealm.All<BeatmapInfo>().Count());
Assert.AreEqual(1, changesTriggered); Assert.AreEqual(1, changesTriggered);
// can access properties without a crash. // can access properties without a crash.
Assert.IsFalse(resolved.Hidden); Assert.IsFalse(resolved.Hidden);
// ReSharper disable once AccessToDisposedClosure // ReSharper disable once AccessToDisposedClosure
updateThreadContext.Write(r => outerRealm.Write(r =>
{ {
// can use with the main context. // can use with the main context.
r.Remove(resolved); r.Remove(resolved);
}); });
}); });
} });
void gotChange(IRealmCollection<BeatmapInfo> sender, ChangeSet changes, Exception error) void gotChange(IRealmCollection<BeatmapInfo> sender, ChangeSet changes, Exception error)
{ {

View File

@ -60,15 +60,12 @@ namespace osu.Game.Tests.Database
KeyBindingContainer testContainer = new TestKeyBindingContainer(); KeyBindingContainer testContainer = new TestKeyBindingContainer();
// Add some excess bindings for an action which only supports 1. // Add some excess bindings for an action which only supports 1.
using (var realm = realmContextFactory.CreateContext()) realmContextFactory.Write(realm =>
using (var transaction = realm.BeginWrite())
{ {
realm.Add(new RealmKeyBinding(GlobalAction.Back, new KeyCombination(InputKey.A))); realm.Add(new RealmKeyBinding(GlobalAction.Back, new KeyCombination(InputKey.A)));
realm.Add(new RealmKeyBinding(GlobalAction.Back, new KeyCombination(InputKey.S))); realm.Add(new RealmKeyBinding(GlobalAction.Back, new KeyCombination(InputKey.S)));
realm.Add(new RealmKeyBinding(GlobalAction.Back, new KeyCombination(InputKey.D))); realm.Add(new RealmKeyBinding(GlobalAction.Back, new KeyCombination(InputKey.D)));
});
transaction.Commit();
}
Assert.That(queryCount(GlobalAction.Back), Is.EqualTo(3)); Assert.That(queryCount(GlobalAction.Back), Is.EqualTo(3));
@ -79,13 +76,13 @@ namespace osu.Game.Tests.Database
private int queryCount(GlobalAction? match = null) private int queryCount(GlobalAction? match = null)
{ {
using (var realm = realmContextFactory.CreateContext()) return realmContextFactory.Run(realm =>
{ {
var results = realm.All<RealmKeyBinding>(); var results = realm.All<RealmKeyBinding>();
if (match.HasValue) if (match.HasValue)
results = results.Where(k => k.ActionInt == (int)match.Value); results = results.Where(k => k.ActionInt == (int)match.Value);
return results.Count(); return results.Count();
} });
} }
[Test] [Test]
@ -95,26 +92,26 @@ namespace osu.Game.Tests.Database
keyBindingStore.Register(testContainer, Enumerable.Empty<RulesetInfo>()); keyBindingStore.Register(testContainer, Enumerable.Empty<RulesetInfo>());
using (var primaryRealm = realmContextFactory.CreateContext()) realmContextFactory.Run(outerRealm =>
{ {
var backBinding = primaryRealm.All<RealmKeyBinding>().Single(k => k.ActionInt == (int)GlobalAction.Back); var backBinding = outerRealm.All<RealmKeyBinding>().Single(k => k.ActionInt == (int)GlobalAction.Back);
Assert.That(backBinding.KeyCombination.Keys, Is.EquivalentTo(new[] { InputKey.Escape })); Assert.That(backBinding.KeyCombination.Keys, Is.EquivalentTo(new[] { InputKey.Escape }));
var tsr = ThreadSafeReference.Create(backBinding); var tsr = ThreadSafeReference.Create(backBinding);
using (var threadedContext = realmContextFactory.CreateContext()) realmContextFactory.Run(innerRealm =>
{ {
var binding = threadedContext.ResolveReference(tsr); var binding = innerRealm.ResolveReference(tsr);
threadedContext.Write(() => binding.KeyCombination = new KeyCombination(InputKey.BackSpace)); innerRealm.Write(() => binding.KeyCombination = new KeyCombination(InputKey.BackSpace));
} });
Assert.That(backBinding.KeyCombination.Keys, Is.EquivalentTo(new[] { InputKey.BackSpace })); Assert.That(backBinding.KeyCombination.Keys, Is.EquivalentTo(new[] { InputKey.BackSpace }));
// check still correct after re-query. // check still correct after re-query.
backBinding = primaryRealm.All<RealmKeyBinding>().Single(k => k.ActionInt == (int)GlobalAction.Back); backBinding = outerRealm.All<RealmKeyBinding>().Single(k => k.ActionInt == (int)GlobalAction.Back);
Assert.That(backBinding.KeyCombination.Keys, Is.EquivalentTo(new[] { InputKey.BackSpace })); Assert.That(backBinding.KeyCombination.Keys, Is.EquivalentTo(new[] { InputKey.BackSpace }));
} });
} }
[TearDown] [TearDown]

View File

@ -60,8 +60,8 @@ namespace osu.Game.Tests.Online
testBeatmapInfo = getTestBeatmapInfo(testBeatmapFile); testBeatmapInfo = getTestBeatmapInfo(testBeatmapFile);
testBeatmapSet = testBeatmapInfo.BeatmapSet; testBeatmapSet = testBeatmapInfo.BeatmapSet;
ContextFactory.Context.Write(r => r.RemoveAll<BeatmapSetInfo>()); ContextFactory.Write(r => r.RemoveAll<BeatmapSetInfo>());
ContextFactory.Context.Write(r => r.RemoveAll<BeatmapInfo>()); ContextFactory.Write(r => r.RemoveAll<BeatmapInfo>());
selectedItem.Value = new PlaylistItem selectedItem.Value = new PlaylistItem
{ {

View File

@ -42,7 +42,7 @@ namespace osu.Game.Tests.Visual.Ranking
{ {
base.LoadComplete(); base.LoadComplete();
using (var realm = realmContextFactory.CreateContext()) realmContextFactory.Run(realm =>
{ {
var beatmapInfo = realm.All<BeatmapInfo>() var beatmapInfo = realm.All<BeatmapInfo>()
.Filter($"{nameof(BeatmapInfo.Ruleset)}.{nameof(RulesetInfo.OnlineID)} = $0", 0) .Filter($"{nameof(BeatmapInfo.Ruleset)}.{nameof(RulesetInfo.OnlineID)} = $0", 0)
@ -50,7 +50,7 @@ namespace osu.Game.Tests.Visual.Ranking
if (beatmapInfo != null) if (beatmapInfo != null)
Beatmap.Value = beatmaps.GetWorkingBeatmap(beatmapInfo); Beatmap.Value = beatmaps.GetWorkingBeatmap(beatmapInfo);
} });
} }
[Test] [Test]

View File

@ -122,11 +122,11 @@ namespace osu.Game.Tests.Visual.UserInterface
[SetUp] [SetUp]
public void Setup() => Schedule(() => public void Setup() => Schedule(() =>
{ {
using (var realm = realmFactory.CreateContext()) realmFactory.Run(realm =>
{ {
// Due to soft deletions, we can re-use deleted scores between test runs // Due to soft deletions, we can re-use deleted scores between test runs
scoreManager.Undelete(realm.All<ScoreInfo>().Where(s => s.DeletePending).ToList()); scoreManager.Undelete(realm.All<ScoreInfo>().Where(s => s.DeletePending).ToList());
} });
leaderboard.Scores = null; leaderboard.Scores = null;
leaderboard.FinishTransforms(true); // After setting scores, we may be waiting for transforms to expire drawables leaderboard.FinishTransforms(true); // After setting scores, we may be waiting for transforms to expire drawables

View File

@ -119,7 +119,8 @@ namespace osu.Game.Beatmaps
/// <param name="beatmapInfo">The beatmap difficulty to hide.</param> /// <param name="beatmapInfo">The beatmap difficulty to hide.</param>
public void Hide(BeatmapInfo beatmapInfo) public void Hide(BeatmapInfo beatmapInfo)
{ {
using (var realm = contextFactory.CreateContext()) contextFactory.Run(realm =>
{
using (var transaction = realm.BeginWrite()) using (var transaction = realm.BeginWrite())
{ {
if (!beatmapInfo.IsManaged) if (!beatmapInfo.IsManaged)
@ -128,6 +129,7 @@ namespace osu.Game.Beatmaps
beatmapInfo.Hidden = true; beatmapInfo.Hidden = true;
transaction.Commit(); transaction.Commit();
} }
});
} }
/// <summary> /// <summary>
@ -136,7 +138,8 @@ namespace osu.Game.Beatmaps
/// <param name="beatmapInfo">The beatmap difficulty to restore.</param> /// <param name="beatmapInfo">The beatmap difficulty to restore.</param>
public void Restore(BeatmapInfo beatmapInfo) public void Restore(BeatmapInfo beatmapInfo)
{ {
using (var realm = contextFactory.CreateContext()) contextFactory.Run(realm =>
{
using (var transaction = realm.BeginWrite()) using (var transaction = realm.BeginWrite())
{ {
if (!beatmapInfo.IsManaged) if (!beatmapInfo.IsManaged)
@ -145,11 +148,13 @@ namespace osu.Game.Beatmaps
beatmapInfo.Hidden = false; beatmapInfo.Hidden = false;
transaction.Commit(); transaction.Commit();
} }
});
} }
public void RestoreAll() public void RestoreAll()
{ {
using (var realm = contextFactory.CreateContext()) contextFactory.Run(realm =>
{
using (var transaction = realm.BeginWrite()) using (var transaction = realm.BeginWrite())
{ {
foreach (var beatmap in realm.All<BeatmapInfo>().Where(b => b.Hidden)) foreach (var beatmap in realm.All<BeatmapInfo>().Where(b => b.Hidden))
@ -157,6 +162,7 @@ namespace osu.Game.Beatmaps
transaction.Commit(); transaction.Commit();
} }
});
} }
/// <summary> /// <summary>
@ -165,8 +171,11 @@ namespace osu.Game.Beatmaps
/// <returns>A list of available <see cref="BeatmapSetInfo"/>.</returns> /// <returns>A list of available <see cref="BeatmapSetInfo"/>.</returns>
public List<BeatmapSetInfo> GetAllUsableBeatmapSets() public List<BeatmapSetInfo> GetAllUsableBeatmapSets()
{ {
using (var context = contextFactory.CreateContext()) return contextFactory.Run(realm =>
return context.All<BeatmapSetInfo>().Where(b => !b.DeletePending).Detach(); {
realm.Refresh();
return realm.All<BeatmapSetInfo>().Where(b => !b.DeletePending).Detach();
});
} }
/// <summary> /// <summary>
@ -176,8 +185,7 @@ namespace osu.Game.Beatmaps
/// <returns>The first result for the provided query, or null if no results were found.</returns> /// <returns>The first result for the provided query, or null if no results were found.</returns>
public ILive<BeatmapSetInfo>? QueryBeatmapSet(Expression<Func<BeatmapSetInfo, bool>> query) public ILive<BeatmapSetInfo>? QueryBeatmapSet(Expression<Func<BeatmapSetInfo, bool>> query)
{ {
using (var context = contextFactory.CreateContext()) return contextFactory.Run(realm => realm.All<BeatmapSetInfo>().FirstOrDefault(query)?.ToLive(contextFactory));
return context.All<BeatmapSetInfo>().FirstOrDefault(query)?.ToLive(contextFactory);
} }
#region Delegation to BeatmapModelManager (methods which previously existed locally). #region Delegation to BeatmapModelManager (methods which previously existed locally).
@ -232,21 +240,20 @@ namespace osu.Game.Beatmaps
public void Delete(Expression<Func<BeatmapSetInfo, bool>>? filter = null, bool silent = false) public void Delete(Expression<Func<BeatmapSetInfo, bool>>? filter = null, bool silent = false)
{ {
using (var context = contextFactory.CreateContext()) contextFactory.Run(realm =>
{ {
var items = context.All<BeatmapSetInfo>().Where(s => !s.DeletePending && !s.Protected); var items = realm.All<BeatmapSetInfo>().Where(s => !s.DeletePending && !s.Protected);
if (filter != null) if (filter != null)
items = items.Where(filter); items = items.Where(filter);
beatmapModelManager.Delete(items.ToList(), silent); beatmapModelManager.Delete(items.ToList(), silent);
} });
} }
public void UndeleteAll() public void UndeleteAll()
{ {
using (var context = contextFactory.CreateContext()) contextFactory.Run(realm => beatmapModelManager.Undelete(realm.All<BeatmapSetInfo>().Where(s => s.DeletePending).ToList()));
beatmapModelManager.Undelete(context.All<BeatmapSetInfo>().Where(s => s.DeletePending).ToList());
} }
public void Undelete(List<BeatmapSetInfo> items, bool silent = false) public void Undelete(List<BeatmapSetInfo> items, bool silent = false)
@ -305,13 +312,13 @@ namespace osu.Game.Beatmaps
// If we seem to be missing files, now is a good time to re-fetch. // If we seem to be missing files, now is a good time to re-fetch.
if (importedBeatmap?.BeatmapSet?.Files.Count == 0) if (importedBeatmap?.BeatmapSet?.Files.Count == 0)
{ {
using (var realm = contextFactory.CreateContext()) contextFactory.Run(realm =>
{ {
var refetch = realm.Find<BeatmapInfo>(importedBeatmap.ID)?.Detach(); var refetch = realm.Find<BeatmapInfo>(importedBeatmap.ID)?.Detach();
if (refetch != null) if (refetch != null)
importedBeatmap = refetch; importedBeatmap = refetch;
} });
} }
return workingBeatmapCache.GetWorkingBeatmap(importedBeatmap); return workingBeatmapCache.GetWorkingBeatmap(importedBeatmap);

View File

@ -98,17 +98,16 @@ namespace osu.Game.Beatmaps
/// <returns>The first result for the provided query, or null if no results were found.</returns> /// <returns>The first result for the provided query, or null if no results were found.</returns>
public BeatmapInfo? QueryBeatmap(Expression<Func<BeatmapInfo, bool>> query) public BeatmapInfo? QueryBeatmap(Expression<Func<BeatmapInfo, bool>> query)
{ {
using (var context = ContextFactory.CreateContext()) return ContextFactory.Run(realm => realm.All<BeatmapInfo>().FirstOrDefault(query)?.Detach());
return context.All<BeatmapInfo>().FirstOrDefault(query)?.Detach();
} }
public void Update(BeatmapSetInfo item) public void Update(BeatmapSetInfo item)
{ {
using (var realm = ContextFactory.CreateContext()) ContextFactory.Write(realm =>
{ {
var existing = realm.Find<BeatmapSetInfo>(item.ID); var existing = realm.Find<BeatmapSetInfo>(item.ID);
realm.Write(r => item.CopyChangesToRealm(existing)); item.CopyChangesToRealm(existing);
} });
} }
} }
} }

View File

@ -8,7 +8,6 @@ using osu.Framework.Allocation;
using osu.Framework.Graphics; using osu.Framework.Graphics;
using osu.Framework.Graphics.Containers; using osu.Framework.Graphics.Containers;
using osu.Framework.Logging; using osu.Framework.Logging;
using osu.Framework.Platform;
using osu.Game.Beatmaps; using osu.Game.Beatmaps;
using osu.Game.Configuration; using osu.Game.Configuration;
using osu.Game.Graphics; using osu.Game.Graphics;
@ -142,7 +141,7 @@ namespace osu.Game.Database
int count = existingBeatmapSets.Count(); int count = existingBeatmapSets.Count();
using (var realm = realmContextFactory.CreateContext()) realmContextFactory.Run(realm =>
{ {
log($"Found {count} beatmaps in EF"); log($"Found {count} beatmaps in EF");
@ -227,7 +226,7 @@ namespace osu.Game.Database
log($"Successfully migrated {count} beatmaps to realm"); log($"Successfully migrated {count} beatmaps to realm");
} }
} });
} }
private BeatmapMetadata getBestMetadata(EFBeatmapMetadata? beatmapMetadata, EFBeatmapMetadata? beatmapSetMetadata) private BeatmapMetadata getBestMetadata(EFBeatmapMetadata? beatmapMetadata, EFBeatmapMetadata? beatmapSetMetadata)
@ -273,7 +272,7 @@ namespace osu.Game.Database
int count = existingScores.Count(); int count = existingScores.Count();
using (var realm = realmContextFactory.CreateContext()) realmContextFactory.Run(realm =>
{ {
log($"Found {count} scores in EF"); log($"Found {count} scores in EF");
@ -341,7 +340,7 @@ namespace osu.Game.Database
log($"Successfully migrated {count} scores to realm"); log($"Successfully migrated {count} scores to realm");
} }
} });
} }
private void migrateSkins(OsuDbContext db) private void migrateSkins(OsuDbContext db)
@ -370,7 +369,8 @@ namespace osu.Game.Database
break; break;
} }
using (var realm = realmContextFactory.CreateContext()) realmContextFactory.Run(realm =>
{
using (var transaction = realm.BeginWrite()) using (var transaction = realm.BeginWrite())
{ {
// only migrate data if the realm database is empty. // only migrate data if the realm database is empty.
@ -401,6 +401,7 @@ namespace osu.Game.Database
transaction.Commit(); transaction.Commit();
} }
});
} }
private static void migrateFiles<T>(IHasFiles<T> fileSource, Realm realm, IHasRealmFiles realmObject) where T : INamedFileInfo private static void migrateFiles<T>(IHasFiles<T> fileSource, Realm realm, IHasRealmFiles realmObject) where T : INamedFileInfo
@ -427,7 +428,8 @@ namespace osu.Game.Database
log("Beginning settings migration to realm"); log("Beginning settings migration to realm");
using (var realm = realmContextFactory.CreateContext()) realmContextFactory.Run(realm =>
{
using (var transaction = realm.BeginWrite()) using (var transaction = realm.BeginWrite())
{ {
// only migrate data if the realm database is empty. // only migrate data if the realm database is empty.
@ -457,6 +459,7 @@ namespace osu.Game.Database
transaction.Commit(); transaction.Commit();
} }
});
} }
private string? getRulesetShortNameFromLegacyID(long rulesetId) => private string? getRulesetShortNameFromLegacyID(long rulesetId) =>

View File

@ -1,20 +0,0 @@
// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence.
// See the LICENCE file in the repository root for full licence text.
using Realms;
namespace osu.Game.Database
{
public interface IRealmFactory
{
/// <summary>
/// The main realm context, bound to the update thread.
/// </summary>
Realm Context { get; }
/// <summary>
/// Create a new realm context for use on the current thread.
/// </summary>
Realm CreateContext();
}
}

View File

@ -30,7 +30,7 @@ namespace osu.Game.Database
/// <summary> /// <summary>
/// A factory which provides both the main (update thread bound) realm context and creates contexts for async usage. /// A factory which provides both the main (update thread bound) realm context and creates contexts for async usage.
/// </summary> /// </summary>
public class RealmContextFactory : IDisposable, IRealmFactory public class RealmContextFactory : IDisposable
{ {
private readonly Storage storage; private readonly Storage storage;
@ -72,13 +72,13 @@ namespace osu.Game.Database
get get
{ {
if (!ThreadSafety.IsUpdateThread) if (!ThreadSafety.IsUpdateThread)
throw new InvalidOperationException(@$"Use {nameof(CreateContext)} when performing realm operations from a non-update thread"); throw new InvalidOperationException(@$"Use {nameof(Run)}/{nameof(Write)} when performing realm operations from a non-update thread");
lock (contextLock) lock (contextLock)
{ {
if (context == null) if (context == null)
{ {
context = CreateContext(); context = createContext();
Logger.Log(@$"Opened realm ""{context.Config.DatabasePath}"" at version {context.Config.SchemaVersion}"); Logger.Log(@$"Opened realm ""{context.Config.DatabasePath}"" at version {context.Config.SchemaVersion}");
} }
@ -124,7 +124,7 @@ namespace osu.Game.Database
private void cleanupPendingDeletions() private void cleanupPendingDeletions()
{ {
using (var realm = CreateContext()) using (var realm = createContext())
using (var transaction = realm.BeginWrite()) using (var transaction = realm.BeginWrite())
{ {
var pendingDeleteScores = realm.All<ScoreInfo>().Where(s => s.DeletePending); var pendingDeleteScores = realm.All<ScoreInfo>().Where(s => s.DeletePending);
@ -169,7 +169,60 @@ namespace osu.Game.Database
/// <returns></returns> /// <returns></returns>
public bool Compact() => Realm.Compact(getConfiguration()); public bool Compact() => Realm.Compact(getConfiguration());
public Realm CreateContext() /// <summary>
/// Run work on realm with a return value.
/// </summary>
/// <remarks>
/// Handles correct context management automatically.
/// </remarks>
/// <param name="action">The work to run.</param>
/// <typeparam name="T">The return type.</typeparam>
public T Run<T>(Func<Realm, T> action)
{
if (ThreadSafety.IsUpdateThread)
return action(Context);
using (var realm = createContext())
return action(realm);
}
/// <summary>
/// Run work on realm.
/// </summary>
/// <remarks>
/// Handles correct context management automatically.
/// </remarks>
/// <param name="action">The work to run.</param>
public void Run(Action<Realm> action)
{
if (ThreadSafety.IsUpdateThread)
action(Context);
else
{
using (var realm = createContext())
action(realm);
}
}
/// <summary>
/// Write changes to realm.
/// </summary>
/// <remarks>
/// Handles correct context management and transaction committing automatically.
/// </remarks>
/// <param name="action">The work to run.</param>
public void Write(Action<Realm> action)
{
if (ThreadSafety.IsUpdateThread)
Context.Write(action);
else
{
using (var realm = createContext())
realm.Write(action);
}
}
private Realm createContext()
{ {
if (isDisposed) if (isDisposed)
throw new ObjectDisposedException(nameof(RealmContextFactory)); throw new ObjectDisposedException(nameof(RealmContextFactory));

View File

@ -51,8 +51,10 @@ namespace osu.Game.Database
return; return;
} }
using (var realm = realmFactory.CreateContext()) realmFactory.Run(realm =>
perform(realm.Find<T>(ID)); {
perform(retrieveFromID(realm, ID));
});
} }
/// <summary> /// <summary>
@ -64,15 +66,15 @@ namespace osu.Game.Database
if (!IsManaged) if (!IsManaged)
return perform(data); return perform(data);
using (var realm = realmFactory.CreateContext()) return realmFactory.Run(realm =>
{ {
var returnData = perform(realm.Find<T>(ID)); var returnData = perform(retrieveFromID(realm, ID));
if (returnData is RealmObjectBase realmObject && realmObject.IsManaged) if (returnData is RealmObjectBase realmObject && realmObject.IsManaged)
throw new InvalidOperationException(@$"Managed realm objects should not exit the scope of {nameof(PerformRead)}."); throw new InvalidOperationException(@$"Managed realm objects should not exit the scope of {nameof(PerformRead)}.");
return returnData; return returnData;
} });
} }
/// <summary> /// <summary>
@ -106,6 +108,22 @@ namespace osu.Game.Database
} }
} }
private T retrieveFromID(Realm realm, Guid id)
{
var found = realm.Find<T>(ID);
if (found == null)
{
// It may be that we access this from the update thread before a refresh has taken place.
// To ensure that behaviour matches what we'd expect (the object *is* available), force
// a refresh to bring in any off-thread changes immediately.
realm.Refresh();
found = realm.Find<T>(ID);
}
return found;
}
public bool Equals(ILive<T>? other) => ID == other?.ID; public bool Equals(ILive<T>? other) => ID == other?.ID;
public override string ToString() => PerformRead(i => i.ToString()); public override string ToString() => PerformRead(i => i.ToString());

View File

@ -21,6 +21,9 @@ namespace osu.Game.Database
/// <param name="data">The realm data.</param> /// <param name="data">The realm data.</param>
public RealmLiveUnmanaged(T data) public RealmLiveUnmanaged(T data)
{ {
if (data.IsManaged)
throw new InvalidOperationException($"Cannot use {nameof(RealmLiveUnmanaged<T>)} with managed instances");
Value = data; Value = data;
} }

View File

@ -34,7 +34,7 @@ namespace osu.Game.Input
{ {
List<string> combinations = new List<string>(); List<string> combinations = new List<string>();
using (var context = realmFactory.CreateContext()) realmFactory.Run(context =>
{ {
foreach (var action in context.All<RealmKeyBinding>().Where(b => string.IsNullOrEmpty(b.RulesetName) && (GlobalAction)b.ActionInt == globalAction)) foreach (var action in context.All<RealmKeyBinding>().Where(b => string.IsNullOrEmpty(b.RulesetName) && (GlobalAction)b.ActionInt == globalAction))
{ {
@ -44,7 +44,7 @@ namespace osu.Game.Input
if (str.Length > 0) if (str.Length > 0)
combinations.Add(str); combinations.Add(str);
} }
} });
return combinations; return combinations;
} }
@ -56,7 +56,8 @@ namespace osu.Game.Input
/// <param name="rulesets">The rulesets to populate defaults from.</param> /// <param name="rulesets">The rulesets to populate defaults from.</param>
public void Register(KeyBindingContainer container, IEnumerable<RulesetInfo> rulesets) public void Register(KeyBindingContainer container, IEnumerable<RulesetInfo> rulesets)
{ {
using (var realm = realmFactory.CreateContext()) realmFactory.Run(realm =>
{
using (var transaction = realm.BeginWrite()) using (var transaction = realm.BeginWrite())
{ {
// intentionally flattened to a list rather than querying against the IQueryable, as nullable fields being queried against aren't indexed. // intentionally flattened to a list rather than querying against the IQueryable, as nullable fields being queried against aren't indexed.
@ -74,6 +75,7 @@ namespace osu.Game.Input
transaction.Commit(); transaction.Commit();
} }
});
} }
private void insertDefaults(Realm realm, List<RealmKeyBinding> existingBindings, IEnumerable<IKeyBinding> defaults, string? rulesetName = null, int? variant = null) private void insertDefaults(Realm realm, List<RealmKeyBinding> existingBindings, IEnumerable<IKeyBinding> defaults, string? rulesetName = null, int? variant = null)

View File

@ -386,11 +386,11 @@ namespace osu.Game.Overlays.Settings.Sections.Input
private void updateStoreFromButton(KeyButton button) private void updateStoreFromButton(KeyButton button)
{ {
using (var realm = realmFactory.CreateContext()) realmFactory.Run(realm =>
{ {
var binding = realm.Find<RealmKeyBinding>(((IHasGuidPrimaryKey)button.KeyBinding).ID); var binding = realm.Find<RealmKeyBinding>(((IHasGuidPrimaryKey)button.KeyBinding).ID);
realm.Write(() => binding.KeyCombinationString = button.KeyBinding.KeyCombinationString); realm.Write(() => binding.KeyCombinationString = button.KeyBinding.KeyCombinationString);
} });
} }
private void updateIsDefaultValue() private void updateIsDefaultValue()

View File

@ -34,10 +34,9 @@ namespace osu.Game.Overlays.Settings.Sections.Input
{ {
string rulesetName = Ruleset?.ShortName; string rulesetName = Ruleset?.ShortName;
List<RealmKeyBinding> bindings; var bindings = realmFactory.Run(realm => realm.All<RealmKeyBinding>()
.Where(b => b.RulesetName == rulesetName && b.Variant == variant)
using (var realm = realmFactory.CreateContext()) .Detach());
bindings = realm.All<RealmKeyBinding>().Where(b => b.RulesetName == rulesetName && b.Variant == variant).Detach();
foreach (var defaultGroup in Defaults.GroupBy(d => d.Action)) foreach (var defaultGroup in Defaults.GroupBy(d => d.Action))
{ {

View File

@ -56,12 +56,7 @@ namespace osu.Game.Rulesets.Configuration
pendingWrites.Clear(); pendingWrites.Clear();
} }
if (realmFactory == null) realmFactory?.Write(realm =>
return true;
using (var context = realmFactory.CreateContext())
{
context.Write(realm =>
{ {
foreach (var c in changed) foreach (var c in changed)
{ {
@ -70,7 +65,6 @@ namespace osu.Game.Rulesets.Configuration
setting.Value = ConfigStore[c].ToString(); setting.Value = ConfigStore[c].ToString();
} }
}); });
}
return true; return true;
} }

View File

@ -100,9 +100,7 @@ namespace osu.Game.Rulesets
private void addMissingRulesets() private void addMissingRulesets()
{ {
using (var context = realmFactory.CreateContext()) realmFactory.Write(realm =>
{
context.Write(realm =>
{ {
var rulesets = realm.All<RulesetInfo>(); var rulesets = realm.All<RulesetInfo>();
@ -168,7 +166,6 @@ namespace osu.Game.Rulesets
availableRulesets.AddRange(detachedRulesets); availableRulesets.AddRange(detachedRulesets);
}); });
} }
}
private void loadFromAppDomain() private void loadFromAppDomain()
{ {

View File

@ -51,8 +51,7 @@ namespace osu.Game.Scoring
/// <returns>The first result for the provided query, or null if no results were found.</returns> /// <returns>The first result for the provided query, or null if no results were found.</returns>
public ScoreInfo Query(Expression<Func<ScoreInfo, bool>> query) public ScoreInfo Query(Expression<Func<ScoreInfo, bool>> query)
{ {
using (var context = contextFactory.CreateContext()) return contextFactory.Run(realm => realm.All<ScoreInfo>().FirstOrDefault(query)?.Detach());
return context.All<ScoreInfo>().FirstOrDefault(query)?.Detach();
} }
/// <summary> /// <summary>
@ -255,16 +254,16 @@ namespace osu.Game.Scoring
public void Delete([CanBeNull] Expression<Func<ScoreInfo, bool>> filter = null, bool silent = false) public void Delete([CanBeNull] Expression<Func<ScoreInfo, bool>> filter = null, bool silent = false)
{ {
using (var context = contextFactory.CreateContext()) contextFactory.Run(realm =>
{ {
var items = context.All<ScoreInfo>() var items = realm.All<ScoreInfo>()
.Where(s => !s.DeletePending); .Where(s => !s.DeletePending);
if (filter != null) if (filter != null)
items = items.Where(filter); items = items.Where(filter);
scoreModelManager.Delete(items.ToList(), silent); scoreModelManager.Delete(items.ToList(), silent);
} });
} }
public void Delete(List<ScoreInfo> items, bool silent = false) public void Delete(List<ScoreInfo> items, bool silent = false)

View File

@ -74,8 +74,7 @@ namespace osu.Game.Scoring
public override bool IsAvailableLocally(ScoreInfo model) public override bool IsAvailableLocally(ScoreInfo model)
{ {
using (var context = ContextFactory.CreateContext()) return ContextFactory.Run(realm => realm.All<ScoreInfo>().Any(s => s.OnlineID == model.OnlineID));
return context.All<ScoreInfo>().Any(b => b.OnlineID == model.OnlineID);
} }
} }
} }

View File

@ -114,9 +114,10 @@ namespace osu.Game.Screens.Select
{ {
CarouselRoot newRoot = new CarouselRoot(this); CarouselRoot newRoot = new CarouselRoot(this);
newRoot.AddChildren(beatmapSets.Select(createCarouselSet).Where(g => g != null)); newRoot.AddChildren(beatmapSets.Select(s => createCarouselSet(s.Detach())).Where(g => g != null));
root = newRoot; root = newRoot;
if (selectedBeatmapSet != null && !beatmapSets.Contains(selectedBeatmapSet.BeatmapSet)) if (selectedBeatmapSet != null && !beatmapSets.Contains(selectedBeatmapSet.BeatmapSet))
selectedBeatmapSet = null; selectedBeatmapSet = null;
@ -178,8 +179,7 @@ namespace osu.Game.Screens.Select
if (!loadedTestBeatmaps) if (!loadedTestBeatmaps)
{ {
using (var realm = realmFactory.CreateContext()) realmFactory.Run(realm => loadBeatmapSets(getBeatmapSets(realm)));
loadBeatmapSets(getBeatmapSets(realm));
} }
} }
@ -209,7 +209,7 @@ namespace osu.Game.Screens.Select
return; return;
foreach (int i in changes.InsertedIndices) foreach (int i in changes.InsertedIndices)
RemoveBeatmapSet(sender[i]); removeBeatmapSet(sender[i].ID);
} }
private void beatmapSetsChanged(IRealmCollection<BeatmapSetInfo> sender, ChangeSet changes, Exception error) private void beatmapSetsChanged(IRealmCollection<BeatmapSetInfo> sender, ChangeSet changes, Exception error)
@ -223,24 +223,21 @@ namespace osu.Game.Screens.Select
// During initial population, we must manually account for the fact that our original query was done on an async thread. // During initial population, we must manually account for the fact that our original query was done on an async thread.
// Since then, there may have been imports or deletions. // Since then, there may have been imports or deletions.
// Here we manually catch up on any changes. // Here we manually catch up on any changes.
var populatedSets = new HashSet<Guid>();
foreach (var s in beatmapSets)
populatedSets.Add(s.BeatmapSet.ID);
var realmSets = new HashSet<Guid>(); var realmSets = new HashSet<Guid>();
foreach (var s in sender)
realmSets.Add(s.ID);
foreach (var s in realmSets) for (int i = 0; i < sender.Count; i++)
realmSets.Add(sender[i].ID);
foreach (var id in realmSets)
{ {
if (!populatedSets.Contains(s)) if (!root.BeatmapSetsByID.ContainsKey(id))
UpdateBeatmapSet(realmFactory.Context.Find<BeatmapSetInfo>(s)); UpdateBeatmapSet(realmFactory.Context.Find<BeatmapSetInfo>(id).Detach());
} }
foreach (var s in populatedSets) foreach (var id in root.BeatmapSetsByID.Keys)
{ {
if (!realmSets.Contains(s)) if (!realmSets.Contains(id))
RemoveBeatmapSet(realmFactory.Context.Find<BeatmapSetInfo>(s)); removeBeatmapSet(id);
} }
signalBeatmapsLoaded(); signalBeatmapsLoaded();
@ -248,10 +245,10 @@ namespace osu.Game.Screens.Select
} }
foreach (int i in changes.NewModifiedIndices) foreach (int i in changes.NewModifiedIndices)
UpdateBeatmapSet(sender[i]); UpdateBeatmapSet(sender[i].Detach());
foreach (int i in changes.InsertedIndices) foreach (int i in changes.InsertedIndices)
UpdateBeatmapSet(sender[i]); UpdateBeatmapSet(sender[i].Detach());
} }
private void beatmapsChanged(IRealmCollection<BeatmapInfo> sender, ChangeSet changes, Exception error) private void beatmapsChanged(IRealmCollection<BeatmapInfo> sender, ChangeSet changes, Exception error)
@ -261,16 +258,30 @@ namespace osu.Game.Screens.Select
return; return;
foreach (int i in changes.InsertedIndices) foreach (int i in changes.InsertedIndices)
UpdateBeatmapSet(sender[i].BeatmapSet); {
var beatmapInfo = sender[i];
var beatmapSet = beatmapInfo.BeatmapSet;
Debug.Assert(beatmapSet != null);
// Only require to action here if the beatmap is missing.
// This avoids processing these events unnecessarily when new beatmaps are imported, for example.
if (root.BeatmapSetsByID.TryGetValue(beatmapSet.ID, out var existingSet)
&& existingSet.BeatmapSet.Beatmaps.All(b => b.ID != beatmapInfo.ID))
{
UpdateBeatmapSet(beatmapSet.Detach());
}
}
} }
private IRealmCollection<BeatmapSetInfo> getBeatmapSets(Realm realm) => realm.All<BeatmapSetInfo>().Where(s => !s.DeletePending && !s.Protected).AsRealmCollection(); private IRealmCollection<BeatmapSetInfo> getBeatmapSets(Realm realm) => realm.All<BeatmapSetInfo>().Where(s => !s.DeletePending && !s.Protected).AsRealmCollection();
public void RemoveBeatmapSet(BeatmapSetInfo beatmapSet) => Schedule(() => public void RemoveBeatmapSet(BeatmapSetInfo beatmapSet) =>
{ removeBeatmapSet(beatmapSet.ID);
var existingSet = beatmapSets.FirstOrDefault(b => b.BeatmapSet.Equals(beatmapSet));
if (existingSet == null) private void removeBeatmapSet(Guid beatmapSetID) => Schedule(() =>
{
if (!root.BeatmapSetsByID.TryGetValue(beatmapSetID, out var existingSet))
return; return;
root.RemoveChild(existingSet); root.RemoveChild(existingSet);
@ -281,35 +292,32 @@ namespace osu.Game.Screens.Select
{ {
Guid? previouslySelectedID = null; Guid? previouslySelectedID = null;
CarouselBeatmapSet existingSet = beatmapSets.FirstOrDefault(b => b.BeatmapSet.Equals(beatmapSet));
// If the selected beatmap is about to be removed, store its ID so it can be re-selected if required // If the selected beatmap is about to be removed, store its ID so it can be re-selected if required
if (existingSet?.State?.Value == CarouselItemState.Selected) if (selectedBeatmapSet?.BeatmapSet.ID == beatmapSet.ID)
previouslySelectedID = selectedBeatmap?.BeatmapInfo.ID; previouslySelectedID = selectedBeatmap?.BeatmapInfo.ID;
var newSet = createCarouselSet(beatmapSet); var newSet = createCarouselSet(beatmapSet);
if (existingSet != null) root.RemoveChild(beatmapSet.ID);
root.RemoveChild(existingSet);
if (newSet == null) if (newSet != null)
{ {
itemsCache.Invalidate();
return;
}
root.AddChild(newSet); 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. // check if we can/need to maintain our current selection.
if (previouslySelectedID != null) if (previouslySelectedID != null)
select((CarouselItem)newSet.Beatmaps.FirstOrDefault(b => b.BeatmapInfo.ID == previouslySelectedID) ?? newSet); select((CarouselItem)newSet.Beatmaps.FirstOrDefault(b => b.BeatmapInfo.ID == previouslySelectedID) ?? newSet);
}
itemsCache.Invalidate(); itemsCache.Invalidate();
Schedule(() => BeatmapSetsChanged?.Invoke());
Schedule(() =>
{
if (!Scroll.UserScrolling)
ScrollToSelected(true);
BeatmapSetsChanged?.Invoke();
});
}); });
/// <summary> /// <summary>
@ -711,8 +719,6 @@ namespace osu.Game.Screens.Select
private CarouselBeatmapSet createCarouselSet(BeatmapSetInfo beatmapSet) private CarouselBeatmapSet createCarouselSet(BeatmapSetInfo beatmapSet)
{ {
beatmapSet = beatmapSet.Detach();
// This can be moved to the realm query if required using: // This can be moved to the realm query if required using:
// .Filter("DeletePending == false && Protected == false && ANY Beatmaps.Hidden == false") // .Filter("DeletePending == false && Protected == false && ANY Beatmaps.Hidden == false")
// //
@ -913,6 +919,8 @@ namespace osu.Game.Screens.Select
{ {
private readonly BeatmapCarousel carousel; private readonly BeatmapCarousel carousel;
public readonly Dictionary<Guid, CarouselBeatmapSet> BeatmapSetsByID = new Dictionary<Guid, CarouselBeatmapSet>();
public CarouselRoot(BeatmapCarousel carousel) public CarouselRoot(BeatmapCarousel carousel)
{ {
// root should always remain selected. if not, PerformSelection will not be called. // root should always remain selected. if not, PerformSelection will not be called.
@ -922,6 +930,28 @@ namespace osu.Game.Screens.Select
this.carousel = carousel; this.carousel = carousel;
} }
public override void AddChild(CarouselItem i)
{
CarouselBeatmapSet set = (CarouselBeatmapSet)i;
BeatmapSetsByID.Add(set.BeatmapSet.ID, set);
base.AddChild(i);
}
public void RemoveChild(Guid beatmapSetID)
{
if (BeatmapSetsByID.TryGetValue(beatmapSetID, out var carouselBeatmapSet))
RemoveChild(carouselBeatmapSet);
}
public override void RemoveChild(CarouselItem i)
{
CarouselBeatmapSet set = (CarouselBeatmapSet)i;
BeatmapSetsByID.Remove(set.BeatmapSet.ID);
base.RemoveChild(i);
}
protected override void PerformSelection() protected override void PerformSelection()
{ {
if (LastSelected == null || LastSelected.Filtered.Value) if (LastSelected == null || LastSelected.Filtered.Value)

View File

@ -4,6 +4,8 @@
using System.Collections.Generic; using System.Collections.Generic;
using System.Linq; using System.Linq;
#nullable enable
namespace osu.Game.Screens.Select.Carousel namespace osu.Game.Screens.Select.Carousel
{ {
/// <summary> /// <summary>
@ -11,7 +13,7 @@ namespace osu.Game.Screens.Select.Carousel
/// </summary> /// </summary>
public class CarouselGroup : CarouselItem public class CarouselGroup : CarouselItem
{ {
public override DrawableCarouselItem CreateDrawableRepresentation() => null; public override DrawableCarouselItem? CreateDrawableRepresentation() => null;
public IReadOnlyList<CarouselItem> Children => InternalChildren; public IReadOnlyList<CarouselItem> Children => InternalChildren;
@ -23,6 +25,10 @@ namespace osu.Game.Screens.Select.Carousel
/// </summary> /// </summary>
private ulong currentChildID; private ulong currentChildID;
private Comparer<CarouselItem>? criteriaComparer;
private FilterCriteria? lastCriteria;
public virtual void RemoveChild(CarouselItem i) public virtual void RemoveChild(CarouselItem i)
{ {
InternalChildren.Remove(i); InternalChildren.Remove(i);
@ -36,10 +42,24 @@ namespace osu.Game.Screens.Select.Carousel
{ {
i.State.ValueChanged += state => ChildItemStateChanged(i, state.NewValue); i.State.ValueChanged += state => ChildItemStateChanged(i, state.NewValue);
i.ChildID = ++currentChildID; i.ChildID = ++currentChildID;
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); InternalChildren.Add(i);
} }
}
public CarouselGroup(List<CarouselItem> items = null) public CarouselGroup(List<CarouselItem>? items = null)
{ {
if (items != null) InternalChildren = items; if (items != null) InternalChildren = items;
@ -67,9 +87,12 @@ namespace osu.Game.Screens.Select.Carousel
base.Filter(criteria); base.Filter(criteria);
InternalChildren.ForEach(c => c.Filter(criteria)); InternalChildren.ForEach(c => c.Filter(criteria));
// IEnumerable<T>.OrderBy() is used instead of List<T>.Sort() to ensure sorting stability // IEnumerable<T>.OrderBy() is used instead of List<T>.Sort() to ensure sorting stability
var criteriaComparer = Comparer<CarouselItem>.Create((x, y) => x.CompareTo(criteria, y)); criteriaComparer = Comparer<CarouselItem>.Create((x, y) => x.CompareTo(criteria, y));
InternalChildren = InternalChildren.OrderBy(c => c, criteriaComparer).ToList(); InternalChildren = InternalChildren.OrderBy(c => c, criteriaComparer).ToList();
lastCriteria = criteria;
} }
protected virtual void ChildItemStateChanged(CarouselItem item, CarouselItemState value) protected virtual void ChildItemStateChanged(CarouselItem item, CarouselItemState value)

View File

@ -55,10 +55,16 @@ namespace osu.Game.Screens.Select.Carousel
updateSelectedIndex(); updateSelectedIndex();
} }
private bool addingChildren;
public void AddChildren(IEnumerable<CarouselItem> items) public void AddChildren(IEnumerable<CarouselItem> items)
{ {
addingChildren = true;
foreach (var i in items) foreach (var i in items)
base.AddChild(i); AddChild(i);
addingChildren = false;
attemptSelection(); attemptSelection();
} }
@ -66,6 +72,7 @@ namespace osu.Game.Screens.Select.Carousel
public override void AddChild(CarouselItem i) public override void AddChild(CarouselItem i)
{ {
base.AddChild(i); base.AddChild(i);
if (!addingChildren)
attemptSelection(); attemptSelection();
} }

View File

@ -147,7 +147,7 @@ namespace osu.Game.Screens.Select.Leaderboards
if (Scope == BeatmapLeaderboardScope.Local) if (Scope == BeatmapLeaderboardScope.Local)
{ {
using (var realm = realmFactory.CreateContext()) realmFactory.Run(realm =>
{ {
var scores = realm.All<ScoreInfo>() var scores = realm.All<ScoreInfo>()
.AsEnumerable() .AsEnumerable()
@ -171,10 +171,10 @@ namespace osu.Game.Screens.Select.Leaderboards
scoreManager.OrderByTotalScoreAsync(scores.ToArray(), cancellationToken) scoreManager.OrderByTotalScoreAsync(scores.ToArray(), cancellationToken)
.ContinueWith(ordered => scoresCallback?.Invoke(ordered.GetResultSafely()), TaskContinuationOptions.OnlyOnRanToCompletion); .ContinueWith(ordered => scoresCallback?.Invoke(ordered.GetResultSafely()), TaskContinuationOptions.OnlyOnRanToCompletion);
});
return null; return null;
} }
}
if (api?.IsLoggedIn != true) if (api?.IsLoggedIn != true)
{ {

View File

@ -87,17 +87,14 @@ namespace osu.Game.Skinning
}; };
// Ensure the default entries are present. // Ensure the default entries are present.
using (var context = contextFactory.CreateContext()) contextFactory.Write(realm =>
using (var transaction = context.BeginWrite())
{ {
foreach (var skin in defaultSkins) foreach (var skin in defaultSkins)
{ {
if (context.Find<SkinInfo>(skin.SkinInfo.ID) == null) if (realm.Find<SkinInfo>(skin.SkinInfo.ID) == null)
context.Add(skin.SkinInfo.Value); realm.Add(skin.SkinInfo.Value);
}
transaction.Commit();
} }
});
CurrentSkinInfo.ValueChanged += skin => CurrentSkin.Value = skin.NewValue.PerformRead(GetSkin); CurrentSkinInfo.ValueChanged += skin => CurrentSkin.Value = skin.NewValue.PerformRead(GetSkin);
@ -113,10 +110,10 @@ namespace osu.Game.Skinning
public void SelectRandomSkin() public void SelectRandomSkin()
{ {
using (var context = contextFactory.CreateContext()) contextFactory.Run(realm =>
{ {
// choose from only user skins, removing the current selection to ensure a new one is chosen. // choose from only user skins, removing the current selection to ensure a new one is chosen.
var randomChoices = context.All<SkinInfo>().Where(s => !s.DeletePending && s.ID != CurrentSkinInfo.Value.ID).ToArray(); var randomChoices = realm.All<SkinInfo>().Where(s => !s.DeletePending && s.ID != CurrentSkinInfo.Value.ID).ToArray();
if (randomChoices.Length == 0) if (randomChoices.Length == 0)
{ {
@ -127,7 +124,7 @@ namespace osu.Game.Skinning
var chosen = randomChoices.ElementAt(RNG.Next(0, randomChoices.Length)); var chosen = randomChoices.ElementAt(RNG.Next(0, randomChoices.Length));
CurrentSkinInfo.Value = chosen.ToLive(contextFactory); CurrentSkinInfo.Value = chosen.ToLive(contextFactory);
} });
} }
/// <summary> /// <summary>
@ -182,8 +179,7 @@ namespace osu.Game.Skinning
/// <returns>The first result for the provided query, or null if no results were found.</returns> /// <returns>The first result for the provided query, or null if no results were found.</returns>
public ILive<SkinInfo> Query(Expression<Func<SkinInfo, bool>> query) public ILive<SkinInfo> Query(Expression<Func<SkinInfo, bool>> query)
{ {
using (var context = contextFactory.CreateContext()) return contextFactory.Run(realm => realm.All<SkinInfo>().FirstOrDefault(query)?.ToLive(contextFactory));
return context.All<SkinInfo>().FirstOrDefault(query)?.ToLive(contextFactory);
} }
public event Action SourceChanged; public event Action SourceChanged;
@ -293,9 +289,9 @@ namespace osu.Game.Skinning
public void Delete([CanBeNull] Expression<Func<SkinInfo, bool>> filter = null, bool silent = false) public void Delete([CanBeNull] Expression<Func<SkinInfo, bool>> filter = null, bool silent = false)
{ {
using (var context = contextFactory.CreateContext()) contextFactory.Run(realm =>
{ {
var items = context.All<SkinInfo>() var items = realm.All<SkinInfo>()
.Where(s => !s.Protected && !s.DeletePending); .Where(s => !s.Protected && !s.DeletePending);
if (filter != null) if (filter != null)
items = items.Where(filter); items = items.Where(filter);
@ -307,7 +303,7 @@ namespace osu.Game.Skinning
scheduler.Add(() => CurrentSkinInfo.Value = Skinning.DefaultSkin.CreateInfo().ToLiveUnmanaged()); scheduler.Add(() => CurrentSkinInfo.Value = Skinning.DefaultSkin.CreateInfo().ToLiveUnmanaged());
skinModelManager.Delete(items.ToList(), silent); skinModelManager.Delete(items.ToList(), silent);
} });
} }
#endregion #endregion

View File

@ -205,7 +205,7 @@ namespace osu.Game.Skinning
private void populateMissingHashes() private void populateMissingHashes()
{ {
using (var realm = ContextFactory.CreateContext()) ContextFactory.Run(realm =>
{ {
var skinsWithoutHashes = realm.All<SkinInfo>().Where(i => !i.Protected && string.IsNullOrEmpty(i.Hash)).ToArray(); var skinsWithoutHashes = realm.All<SkinInfo>().Where(i => !i.Protected && string.IsNullOrEmpty(i.Hash)).ToArray();
@ -221,7 +221,7 @@ namespace osu.Game.Skinning
Logger.Error(e, $"Existing skin {skin} has been deleted during hash recomputation due to being invalid"); Logger.Error(e, $"Existing skin {skin} has been deleted during hash recomputation due to being invalid");
} }
} }
} });
} }
private Skin createInstance(SkinInfo item) => item.CreateInstance(skinResources); private Skin createInstance(SkinInfo item) => item.CreateInstance(skinResources);

View File

@ -165,8 +165,7 @@ namespace osu.Game.Stores
public override bool IsAvailableLocally(BeatmapSetInfo model) public override bool IsAvailableLocally(BeatmapSetInfo model)
{ {
using (var context = ContextFactory.CreateContext()) return ContextFactory.Run(realm => realm.All<BeatmapInfo>().Any(b => b.OnlineID == model.OnlineID));
return context.All<BeatmapInfo>().Any(b => b.OnlineID == model.OnlineID);
} }
public override string HumanisedModelName => "beatmap"; public override string HumanisedModelName => "beatmap";

View File

@ -320,7 +320,7 @@ namespace osu.Game.Stores
/// <param name="cancellationToken">An optional cancellation token.</param> /// <param name="cancellationToken">An optional cancellation token.</param>
public virtual Task<ILive<TModel>?> Import(TModel item, ArchiveReader? archive = null, bool lowPriority = false, CancellationToken cancellationToken = default) public virtual Task<ILive<TModel>?> Import(TModel item, ArchiveReader? archive = null, bool lowPriority = false, CancellationToken cancellationToken = default)
{ {
using (var realm = ContextFactory.CreateContext()) return ContextFactory.Run(realm =>
{ {
cancellationToken.ThrowIfCancellationRequested(); cancellationToken.ThrowIfCancellationRequested();
@ -414,7 +414,7 @@ namespace osu.Game.Stores
} }
return Task.FromResult((ILive<TModel>?)item.ToLive(ContextFactory)); return Task.FromResult((ILive<TModel>?)item.ToLive(ContextFactory));
} });
} }
private string computeHashFast(ArchiveReader reader) private string computeHashFast(ArchiveReader reader)

View File

@ -165,7 +165,7 @@ namespace osu.Game.Stores
public bool Delete(TModel item) public bool Delete(TModel item)
{ {
using (var realm = ContextFactory.CreateContext()) return ContextFactory.Run(realm =>
{ {
if (!item.IsManaged) if (!item.IsManaged)
item = realm.Find<TModel>(item.ID); item = realm.Find<TModel>(item.ID);
@ -175,12 +175,12 @@ namespace osu.Game.Stores
realm.Write(r => item.DeletePending = true); realm.Write(r => item.DeletePending = true);
return true; return true;
} });
} }
public void Undelete(TModel item) public void Undelete(TModel item)
{ {
using (var realm = ContextFactory.CreateContext()) ContextFactory.Run(realm =>
{ {
if (!item.IsManaged) if (!item.IsManaged)
item = realm.Find<TModel>(item.ID); item = realm.Find<TModel>(item.ID);
@ -189,7 +189,7 @@ namespace osu.Game.Stores
return; return;
realm.Write(r => item.DeletePending = false); realm.Write(r => item.DeletePending = false);
} });
} }
public abstract bool IsAvailableLocally(TModel model); public abstract bool IsAvailableLocally(TModel model);

View File

@ -92,8 +92,7 @@ namespace osu.Game.Stores
int removedFiles = 0; int removedFiles = 0;
// can potentially be run asynchronously, although we will need to consider operation order for disk deletion vs realm removal. // can potentially be run asynchronously, although we will need to consider operation order for disk deletion vs realm removal.
using (var realm = realmFactory.CreateContext()) realmFactory.Write(realm =>
using (var transaction = realm.BeginWrite())
{ {
// TODO: consider using a realm native query to avoid iterating all files (https://github.com/realm/realm-dotnet/issues/2659#issuecomment-927823707) // TODO: consider using a realm native query to avoid iterating all files (https://github.com/realm/realm-dotnet/issues/2659#issuecomment-927823707)
var files = realm.All<RealmFile>().ToList(); var files = realm.All<RealmFile>().ToList();
@ -116,9 +115,7 @@ namespace osu.Game.Stores
Logger.Error(e, $@"Could not delete databased file {file.Hash}"); Logger.Error(e, $@"Could not delete databased file {file.Hash}");
} }
} }
});
transaction.Commit();
}
Logger.Log($@"Finished realm file store cleanup ({removedFiles} of {totalFiles} deleted)"); Logger.Log($@"Finished realm file store cleanup ({removedFiles} of {totalFiles} deleted)");
} }