// Copyright (c) 2022 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; using System.Linq; namespace PerformanceFish; public abstract class FishPatch : SingletonFactory, IExposable, IHasDescription { public virtual bool ShouldBenchmark => false; public virtual bool Enabled { get => _enabled; set { _enabled = value; if (value) { if (ShouldBePatched) TryPatch(); } else { var shouldBePatched = ShouldBePatched; TryUnpatch(); ShouldBePatched = shouldBePatched; } } } private bool _enabled = true; public virtual bool DefaultState => true; public virtual int PrefixMethodPriority => TryGetPriority(PrefixMethodInfo); public virtual int PostfixMethodPriority => TryGetPriority(PostfixMethodInfo); public virtual int TranspilerMethodPriority => TryGetPriority(TranspilerMethodInfo); public virtual int FinalizerMethodPriority => TryGetPriority(FinalizerMethodInfo); public MethodInfo? HarmonyMethodInfo => HarmonyMethodInfos.FirstOrDefault(); public List HarmonyMethodInfos { get; } = new(); public MethodInfo? BenchmarkHarmonyMethodInfo => BenchmarkHarmonyMethodInfos.FirstOrDefault(); public List BenchmarkHarmonyMethodInfos { get; } = new(); private bool Patched { get; set; } private bool ShouldBePatched { get; set; } public virtual string? Name => null; public virtual string? Description => null; public virtual Delegate? TargetMethodGroup => null; public virtual IEnumerable? TargetMethodGroups => null; public virtual Expression? TargetMethod => null; public virtual IEnumerable>? TargetMethods => null; public virtual MethodBase TargetMethodInfo => TargetMethod != null ? SymbolExtensions.GetMethodInfo(TargetMethod) : TargetMethodGroup?.Method ?? throw new MissingMethodException(GetType().ToString()); public virtual IEnumerable TargetMethodInfos => _targetMethodInfos ??= (TargetMethods?.Select(m => SymbolExtensions.GetMethodInfo(m)) ?? TargetMethodGroups?.Select(m => m.Method) ?? (TargetMethodInfo != null ? new MethodBase[] { TargetMethodInfo }.AsEnumerable() : throw new MissingMethodException(GetType().ToString()))).ToArray(); private MethodBase[]? _targetMethodInfos; public virtual MethodInfo ReversePatchMethodInfo => GetType().GetMethods(AccessTools.all) .FirstOrDefault(m => m.Name.ToUpperInvariant() == "REVERSEPATCH" || (m.HasAttribute() && !m.HasAttribute())); public virtual MethodInfo ReversePatchTranspilerMethodInfo => GetType().GetMethods(AccessTools.all) .FirstOrDefault(m => m.Name.ToUpperInvariant() == "REVERSEPATCHTRANSPILER" || (m.HasAttribute() && m.HasAttribute())); public virtual MethodInfo PrefixMethodInfo => GetType().GetMethods(AccessTools.all) .FirstOrDefault(m => m.Name.ToUpperInvariant() == "PREFIX" || m.HasAttribute()); public virtual MethodInfo PostfixMethodInfo => GetType().GetMethods(AccessTools.all) .FirstOrDefault(m => m.Name.ToUpperInvariant() == "POSTFIX" || m.HasAttribute()); public virtual MethodInfo TranspilerMethodInfo => GetType().GetMethods(AccessTools.all) .FirstOrDefault(m => m.Name.ToUpperInvariant() == "TRANSPILER" || (m.HasAttribute() && !m.HasAttribute())); public virtual MethodInfo FinalizerMethodInfo => GetType().GetMethods(AccessTools.all) .FirstOrDefault(m => m.Name.ToUpperInvariant() == "FINALIZER" || m.HasAttribute()); internal bool IsBenchmarking => ShouldBenchmark && !FinishedBenchmarking; internal bool FinishedBenchmarking = false; protected FishPatch() => _enabled = DefaultState; public virtual MethodInfo? TryPatch() { if (!TargetMethodInfos?.Any() ?? true) { Log.Error($"Tried to apply patches for {GetType().Name}, but it lacks a target method for patching."); return null; } if (PrefixMethodInfo is null && PostfixMethodInfo is null && TranspilerMethodInfo is null && FinalizerMethodInfo is null && ReversePatchMethodInfo is null) { Log.Error($"Tried to apply patches for {GetType().Name}, but there are none. This is likely not intended."); return null; } ShouldBePatched = true; if (Enabled && !Patched) ApplyPatch(); return HarmonyMethodInfo; } protected virtual void ApplyPatch() { Patched = true; foreach (var method in TargetMethodInfos!) { DebugLog.Message($"Performance Fish is applying {GetType().Name} on {method.FullDescription()}"); try { if (IsBenchmarking) { Patched = false; if (AccessTools.EnumeratorMoveNext(method) is { } moveNext) { BenchmarkHarmonyMethodInfos.Add(PerformanceFishMod.Harmony.Patch(moveNext, prefix: new(methodof(Benchmarking.Prefix), Priority.First + 1), postfix: new(methodof(Benchmarking.Postfix), Priority.Last - 1))); } BenchmarkHarmonyMethodInfos.Add(PerformanceFishMod.Harmony.Patch(method, prefix: new(methodof(Benchmarking.Prefix), Priority.First + 1), postfix: new(methodof(Benchmarking.Postfix), Priority.Last - 1))); } else { if (ReversePatchMethodInfo != null) HarmonyMethodInfos.Add(Harmony.ReversePatch(method, new(ReversePatchMethodInfo), ReversePatchTranspilerMethodInfo)); if (PrefixMethodInfo != null || PostfixMethodInfo != null || TranspilerMethodInfo != null || FinalizerMethodInfo != null) { HarmonyMethodInfos.Add(PerformanceFishMod.Harmony.Patch(method, prefix: PrefixMethodInfo != null ? new(PrefixMethodInfo, PrefixMethodPriority) : null, postfix: PostfixMethodInfo != null ? new(PostfixMethodInfo, PostfixMethodPriority) : null, transpiler: TranspilerMethodInfo != null ? new(TranspilerMethodInfo, TranspilerMethodPriority) : null, finalizer: FinalizerMethodInfo != null ? new(FinalizerMethodInfo, FinalizerMethodPriority) : null)); } } } catch (Exception e) { Log.Error($"Performance Fish encountered an exception while trying to patch {method.FullDescription()} with {PrefixMethodInfo.FullDescription()}" + $", {PostfixMethodInfo.FullDescription()}, {TranspilerMethodInfo.FullDescription()}, {FinalizerMethodInfo.FullDescription()}:\n{e}"); } } } public virtual void TryUnpatch() { ShouldBePatched = false; if (/*!Enabled &&*/ Patched) ApplyUnpatching(); } protected virtual void ApplyUnpatching() { Patched = false; foreach (var method in TargetMethodInfos) { if (PrefixMethodInfo != null) { try { PerformanceFishMod.Harmony.Unpatch(method, PrefixMethodInfo); } catch (Exception e) { Log.Error($"Performance Fish encountered an exception when unpatching {PrefixMethodInfo.FullDescription()} from {method.FullDescription()}:\n{e}"); } } if (PostfixMethodInfo != null) { try { PerformanceFishMod.Harmony.Unpatch(method, PostfixMethodInfo); } catch (Exception e) { Log.Error($"Performance Fish encountered an exception when unpatching {PostfixMethodInfo.FullDescription()} from {method.FullDescription()}:\n{e}"); } } if (TranspilerMethodInfo != null) { try { PerformanceFishMod.Harmony.Unpatch(method, TranspilerMethodInfo); } catch (Exception e) { Log.Error($"Performance Fish encountered an exception when unpatching {TranspilerMethodInfo.FullDescription()} from {method.FullDescription()}:\n{e}"); } } if (FinalizerMethodInfo != null) { try { PerformanceFishMod.Harmony.Unpatch(method, FinalizerMethodInfo); } catch (Exception e) { Log.Error($"Performance Fish encountered an exception when unpatching {FinalizerMethodInfo.FullDescription()} from {method.FullDescription()}:\n{e}"); } } } } private static int TryGetPriority(MethodInfo info) => info?.TryGetAttribute()?.info.priority ?? Priority.Normal; public virtual void ExposeData() => Scribe_Values.Look(ref _enabled, "enabled", DefaultState); } internal static class Benchmarking { internal static int SwitchFrame = int.MaxValue; internal static int EndFrame = int.MaxValue; public static void Prefix(MethodBase __originalMethod, out Results __state) { if (!Cache.ByReference.Get.TryGetValue(__originalMethod, out __state)) Cache.ByReference.Get[__originalMethod] = __state = new(__originalMethod); __state.Stopwatch.Start(); } public static void Postfix(Results __state) { __state.Stopwatch.Stop(); __state.Count++; //Log.Message($"SwitchFrame is {SwitchFrame}, EndFrame is {EndFrame}, CurrentFrame is {Time.frameCount}"); //if (__state.Stopwatch.ElapsedMilliseconds > 100 || __state.Count > 100000) if (__state.Switched ? GenTicks.TicksGame > EndFrame : GenTicks.TicksGame > SwitchFrame) PrintResultsAndStop(__state); } public static void PrintResultsAndStop(Results results) { var ticks = results.Stopwatch.ElapsedTicks; Log.Message($"{results.Method.FullDescription()} took {ticks} ticks for {results.Count} calls. This equals {ticks * 1000L / results.Count} ticks per 1000 calls on average."); results.Count = 0; results.Switched = true; results.Stopwatch.Reset(); var fishPatch = PerformanceFishMod.AllPatchClasses .SelectMany(patchClass => patchClass.Patches) .First(patch => patch.TargetMethodInfos.Contains(results.Method)); if (fishPatch.FinishedBenchmarking) { foreach (var target in fishPatch.TargetMethodInfos) { if (AccessTools.EnumeratorMoveNext(target) is { } moveNext) { PerformanceFishMod.Harmony.Unpatch(moveNext, methodof(Prefix)); PerformanceFishMod.Harmony.Unpatch(moveNext, methodof(Postfix)); } PerformanceFishMod.Harmony.Unpatch(target, methodof(Prefix)); PerformanceFishMod.Harmony.Unpatch(target, methodof(Postfix)); } } else { fishPatch.FinishedBenchmarking = true; fishPatch.TryPatch(); } } public class Results { public int Count; public Stopwatch Stopwatch; public MethodBase Method; public bool Switched; public Results(MethodBase method) { Stopwatch = new(); Method = method; } } } public abstract class FirstPriorityFishPatch : FishPatch { public override int PrefixMethodPriority => Priority.First; public override int PostfixMethodPriority => Priority.First; public override int TranspilerMethodPriority => Priority.First; public override int FinalizerMethodPriority => Priority.First; } public static class FishPatchExtensions { public static void PatchAll(this IEnumerable patches) { foreach (var patch in patches) patch.TryPatch(); } public static void UnpatchAll(this IEnumerable patches) { foreach (var patch in patches) patch.TryUnpatch(); } } public class FishPatchHolder : IExposable, IEnumerable { public ConcurrentDictionary All => _all; public T Get() where T : FishPatch => (T)All[typeof(T)]; public void Add(FishPatch patch) => All[patch.GetType()] = patch; public void PatchAll() => All.Values.PatchAll(); public void UnpatchAll() => All.Values.UnpatchAll(); public FishPatch this[Type type] => All[type]; public FishPatchHolder(Type type) { _type = type; AddPatchesRecursively(type); } public void Scribe() => ExposeData(); // Exposable.Scribe(this, _type.FullName); // directly calling ExposeData prevents creating nested nodes in the config file. Looks cleaner imo. public void ExposeData() { foreach (var (type, patch) in All) Exposable.Scribe(patch, type.FullName); } private void AddPatchesRecursively(Type type) { if (typeof(FishPatch).IsAssignableFrom(type) && !All.ContainsKey(type)) { if (PerformanceFishMod.AllPatchClasses is { } allPatches) RemoveDupes(allPatches, type); All.TryAdd(type, FishPatch.Get(type)); } foreach (var nestedType in type.GetNestedTypes(AccessTools.all)) AddPatchesRecursively(nestedType); } private void RemoveDupes(IHasFishPatch[] patches, Type type) { var dupeClasses = patches .Where(patchClass => patchClass.GetType() != _type && patchClass.Patches.All.ContainsKey(type)); if (!dupeClasses.Any()) return; foreach (var patchClass in dupeClasses) { patchClass.Patches[type].Enabled = false; patchClass.Patches.All.TryRemove(type, out _); Log.Warning($"Performance Fish removed a duplicate patch from {patchClass.GetType().FullName}. This is likely caused by no longer valid mod configs"); } } public IEnumerator GetEnumerator() => All.Values.GetEnumerator(); IEnumerator IEnumerable.GetEnumerator() => All.Values.GetEnumerator(); private ConcurrentDictionary _all = new(); private Type _type; } public interface IHasFishPatch { public FishPatchHolder Patches { get; } public bool RequiresLoadedGameForPatching { get; } } public interface IHasDescription { public string? Description { get; } } public abstract class ClassWithFishPatches : SingletonFactory, IHasFishPatch { public virtual FishPatchHolder Patches { get; } public virtual bool RequiresLoadedGameForPatching => false; protected ClassWithFishPatches() => Patches = new(GetType()); }