// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. // See the LICENCE file in the repository root for full licence text. using System; using System.Diagnostics; using osu.Framework.Development; using osu.Framework.Statistics; using Realms; #nullable enable namespace osu.Game.Database { /// /// Provides a method of working with realm objects over longer application lifetimes. /// /// The underlying object type. public class RealmLive : ILive where T : RealmObject, IHasGuidPrimaryKey { public Guid ID { get; } public bool IsManaged => data.IsManaged; /// /// The original live data used to create this instance. /// private T data; private bool dataIsFromUpdateThread; private readonly RealmAccess realm; /// /// Construct a new instance of live realm data. /// /// The realm data. /// The realm factory the data was sourced from. May be null for an unmanaged object. public RealmLive(T data, RealmAccess realm) { this.data = data; this.realm = realm; ID = data.ID; dataIsFromUpdateThread = ThreadSafety.IsUpdateThread; } /// /// Perform a read operation on this live object. /// /// The action to perform. public void PerformRead(Action perform) { if (!IsManaged) { perform(data); return; } realm.Run(r => { if (ThreadSafety.IsUpdateThread) { ensureDataIsFromUpdateThread(); perform(data); return; } perform(retrieveFromID(r, ID)); RealmLiveStatistics.USAGE_ASYNC.Value++; }); } /// /// Perform a read operation on this live object. /// /// The action to perform. public TReturn PerformRead(Func perform) { if (!IsManaged) return perform(data); if (ThreadSafety.IsUpdateThread) { ensureDataIsFromUpdateThread(); return perform(data); } return realm.Run(r => { var returnData = perform(retrieveFromID(r, ID)); RealmLiveStatistics.USAGE_ASYNC.Value++; if (returnData is RealmObjectBase realmObject && realmObject.IsManaged) throw new InvalidOperationException(@$"Managed realm objects should not exit the scope of {nameof(PerformRead)}."); return returnData; }); } /// /// Perform a write operation on this live object. /// /// The action to perform. public void PerformWrite(Action perform) { if (!IsManaged) throw new InvalidOperationException(@"Can't perform writes on a non-managed underlying value"); PerformRead(t => { var transaction = t.Realm.BeginWrite(); perform(t); transaction.Commit(); RealmLiveStatistics.WRITES.Value++; }); } public T Value { get { if (!IsManaged) return data; if (!ThreadSafety.IsUpdateThread) throw new InvalidOperationException($"Can't use {nameof(Value)} on managed objects from non-update threads"); ensureDataIsFromUpdateThread(); return data; } } private void ensureDataIsFromUpdateThread() { Debug.Assert(ThreadSafety.IsUpdateThread); if (dataIsFromUpdateThread && !data.Realm.IsClosed) { RealmLiveStatistics.USAGE_UPDATE_IMMEDIATE.Value++; return; } dataIsFromUpdateThread = true; data = retrieveFromID(realm.Realm, ID); RealmLiveStatistics.USAGE_UPDATE_REFETCH.Value++; } private T retrieveFromID(Realm realm, Guid id) { var found = realm.Find(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(ID); } return found; } public bool Equals(ILive? other) => ID == other?.ID; public override string ToString() => PerformRead(i => i.ToString()); } internal static class RealmLiveStatistics { public static readonly GlobalStatistic WRITES = GlobalStatistics.Get(@"Realm", @"Live writes"); public static readonly GlobalStatistic USAGE_UPDATE_IMMEDIATE = GlobalStatistics.Get(@"Realm", @"Live update read (fast)"); public static readonly GlobalStatistic USAGE_UPDATE_REFETCH = GlobalStatistics.Get(@"Realm", @"Live update read (slow)"); public static readonly GlobalStatistic USAGE_ASYNC = GlobalStatistics.Get(@"Realm", @"Live async read"); } }