// Copyright (c) 2023 bradson // This Source Code Form is subject to the terms of the Mozilla Public // License, v. 2.0. If a copy of the MPL was not distributed with this // file, You can obtain one at https://mozilla.org/MPL/2.0/. using System.Collections.Concurrent; using System.Diagnostics.CodeAnalysis; using System.Linq; using System.Reflection.Emit; using JetBrains.Annotations; namespace PerformanceFish.Cache; public static class Utility { public static event Action? ThingDestroyed; // TODO private static volatile int _destroyedThingCounter, _lastGCTick, _gcRate = 1000; private static int _currentGCProgress; private static volatile bool _gcInProgress; private static ICollection[]? _cachesToProcessForGC; private static object _gcLock = new(); public static void NotifyThingDestroyed(Thing thing) { ThingDestroyed?.Invoke(thing); if (Interlocked.Increment(ref _destroyedThingCounter) < 100 || Current.programStateInt != ProgramState.Playing || TickHelper.TicksGame - _lastGCTick < _gcRate) { return; } ContinueGC(); } [MethodImpl(MethodImplOptions.NoInlining)] private static void ContinueGC() { lock (_gcLock) { if (_gcInProgress) return; _gcInProgress = true; if (_cachesToProcessForGC is null || _currentGCProgress >= _cachesToProcessForGC.Length) { _cachesToProcessForGC = All.ToArray(); _gcRate = GenDate.TicksPerTwelfth / _cachesToProcessForGC.CountNonEmpty(); _currentGCProgress = 0; _destroyedThingCounter = 0; } while (_cachesToProcessForGC[_currentGCProgress].Count == 0 && _cachesToProcessForGC.Length - _currentGCProgress > 1) { _currentGCProgress++; } if (_currentGCProgress < _cachesToProcessForGC.Length) _cachesToProcessForGC[_currentGCProgress++].Clear(); _lastGCTick = TickHelper.TicksGame; _gcInProgress = false; } } private static int CountNonEmpty(this ICollection[] collection) { var count = 0; for (var i = collection.Length; i-- > 0;) { if (collection.Length > 0) count++; } return count; } public static ConcurrentBag All { get; } = new(); public static void Clear() { foreach (var cache in All) cache.Clear(); } public static void LogCurrentCacheUtilization() { var allCaches = All.ToArray(); Log.Message($"Total utilized caches: {allCaches.CountNonEmpty()}\n\n" + $"Individual counts:\n{string.Join("\n", allCaches.Select(static cache => cache.GetType()).Distinct().Select(type => $"{type.FullDescription()} :: {allCaches.Where(cache => cache.GetType() == type) .Select(static cache => cache.Count).Sum()}{TryCalculateSize( type, allCaches)}"))}\n============================================================="); } private static string TryCalculateSize(Type type, ICollection[] allCaches) => (type.GetProperty(nameof(FishTable.SizeEstimate))?.GetMethod ?? (type.IsGenericType && type.GetGenericTypeDefinition() == typeof(Dictionary<,>) ? AccessTools.Method(typeof(FisheryLib.CollectionExtensions), nameof(FisheryLib.CollectionExtensions.GetSizeEstimate), generics: type.GetGenericArguments()) : null)) is { } sizeGetter ? $" ({(allCaches.Where(cache => cache.GetType() == type) .Select(cache => (long)(uint)sizeGetter.Invoke(sizeGetter.IsStatic ? null : cache, sizeGetter.IsStatic ? new object[] { cache } : null)).Sum() is var sizeInBytes && sizeInBytes > 1024L * 1024L ? $"{Math.Round(sizeInBytes / (1024d * 1024d), 2)} MiB" : sizeInBytes > 1024L ? $"{Math.Round(sizeInBytes / 1024d, 2)} KiB" : $"{sizeInBytes} B")})" : ""; private static void Clear(this ICollection cache) { switch (cache) { case IClearable clearable: clearable.Clear(); break; case IDictionary dictionary: dictionary.Clear(); break; case IList list: list.Clear(); break; default: ref var clearMethod = ref ClearMethods.GetOrAddReference(cache.GetType().TypeHandle.Value); if (clearMethod.Action is null) AssignClearMethod(cache, out clearMethod.Action); clearMethod.Action(cache); break; } } private static void AssignClearMethod(ICollection cache, out Action clearMethod) => clearMethod = Unsafe.As>(Array.Find(cache.GetType().GetMethods(), static m => m.Name == nameof(IList.Clear) && m.GetParameters().Length == 0) .CreateDelegate(typeof(Action<>).MakeGenericType(cache.GetType()))); private static FishTable ClearMethods => _clearMethods ??= new(); [ThreadStatic] private static FishTable? _clearMethods; private record struct ClearAction { public Action? Action; } [MethodImpl(MethodImplOptions.NoInlining | MethodImplOptions.NoOptimization)] public static T AddNew() where T : class, ICollection, new() { var newCollection = Reflection.New(); All.Add(newCollection); return newCollection; } [MethodImpl(MethodImplOptions.NoInlining | MethodImplOptions.NoOptimization)] public static FishTable AddNew(Action>? onEntryAdded = null, Action>? onEntryRemoved = null) { var newCollection = new FishTable(); if (onEntryAdded != null) newCollection.EntryAdded += onEntryAdded; if (onEntryRemoved != null) newCollection.EntryRemoved += onEntryRemoved; All.Add(newCollection); return newCollection; } [MethodImpl(MethodImplOptions.NoInlining | MethodImplOptions.NoOptimization)] public static T AddNewUnclearable() where T : class, ICollection, new() { var newCollection = Reflection.New(); return newCollection; } [MethodImpl(MethodImplOptions.NoInlining | MethodImplOptions.NoOptimization)] public static FishTable AddNewUnclearable( Action>? onEntryAdded = null, Action>? onEntryRemoved = null) { var newCollection = new FishTable(); if (onEntryAdded != null) newCollection.EntryAdded += onEntryAdded; if (onEntryRemoved != null) newCollection.EntryRemoved += onEntryRemoved; return newCollection; } [MethodImpl(MethodImplOptions.NoInlining | MethodImplOptions.NoOptimization)] public static T AddNewWithRef(AccessTools.FieldRef fieldRef) where T : ICollection, new() { var collectionRef = Reflection.New, AccessTools.FieldRef>(fieldRef); All.Add(collectionRef); return Reflection.New(); } [MethodImpl(MethodImplOptions.NoInlining | MethodImplOptions.NoOptimization)] public static T AddNew(IEqualityComparer comparer) where T : ICollection, new() { var newCollection = (T)Activator.CreateInstance(typeof(T), comparer); // different comparer subclasses can All.Add(newCollection); // require different constructors. return newCollection; // Reflection.New can't use arguments } // for resolving method calls (yet) } public delegate int IndexGetter(T item); public interface IClearable { public void Clear(); } public interface IFishBool { public bool Value { [MethodImpl(MethodImplOptions.AggressiveInlining)] get; } } public struct FishBool : IFishBool { private byte _value; public bool Value { [MethodImpl(MethodImplOptions.AggressiveInlining)] get => _value == 1; [MethodImpl(MethodImplOptions.AggressiveInlining)] set => _value = value ? (byte)1 : (byte)0; } public struct True : IFishBool { public bool Value => true; } public struct False : IFishBool { public bool Value => false; } } [PublicAPI] public class CollectionRef : IClearable, ICollection where T : ICollection { private readonly AccessTools.FieldRef _fieldRef; private static readonly ClearInvoker? _clearInvoker = CreateClearDelegate(); public CollectionRef(AccessTools.FieldRef fieldRef) => _fieldRef = fieldRef; public ref T Get => ref _fieldRef(); public void Create() => Get = Reflection.New(); public void Clear() { if (_clearInvoker is null) ThrowForMissingClearMethod(); _clearInvoker(ref Get); } [DoesNotReturn] private static void ThrowForMissingClearMethod() => throw new InvalidOperationException( $"Tried to call Clear() on invalid collection of type {typeof(T).FullName}"); public IEnumerator GetEnumerator() => Get.GetEnumerator(); public void CopyTo(Array array, int index) => Get.CopyTo(array, index); public int Count => Get.Count; public object SyncRoot => Get.SyncRoot; public bool IsSynchronized => Get.IsSynchronized; private static ClearInvoker? CreateClearDelegate() { var clearMethod = AccessTools.Method(typeof(T), nameof(IList.Clear), Type.EmptyTypes); if (clearMethod is null) return null; var dm = new DynamicMethod($"ClearInvoker_{typeof(T).FullName}", null, new[] { typeof(T).MakeByRefType() }, typeof(CollectionRef), true); var il = dm.GetILGenerator(); il.Emit(FishTranspiler.Argument(0)); il.Emit(FishTranspiler.Call(clearMethod)); il.Emit(FishTranspiler.Return); return (ClearInvoker)dm.CreateDelegate(typeof(ClearInvoker)); } private delegate void ClearInvoker(ref T collectionRef); }