Performance Fish

This commit is contained in:
bbradson
2022-10-24 18:24:02 +02:00
parent 594b32b4ef
commit 2efa8e466c
69 changed files with 9147 additions and 3 deletions
+399
View File
@@ -0,0 +1,399 @@
// 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<FishPatch>, 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<MethodInfo> HarmonyMethodInfos { get; } = new();
public MethodInfo? BenchmarkHarmonyMethodInfo => BenchmarkHarmonyMethodInfos.FirstOrDefault();
public List<MethodInfo> 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<Delegate>? TargetMethodGroups => null;
public virtual Expression<Action>? TargetMethod => null;
public virtual IEnumerable<Expression<Action>>? TargetMethods => null;
public virtual MethodBase TargetMethodInfo
=> TargetMethod != null
? SymbolExtensions.GetMethodInfo(TargetMethod)
: TargetMethodGroup?.Method
?? throw new MissingMethodException(GetType().ToString());
public virtual IEnumerable<MethodBase> 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<HarmonyReversePatch>() && !m.HasAttribute<HarmonyTranspiler>()));
public virtual MethodInfo ReversePatchTranspilerMethodInfo => GetType().GetMethods(AccessTools.all)
.FirstOrDefault(m => m.Name.ToUpperInvariant() == "REVERSEPATCHTRANSPILER" || (m.HasAttribute<HarmonyTranspiler>() && m.HasAttribute<HarmonyReversePatch>()));
public virtual MethodInfo PrefixMethodInfo => GetType().GetMethods(AccessTools.all)
.FirstOrDefault(m => m.Name.ToUpperInvariant() == "PREFIX" || m.HasAttribute<HarmonyPrefix>());
public virtual MethodInfo PostfixMethodInfo => GetType().GetMethods(AccessTools.all)
.FirstOrDefault(m => m.Name.ToUpperInvariant() == "POSTFIX" || m.HasAttribute<HarmonyPostfix>());
public virtual MethodInfo TranspilerMethodInfo => GetType().GetMethods(AccessTools.all)
.FirstOrDefault(m => m.Name.ToUpperInvariant() == "TRANSPILER" || (m.HasAttribute<HarmonyTranspiler>() && !m.HasAttribute<HarmonyReversePatch>()));
public virtual MethodInfo FinalizerMethodInfo => GetType().GetMethods(AccessTools.all)
.FirstOrDefault(m => m.Name.ToUpperInvariant() == "FINALIZER" || m.HasAttribute<HarmonyFinalizer>());
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<HarmonyPriority>()?.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<MethodBase, Results>.Get.TryGetValue(__originalMethod, out __state))
Cache.ByReference<MethodBase, Results>.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<FishPatch> patches)
{
foreach (var patch in patches)
patch.TryPatch();
}
public static void UnpatchAll(this IEnumerable<FishPatch> patches)
{
foreach (var patch in patches)
patch.TryUnpatch();
}
}
public class FishPatchHolder : IExposable, IEnumerable<FishPatch>
{
public ConcurrentDictionary<Type, FishPatch> All => _all;
public T Get<T>() 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<FishPatch> GetEnumerator() => All.Values.GetEnumerator();
IEnumerator IEnumerable.GetEnumerator() => All.Values.GetEnumerator();
private ConcurrentDictionary<Type, FishPatch> _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<ClassWithFishPatches>, IHasFishPatch
{
public virtual FishPatchHolder Patches { get; }
public virtual bool RequiresLoadedGameForPatching => false;
protected ClassWithFishPatches() => Patches = new(GetType());
}