diff --git a/IPA.Loader/Config/ConfigRuntime.cs b/IPA.Loader/Config/ConfigRuntime.cs index 0729d5ff..d9c4a203 100644 --- a/IPA.Loader/Config/ConfigRuntime.cs +++ b/IPA.Loader/Config/ConfigRuntime.cs @@ -64,19 +64,19 @@ namespace IPA.Config => ShutdownRuntime(); internal static void ShutdownRuntime() { - try - { - watcherTrackConfigs.Clear(); - var watchList = watchers.ToArray(); - watchers.Clear(); - - foreach (var pair in watchList) - pair.Value.EnableRaisingEvents = false; - - loadScheduler.Join(); // we can wait for the loads to finish - saveThread.Abort(); // eww, but i don't like any of the other potential solutions - - SaveAll(); + try + { + watcherTrackConfigs.Clear(); + var watchList = watchers.ToArray(); + watchers.Clear(); + + foreach (var pair in watchList) + pair.Value.EnableRaisingEvents = false; + + loadScheduler.Join(); // we can wait for the loads to finish + saveThread.Abort(); // eww, but i don't like any of the other potential solutions + + SaveAll(); } catch { diff --git a/IPA.Loader/Config/Stores/CustomObjectConverter.cs b/IPA.Loader/Config/Stores/CustomObjectConverter.cs index e361a3e2..be881cc7 100644 --- a/IPA.Loader/Config/Stores/CustomObjectConverter.cs +++ b/IPA.Loader/Config/Stores/CustomObjectConverter.cs @@ -85,56 +85,56 @@ namespace IPA.Config.Stores.Converters => Serialize(obj, parent); } - /// - /// A for custom value types, serialized identically to the reference types serialized with - /// . - /// + /// + /// A for custom value types, serialized identically to the reference types serialized with + /// . + /// /// the type of the value to convert - public class CustomValueTypeConverter : ValueConverter where T : struct - { - private static readonly GeneratedStoreImpl.SerializeObject serialize - = GeneratedStoreImpl.GetSerializerDelegate(); - private static readonly GeneratedStoreImpl.DeserializeObject deserialize - = GeneratedStoreImpl.GetDeserializerDelegate(); - + public class CustomValueTypeConverter : ValueConverter where T : struct + { + private static readonly GeneratedStoreImpl.SerializeObject serialize + = GeneratedStoreImpl.GetSerializerDelegate(); + private static readonly GeneratedStoreImpl.DeserializeObject deserialize + = GeneratedStoreImpl.GetDeserializerDelegate(); + /// /// Deserializes into a with the given . /// /// the to deserialize /// the parent object that will own the deserialized value /// the deserialized value - /// - public static T Deserialize(Value value, object parent) - => deserialize(value, parent); - + /// + public static T Deserialize(Value value, object parent) + => deserialize(value, parent); + /// /// Serializes into a corresponding structure. /// /// the object to serialize /// the tree that represents - /// - public static Value Serialize(T obj) - => serialize(obj); - + /// + public static Value Serialize(T obj) + => serialize(obj); + /// /// Deserializes into a with the given . /// /// the to deserialize /// the parent object that will own the deserialized value /// the deserialized value - /// - public override T FromValue(Value value, object parent) - => Deserialize(value, parent); - + /// + public override T FromValue(Value value, object parent) + => Deserialize(value, parent); + /// /// Serializes into a structure, given . /// /// the object to serialize /// the parent object that owns /// the tree that represents - /// - public override Value ToValue(T obj, object parent) - => Serialize(obj); + /// + public override Value ToValue(T obj, object parent) + => Serialize(obj); } } diff --git a/IPA.Loader/Config/Stores/GeneratedStoreImpl/GeneratedStoreImpl.cs b/IPA.Loader/Config/Stores/GeneratedStoreImpl/GeneratedStoreImpl.cs index f820202c..e75aa819 100644 --- a/IPA.Loader/Config/Stores/GeneratedStoreImpl/GeneratedStoreImpl.cs +++ b/IPA.Loader/Config/Stores/GeneratedStoreImpl/GeneratedStoreImpl.cs @@ -16,7 +16,8 @@ using Boolean = IPA.Config.Data.Boolean; using System.Collections; using IPA.Utilities; using System.ComponentModel; -using System.Collections.Concurrent; +using System.Collections.Concurrent; +using IPA.Utilities.Async; #if NET3 using Net3_Proxy; using Array = Net3_Proxy.Array; @@ -38,38 +39,13 @@ namespace IPA.Config.Stores internal static T Create(IGeneratedStore parent) where T : class => (T)Create(typeof(T), parent); private static IConfigStore Create(Type type, IGeneratedStore parent) - => GetCreator(type)(parent); - - private static readonly ConcurrentDictionary generatedCreators - = new ConcurrentDictionary(); + => GetCreator(type)(parent); + + private static readonly SingleCreationValueCache generatedCreators + = new SingleCreationValueCache(); private static (GeneratedStoreCreator ctor, Type type) GetCreatorAndGeneratedType(Type t) - { - retry: - if (generatedCreators.TryGetValue(t, out var gen)) - { - if (gen.wh != null) - { - gen.wh.Wait(); - goto retry; // this isn't really a good candidate for a loop - // the loop condition will never be hit, and this should only - // jump back to the beginning in exceptional situations - } - return (gen.ctor, gen.type); - } - else - { - var wh = new ManualResetEventSlim(false); - var cmp = (wh, (GeneratedStoreCreator)null, (Type)null); - if (!generatedCreators.TryAdd(t, cmp)) - goto retry; // someone else beat us to the punch, retry getting their value and wait for them - var (ctor, type) = MakeCreator(t); - while (!generatedCreators.TryUpdate(t, (null, ctor, type), cmp)) - throw new InvalidOperationException("Somehow, multiple MakeCreators started running for the same target type!"); - wh.Set(); - return (ctor, type); - } - } + => generatedCreators.GetOrAdd(t, MakeCreator); internal static GeneratedStoreCreator GetCreator(Type t) => GetCreatorAndGeneratedType(t).ctor; @@ -111,6 +87,7 @@ namespace IPA.Config.Stores } } + // TODO: does this need to be a SingleCreationValueCache or similar? private static readonly Dictionary> TypeRequiredConverters = new Dictionary>(); private static void CreateAndInitializeConvertersFor(Type type, IEnumerable structure) { diff --git a/IPA.Loader/Config/Stores/GeneratedStoreImpl/ObjectStructure.cs b/IPA.Loader/Config/Stores/GeneratedStoreImpl/ObjectStructure.cs index 963dd712..aa443657 100644 --- a/IPA.Loader/Config/Stores/GeneratedStoreImpl/ObjectStructure.cs +++ b/IPA.Loader/Config/Stores/GeneratedStoreImpl/ObjectStructure.cs @@ -2,6 +2,7 @@ using IPA.Config.Stores.Converters; using IPA.Logging; using IPA.Utilities; +using IPA.Utilities.Async; using System; using System.Collections.Generic; using System.Linq; @@ -127,14 +128,11 @@ namespace IPA.Config.Stores return true; } - private static readonly Dictionary objectStructureCache = new Dictionary(); + private static readonly SingleCreationValueCache objectStructureCache + = new SingleCreationValueCache(); private static IEnumerable ReadObjectMembers(Type type) - { - if (!objectStructureCache.TryGetValue(type, out var structure)) - objectStructureCache.Add(type, structure = ReadObjectMembersInternal(type).ToArray()); - return structure; - } + => objectStructureCache.GetOrAdd(type, t => ReadObjectMembersInternal(type).ToArray()); private static IEnumerable ReadObjectMembersInternal(Type type) { diff --git a/IPA.Loader/IPA.Loader.csproj b/IPA.Loader/IPA.Loader.csproj index 6354d825..c5b0d59e 100644 --- a/IPA.Loader/IPA.Loader.csproj +++ b/IPA.Loader/IPA.Loader.csproj @@ -149,6 +149,7 @@ + diff --git a/IPA.Loader/Loader/Features/Feature.cs b/IPA.Loader/Loader/Features/Feature.cs index ba5c4ecb..20d07728 100644 --- a/IPA.Loader/Loader/Features/Feature.cs +++ b/IPA.Loader/Loader/Features/Feature.cs @@ -88,6 +88,8 @@ namespace IPA.Loader.Features /// if this will be stored on the plugin metadata, otherwise protected internal virtual bool StoreOnPlugin => true; + // TODO: rework features to take arguments as JSON objects + static Feature() { Reset(); diff --git a/IPA.Loader/Loader/PluginComponent.cs b/IPA.Loader/Loader/PluginComponent.cs index e6bbd98e..add00600 100644 --- a/IPA.Loader/Loader/PluginComponent.cs +++ b/IPA.Loader/Loader/PluginComponent.cs @@ -1,8 +1,8 @@ using IPA.Config; using IPA.Loader.Composite; -using IPA.Utilities; -using IPA.Utilities.Async; -using System.Diagnostics.CodeAnalysis; +using IPA.Utilities; +using IPA.Utilities.Async; +using System.Diagnostics.CodeAnalysis; using UnityEngine; using UnityEngine.SceneManagement; // ReSharper disable UnusedMember.Local @@ -28,7 +28,7 @@ namespace IPA.Loader if (!initialized) { - UnityGame.SetMainThread(); + UnityGame.SetMainThread(); UnityGame.EnsureRuntimeGameVersion(); PluginManager.Load(); @@ -47,16 +47,16 @@ namespace IPA.Loader SceneManager.activeSceneChanged += OnActiveSceneChanged; SceneManager.sceneLoaded += OnSceneLoaded; - SceneManager.sceneUnloaded += OnSceneUnloaded; - - var unitySched = UnityMainThreadTaskScheduler.Default as UnityMainThreadTaskScheduler; - if (!unitySched.IsRunning) + SceneManager.sceneUnloaded += OnSceneUnloaded; + + var unitySched = UnityMainThreadTaskScheduler.Default as UnityMainThreadTaskScheduler; + if (!unitySched.IsRunning) StartCoroutine(unitySched.Coroutine()); - initialized = true; - -#if DEBUG - Config.Stores.GeneratedStoreImpl.DebugSaveAssembly("GeneratedAssembly.dll"); + initialized = true; + +#if DEBUG + Config.Stores.GeneratedStoreImpl.DebugSaveAssembly("GeneratedAssembly.dll"); #endif } } diff --git a/IPA.Loader/Utilities/Accessor.cs b/IPA.Loader/Utilities/Accessor.cs index ad1cd89c..1433f02c 100644 --- a/IPA.Loader/Utilities/Accessor.cs +++ b/IPA.Loader/Utilities/Accessor.cs @@ -1,8 +1,11 @@ -using System; +using IPA.Utilities.Async; +using System; +using System.Collections.Concurrent; using System.Collections.Generic; using System.Linq; using System.Reflection; using System.Reflection.Emit; +using System.Threading; #if NET3 using Net3_Proxy; using Array = Net3_Proxy.Array; @@ -25,9 +28,6 @@ namespace IPA.Utilities /// a reference to the field's value public delegate ref U Accessor(ref T obj); - // field name -> accessor - private static readonly Dictionary accessors = new Dictionary(); - private static Accessor MakeAccessor(string fieldName) { var field = typeof(T).GetField(fieldName, BindingFlags.Public | BindingFlags.NonPublic | BindingFlags.Instance | BindingFlags.DeclaredOnly); @@ -51,6 +51,9 @@ namespace IPA.Utilities return (Accessor)dyn.CreateDelegate(typeof(Accessor)); } + // field name -> accessor + private static readonly SingleCreationValueCache accessors = new SingleCreationValueCache(); + /// /// Gets an for the field named on . /// @@ -58,11 +61,7 @@ namespace IPA.Utilities /// an accessor for the field /// if the field does not exist on public static Accessor GetAccessor(string name) - { - if (!accessors.TryGetValue(name, out var accessor)) - accessors.Add(name, accessor = MakeAccessor(name)); - return accessor; - } + => accessors.GetOrAdd(name, MakeAccessor); /// /// Accesses a field for an object by name. @@ -149,8 +148,6 @@ namespace IPA.Utilities /// the new property value public delegate void Setter(ref T obj, U val); - private static readonly Dictionary props = new Dictionary(); - private static (Getter, Setter) MakeAccessors(string propName) { var prop = typeof(T).GetProperty(propName, BindingFlags.Public | BindingFlags.NonPublic | BindingFlags.Instance | BindingFlags.DeclaredOnly); @@ -201,12 +198,11 @@ namespace IPA.Utilities return (getter, setter); } + private static readonly SingleCreationValueCache props + = new SingleCreationValueCache(); + private static (Getter get, Setter set) GetAccessors(string propName) - { - if (!props.TryGetValue(propName, out var access)) - props.Add(propName, access = MakeAccessors(propName)); - return access; - } + => props.GetOrAdd(propName, MakeAccessors); /// /// Gets a for the property identified by . @@ -289,8 +285,6 @@ namespace IPA.Utilities /// the delegate type to create, and to use as a signature to search for public static class MethodAccessor where TDelegate : Delegate { - private static readonly Dictionary methods = new Dictionary(); - static MethodAccessor() { // ensure that first argument of delegate type is valid @@ -325,6 +319,8 @@ namespace IPA.Utilities return (TDelegate)Delegate.CreateDelegate(AccessorDelegateInfo.Type, method, true); } + private static readonly SingleCreationValueCache methods = new SingleCreationValueCache(); + /// /// Gets a delegate to the named method with the signature specified by . /// @@ -333,11 +329,7 @@ namespace IPA.Utilities /// if does not represent the name of a method with the given signature /// if the method found returns a type incompatable with the return type of public static TDelegate GetDelegate(string name) - { - if (!methods.TryGetValue(name, out var del)) - methods.Add(name, del = MakeDelegate(name)); - return del; - } + => methods.GetOrAdd(name, MakeDelegate); } } diff --git a/IPA.Loader/Utilities/Async/SingleCreationValueCache.cs b/IPA.Loader/Utilities/Async/SingleCreationValueCache.cs new file mode 100644 index 00000000..71d42e8a --- /dev/null +++ b/IPA.Loader/Utilities/Async/SingleCreationValueCache.cs @@ -0,0 +1,154 @@ +using System; +using System.Collections; +using System.Collections.Concurrent; +using System.Collections.Generic; +using System.Diagnostics; +using System.Linq; +using System.Reflection; +using System.Text; +using System.Threading; +using System.Threading.Tasks; + +namespace IPA.Utilities.Async +{ + /// + /// A dictionary-like type intended for thread-safe value caches whose values are created only once ever. + /// + /// the key type of the cache + /// the value type of the cache + /// + /// This object basically wraps a with some special handling + /// to ensure that values are only created once ever, without having multiple parallel constructions. + /// + public class SingleCreationValueCache + { + private readonly ConcurrentDictionary dict; + + private static KeyValuePair ExpandKeyValuePair(KeyValuePair kvp) + => new KeyValuePair(kvp.Key, (null, kvp.Value)); + private static KeyValuePair CompressKeyValuePair(KeyValuePair kvp) + => new KeyValuePair(kvp.Key, kvp.Value.value); + + #region Constructors + /// + /// Initializes a new instance of the + /// class that is empty, has the default concurrency level, has the default initial + /// capacity, and uses the default comparer for the key type. + /// + public SingleCreationValueCache() + => dict = new ConcurrentDictionary(); + /// + /// Initializes a new instance of the + /// class that contains elements copied from the specified , + /// has the default concurrency level, has the default initial capacity, and uses + /// the default comparer for the key type. + /// + /// the whose element are to be used for the new cache + /// when any arguments are null + /// contains duplicate keys + public SingleCreationValueCache(IEnumerable> collection) + => dict = new ConcurrentDictionary(collection.Select(ExpandKeyValuePair)); + /// + /// Initializes a new instance of the + /// class that is empty, has the default concurrency level and capacity, and uses + /// the specified . + /// + /// the equality comparer to use when comparing keys + /// is null + public SingleCreationValueCache(IEqualityComparer comparer) + => dict = new ConcurrentDictionary(comparer); + /// + /// Initializes a new instance of the + /// class that contains elements copied from the specified + /// has the default concurrency level, has the default initial capacity, and uses + /// the specified . + /// + /// the whose elements are to be used for the new cache + /// the equality comparer to use when comparing keys + /// or is null + public SingleCreationValueCache(IEnumerable> collection, IEqualityComparer comparer) + => dict = new ConcurrentDictionary(collection.Select(ExpandKeyValuePair), comparer); + #endregion + + /// + /// Gets a value that indicates whether this cache is empty. + /// + public bool IsEmpty => dict.IsEmpty; + /// + /// Gets the number of elements that this cache contains. + /// + public int Count => dict.Count; + + /// + /// Clears the cache. + /// + public void Clear() => dict.Clear(); + /// + /// Gets a value indicating whether or not this cache contains . + /// + /// the key to search for + /// if the cache contains the key, otherwise + public bool ContainsKey(TKey key) => dict.ContainsKey(key); + /// + /// Copies the key-value pairs stored by the cache to a new array, filtering all elements that are currently being + /// created. + /// + /// an array containing a snapshot of the key-value pairs contained in this cache + public KeyValuePair[] ToArray() + => dict.ToArray().Where(k => k.Value.wh == null).Select(CompressKeyValuePair).ToArray(); + + /// + /// Attempts to get the value associated with the specified key from the cache. + /// + /// the key to search for + /// the value retrieved, if any + /// if the value was found, otherwise + public bool TryGetValue(TKey key, out TValue value) + { + if (dict.TryGetValue(key, out var pair) && pair.wh != null) + { + value = pair.val; + return true; + } + value = default; + return false; + } + + /// + /// Gets the value associated with the specified key from the cache. If it does not exist, and + /// no creators are currently running for this key, then the creator is called to create the value + /// and the value is added to the cache. If there is a creator currently running for the key, then + /// this waits for the creator to finish and retrieves the value. + /// + /// the key to search for + /// the delegate to use to create the value if it does not exist + /// the value that was found, or the result of + public TValue GetOrAdd(TKey key, Func creator) + { + retry: + if (dict.TryGetValue(key, out var value)) + { + if (value.wh != null) + { + value.wh.Wait(); + goto retry; // this isn't really a good candidate for a loop + // the loop condition will never be hit, and this should only + // jump back to the beginning in exceptional situations + } + return value.val; + } + else + { + var wh = new ManualResetEventSlim(false); + var cmp = (wh, default(TValue)); + if (!dict.TryAdd(key, cmp)) + goto retry; // someone else beat us to the punch, retry getting their value and wait for them + var val = creator(key); + while (!dict.TryUpdate(key, (null, val), cmp)) + throw new InvalidOperationException(); + wh.Set(); + return val; + } + } + } +}