Please disclose if any significant portion of your mod was created using AI tools by adding the 'AI Generated' category. Failing to do so may result in the mod being removed from Thunderstore.
Decompiled source of ValPerfMonitor v1.0.4
plugins/ValPerfMonitor.dll
Decompiled a day ago
The result has been truncated due to the large size, download it to view full contents!
using System; using System.Collections; using System.Collections.Generic; using System.Diagnostics; using System.Globalization; using System.IO; using System.Linq; using System.Net; using System.Reflection; using System.Reflection.Emit; using System.Runtime.CompilerServices; using System.Runtime.InteropServices; using System.Runtime.Versioning; using System.Text; using System.Text.RegularExpressions; using System.Threading; using BepInEx; using BepInEx.Bootstrap; using BepInEx.Configuration; using BepInEx.Logging; using HarmonyLib; using Microsoft.CodeAnalysis; using Unity.Profiling; using UnityEngine; using UnityEngine.SceneManagement; [assembly: CompilationRelaxations(8)] [assembly: RuntimeCompatibility(WrapNonExceptionThrows = true)] [assembly: Debuggable(DebuggableAttribute.DebuggingModes.IgnoreSymbolStoreSequencePoints)] [assembly: InternalsVisibleTo("ProbeTest")] [assembly: InternalsVisibleTo("RepTest")] [assembly: TargetFramework(".NETFramework,Version=v4.7.2", FrameworkDisplayName = ".NET Framework 4.7.2")] [assembly: AssemblyCompany("ValPerfMonitor")] [assembly: AssemblyConfiguration("Release")] [assembly: AssemblyFileVersion("1.0.4.0")] [assembly: AssemblyInformationalVersion("1.0.4+48297f7891792c5f310383ce0d3f9e1b7f69c6d0")] [assembly: AssemblyProduct("ValPerfMonitor")] [assembly: AssemblyTitle("ValPerfMonitor")] [assembly: AssemblyVersion("1.0.4.0")] [module: RefSafetyRules(11)] namespace Microsoft.CodeAnalysis { [CompilerGenerated] [Embedded] internal sealed class EmbeddedAttribute : Attribute { } } namespace System.Runtime.CompilerServices { [CompilerGenerated] [Embedded] [AttributeUsage(AttributeTargets.Module, AllowMultiple = false, Inherited = false)] internal sealed class RefSafetyRulesAttribute : Attribute { public readonly int Version; public RefSafetyRulesAttribute(int P_0) { Version = P_0; } } } namespace ValPerfMonitor { internal sealed class AbTest { private sealed class Removed { public MethodBase Original; public Patch Patch; public HarmonyPatchType Type; } public sealed class Phase { public bool On; public int Capture; public double FrameMs; public double CpuMainMs; public double Fps1Low; public double GcPerMin; public double SlowPerMin; public double Seconds; public double GarbageKbPerFrame; public List<string> Weathers = new List<string>(); public float MovedM; public int ObjectsMin = -1; public int ObjectsMax = -1; public bool HasPos; public Vector3 StartPos; public string WeatherText { get { if (Weathers.Count != 0) { return string.Join(" -> ", Weathers); } return "?"; } } } private sealed class Comparison { public double Diff; public double Spread; public int Pairs; public int LeftOut; public List<string> Problems = new List<string>(); public bool Real; public bool SameWay; public List<double> PairDiffs = new List<double>(); } [Serializable] [CompilerGenerated] private sealed class <>c { public static readonly <>c <>9 = new <>c(); public static Func<Removed, string> <>9__39_0; public static Func<MethodInfo, string> <>9__39_1; public static Func<ModEntry, bool> <>9__45_0; public static Func<ModEntry, string> <>9__45_3; public static Func<Phase, bool> <>9__61_0; public static Func<Phase, double> <>9__61_1; public static Func<Phase, double> <>9__61_2; public static Func<Phase, bool> <>9__69_0; public static Func<Phase, bool> <>9__69_1; public static Func<double, string> <>9__69_2; public static Func<Phase, bool> <>9__69_3; public static Func<Phase, double> <>9__69_10; public static Func<Phase, double> <>9__69_11; public static Func<Phase, double> <>9__69_12; public static Func<Phase, double> <>9__69_4; public static Func<Phase, double> <>9__69_5; public static Func<Phase, double> <>9__69_6; public static Func<Phase, double> <>9__69_7; public static Func<Phase, double> <>9__69_8; public static Func<Phase, double> <>9__69_9; public static Func<double, bool> <>9__71_0; public static Func<double, bool> <>9__71_1; public static Func<Phase, double> <>9__73_1; public static Func<Phase, double> <>9__73_2; public static Func<Phase, double> <>9__73_3; public static Func<Phase, double> <>9__73_4; public static Func<Phase, double> <>9__73_5; public static Func<Phase, double> <>9__73_6; public static Func<Phase, double> <>9__73_7; public static Func<Phase, double> <>9__73_8; internal string <Describe>b__39_0(Removed r) { //IL_0006: Unknown result type (might be due to invalid IL or missing references) return $"{r.Type} {Instrumenter.Describe(r.Patch.PatchMethod)} on {Instrumenter.Describe(r.Original)}"; } internal string <Describe>b__39_1(MethodInfo m) { return "skip " + Instrumenter.Describe(m); } internal bool <FindMod>b__45_0(ModEntry m) { return !m.IsGame; } internal string <FindMod>b__45_3(ModEntry m) { return m.Name; } internal bool <MainMetric>b__61_0(Phase x) { return !double.IsNaN(x.CpuMainMs); } internal double <MainMetric>b__61_1(Phase x) { return x.CpuMainMs; } internal double <MainMetric>b__61_2(Phase x) { return x.FrameMs; } internal bool <Judge>b__69_0(Phase x) { return x.On; } internal bool <Judge>b__69_1(Phase x) { return !x.On; } internal string <Judge>b__69_2(double d) { return Signed(d); } internal bool <Judge>b__69_3(Phase x) { return !double.IsNaN(x.GarbageKbPerFrame); } internal double <Judge>b__69_10(Phase x) { return x.GarbageKbPerFrame; } internal double <Judge>b__69_11(Phase x) { return x.GarbageKbPerFrame; } internal double <Judge>b__69_12(Phase x) { return x.GarbageKbPerFrame; } internal double <Judge>b__69_4(Phase x) { return x.GcPerMin; } internal double <Judge>b__69_5(Phase x) { return x.GcPerMin; } internal double <Judge>b__69_6(Phase x) { return x.Fps1Low; } internal double <Judge>b__69_7(Phase x) { return x.Fps1Low; } internal double <Judge>b__69_8(Phase x) { return x.SlowPerMin; } internal double <Judge>b__69_9(Phase x) { return x.SlowPerMin; } internal bool <Compare>b__71_0(double d) { return d > 0.0; } internal bool <Compare>b__71_1(double d) { return d < 0.0; } internal double <CsvLine>b__73_1(Phase x) { return x.CpuMainMs; } internal double <CsvLine>b__73_2(Phase x) { return x.CpuMainMs; } internal double <CsvLine>b__73_3(Phase x) { return x.GarbageKbPerFrame; } internal double <CsvLine>b__73_4(Phase x) { return x.GarbageKbPerFrame; } internal double <CsvLine>b__73_5(Phase x) { return x.FrameMs; } internal double <CsvLine>b__73_6(Phase x) { return x.FrameMs; } internal double <CsvLine>b__73_7(Phase x) { return x.GcPerMin; } internal double <CsvLine>b__73_8(Phase x) { return x.GcPerMin; } } private const float MaxMoveM = 10f; private const double MaxObjectChange = 0.15; public const int MaxRounds = 6; public readonly string What; public readonly string Label; public readonly float Seconds; public readonly List<bool> Plan = new List<bool>(); public readonly List<Phase> Phases = new List<Phase>(); public string Error; public string Verdict = ""; public readonly List<string> VerdictLines = new List<string>(); public bool Done; public int Step = -1; public int RoundsUsed; public int RoundsLeftOut; private readonly int _requestedRounds; private Phase _turn = new Phase(); private int _sampleCountdown; private readonly List<Removed> _removed = new List<Removed>(); private readonly List<MethodInfo> _skipped = new List<MethodInfo>(); private readonly Harmony _skipHarmony; private readonly ManualLogSource _log; private readonly string _crumbPath; private bool _off; private int _savedTarget; private int _savedVsync; private bool _uncapped; private long _prevHeap = -1L; private long _heapBytes; private int _heapFrames; private ulong _prevCycles; private ulong _cycles; private int _cpuFrames; private static readonly MethodInfo SkipVoid = AccessTools.Method(typeof(AbTest), "SkipPrefix", (Type[])null, (Type[])null); private static readonly MethodInfo SkipBool = AccessTools.Method(typeof(AbTest), "SkipPrefixKeepGoing", (Type[])null, (Type[])null); public const string CsvHeader = "session,time,what,seconds_per_turn,turns,cpu_ms_normal,cpu_ms_off,garbage_kb_per_frame_normal,garbage_kb_per_frame_off,frame_ms_normal,frame_ms_off,gc_per_min_normal,gc_per_min_off,verdict,rounds_used,rounds_left_out,game_version,mods_fingerprint\n"; public int PatchCount => _removed.Count; public int SkipCount => _skipped.Count; public string Status { get { if (!Done) { return string.Format("A/B test {0}: turn {1}/{2} ({3})", What, Step + 1, Plan.Count, Plan[Math.Max(0, Step)] ? "normal" : "taken off"); } return "A/B test finished"; } } public IEnumerable<string> Describe() { return _removed.Select((Removed r) => $"{r.Type} {Instrumenter.Describe(r.Patch.PatchMethod)} on {Instrumenter.Describe(r.Original)}").Concat(_skipped.Select((MethodInfo m) => "skip " + Instrumenter.Describe(m))); } private static bool SkipPrefix() { return false; } private static bool SkipPrefixKeepGoing(ref bool __result) { __result = true; return false; } public AbTest(string mod, string patchesOn, string skip, float seconds, int rounds, string label, ModRegistry registry, string harmonyId, string rootFolder, ManualLogSource log) { //IL_006d: Unknown result type (might be due to invalid IL or missing references) //IL_0077: Expected O, but got Unknown _log = log; _skipHarmony = new Harmony(harmonyId); _crumbPath = Path.Combine(rootFolder, "ab-test-in-progress.txt"); Seconds = Math.Max(10f, seconds); rounds = Math.Max(1, Math.Min(6, rounds)); _requestedRounds = rounds; for (int i = 0; i < rounds; i++) { AddRound(); } if (!string.IsNullOrWhiteSpace(skip)) { What = "skip " + skip.Trim(); FindSkip(skip.Trim(), registry); } else if (!string.IsNullOrWhiteSpace(mod)) { ModEntry modEntry = FindMod(mod.Trim(), registry); if (modEntry == null) { return; } What = modEntry.Name + (string.IsNullOrWhiteSpace(patchesOn) ? " (all its patches)" : (" (its patches on " + patchesOn.Trim() + ")")); FindPatches(modEntry, patchesOn, registry, harmonyId); } else { Error = "say which mod (mod) or which mod method to skip (skip)"; } Label = (string.IsNullOrWhiteSpace(label) ? ("AB " + What) : label.Trim()); if (Error == null && _removed.Count == 0 && _skipped.Count == 0) { Error = "nothing to take off: no matching patches or methods found"; } } private ModEntry FindMod(string name, ModRegistry registry) { List<ModEntry> source = registry.Visible.Where((ModEntry m) => !m.IsGame).ToList(); List<ModEntry> list = source.Where((ModEntry m) => string.Equals(m.Guid, name, StringComparison.OrdinalIgnoreCase) || string.Equals(m.Name, name, StringComparison.OrdinalIgnoreCase)).ToList(); if (list.Count == 0) { list = source.Where((ModEntry m) => m.Name.IndexOf(name, StringComparison.OrdinalIgnoreCase) >= 0).ToList(); } if (list.Count == 1) { return list[0]; } Error = ((list.Count == 0) ? ("no mod called '" + name + "'") : ("'" + name + "' matches several mods: " + string.Join(", ", from m in list.Take(8) select m.Name))); return null; } private void FindPatches(ModEntry entry, string patchesOn, ModRegistry registry, string harmonyId) { HashSet<MethodBase> hashSet = null; if (!string.IsNullOrWhiteSpace(patchesOn)) { hashSet = new HashSet<MethodBase>(ResolveMethods(patchesOn.Trim())); if (hashSet.Count == 0) { Error = "method '" + patchesOn + "' not found (use Type:Method)"; return; } } string monitor = harmonyId.Substring(0, harmonyId.LastIndexOf('.')); List<MethodBase> list; try { list = Harmony.GetAllPatchedMethods().ToList(); } catch (Exception ex) { Error = "could not list patches: " + ex.Message; return; } foreach (MethodBase item in list) { MethodBase original = item; if (hashSet == null || hashSet.Contains(original)) { Patches patchInfo; try { patchInfo = Harmony.GetPatchInfo(original); } catch { continue; } if (patchInfo != null) { Take(patchInfo.Prefixes, (HarmonyPatchType)1); Take(patchInfo.Postfixes, (HarmonyPatchType)2); Take(patchInfo.Transpilers, (HarmonyPatchType)3); Take(patchInfo.Finalizers, (HarmonyPatchType)4); Take(patchInfo.ILManipulators, (HarmonyPatchType)6); } } void Take(IEnumerable<Patch> enumerable, HarmonyPatchType type) { //IL_0085: Unknown result type (might be due to invalid IL or missing references) //IL_0086: Unknown result type (might be due to invalid IL or missing references) foreach (Patch item2 in enumerable) { if (!(item2.PatchMethod == null) && !item2.owner.StartsWith(monitor, StringComparison.Ordinal) && registry.Resolve(item2.PatchMethod.DeclaringType?.Assembly, item2.owner) == entry) { _removed.Add(new Removed { Original = original, Patch = item2, Type = type }); } } } } } private void FindSkip(string spec, ModRegistry registry) { Assembly assembly = typeof(ZNet).Assembly; foreach (MethodInfo item in ResolveMethods(spec).OfType<MethodInfo>()) { Assembly assembly2 = item.DeclaringType?.Assembly; if (assembly2 == null || assembly2 == assembly || assembly2.GetName().Name.StartsWith("assembly_") || assembly2.GetName().Name.StartsWith("UnityEngine")) { Error = "only mod methods can be skipped, not the game's own"; return; } if (!item.IsAbstract && !item.ContainsGenericParameters && item.GetMethodBody() != null) { if (item.ReturnType == typeof(void)) { _skipped.Add(item); } else if (item.ReturnType == typeof(bool) && IsHarmonyPrefix(item)) { _skipped.Add(item); } } } if (_skipped.Count == 0 && Error == null) { Error = "'" + spec + "': no method that can be skipped safely (it must return nothing, or be a Harmony prefix)"; } } private static bool IsHarmonyPrefix(MethodInfo m) { try { return Harmony.GetAllPatchedMethods().Any((MethodBase o) => Harmony.GetPatchInfo(o)?.Prefixes.Any((Patch p) => p.PatchMethod == m) ?? false); } catch { return false; } } private void AddRound() { bool flag = Plan.Count / 2 % 2 == 0; Plan.Add(flag); Plan.Add(!flag); } private static List<MethodBase> ResolveMethods(string spec) { int num = spec.LastIndexOf(':'); if (num <= 0 || num == spec.Length - 1) { return new List<MethodBase>(); } Type type = null; try { type = AccessTools.TypeByName(spec.Substring(0, num)); } catch { } if (type == null) { return new List<MethodBase>(); } string name = spec.Substring(num + 1); return (from m in type.GetMethods(BindingFlags.DeclaredOnly | BindingFlags.Instance | BindingFlags.Static | BindingFlags.Public | BindingFlags.NonPublic) where m.Name == name select m).Cast<MethodBase>().ToList(); } public void Begin(Plugin p) { _savedTarget = Application.targetFrameRate; _savedVsync = QualitySettings.vSyncCount; _uncapped = true; StartPhase(p, 0); } private void Uncap() { if (_uncapped) { QualitySettings.vSyncCount = 0; Application.targetFrameRate = -1; } } private void RestoreCap() { if (_uncapped) { _uncapped = false; QualitySettings.vSyncCount = _savedVsync; Application.targetFrameRate = _savedTarget; } } public void Frame(bool use) { if (--_sampleCountdown <= 0) { _sampleCountdown = 30; SampleConditions(); } if (CpuClock.Available) { ulong num = CpuClock.Read(); if (use && _prevCycles != 0L && num >= _prevCycles) { _cycles += num - _prevCycles; _cpuFrames++; } _prevCycles = num; } long totalMemory; try { totalMemory = GC.GetTotalMemory(forceFullCollection: false); } catch { return; } if (use && _prevHeap >= 0 && totalMemory >= _prevHeap) { _heapBytes += totalMemory - _prevHeap; _heapFrames++; } _prevHeap = totalMemory; } public void Tick(Plugin p) { Capture current = p.Current; if (Done || current == null || current.MeasuredSeconds < (double)Seconds) { return; } RecordPhase(p, current); if (Step + 1 == Plan.Count && Plan.Count / 2 < 6 && _requestedRounds >= 2) { string text = WhyAnotherRound(); if (text != null) { AddRound(); _log.LogInfo((object)$"A/B test: adding round {Plan.Count / 2} ({text})."); } } if (Step + 1 < Plan.Count) { StartPhase(p, Step + 1); } else { Finish(p); } } private void SampleConditions() { //IL_00ac: Unknown result type (might be due to invalid IL or missing references) //IL_00b1: Unknown result type (might be due to invalid IL or missing references) //IL_00f2: Unknown result type (might be due to invalid IL or missing references) //IL_00f7: Unknown result type (might be due to invalid IL or missing references) //IL_00d2: Unknown result type (might be due to invalid IL or missing references) //IL_00d4: Unknown result type (might be due to invalid IL or missing references) try { EnvMan instance = EnvMan.instance; string text = (((Object)(object)instance != (Object)null) ? instance.GetCurrentEnvironment() : null)?.m_name; if (!string.IsNullOrEmpty(text) && (_turn.Weathers.Count == 0 || _turn.Weathers[_turn.Weathers.Count - 1] != text) && _turn.Weathers.Count < 6) { _turn.Weathers.Add(text); } } catch { } Player localPlayer = Player.m_localPlayer; if ((Object)(object)localPlayer != (Object)null) { Vector3 position = ((Component)localPlayer).transform.position; if (!_turn.HasPos) { _turn.HasPos = true; _turn.StartPos = position; } else { _turn.MovedM = Math.Max(_turn.MovedM, Vector3.Distance(_turn.StartPos, position)); } } int num = GameContext.LoadedObjects(); if (num >= 0) { _turn.ObjectsMin = ((_turn.ObjectsMin < 0) ? num : Math.Min(_turn.ObjectsMin, num)); _turn.ObjectsMax = Math.Max(_turn.ObjectsMax, num); } } private static string RoundProblem(Phase a, Phase b) { //IL_007b: Unknown result type (might be due to invalid IL or missing references) //IL_0081: Unknown result type (might be due to invalid IL or missing references) if (a.Weathers.Concat(b.Weathers).Distinct().ToList() .Count > 1) { return "weather changed (" + a.WeatherText + " | " + b.WeatherText + ")"; } if (a.HasPos && b.HasPos) { float num = Math.Max(Math.Max(a.MovedM, b.MovedM), Vector3.Distance(a.StartPos, b.StartPos)); if (num > 10f) { return $"player moved {num:0} m"; } } int num2 = Math.Min(a.ObjectsMin, b.ObjectsMin); int num3 = Math.Max(a.ObjectsMax, b.ObjectsMax); if (num2 > 0 && (double)(num3 - num2) > 0.15 * (double)num3) { return $"loaded objects changed ({num2} to {num3})"; } return null; } private string WhyAnotherRound() { string name; Comparison comparison = Compare(MainMetric(out name)); if (comparison.LeftOut > 0 && comparison.Pairs < _requestedRounds) { return $"{comparison.LeftOut} round(s) left out because conditions changed"; } if (comparison.Pairs >= 2 && !comparison.Real && comparison.SameWay) { return "every round agrees but the sizes vary"; } return null; } private Func<Phase, double> MainMetric(out string name) { bool flag = Phases.All((Phase x) => !double.IsNaN(x.CpuMainMs)); name = (flag ? "main-thread CPU" : "frame time"); object obj; if (!flag) { obj = <>c.<>9__61_2; if (obj == null) { return <>c.<>9__61_2 = (Phase x) => x.FrameMs; } } else { obj = (Func<Phase, double>)((Phase x) => x.CpuMainMs); } return (Func<Phase, double>)obj; } private void StartPhase(Plugin p, int step) { Step = step; bool flag = Plan[step]; if (flag) { PutBack(); } else { TakeOff(); } Uncap(); _prevHeap = -1L; _heapBytes = 0L; _heapFrames = 0; _prevCycles = 0uL; _cycles = 0uL; _cpuFrames = 0; _turn = new Phase(); _sampleCountdown = 0; p.StartCapture(CaptureMode.Baseline, string.Format("{0} {1} {2}", Label, flag ? "normal" : "off", step + 1)); } private void RecordPhase(Plugin p, Capture c) { Report.Summary summary = Report.Summarize(c.Stats); double num = ((_cpuFrames > 0) ? (CpuClock.ToMs(_cycles) / (double)_cpuFrames) : double.NaN); if (double.IsNaN(num) && p.Counters != null && p.Counters.FrameTimingSeen && p.Counters.TotTimingSamples > 0) { num = p.Counters.TotCpuMain / (double)p.Counters.TotTimingSamples; } double num2 = Math.Max(1.0, c.MeasuredSeconds); SampleConditions(); Phase turn = _turn; turn.On = Plan[Step]; turn.Capture = c.Number; turn.FrameMs = ((summary.Fps > 0.0) ? (1000.0 / summary.Fps) : double.NaN); turn.CpuMainMs = num; turn.Fps1Low = summary.Fps1Low; turn.GcPerMin = summary.GcPerMin; turn.SlowPerMin = (double)c.Stats.SpikeCount / num2 * 60.0; turn.Seconds = num2; turn.GarbageKbPerFrame = ((_heapFrames == 0) ? double.NaN : ((double)_heapBytes / 1024.0 / (double)_heapFrames)); Phases.Add(turn); _turn = new Phase(); } private void TakeOff() { //IL_0060: Unknown result type (might be due to invalid IL or missing references) //IL_010c: Unknown result type (might be due to invalid IL or missing references) //IL_0111: Unknown result type (might be due to invalid IL or missing references) //IL_0125: Expected O, but got Unknown if (_off) { return; } _off = true; try { File.WriteAllText(_crumbPath, "A/B test had these taken off (only for that session):\n" + string.Join("\n", Describe()) + "\n"); } catch { } foreach (Removed item in _removed) { try { new Harmony(item.Patch.owner).Unpatch(item.Original, item.Patch.PatchMethod); } catch (Exception ex) { _log.LogWarning((object)("A/B test: could not take off " + Instrumenter.Describe(item.Patch.PatchMethod) + ": " + ex.Message)); } } foreach (MethodInfo item2 in _skipped) { try { _skipHarmony.Patch((MethodBase)item2, new HarmonyMethod((item2.ReturnType == typeof(bool)) ? SkipBool : SkipVoid) { priority = 800 }, (HarmonyMethod)null, (HarmonyMethod)null, (HarmonyMethod)null, (HarmonyMethod)null); } catch (Exception ex2) { _log.LogWarning((object)("A/B test: could not skip " + Instrumenter.Describe(item2) + ": " + ex2.Message)); } } } public void PutBack() { //IL_00fb: Unknown result type (might be due to invalid IL or missing references) //IL_0100: Unknown result type (might be due to invalid IL or missing references) //IL_0112: Unknown result type (might be due to invalid IL or missing references) //IL_0124: Unknown result type (might be due to invalid IL or missing references) //IL_0136: Unknown result type (might be due to invalid IL or missing references) //IL_014f: Expected O, but got Unknown //IL_015b: Unknown result type (might be due to invalid IL or missing references) //IL_0162: Expected O, but got Unknown //IL_0164: Unknown result type (might be due to invalid IL or missing references) //IL_0169: Unknown result type (might be due to invalid IL or missing references) //IL_016b: Unknown result type (might be due to invalid IL or missing references) //IL_016e: Unknown result type (might be due to invalid IL or missing references) //IL_018c: Expected I4, but got Unknown if (!_off) { return; } _off = false; int num = 0; foreach (MethodInfo item in _skipped) { try { _skipHarmony.Unpatch((MethodBase)item, (HarmonyPatchType)0, _skipHarmony.Id); } catch (Exception ex) { num++; _log.LogError((object)("A/B test: could not un-skip " + Instrumenter.Describe(item) + ": " + ex.Message)); } } foreach (Removed item2 in _removed) { Patch p = item2.Patch; try { Patches patchInfo = Harmony.GetPatchInfo(item2.Original); if (patchInfo != null && All(patchInfo).Any((Patch x) => x.PatchMethod == p.PatchMethod)) { continue; } } catch { } HarmonyMethod val = new HarmonyMethod(p.PatchMethod) { priority = p.priority, before = p.before, after = p.after, debug = p.debug }; try { Harmony val2 = new Harmony(p.owner); HarmonyPatchType type = item2.Type; switch (type - 1) { case 0: val2.Patch(item2.Original, val, (HarmonyMethod)null, (HarmonyMethod)null, (HarmonyMethod)null, (HarmonyMethod)null); break; case 1: val2.Patch(item2.Original, (HarmonyMethod)null, val, (HarmonyMethod)null, (HarmonyMethod)null, (HarmonyMethod)null); break; case 2: val2.Patch(item2.Original, (HarmonyMethod)null, (HarmonyMethod)null, val, (HarmonyMethod)null, (HarmonyMethod)null); break; case 3: val2.Patch(item2.Original, (HarmonyMethod)null, (HarmonyMethod)null, (HarmonyMethod)null, val, (HarmonyMethod)null); break; case 5: val2.Patch(item2.Original, (HarmonyMethod)null, (HarmonyMethod)null, (HarmonyMethod)null, (HarmonyMethod)null, val); break; case 4: break; } } catch (Exception ex2) { num++; _log.LogError((object)("A/B test: could not put back " + Instrumenter.Describe(p.PatchMethod) + " on " + Instrumenter.Describe(item2.Original) + ": " + ex2.Message)); } } if (num > 0) { _log.LogError((object)$"A/B test: {num} patch(es) could not be put back. Restart the game before playing on."); return; } try { File.Delete(_crumbPath); } catch { } } private static IEnumerable<Patch> All(Patches i) { return i.Prefixes.Concat(i.Postfixes).Concat(i.Transpilers).Concat(i.Finalizers) .Concat(i.ILManipulators); } public void Abort() { PutBack(); RestoreCap(); Done = true; if (Phases.Count > 0) { Judge(); } } private void Finish(Plugin p) { PutBack(); RestoreCap(); Done = true; Judge(); p.AbFinished(this); } private void Judge() { VerdictLines.Clear(); List<Phase> list = Phases.Where((Phase x) => x.On).ToList(); List<Phase> list2 = Phases.Where((Phase x) => !x.On).ToList(); if (list.Count == 0 || list2.Count == 0) { Verdict = "not enough turns to compare"; return; } string name; Func<Phase, double> func = MainMetric(out name); Comparison comparison = Compare(func); RoundsUsed = comparison.Pairs; RoundsLeftOut = comparison.LeftOut; if (comparison.Pairs == 0 && comparison.LeftOut > 0) { Verdict = "NO RESULT: conditions changed in every round (" + string.Join("; ", comparison.Problems) + "). Stand still in clear, pinned weather and run it again"; VerdictLines.Add(Verdict); return; } double num = list.Average(func); string text = ((num > 0.0) ? $" ({100.0 * Math.Abs(comparison.Diff) / num:0.0}%)" : ""); string text2 = "each normal turn vs the off turn next to it: " + string.Join(", ", comparison.PairDiffs.Select((double d) => Signed(d))) + " ms"; if (comparison.Pairs < 2) { Verdict = name + " " + Signed(comparison.Diff) + " ms per frame lower with it off" + text + ", but only one round: can't tell real from normal variation"; } else if (comparison.Real && Math.Abs(comparison.Diff) >= 0.05) { Verdict = ((comparison.Diff > 0.0) ? $"REAL COST: taking it off saved {comparison.Diff:0.000} ms of {name} per frame{text}; faster in every round ({text2})" : $"REAL: the game used {0.0 - comparison.Diff:0.000} ms more {name} per frame with it off{text}; slower in every round ({text2})"); } else if (comparison.SameWay && comparison.Pairs >= 3 && Math.Abs(comparison.Diff) >= 0.05) { Verdict = "LEANS " + ((comparison.Diff > 0.0) ? "COST" : "SLOWER WITHOUT IT") + ": every round pointed the same way (" + Signed(comparison.Diff) + " ms per frame on average" + text + "), but the rounds vary too much in size to be sure; run more rounds (" + text2 + ")"; } else { Verdict = "NO CLEAR DIFFERENCE in " + name + ": " + Signed(comparison.Diff) + " ms per frame on average, but the rounds disagree or the difference is smaller than their spread (" + text2 + ")"; } VerdictLines.Add(Verdict); if (comparison.LeftOut > 0) { VerdictLines.Add(string.Format("Left out {0} round(s) because conditions changed: {1}", comparison.LeftOut, string.Join("; ", comparison.Problems))); } if (Phases.All((Phase x) => !double.IsNaN(x.GarbageKbPerFrame))) { Comparison comparison2 = Compare((Phase x) => x.GarbageKbPerFrame); VerdictLines.Add($"Garbage per frame (whole game): {list.Average((Phase x) => x.GarbageKbPerFrame):0.0} KB normal, {list2.Average((Phase x) => x.GarbageKbPerFrame):0.0} KB off" + ((comparison2.Pairs < 2) ? "" : (comparison2.Real ? $" (real: {comparison2.Diff:0.0} KB less per frame with it off in every round)" : " (rounds disagree: within normal variation)"))); } VerdictLines.Add($"Garbage collections per minute: {list.Average((Phase x) => x.GcPerMin):0.0} normal, {list2.Average((Phase x) => x.GcPerMin):0.0} off" + ((Seconds < 45f) ? " (short turns hold only a few collections: rough)" : "")); VerdictLines.Add($"1% low FPS: {list.Average((Phase x) => x.Fps1Low):0.0} normal, {list2.Average((Phase x) => x.Fps1Low):0.0} off; slow frames per minute: {list.Average((Phase x) => x.SlowPerMin):0.0} normal, {list2.Average((Phase x) => x.SlowPerMin):0.0} off"); } private Comparison Compare(Func<Phase, double> f) { Comparison comparison = new Comparison(); for (int i = 0; i + 1 < Phases.Count; i += 2) { Phase phase = Phases[i]; Phase phase2 = Phases[i + 1]; if (phase.On != phase2.On) { string text = RoundProblem(phase, phase2); if (text != null) { comparison.LeftOut++; comparison.Problems.Add($"round {i / 2 + 1}: {text}"); } else { comparison.PairDiffs.Add(phase.On ? (f(phase) - f(phase2)) : (f(phase2) - f(phase))); } } } comparison.Pairs = comparison.PairDiffs.Count; if (comparison.Pairs == 0) { return comparison; } comparison.Diff = comparison.PairDiffs.Average(); comparison.Spread = comparison.PairDiffs.Max() - comparison.PairDiffs.Min(); comparison.SameWay = comparison.PairDiffs.All((double d) => d > 0.0) || comparison.PairDiffs.All((double d) => d < 0.0); comparison.Real = comparison.Pairs >= 2 && comparison.SameWay && Math.Abs(comparison.Diff) > comparison.Spread; return comparison; } private static string Signed(double v) { return ((v >= 0.0) ? "+" : "") + v.ToString("0.000"); } public string CsvLine(string session, ModRegistry registry) { StringBuilder stringBuilder = new StringBuilder(); StringBuilder stringBuilder2 = stringBuilder.Append(Csv.Q(session)).Append(',').Append(DateTime.Now.ToString("HH:mm:ss", Csv.Inv)) .Append(',') .Append(Csv.Q(What)) .Append(','); float seconds = Seconds; stringBuilder2.Append(seconds.ToString("0", Csv.Inv)).Append(',').Append(Phases.Count) .Append(','); Func<bool, Func<Phase, double>, string> func = delegate(bool on, Func<Phase, double> f) { List<Phase> list = Phases.Where((Phase x) => x.On == on).ToList(); return (list.Count != 0) ? Csv.F(list.Average(f), "0.000") : ""; }; stringBuilder.Append(func(arg1: true, (Phase x) => x.CpuMainMs)).Append(',').Append(func(arg1: false, (Phase x) => x.CpuMainMs)) .Append(',') .Append(func(arg1: true, (Phase x) => x.GarbageKbPerFrame)) .Append(',') .Append(func(arg1: false, (Phase x) => x.GarbageKbPerFrame)) .Append(',') .Append(func(arg1: true, (Phase x) => x.FrameMs)) .Append(',') .Append(func(arg1: false, (Phase x) => x.FrameMs)) .Append(',') .Append(func(arg1: true, (Phase x) => x.GcPerMin)) .Append(',') .Append(func(arg1: false, (Phase x) => x.GcPerMin)) .Append(',') .Append(Csv.Q(Verdict)) .Append(',') .Append(RoundsUsed) .Append(',') .Append(RoundsLeftOut) .Append(',') .Append(Csv.Q(RunStamp.GameVersion)) .Append(',') .Append(Csv.Q(RunStamp.ModsFingerprint(registry))) .Append('\n'); return stringBuilder.ToString(); } public static void CheckLeftover(string rootFolder, ManualLogSource log) { string path = Path.Combine(rootFolder, "ab-test-in-progress.txt"); try { if (File.Exists(path)) { log.LogWarning((object)("The game closed during an A/B test last time. Nothing needs repairing (patches are only taken off for that session); if it crashed, the cause may be what was taken off:\n" + File.ReadAllText(path))); File.Delete(path); } } catch { } } } internal static class AllocCheck { public sealed class Result { public string Method = ""; public string Instance = ""; public string Error; public int Calls; public int GcRuns; public double BytesPerCall; public double MicrosPerCall; } private delegate void Loop(object target, object[] args, int count); public static Result Run(string typeName, string methodName, int calls, string timeArg) { Result result = new Result(); try { Type type = AccessTools.TypeByName(typeName); if (type == null) { result.Error = "type " + typeName + " not found"; return result; } List<MethodInfo> list = (from m in AccessTools.GetDeclaredMethods(type) where m.Name == methodName && !m.IsAbstract && !m.ContainsGenericParameters select m).ToList(); if (list.Count == 0) { result.Error = typeName + "." + methodName + " not found"; return result; } if (list.Count > 1) { result.Error = $"{methodName} has {list.Count} overloads; not supported"; return result; } MethodInfo methodInfo = list[0]; result.Method = type.Name + "." + methodInfo.Name + "(" + string.Join(", ", from p in methodInfo.GetParameters() select p.ParameterType.Name) + ")"; object obj = null; if (!methodInfo.IsStatic) { obj = FindInstance(type); if (obj == null) { result.Error = "no live " + type.Name + " found"; return result; } result.Instance = Describe(obj); } ParameterInfo[] parameters = methodInfo.GetParameters(); object[] array = new object[parameters.Length]; for (int num = 0; num < parameters.Length; num++) { if (parameters[num].ParameterType.IsByRef && !parameters[num].IsOut) { result.Error = "ref parameter " + parameters[num].Name + " not supported"; return result; } if (!Fill(parameters[num].ParameterType.IsByRef ? parameters[num].ParameterType.GetElementType() : parameters[num].ParameterType, timeArg, out array[num])) { result.Error = "can't fill parameter " + parameters[num].Name + " (" + parameters[num].ParameterType.Name + ")"; return result; } object obj2 = array[num]; Object val = (Object)((obj2 is Object) ? obj2 : null); if (val != null && num == 0 && methodInfo.IsStatic) { result.Instance = Describe(val); } } Loop loop = BuildLoop(methodInfo); loop(obj, array, 20); calls = Mathf.Clamp(calls, 10, 1000000); int num2 = GC.CollectionCount(0); long totalMemory = GC.GetTotalMemory(forceFullCollection: false); Stopwatch stopwatch = Stopwatch.StartNew(); loop(obj, array, calls); stopwatch.Stop(); long totalMemory2 = GC.GetTotalMemory(forceFullCollection: false); result.GcRuns = GC.CollectionCount(0) - num2; result.Calls = calls; result.BytesPerCall = ((result.GcRuns > 0) ? double.NaN : ((double)(totalMemory2 - totalMemory) / (double)calls)); result.MicrosPerCall = stopwatch.Elapsed.TotalMilliseconds * 1000.0 / (double)calls; if (result.GcRuns > 0) { result.Error = "a garbage collection ran during the check; run it again"; } } catch (Exception ex) { result.Error = (ex.InnerException ?? ex).GetType().Name + ": " + (ex.InnerException ?? ex).Message; } return result; } private static object FindInstance(Type type) { string[] array = new string[4] { "instance", "m_instance", "Instance", "m_localPlayer" }; foreach (string text in array) { FieldInfo fieldInfo = AccessTools.Field(type, text); if (fieldInfo != null && fieldInfo.IsStatic && type.IsAssignableFrom(fieldInfo.FieldType)) { object value = fieldInfo.GetValue(null); if (value != null) { Object val = (Object)((value is Object) ? value : null); if (val == null || !(val == (Object)null)) { return value; } } } PropertyInfo propertyInfo = AccessTools.Property(type, text); if (!(propertyInfo != null)) { continue; } MethodInfo? getMethod = propertyInfo.GetGetMethod(nonPublic: true); if ((object)getMethod == null || !getMethod.IsStatic || !type.IsAssignableFrom(propertyInfo.PropertyType)) { continue; } object value2 = propertyInfo.GetValue(null, null); if (value2 != null) { Object val2 = (Object)((value2 is Object) ? value2 : null); if (val2 == null || !(val2 == (Object)null)) { return value2; } } } if (typeof(Object).IsAssignableFrom(type)) { Object[] array2 = Object.FindObjectsByType(type, (FindObjectsInactive)0, (FindObjectsSortMode)0); Player player = Player.m_localPlayer; if (array2.Length == 0) { return null; } if ((Object)(object)player == (Object)null || !typeof(Component).IsAssignableFrom(type)) { return array2[0]; } return array2.Cast<Component>().OrderBy(delegate(Component c) { //IL_0006: Unknown result type (might be due to invalid IL or missing references) //IL_0016: Unknown result type (might be due to invalid IL or missing references) //IL_001b: Unknown result type (might be due to invalid IL or missing references) //IL_0020: Unknown result type (might be due to invalid IL or missing references) Vector3 val3 = c.transform.position - ((Component)player).transform.position; return ((Vector3)(ref val3)).sqrMagnitude; }).First(); } return null; } private static bool Fill(Type t, string timeArg, out object value) { //IL_001c: Unknown result type (might be due to invalid IL or missing references) //IL_000f: Unknown result type (might be due to invalid IL or missing references) //IL_0021: Unknown result type (might be due to invalid IL or missing references) //IL_0097: Unknown result type (might be due to invalid IL or missing references) //IL_00b3: Unknown result type (might be due to invalid IL or missing references) //IL_00b4: Unknown result type (might be due to invalid IL or missing references) Player localPlayer = Player.m_localPlayer; Vector3 val = (((Object)(object)localPlayer != (Object)null) ? ((Component)localPlayer).transform.position : Vector3.zero); value = null; if (t == typeof(float)) { float result; if (timeArg == "future") { value = Time.time + 1000000f; } else if (float.TryParse(timeArg, NumberStyles.Float, CultureInfo.InvariantCulture, out result)) { value = result; } else { value = Time.time; } return true; } if (t == typeof(Vector3)) { value = val; return true; } if (t == typeof(Vector2s)) { value = ZoneSystem.GetZone(val); return true; } if (t == typeof(long)) { value = ZDOMan.GetSessionID(); return true; } if (t == typeof(int)) { value = 0; return true; } if (t == typeof(bool)) { value = false; return true; } if (t == typeof(string)) { value = ""; return true; } if (!t.IsValueType) { value = FindInstance(t); return value != null; } return false; } private static string Describe(object o) { //IL_0027: Unknown result type (might be due to invalid IL or missing references) Component val = (Component)((o is Component) ? o : null); if (val == null) { return o.GetType().Name; } return $"{((Object)val).name} at {val.transform.position}"; } private static Loop BuildLoop(MethodInfo method) { ParameterInfo[] parameters = method.GetParameters(); DynamicMethod dynamicMethod = new DynamicMethod("ValPerfMonitor_AllocCheck", typeof(void), new Type[3] { typeof(object), typeof(object[]), typeof(int) }, method.DeclaringType, skipVisibility: true); ILGenerator iLGenerator = dynamicMethod.GetILGenerator(); LocalBuilder localBuilder = null; if (!method.IsStatic) { localBuilder = iLGenerator.DeclareLocal(method.DeclaringType); iLGenerator.Emit(OpCodes.Ldarg_0); iLGenerator.Emit(method.DeclaringType.IsValueType ? OpCodes.Unbox_Any : OpCodes.Castclass, method.DeclaringType); iLGenerator.Emit(OpCodes.Stloc, localBuilder); } LocalBuilder[] array = new LocalBuilder[parameters.Length]; for (int i = 0; i < parameters.Length; i++) { Type type = (parameters[i].ParameterType.IsByRef ? parameters[i].ParameterType.GetElementType() : parameters[i].ParameterType); array[i] = iLGenerator.DeclareLocal(type); iLGenerator.Emit(OpCodes.Ldarg_1); iLGenerator.Emit(OpCodes.Ldc_I4, i); iLGenerator.Emit(OpCodes.Ldelem_Ref); iLGenerator.Emit(type.IsValueType ? OpCodes.Unbox_Any : OpCodes.Castclass, type); iLGenerator.Emit(OpCodes.Stloc, array[i]); } LocalBuilder local = iLGenerator.DeclareLocal(typeof(int)); Label label = iLGenerator.DefineLabel(); Label label2 = iLGenerator.DefineLabel(); iLGenerator.Emit(OpCodes.Ldc_I4_0); iLGenerator.Emit(OpCodes.Stloc, local); iLGenerator.Emit(OpCodes.Br, label); iLGenerator.MarkLabel(label2); if (localBuilder != null) { iLGenerator.Emit(method.DeclaringType.IsValueType ? OpCodes.Ldloca : OpCodes.Ldloc, localBuilder); } for (int j = 0; j < parameters.Length; j++) { iLGenerator.Emit(parameters[j].ParameterType.IsByRef ? OpCodes.Ldloca : OpCodes.Ldloc, array[j]); } iLGenerator.Emit((method.IsStatic || method.DeclaringType.IsValueType) ? OpCodes.Call : OpCodes.Callvirt, method); if (method.ReturnType != typeof(void)) { iLGenerator.Emit(OpCodes.Pop); } iLGenerator.Emit(OpCodes.Ldloc, local); iLGenerator.Emit(OpCodes.Ldc_I4_1); iLGenerator.Emit(OpCodes.Add); iLGenerator.Emit(OpCodes.Stloc, local); iLGenerator.MarkLabel(label); iLGenerator.Emit(OpCodes.Ldloc, local); iLGenerator.Emit(OpCodes.Ldarg_2); iLGenerator.Emit(OpCodes.Blt, label2); iLGenerator.Emit(OpCodes.Ret); return (Loop)dynamicMethod.CreateDelegate(typeof(Loop)); } } internal static class AllocCounter { private static ProfilerRecorder _recorder; private static object _sink; public static string Method { get; private set; } = "unavailable"; public static bool Exact { get; private set; } public static bool Available { get; private set; } public static void Init(ManualLogSource log) { //IL_006d: Unknown result type (might be due to invalid IL or missing references) //IL_007a: Unknown result type (might be due to invalid IL or missing references) //IL_007f: Unknown result type (might be due to invalid IL or missing references) try { MethodInfo method = typeof(GC).GetMethod("GetAllocatedBytesForCurrentThread", BindingFlags.Static | BindingFlags.Public); if (method != null) { Func<long> func = (Func<long>)Delegate.CreateDelegate(typeof(Func<long>), method); if (Verify(func, requireSmall: true)) { Use(func, "GC.GetAllocatedBytesForCurrentThread (exact)", exact: true, log); return; } } } catch (Exception ex) { log.LogDebug((object)("Per-thread allocation counter unusable: " + ex.Message)); } try { _recorder = ProfilerRecorder.StartNew(ProfilerCategory.Memory, "GC Allocated In Frame", 1, (ProfilerRecorderOptions)24); if (((ProfilerRecorder)(ref _recorder)).Valid) { Func<long> func2 = () => ((ProfilerRecorder)(ref _recorder)).CurrentValue; if (Verify(func2, requireSmall: true)) { Use(func2, "Unity 'GC Allocated In Frame' counter (exact, main thread + jobs)", exact: true, log); return; } } ((ProfilerRecorder)(ref _recorder)).Dispose(); } catch (Exception ex2) { log.LogDebug((object)("Unity allocation counter unusable: " + ex2.Message)); } try { Func<long> func3 = () => GC.GetTotalMemory(forceFullCollection: false); if (Verify(func3, requireSmall: false)) { Use(func3, "GC.GetTotalMemory (approximate: heap-block granularity)", exact: false, log); return; } } catch { } log.LogWarning((object)"No working allocation counter found; Allocations mode is unavailable on this runtime."); } private static void Use(Func<long> f, string method, bool exact, ManualLogSource log) { Probe.ReadAllocated = f; Method = method; Exact = exact; Available = true; log.LogInfo((object)("Allocation counter: " + method)); } private static bool Verify(Func<long> read, bool requireSmall) { long num = read(); byte[] sink = new byte[262144]; long num2 = read(); _sink = sink; if (num2 - num < 204800) { return false; } if (!requireSmall) { return true; } long num3 = read(); object[] array = new object[2000]; for (int i = 0; i < array.Length; i++) { array[i] = new object(); } long num4 = read(); _sink = array; _sink = null; return num4 - num3 >= 16000; } } internal sealed class BackgroundWriter : IDisposable { private readonly Queue<Action> _queue = new Queue<Action>(); private readonly Thread _thread; private readonly ManualLogSource _log; private readonly AutoResetEvent _signal = new AutoResetEvent(initialState: false); private volatile bool _stop; private volatile bool _busy; private static readonly Encoding Utf8 = new UTF8Encoding(encoderShouldEmitUTF8Identifier: false); public bool Busy => _busy; public BackgroundWriter(ManualLogSource log) { _log = log; _thread = new Thread(Run) { IsBackground = true, Name = "ValPerfMonitor writer", Priority = ThreadPriority.BelowNormal }; _thread.Start(); } public void Append(string path, string text) { Enqueue(delegate { Save(path, text, append: true); }); } public void Write(string path, string text) { Enqueue(delegate { Save(path, text, append: false); }); } public void Job(Func<(string path, string text)> build) { Enqueue(delegate { var (path, text) = build(); Save(path, text, append: false); }); } private void Enqueue(Action a) { lock (_queue) { _queue.Enqueue(a); } _signal.Set(); } private static void Save(string path, string text, bool append) { Directory.CreateDirectory(Path.GetDirectoryName(path)); if (append) { File.AppendAllText(path, text, Utf8); } else { File.WriteAllText(path, text, Utf8); } } private void Run() { do { _signal.WaitOne(500); while (true) { Action action; lock (_queue) { if (_queue.Count == 0) { break; } action = _queue.Dequeue(); goto IL_0049; } IL_0049: _busy = true; try { action(); } catch (Exception ex) { _log.LogWarning((object)("Log write failed: " + ex.Message)); } finally { _busy = false; } } } while (!_stop); } public void Dispose() { _stop = true; _signal.Set(); _thread.Join(5000); } } internal static class Csv { public static readonly CultureInfo Inv = CultureInfo.InvariantCulture; private static readonly char[] Special = new char[4] { ',', '"', '\n', '\r' }; public static string Q(string s) { if (string.IsNullOrEmpty(s)) { return ""; } if (s.IndexOfAny(Special) >= 0) { return "\"" + s.Replace("\"", "\"\"") + "\""; } return s; } public static string F(double v, string fmt) { if (!double.IsNaN(v) && !double.IsInfinity(v)) { return v.ToString(fmt, Inv); } return ""; } public static string Json(string s) { if (s == null) { return "null"; } StringBuilder stringBuilder = new StringBuilder(s.Length + 2).Append('"'); foreach (char c in s) { switch (c) { case '"': stringBuilder.Append("\\\""); continue; case '\\': stringBuilder.Append("\\\\"); continue; case '\n': stringBuilder.Append("\\n"); continue; case '\t': stringBuilder.Append("\\t"); continue; case '<': stringBuilder.Append("\\u003c"); continue; case '\r': continue; } if (c < ' ') { StringBuilder stringBuilder2 = stringBuilder.Append("\\u"); int num = c; stringBuilder2.Append(num.ToString("x4")); } else { stringBuilder.Append(c); } } return stringBuilder.Append('"').ToString(); } public static string JNum(double v, string fmt = "0.####") { if (!double.IsNaN(v) && !double.IsInfinity(v)) { return v.ToString(fmt, Inv); } return "null"; } } [AttributeUsage(AttributeTargets.Method)] internal sealed class BridgeToolAttribute : Attribute { public string Name { get; } public string Description { get; } public BridgeToolAttribute(string name, string description) { Name = name; Description = description; } } internal static class BridgeCommands { private static Plugin P => Plugin.Instance; [BridgeTool("perf/status", "What ValPerfMonitor is doing: capture, mode, suite step, seconds counted, warm-up")] private static object Status() { Plugin p = P; if ((Object)(object)p == (Object)null) { return new { success = false, error = "monitor not started" }; } Capture current = p.Current; return new { success = true, recording = (current != null), capture = (current?.Number ?? 0), mode = (current?.Mode.ToString() ?? ""), label = (current?.Label ?? ""), secondsCounted = ((current == null) ? 0.0 : Math.Round(current.MeasuredSeconds, 1)), suite = ((p.Suite == null) ? "" : $"step {p.Suite.Step + 1}/{p.Suite.Steps.Count}"), abTest = ((p.Ab == null) ? "" : p.Ab.Status), warmingUp = p.WarmingUp, worldReady = p.WorldReady, results = p.Results.Count }; } [BridgeTool("perf/start_suite", "Run the full suite (Baseline, Timing, Allocations, Trace) at the current spot with this label")] private static object StartSuite(string label = "", float seconds = 60f) { Plugin p = P; if ((Object)(object)p == (Object)null) { return new { success = false, error = "monitor not started" }; } if (!p.WorldReady) { return new { success = false, error = "not in a world yet (or the world is still loading)" }; } if (label.Length > 0) { p.CfgLabel.Value = label; } if (seconds >= 15f) { p.CfgSuiteSeconds.Value = seconds; } p.StartTimed(fullSuite: true, CaptureMode.Timing); return new { success = (p.Suite != null), label = p.CfgLabel.Value, secondsPerMode = p.CfgSuiteSeconds.Value }; } [BridgeTool("perf/split_startup", "On the next launch, split these mods' startup into their own methods (comma-separated names or GUIDs) and/or the N slowest mods of this launch; empty and 0 = off. Results in perf/load_times after restart")] private static object SplitStartup(string mods = "", int slowest = 0) { Plugin p = P; if ((Object)(object)p == (Object)null) { return new { success = false, error = "monitor not started" }; } p.CfgSplitStartupMods.Value = mods ?? ""; p.CfgSplitStartupSlowest.Value = Math.Max(0, Math.Min(20, slowest)); return new { success = true, mods = p.CfgSplitStartupMods.Value, slowest = p.CfgSplitStartupSlowest.Value, note = "takes effect on the next game launch" }; } [BridgeTool("perf/stop", "Stop whatever is recording and save it")] private static object Stop() { Plugin p = P; if ((Object)(object)p == (Object)null) { return new { success = false, error = "monitor not started" }; } p.StopTest(); return new { success = true }; } [BridgeTool("perf/results", "Captures finished this session: mode, label, FPS, 1% low, heaviest mod, report path")] private static object Results() { Plugin p = P; if ((Object)(object)p == (Object)null) { return new { success = false, error = "monitor not started" }; } return new { success = true, captures = p.Results.Select((CaptureResult r) => new { Number = r.Number, mode = r.Mode.ToString(), Label = r.Label, seconds = Math.Round(r.Seconds, 0), fps = Math.Round(r.Fps, 1), fps1Low = Math.Round(r.Fps1Low, 1), modsPerFrame = (double.IsNaN(r.ModsPerFrame) ? ((double?)null) : new double?(Math.Round(r.ModsPerFrame, 3))), Unit = r.Unit, heaviest = r.Top, Report = r.Report, numbers = ((r.Mode != CaptureMode.Allocations) ? "measured" : (AllocCounter.Exact ? "exact" : "estimates: see 'How sure' in the report")) }).ToList(), compare = Path.Combine(p.RootFolder, "compare.html") }; } [BridgeTool("perf/load_times", "Startup and world load times this session, with the slowest mods and steps")] private static object LoadTimes() { LoadTimes loadTimes = P?.Loads; if (loadTimes == null) { return new { success = false, error = "monitor not started" }; } return new { success = true, startupDone = loadTimes.StartupDone, totalToMenuSeconds = Round(loadTimes.TotalToMenu), modLoadingSeconds = Round(loadTimes.ModLoading), modsToMenuSeconds = Round(loadTimes.ModsToMenu), slowestMods = (from m in loadTimes.Mods.OrderByDescending((LoadTimes.ModLoad m) => m.Seconds).Take(10) select new { Name = m.Name, seconds = Round(m.Seconds) }).ToList(), startupSteps = (from s in loadTimes.StartupSteps.Take(10) select new { step = s.label, calls = s.calls, seconds = Round(s.seconds) }).ToList(), startupModsInSteps = (from s in loadTimes.StartupStepMods.Take(20) select new { step = s.step, mod = s.mod, calls = s.calls, seconds = Round(s.seconds) }).ToList(), startupSplit = StartupDetail.Mods.Select((StartupDetail.ModResult m) => new { mod = m.Name, timedMethods = m.Methods, slowest = (from r in StartupDetail.Results.Where((StartupDetail.MethodStat r) => r.Mod == m.Name).Take(15) select new { method = r.Name, Calls = r.Calls, totalSeconds = Round(StartupDetail.Seconds(r.Total)), ownSeconds = Round(StartupDetail.Seconds(r.Self)) }).ToList() }).ToList(), startupSplitPending = (StartupDetail.Active && !StartupDetail.Finished), worldLoads = loadTimes.WorldLoads.Select((LoadTimes.WorldLoad w) => new { When = w.When, spawnSeconds = Round(w.SpawnSeconds), screenGoneSeconds = Round(w.ScreenSeconds), hitchesStoppedSeconds = Round(w.SmoothSeconds), Hitches = w.Hitches, steps = (from s in w.Steps.Take(10) select new { step = s.label, calls = s.calls, seconds = Round(s.seconds) }).ToList(), modsInSteps = (from s in w.StepMods.Take(20) select new { step = s.step, mod = s.mod, calls = s.calls, seconds = Round(s.seconds) }).ToList() }).ToList() }; } [BridgeTool("perf/patches_on", "Which mods patch a game method (type and method name, e.g. ZNetScene / Awake): prefixes, postfixes, transpilers, finalizers")] private static object PatchesOn(string type, string method) { Type type2 = AccessTools.TypeByName(type); if (type2 == null) { return new { success = false, error = "type " + type + " not found" }; } List<MethodInfo> list = (from m in AccessTools.GetDeclaredMethods(type2) where m.Name == method select m).ToList(); if (list.Count == 0) { return new { success = false, error = type + "." + method + " not found" }; } ModRegistry reg = P?.Registry; return new { success = true, overloads = list.Select(delegate(MethodInfo m) { Patches patchInfo = Harmony.GetPatchInfo((MethodBase)m); return new { signature = m.ToString(), prefixes = patchInfo?.Prefixes.Select(Describe).ToList(), postfixes = patchInfo?.Postfixes.Select(Describe).ToList(), transpilers = patchInfo?.Transpilers.Select(Describe).ToList(), finalizers = patchInfo?.Finalizers.Select(Describe).ToList() }; }).ToList() }; object Describe(Patch x) { return new { mod = (reg?.Resolve(x.PatchMethod?.DeclaringType?.Assembly, x.owner)?.Name ?? x.owner), patch = x.PatchMethod?.DeclaringType?.FullName + "." + x.PatchMethod?.Name, priority = x.priority }; } } [BridgeTool("perf/alloc_check", "Confirm a suspected garbage source: call a method (type and method name, e.g. ZDOMan / ReleaseNearbyZDOS) many times right now and report bytes and microseconds per call. Arguments are filled from the live game; floats use timeArg: now, future or a number. The method really runs.")] private static object AllocCheckTool(string type, string method, int calls = 2000, string timeArg = "now") { AllocCheck.Result result = AllocCheck.Run(type, method, calls, timeArg); return new { success = (result.Error == null), error = result.Error, Method = result.Method, Instance = result.Instance, Calls = result.Calls, GcRuns = result.GcRuns, bytesPerCall = (double.IsNaN(result.BytesPerCall) ? ((double?)null) : new double?(Math.Round(result.BytesPerCall, 1))), microsPerCall = Math.Round(result.MicrosPerCall, 2) }; } [BridgeTool("perf/worldgen_cost", "How much a world-generation mod (default Riverheim) adds to the game's alternative biome check during world load: times a sample of the same world-generator calls with the mod's world-generator patches on, then briefly off, and scales up to the full map. Patches are put back straight away")] private static object WorldGenCostTool(string mod = "Riverheim", int points = 100000) { Plugin p = P; if ((Object)(object)p == (Object)null) { return new { success = false, error = "monitor not started" }; } WorldGenCost.Result result = WorldGenCost.Run(mod, points, p.Registry, "valheimmods.ValPerfMonitor"); double v = double.NaN; LoadTimes.WorldLoad worldLoad = p.Loads?.WorldLoads.LastOrDefault(); if (worldLoad != null) { (string, int, double) tuple = worldLoad.Steps.FirstOrDefault(((string label, int calls, double seconds) s) => s.label.StartsWith("Alternative biome check", StringComparison.Ordinal)); if (tuple.Item1 != null) { v = tuple.Item3; } } return new { success = (result.Error == null), error = result.Error, Mod = result.Mod, patchesTakenOff = result.PatchesTakenOff, patches = result.Patches, pointsSampled = result.Points, pointsInFullCheck = result.FullPoints, microsPerPointWithMod = Math.Round(result.MicrosWith, 3), microsPerPointWithout = (double.IsNaN(result.MicrosWithout) ? ((double?)null) : new double?(Math.Round(result.MicrosWithout, 3))), estimatedSecondsWithMod = Round(result.SecondsWith), estimatedSecondsWithout = Round(result.SecondsWithout), measuredAtLastWorldLoad = Round(v), note = (result.Note + " Estimates cover only the per-point world-generator calls; the check also groups the points into regions afterwards.").Trim() }; } [BridgeTool("perf/objects", "Objects loaded right now, grouped by prefab, most common first, with mods whose components are on them and how many are stacked exactly on a copy of themselves. Shows what fills the scene")] private static object Objects(int count = 25) { int total; int stackedTotal; List<(string, string, int, int)> list = ObjectLoads.Loaded(Math.Max(1, count), out total, out stackedTotal); if (list == null) { return new { success = false, error = "not in a world yet" }; } return new { success = true, loaded = total, stacked = stackedTotal, objects = Enumerable.Select(list, ((string prefab, string mods, int count, int stacked) x) => new { prefab = x.prefab, scriptsFrom = x.mods, count = x.count, stacked = x.stacked }).ToList(), note = "stacked: copies sitting exactly on another object of the same prefab (wasted)." }; } [BridgeTool("perf/slow_objects", "Objects (prefabs) that took longest to spawn this session: count, total, average and worst ms, and mods whose components are on them. byMax=true sorts by the worst single spawn")] private static object SlowObjects(int count = 20, bool byMax = false) { Plugin p = P; if ((Object)(object)p == (Object)null) { return new { success = false, error = "monitor not started" }; } p.WriteSlowObjects(); return new { success = true, spawned = ObjectLoads.Objects, objects = (from r in ObjectLoads.Top(Math.Max(1, count), byMax) select new { Prefab = r.Prefab, scriptsFrom = r.ModHint, Count = r.Count, totalMs = Math.Round(r.TotalMs, 1), avgMs = Math.Round(r.AvgMs, 3), maxMs = Math.Round(r.MaxMs, 2), Over5Ms = r.Over5Ms, splitSpawns = r.SplitSpawns, unityAndOtherAvgMs = ((r.SplitSpawns == 0) ? ((double?)null) : new double?(Math.Round(r.OtherAvgMs, 3))), parts = (from x in r.Parts.Take(8) select new { part = x.name, avgMs = Math.Round(x.avgMs, 3) }).ToList() }).ToList(), note = "parts: own time per spawn of each script's startup code and each mod patch on it, only for spawns after the object was first seen slow (splitSpawns). unityAndOtherAvgMs: the rest (Unity copying the object, game code not split out)." }; } [BridgeTool("perf/ab_test", "Find out if a mod (or one of its methods) really costs frame time: take its Harmony patches off (mod, optionally only those on patchesOn = Type:Method) or skip one mod method (skip = Namespace.Type:Method), and put them back, in turns, measuring each turn with no probes. Stand still; read the result with perf/ab_result")] private static object AbTestTool(string mod = "", string patchesOn = "", string skip = "", float seconds = 20f, int rounds = 3, string label = "") { Plugin p = P; if ((Object)(object)p == (Object)null) { return new { success = false, error = "monitor not started" }; } AbTest abTest = p.StartAb(mod, patchesOn, skip, seconds, rounds, label); if (abTest.Error != null) { return new { success = false, error = abTest.Error }; } return new { success = true, what = abTest.What, turns = abTest.Plan.Count, secondsPerTurn = abTest.Seconds, patchesTakenOff = abTest.PatchCount, methodsSkipped = abTest.SkipCount, list = abTest.Describe().Take(30).ToList(), expectedMinutes = Math.Round((double)((float)abTest.Plan.Count * (abTest.Seconds + p.CfgSwitchWarmup.Value + 3f)) / 60.0, 1) }; } [BridgeTool("perf/ab_result", "Result of the running or last A/B test: each turn and the verdict (real cost or not)")] private static object AbResult() { Plugin p = P; if ((Object)(object)p == (Object)null) { return new { success = false, error = "monitor not started" }; } AbTest abTest = p.Ab ?? p.LastAb; if (abTest == null) { return new { success = false, error = "no A/B test this session" }; } return new { success = true, what = abTest.What, done = abTest.Done, running = (p.Ab != null), status = abTest.Status, turns = abTest.Phases.Select((AbTest.Phase x) => new { state = (x.On ? "normal" : "off"), Capture = x.Capture, cpuMainMs = (double.IsNaN(x.CpuMainMs) ? ((double?)null) : new double?(Math.Round(x.CpuMainMs, 3))), frameMs = Math.Round(x.FrameMs, 3), garbageKbPerFrame = (double.IsNaN(x.GarbageKbPerFrame) ? ((double?)null) : new double?(Math.Round(x.GarbageKbPerFrame, 2))), fps1Low = Math.Round(x.Fps1Low, 1), gcPerMin = Math.Round(x.GcPerMin, 1), slowFramesPerMin = Math.Round(x.SlowPerMin, 1) }).ToList(), verdict = abTest.Verdict, details = abTest.VerdictLines }; } private static double? Round(double v) { if (!double.IsNaN(v)) { return Math.Round(v, 2); } return null; } } internal sealed class IntervalPoint { public double T; public double Fps; public double FrameAvg; public double P99; public double Mods; public double Game; public double Monitor; public double Overhead; public double HeapMb; public int Gc; public int Excluded; public int Frames; public string Draw; public string Tris; public string Gpu; public string CpuMain; public string Context; } internal sealed class Capture { public sealed class OwnerRow { public ModEntry Mod; public double Avg; public double Pct; public double Max; public double CallsPerFrame; public double Total; public SourceInfo Top; } public readonly int Number; public readonly CaptureMode Mode; public readonly string Label; public readonly string Folder; public readonly Stats Stats; public readonly DateTime Started = DateTime.Now; public readonly List<IntervalPoint> Points = new List<IntervalPoint>(); public int TracesWritten; public bool Finished; private readonly ModRegistry _registry; private readonly Instrumenter _instr; private readonly UnityCounters _counters; private readonly BackgroundWriter _writer; public readonly NetworkStats Net; private readonly string _modeName; private double _lastIntervalAt = -1.0; private int _spikeRows; private const int MaxSpikeRows = 20000; private const int MaxLabelInFolder = 60; public readonly List<SpikeRecord> AllSpikes = new List<SpikeRecord>(); internal static readonly int[] TraceSourceCopy = new int[400000]; internal static readonly long[] TraceTicksCopy = new long[400000]; private static volatile bool _traceBusy; public double MeasuredSeconds => Stats.TotRawSum / 1000.0; public Capture(int number, CaptureMode mode, string label, string sessionFolder, ModRegistry registry, Instrumenter instr, UnityCounters counters, BackgroundWriter writer, NetworkStats net, int ringSize) { Number = number; Mode = mode; Label = (string.IsNullOrWhiteSpace(label) ? "" : label.Trim()); _registry = registry; _instr = instr; _counters = counters; _writer = writer; Net = net; _modeName = mode.ToString(); string text = new string(Label.Select((char c) => (!char.IsLetterOrDigit(c) && c != '-' && c != '_') ? '_' : c).ToArray()); if (text.Length > 60) { text = text.Substring(0, 60).TrimEnd(new char[1] { '_' }) + "_" + ShortHash(Label); } Folder = Path.Combine(sessionFolder, string.Format("{0:00}_{1}{2}", number, _modeName, (text.Length > 0) ? ("_" + text) : "")); Stats = new Stats(registry, mode, ringSize, (mode == CaptureMode.Baseline) ? 0.0 : instr.CallOverheadTicks[(int)mode]); counters.ResetAll(); net.ResetCapture(); net.Recording = mode != CaptureMode.Baseline; string text2 = Stats.Unit.ToLowerInvariant(); writer.Write(Path.Combine(Folder, "frames.csv"), "time,session_s,frames_counted,frames_excluded,fps,frame_ms_avg,frame_ms_p50,frame_ms_p95,frame_ms_p99,frame_ms_max,raw_frame_ms_avg,monitor_ms_per_frame,probe_overhead_ms_per_frame,mods_" + text2 + "_per_frame,game_" + text2 + "_per_frame,probe_calls_per_frame,gc_frames,heap_mb," + string.Join(",", counters.Counters.Select((UnityCounters.Counter c) => c.Column)) + ",cpu_main_ms,cpu_render_ms,gpu_ms,present_wait_ms,scene,in_world,pos_x,pos_z,biome,weather,day_fraction,players,is_server,zdos,instances,characters,pieces_within_50m,building_pieces_loaded\n"); if (mode != CaptureMode.Baseline) { writer.Write(Path.Combine(Folder, "timeline.csv"), "time,session_s,kind,owner,guid,avg_" + text2 + "_per_frame,max_" + text2 + "_in_a_frame,pct_of_frame,calls_per_frame,frames_over_owner_spike\n"); } writer.Write(Path.Combine(Folder, "network.csv"), "time,session_s,kind,name,owner,sent_per_s,sent_kb_per_s,handled_per_s,received_kb_per_s\n"); writer.Write(Path.Combine(Folder, "spikes.csv"), "session_s,frame_ms,raw_frame_ms,gc_in_frame,likely_cause,mods_" + text2 + ",game_" + text2 + ",top_owner,top_" + text2 + ",second_owner,second_" + text2 + ",third_owner,third_" + text2 + ",context,trace_file\n"); } private static string ShortHash(string s) { uint num = 2166136261u; foreach (char c in s) { num ^= c; num *= 16777619; } return num.ToString("x8"); } public void WriteInterval(double sessionSeconds, GameContext ctx) { Stats stats = Stats; Net.SampleConnection(); double seconds = ((_lastIntervalAt < 0.0) ? (stats.IvRawSum / 1000.0) : (sessionSeconds - _lastIntervalAt)); _lastIntervalAt = sessionSeconds; string text = Net.IntervalRows(DateTime.Now.ToString("HH:mm:ss", Csv.Inv) + "," + Csv.F(sessionSeconds, "0.0") + ",", seconds); if (text.Length > 0) { _writer.Append(Path.Combine(Folder, "network.csv"), text); } if (stats.IvFrames == 0) { stats.ResetInterval(); _counters.ResetInterval(); return; } int ivFrames = stats.IvFrames; double num = stats.IvCorrSum / (double)ivFrames; IntervalPoint intervalPoint = new IntervalPoint { T = sessionSeconds, Frames = ivFrames, Excluded = stats.IvExcluded, Fps = ((num > 0.0) ? (1000.0 / num) : 0.0), FrameAvg = num, P99 = Stats.Percentile(stats.IvFrameTimes, 99.0), Mods = stats.IvModsSum / (double)ivFrames, Game = stats.IvGameSum / (double)ivFrames, Monitor = stats.IvMonitorSum / (double)ivFrames, Overhead = stats.IvOverheadSum / (double)ivFrames, Gc = stats.IvGcFrames, HeapMb = (double)GC.GetTotalMemory(forceFullCollection: false) / 1048576.0, Context = ctx.Short, Draw = Counter("draw_calls"), Tris = Counter("triangles_k"), Gpu = _counters.Timing(interval: true, 2), CpuMain = _counters.Timing(interval: true, 0) }; Points.Add(intervalPoint); StringBuilder stringBuilder = new StringBuilder(); stringBuilder.Append(DateTime.Now.ToString("HH:mm:ss", Csv.Inv)).Append(',').Append(Csv.F(sessionSeconds, "0.0")) .Append(',') .Append(ivFrames) .Append(',') .Append(stats.IvExcluded) .Append(',') .Append(Csv.F(intervalPoint.Fps, "0.0")) .Append(',') .Append(Csv.F(num, "0.00")) .Append(',') .Append(Csv.F(Stats.Percentile(stats.IvFrameTimes, 50.0), "0.00")) .Append(',') .Append(Csv.F(Stats.Percentile(stats.IvFrameTimes, 95.0), "0.00")) .Append(',') .Append(Csv.F(intervalPoint.P99, "0.00")) .Append(',') .Append(Csv.F(Stats.Percentile(stats.IvFrameTimes, 100.0), "0.00")) .Append(',') .Append(Csv.F(stats.IvRawSum / (double)ivFrames, "0.00")) .Append(',') .Append(Csv.F(intervalPoint.Monitor, "0.000")) .Append(',') .Append(Csv.F(intervalPoint.Overhead, "0.000")) .Append(',') .Append((Mode == CaptureMode.Baseline) ? "" : Csv.F(intervalPoint.Mods, "0.000")) .Append(',') .Append((Mode == CaptureMode.Baseline) ? "" : Csv.F(intervalPoint.Game, "0.000")) .Append(',') .Append(Csv.F((double)stats.IvCalls / (double)ivFrames, "0")) .Append(',') .Append(stats.IvGcFrames) .Append(',') .Append(Csv.F(intervalPoint.HeapMb, "0")) .Append(','); foreach (UnityCounters.Counter counter in _counters.Counters) { stringBuilder.Append(UnityCounters.Avg(counter, interval: true)).Append(','); } stringBuilder.Append(_counters.Timing(interval: true, 0)).Append(',').Append(_counters.Timing(interval: true, 1)) .Append(',') .Append(_counters.Timing(interval: true, 2)) .Append(',') .Append(_counters.Timing(interval: true, 3)) .Append(',') .Append(ctx.ToCsv()) .Append('\n'); _writer.Append(Path.Combine(Folder, "frames.csv"), stringBuilder.ToString()); if (Mode != CaptureMode.Baseline) { stringBuilder.Length = 0; string value = DateTime.Now.ToString("HH:mm:ss", Csv.Inv) + "," + Csv.F(sessionSeconds, "0.0") + ","; foreach (ModEntry item in from m in _registry.Visible where m.IvCalls > 0 orderby m.IvSum descending select m) { double num2 = item.IvSum / (double)ivFrames; stringBuilder.Append(value).Append(item.Kind).Append(',') .Append(Csv.Q(item.Name)) .Append(',') .Append(Csv.Q(item.Guid)) .Append(',') .Append(Csv.F(num2, "0.0000")) .Append(',') .Append(Csv.F(item.IvMax, "0.000")) .Append(',') .Append((Mode == CaptureMode.Allocations) ? "" : Csv.F((num > 0.0) ? (100.0 * num2 / num) : 0.0, "0.00")) .Append(',') .Append(Csv.F((double)item.IvCalls / (double)ivFrames, "0.0")) .Append(',') .Append(item.IvSpikeFrames) .Append('\n'); } _writer.Append(Path.Combine(Folder, "timeline.csv"), stringBuilder.ToString()); } WriteSpikes(); stats.ResetInterval(); _counters.ResetInterval(); } private string Counter(string column) { UnityCounters.Counter counter = _counters.Counters.FirstOrDefault((UnityCounters.Counter x) => x.Column == column); if (counter != null) { return UnityCounters.Avg(counter, interval: true); } return ""; } public void WriteSpikes() { List<SpikeRecord> pendingSpikes = Stats.PendingSpikes; if (pendingSpikes.Count == 0) { return; } StringBuilder stringBuilder = new StringBuilder(); foreach (SpikeRecord item in pendingSpikes) { if (_spikeRows++ >= 20000) { break; } stringBuilder.Append(Csv.F(item.SessionSeconds, "0.00")).Append(',').Append(Csv.F(item.CorrectedMs, "0.0")) .Append(',') .Append(Csv.F(item.RawMs, "0.0")) .Append(',') .Append(item.Gc ? "1" : "0") .Append(',') .Append(Csv.Q(item.Cause)) .Append(',') .Append(Csv.F(item.ModsValue, "0.00")) .Append(',') .Append(Csv.F(item.GameValue, "0.00")) .Append(',') .Append(Csv.Q(item.Owner1)) .Append(',') .Append(Csv.F(item.Value1, "0.00")) .Append(',') .Append(Csv.Q(item.Owner2)) .Append(',') .Append(Csv.F(item.Value2, "0.00")) .Append(',') .Append(Csv.Q(item.Owner3)) .Append(',') .Append(Csv.F(item.Value3, "0.00")) .Append(',') .Append(Csv.Q(item.Context)) .Append(',') .Append(Csv.Q(item.TraceFile)) .Append('\n'); } AllSpikes.AddRange(pendingSpikes.Take(Math.Max(0, 500 - AllSpikes.Count))); pendingSpikes.Clear(); _writer.Append(Path.Combine(Folder, "spikes.csv"), stringBuilder.ToString()); } public List<OwnerRow> OwnerRows() { long frames = Math.Max(1L, Stats.TotFrames); double frameAvg = Stats.TotCorrSum / (double)frames; Dictionary<ModEntry, SourceInfo> bySource = (from x in _registry.Sources where x != null && !x.Mod.Hidden && Probe.SourceCalls[x.Id] > 0 group x by x.Mod).ToDictionary((IGrouping<ModEntry, SourceInfo> g) => g.Key, (IGrouping<ModEntry, SourceInfo> g) => g.OrderByDescending((SourceInfo x) => Probe.SourceSelf[x.Id]).First()); return (from r in (from m in _registry.Visible where m.IsPlugin || m.PatchCount > 0 || m.TotCalls > 0 where !m.IsGame || m.TotCalls > 0 select m).Select(delegate(ModEntry m) { bySource.TryGetValue(m, out var value); double num = m.TotSum / (double)frames; return new OwnerRow { Mod = m, Avg = num, Pct = ((Mode == CaptureMode.Allocations || frameAvg <= 0.0) ? double.NaN : (100.0 * num / frameAvg)), Max = m.TotMax, CallsPerFrame = (double)m.TotCalls / (double)frames, Total = m.TotSum, Top = value }; }) orderby r.Avg descending, r.Mod.Name select r).ToList(); } public void WriteSummaries() { if (Mode == CaptureMode.Baseline) { return; } _writer.Write(Path.Combine(Folder, "summary_network.csv"), Net.SummaryCsv(MeasuredSeconds)); long num = Math.Max(1L, Stats.TotFrames); string text = Stats.Unit.ToLowerInvariant(); StringBuilder stringBuilder = new StringBuilder(); stringBuilder.Append("rank,kind,name,guid,version,avg_" + text + "_per_frame,pct_of_frame,max_" + text + "_in_a_frame,calls_per_frame,total_" + text + ",").Append("prefixes,postfixes,finalizers,transpilers,unity_callbacks,coroutines,measured_methods,unmeasurable,heaviest_method\n"); int num2 = 0; foreach (OwnerRow item in OwnerRows()) { ModEntry mod = item.Mod; stringBuilder.Append(++num2).Append(',').Append(mod.Kind) .Append(',') .Append(Csv.Q(mod.Name)) .Append(',') .Append(Csv.Q(mod.Guid)) .Append(',') .Append(Csv.Q(mod.Version)) .Append(',') .Append(Csv.F(item.Avg, "0.0000")) .Append(',') .Append(Csv.F(item.Pct, "0.00")) .Append(',') .Append(Csv.F(item.Max, "0.000")) .Append(',') .Append(Csv.F(item.CallsPerFrame, "0.0")) .Append(',') .Append(Csv.F(item.Total, "0.0")) .Append(',') .Append(mod.Prefixes) .Append(',') .Append(mod.Postfixes) .Append(',') .Append(mod.Finalizers) .Append(',') .Append(mod.Transpilers) .Append(',') .Append(mod.UnityCallbacks) .Append(',') .Append(mod.Coroutines) .Append(',') .Append(mod.Instrumented) .Append(',') .Append(mod.Failed) .Append(',') .Append(Csv.Q((item.Top == null) ? "" : (item.Top.Kind + " " + item.Top.Name))) .Append('\n'); } _writer.Write(Path.Combine(Folder, "summary_owners.csv"), stringBuilder.ToString()); stringBuilder.Length = 0; bool flag = Mode == CaptureMode.Allocations; stringBuilder.Append("owner,owner_kind,hook,method,attached_to,calls,calls_per_frame,total_" + text + ",avg_" + text + "_per_frame,").Append(flag ? "avg_bytes_per_call,max_bytes_single_call,how_sure\n" : "avg_us_per_call,max_us_single_call\n"); double valuePerRaw = Stats.ValuePerRaw; double num3 = (flag ? 1.0 : (1000000.0 / (double)Stopwatch.Frequency)); foreach (SourceInfo item2 in from x in _registry.Sources where x != null && !x.Mod.Hidden && Probe.SourceCalls[x.Id] > 0 orderby Probe.SourceSelf[x.Id] descending select x) { long num4 = Probe.SourceCalls[item2.Id]; long num5 = Math.Max(0L, Probe.SourceSelf[item2.Id]); double num6 = (double)num5 * valuePerRaw; string s = ((item2.Targets.Count <= 3) ? string.Join(" | ", item2.Targets) : (string.Join(" | ", item2.Targets.Take(3)) + $" | +{item2.Targets.Count - 3} more")); stringBuilder.Append(Csv.Q(item2.Mod.Name)).Append(',').Append(item2.Mod.Kind) .Append(',') .Append(item2.Kind) .Append(',') .Append(Csv.Q(item2.Name)) .Append(',') .Append(Csv.Q(s)) .Append(',') .Append(num4) .Append(',') .Append(Csv.F((double)num4 / (double)num, "0.00")) .Append(',') .Append(Csv.F(num6, "0.000")) .Append(',') .Append(Csv.F(num6 / (double)num, "0.0000")) .Append(',') .Append(Csv.F((double)num5 * num3 / (double)num4, "0.00")) .Append(',') .Append(Csv.F((double)Probe.SourceMax[item2.Id] * num3, "0.0")); if (flag) { stringBuilder.Append(',').Append(Csv.Q(GarbageConfidence.ForSource(Mode, item2.Id, num6 / (double)num))); } stringBuilder.Append('\n'); } _writer.Write(Path.Combine(Folder, "summary_methods.csv"), stringBuilder.ToString()); } public string QueueTrace(double sessionSeconds, long frameStartTicks, long frameEndTicks, float frameMs, string[] sourceNames) { if (_traceBusy || Probe.TraceCount == 0) { return ""; } int count = Probe.TraceCount; bool overflow = Probe.TraceOverflow; Array.Copy(Probe.TraceSource, TraceSourceCopy, count); Array.Copy(Probe.TraceTicks, TraceTicksCopy, count); _traceBusy = true; TracesWritten++; string text = $"frame_{sessionSeconds:000000.00}s_{frameMs:0}ms.speedscope.json".Replace(',', '.'); string path = Path.Combine(Folder, "traces", text); string title = string.Format("{0} capture {1}{2}: frame at {3:0.00}s, {4:0.0} ms", _modeName, Number, (Label.Length > 0) ? (" (" + Label + ")") : "", sessionSeconds, frameMs); _writer.Job(delegate { try { return (path: path, text: BuildSpeedscope(count, overflow, frameStartTicks, frameEndTicks, sourceNames, title)); } finally { _traceBusy = false; } }); return "traces/" + text; } internal static string BuildSpeedscope(int count, bool overflow, long start, long end, string[] names, string title) { double num = 1000.0 / (double)Stopwatch.Frequency; Dictionary<int, int> dictionary = new Dictionary<int, int>(); List<string> list = new List<string> { overflow ? "Frame (unmeasured engine/render/vanilla time; TRACE BUFFER FULL, later calls missing)" : "Frame (time not inside any measured method: engine, rendering, unhooked vanilla code)" }; StringBuilder stringBuilder = new StringBuilder(count * 32); stringBuilder.Append("{\"type\":\"O\",\"frame\":0,\"at\":0}"); Stack<int> stack = new Stack<int>(); double num2 = (double)(end - start) * num; for (int i = 0; i < count; i++) { int num3 = TraceSourceCopy[i]; double v = Math.Min(num2, Math.Max(0.0, (double)(TraceTicksCopy[i] - start) * num)); if (num3 >= 0) { if (!dictionary.TryGetValue(num3, out var value)) { value = (dictionary[num3] = list.Count); list.Add((num3 < names.Length && names[num3] != null) ? names[num3] : ("source " + num3)); } stack.Push(num3); stringBuilder.Append(",{\"type\":\"O\",\"frame\":").Append(value).Append(",\"at\":") .Append(Csv.JNum(v, "0.#####")) .Append('}'); } else { int num4 = ~num3; if (stack.Count != 0 && stack.Peek() == num4) { stack.Pop(); stringBuilder.Append(",{\"type\":\"C\",\"frame\":").Append(dictionary[num4]).Append(",\"at\":") .Append(Csv.JNum(v, "0.#####")) .Append('}'); } } } while (stack.Count > 0) { stringBuilder.Append(",{\"type\":\"C\",\"frame\":").Append(dictionary[stack.Pop()]).Append(",\"at\":") .Append(Csv.JNum(num2, "0.#####")) .Append('}'); } stringBuilder.Append(",{\"type\":\"C\",\"frame\":0,\"at\":").Append(Csv.JNum(num2, "0.#####")).Append('}'); StringBuilder stringBuilder2 = new StringBuilder(stringBuilder.Length + list.Count * 64 + 512); stringBuilder2.Append("{\"$schema\":\"https://www.speedscope.app/file-format-schema.json\",\"exporter\":\"ValPerfMonitor\",\"name\":").Append(Csv.Json(title)).Append(",\"activeProfileIndex\":0,\"shared\":{\"frames\":["); for (int j = 0; j < list.Count; j++) { if (j > 0) { stringBuilder2.Append(','); } stringBuilder2.Append("{\"name\":").Append(Csv.Json(list[j])).Append('}'); } stringBuilder2.Append("]},\"profiles\":[{\"type\":\"evented\",\"name\":").Append(Csv.Json(title)).Append(",\"unit\":\"milliseconds\",\"startValue\":0,\"endValue\":") .Append(Csv.JNum(num2, "0.#####")) .Append(",\"events\":[") .Append((object?)stringBuilder) .Append("]}]}"); return stringBuilder2.ToString(); } } internal static class CpuClock { private static double _cyclesPerMs; public static bool Available { get; private set; } [DllImport("kernel32.dll")] private static extern IntPtr GetCurrentThread(); [DllImport("kernel32.dll")] private static extern bool QueryThreadCycleTime(IntPtr thread, out ulong cycles); public static void Init(ManualLogSource log) { try { double num = 0.0; for (int i = 0; i < 3; i++) { ulong num2 = Read(); long timestamp = Stopwatch.GetTimestamp(); long num3 = timestamp + Stopwatch.Frequency / 100; long timestamp2; do { timestamp2 = Stopwatch.GetTimestamp(); } while (timestamp2 < num3); ulong num4 = Read(); double num5 = (double)(timestamp2 - timestamp) * 1000.0 / (double)Stopwatch.Frequency; num = Math.Max(num, (double)(num4 - num2) / num5); } _cyclesPerMs = num; Available = num > 1000.0; log.LogInfo((object)(Available ? $"Main-thread CPU clock: {num / 1000.0:0} MHz cycle counter." : "Main-thread CPU clock unavailable.")); } catch (Exception ex) { log.LogInfo((object)("Main-thread CPU clock unavailable: " + ex.Message)); } } public static ulong Read() { QueryThreadCycleTime(GetCurrentThread(), out var cycles); return cycles; } public static double ToMs(ulong cycles) { if (!Available) { return double.NaN; } return (double)cycles / _cyclesPerMs; } } internal sealed class GameContext { public string Scene = ""; public string Biome = ""; public string Weather = ""; public bool InWorld; public bool IsServer; public float X; public float Z; public float DayFraction; public int Players; public int Zdos = -1; public int Instances = -1; public int Characters = -1; public int PiecesNearby = -1; public int WearNTear = -1; public const string CsvHeader = "scene,in_world,pos_x,pos_z,biome,weather,day_fraction,players,is_server,zdos,instances,characters,pieces_within_50m,building_pieces_loaded"; private static readonly FieldRef<ZNetScene, Dictionary<ZDO, ZNetView>> Instances_ = Safe(() => AccessTools.FieldRefAccess<ZNetScene, Dictionary<ZDO, ZNetView>>("m_instances")); private static readonly FieldRef<ZDOMan, Dictionary<ZDOID, ZDO>> Objects_ = Safe(() => AccessTools.FieldRefAccess<ZDOMan, Dictionary<ZDOID, ZDO>>("m_objectsByID")); private static readonly FieldRef<List<WearNTear>> AllWnt_ = Safe(() => AccessTools.StaticFieldRefAccess<List<WearNTear>>(AccessTools.Field(typeof(WearNTear), "s_allInstances"))); private readonly List<Piece> _pieces = new List<Piece>(); public string Short { get; private set; } = ""; public static int LoadedObjects() { ZNetScene instance = ZNetScene.instance; if ((Object)(object)instance == (Object)null || Instances_ == null) { return -1; } try { return Instances_.Invoke(instance).Count; } catch { return -1; } } private static T Safe<T>(Func<T> f) where T : class { try { return f(); } catch { return null; } } public void Sample(bool full) { //IL_0001: Unknown result type (might be due to invalid IL or missing references) //IL_0006: Unknown result type (might be due to invalid IL or missing references) //IL_0080: Unknown result type (might be due to invalid IL or missing references) //IL_0085: Unknown result type (might be due to invalid IL or missing references) //IL_0087: Unknown result type (might be due to invalid IL or missing references) //IL_0093: Unknown result type (might be due to invalid IL or missing references) //IL_00a0: Unknown result type (might be due to invalid IL or missing references) //IL_00a5: Unknown result type (might be due to invalid IL or missing references) //IL_0227: Unknown result type (might be due to invalid IL or missing references) try { Scene activeScene = SceneManager.GetActiveScene(); Scene = ((Scene)(ref activeScene)).name; } catch { Scene = ""; } Player localPlayer = Player.m_localPlayer; InWorld = (Object)(object)localPlayer != (Object)null; try { ZNet instance = ZNet.instance; IsServer = (Object)(object)instance != (Object)null && instance.IsServer(); Players = (((Object)(object)instance != (Object)null) ? instance.GetNrOfPlayers() : 0); } catch { } if ((Object)(object)localPlayer != (Object)null) { Vector3 position = ((Component)localPlayer).transform.position; X = position.x; Z = position.z; try { Biome = ((object)localPlayer.GetCurrentBiome()/*cast due to .constrained prefix*/).ToString(); } catch { Biome = ""; } } else { Biome = ""; X = (Z = 0f); } try { EnvMan instance2 = EnvMan.instance; Weather = ((!((Object)(object)instance2 != (Object)null)) ? "" : (instance2.GetCurrentEnvironment()?.m_name ?? "")); DayFraction = (((Object)(object)instance2 != (Object)null) ? instance2.GetDayFraction() : 0f); } catch { } try { Instances = (((Object)(object)ZNetScene.instance != (Object)null && Instances_ != null) ? Instances_.Invoke(ZNetScene.instance).Count : (-1)); } catch { Instances = -1; } try { Zdos = ((ZDOMan.instance != null && Objects_ != null) ? Objects_.Invoke(ZDOMan.instance).Count : (-1)); } catch { Zdos = -1; } try { Characters = Character.GetAllCharacters().Count; } catch { Characters = -1; } try { WearNTear = ((AllWnt_ != null) ? AllWnt_.Invoke().Count : (-1)); } catch { WearNTear = -1; } if (full && (Object)(object)localPlayer != (Object)null) { try { _pieces.Clear(); Piece.GetAllPiecesInRadius(((Component)localPlayer).transform.position, 50f, _pieces); PiecesNearby = _pieces.Count; _pieces.Clear(); } catch { PiecesNearby = -1; } } else if ((Object)(object)localPlayer == (Object)null) { PiecesNearby = -1; } Short = (InWorld ? string.Format("{0}, {1} objects loaded{2}{3}", Biome, Instances, (PiecesNearby >= 0) ? $", {PiecesNearby} pieces within 50 m" : "", (Players > 1) ? $", {Players} players" : "") : ((Scene.Length > 0) ? Scene : "loading")); } public string ToCsv() { CultureInfo invariantCulture = CultureInfo.InvariantCulture; return string.Join(",", Csv.Q(Scene), InWorld ? "1" : "0", InWorld ? X.ToString("0", invariantCulture) : "", InWorld ? Z.ToString("0", invariantCulture) : "", Csv.Q(Biome), Csv.Q(Weather), InWorld ? DayFraction.ToString("0.00", invariantCulture) : "", Players.ToString(invariantCulture), IsServer ? "1" : "0", N(Zdos), N(Instances), N(Characters), N(PiecesNearby), N(WearNTear)); } private static string N(int v) { if (v >= 0) { return v.ToString(CultureInfo.InvariantCulture); } return ""; } } internal static class GarbageConfidence { public const string Exact = "exact"; public const string Real = "likely real"; public const string Noise = "could be noise"; public const string Unchecked = "unchecked (no Timing capture yet)"; public const string Measured = "measured"; private const double RealRatio = 3.0; private static double[] _msPerFrame = Array.Empty<double>(); private static long _prevHeap = -1L; private static double _bytes; private static double _ms; public static int TimingCapture { get; private set; } public static double KbPerMs { get { if (!(_ms > 0.0)) { return double.NaN; } return _bytes / 1024.0 / _ms; } } public static string Explain { get { if (!AllocCounter.Exact) { return $"Garbage numbers are estimates: the counter also sees other threads' garbage. 'likely real' = at least {3.0:0}x more than it could have picked up by " + "chance while running (its own time per frame from the last Timing capture x the game's total garbage rate); 'could be noise' = not clearly above that; confirm a game method with perf/alloc_check before fixing it."; } return "Garbage numbers are exact on this runtime."; } } public static void TakeTiming(Capture c) { if (c.Mode != CaptureMode.Timing && c.Mode != CaptureMode.Trace) { return; } long totFrames = c.Stats.TotFrames; if (totFrames != 0L) { double[] array = new double[Probe.SourceCount]; for (int i = 0; i < array.Length; i++) { array[i] = (double)Math.Max(0L, Probe.SourceSelf[i]) * c.Stats.ValuePerRaw / (double)totFrames; } _msPerFrame = array; TimingCapture = c.Number; } } public static void ResetRate() { _prevHeap = -1L; _bytes = (_ms = 0.0); } public static void Frame(bool use, float rawMs) { long totalMemory; try { totalMemory = GC.GetTotalMemory(forceFullCollection: false); } catch { return; } if (use && _prevHeap >= 0 && totalMemory >= _prevHeap) { _bytes += totalMemory - _prevHeap; _ms += rawMs; } _prevHeap = totalMemory; } public static double NoiseKb(int sourceId) { if (sourceId < 0 || sourceId >= _msPerFrame.Length || double.IsNaN(KbPerMs)) { return double.NaN; } return _msPerFrame[sourceId] * KbPerMs; } public static string Verdict(CaptureMode mode, double chargedKb, double noiseKb) { if (mode != CaptureMode.Allocations) { return "measured"; } if (AllocCounter.Exact) { return "exact"; } if (double.IsNaN(noiseKb)) { return "unchecked (no Timing capture yet)"; } if (!(chargedKb >= 3.0 * noiseKb)) { return "could be noise"; } return "likely real"; } public static string ForSource(CaptureMode mode, int id, double chargedKb) { return Verdict(mode, chargedKb, NoiseKb(id)); } public static string ForMod(CaptureMode mode, IEnumerable<SourceInfo> sources, double chargedKb) { if (mode != CaptureMode.Allocations || AllocCounter.Exact) { return Verdict(mode, chargedKb, 0.0); } double num = 0.0; bool flag = false; foreach (SourceInfo source in sources) { double num2 = NoiseKb(source.Id); if (!double.IsNaN(num2)) { num += num2; flag = true; } } return Verdict(mode, chargedKb, flag ? num : double.NaN); } } internal sealed class InputBlock { private readonly Harmony _harmony = new Harmony("valheimmods.ValPerfMonitor.panel"); private readonly ManualLogSource _log; private static bool _active; public InputBlock(ManualLogSource log) { //IL_0006: Unknown result type (might be due to invalid IL or missing references) //IL_0010: Expected O, but got Unknown _log = log; } public void Set(bool on) { //IL_003f: Unknown result type (might be due to invalid IL or missing references) //IL_004c: Expected O, but got Unknown if (on == _active) { return; } _active = on; try { if (on) { _harmony.Patch((MethodBase)AccessTools.Method(typeof(TextInput), "IsVisible", (Type[])null, (Type[])null), (HarmonyMethod)null, new HarmonyMethod(typeof(InputBlock), "Postfix", (Type[])null), (HarmonyMethod)null, (HarmonyMethod)null, (HarmonyMethod)null); } else { _harmony.UnpatchSelf(); } } catch (Exception ex) { _log.LogWarning((object)("Could not " + (on ? "free" : "restore") + " the mouse cursor: " + ex.Message)); } } private static void Postfix(ref bool __result) { if (_active) { __result = true; } } } internal sealed class Instrumenter { private static readonly string[] UnityCallbacks = new string[4] { "Update", "LateUpdate", "FixedUpdate", "OnGUI" }; private static readonly string[] GameLoopNames = new string[8] { "Update", "LateUpdate", "FixedUpdate", "OnGUI", "CustomUpdate", "CustomFixedUpdate", "CustomLateUpdate", "UpdateAI" }; private static readonly string[] GameExtras = new string[18] { "WearNTear:UpdateSupport", "ZNetScene:CreateDestroyObjects", "ZDOMan:Update", "ZDOMan:SendZDOs", "ZRoutedRpc:HandleRoutedRPC", "ZRpc:HandlePackage", "ZSteamSocket:Update", "ZoneSystem:CreateLocalZones", "ZoneSystem:CreateGhostZones", "Heightmap:Regenerate", "ZoneSystem:SpawnZone", "ZoneSystem:PlaceLocations", "ZoneSystem:PlaceVegetation", "ZNetScene:CreateObjectsSorted", "ZNetScene:CreateObject", "ZNetScene:RemoveObjects", "ZDOMan:ReleaseNearbyZDOS", "ClutterSystem:GenerateVegPatch" }; private readonly Harmony _harmony; private readonly ModRegistry _registry; private readonly ManualLogSource _log; private readonly Assembly _self; private readonly HashSet<string> _excludedGuids; private readonly SafetyGuard _guard; private readonly Queue<SourceInfo> _pending = new Queue<SourceInfo>(); private readonly Dictionary<MethodBase, SourceInfo> _known = new Dictionary<MethodBase, SourceInfo>(); private readonly List<SourceInfo> _all = new List<SourceInfo>(); private readonly HashSet<(MethodBase, MethodBase)> _seenTranspilers = new HashSet<(MethodBase, MethodBase)>(); private readonly HashSet<MethodBase> _seenTargets = new HashSet<MethodBase>(); private readonly Queue<MethodBase> _rebuild = new Queue<MethodBase>(); private readonly HashSet<MethodBase> _rebuildQueued = new HashSet<MethodBase>(); private readonly Dictionary<MethodBase, long> _rebuiltAt = new Dictionary<MethodBase, long>(); private long _seq; private long _hookStart; private long _attachTicks; private long _rebuildTicks; private int _hookFrames; private int _attachCount; private int _rebuildCount; private int _rebuildsNotNeeded; private bool _includeUnity; private bool _includeGame; private bool _scannedGameExtras; private bool _scannedCoroutines; public bool IncludeCoroutines; public string ExtraMethods = ""; private bool _scannedExtraMethods; private readonly HashSet<Type> _seenBehaviourTypes = new HashSet<Type>(); private readonly List<string> _failedNames = new List<string>(); private bool _calibrationHooked; public readonly double[] CallOverheadTicks = new double[4]; public readonly double[] ChildOverhead = new double[4]; private const int ChildrenPerParent = 32; private int _calParentA = -1; private int _calParentB = -1; public bool Hooked { get; private set; } public int Instrumented => _all.Count((SourceInfo s) => s.Attached); public int Failed { get; private set; } public int Unsafe { get; private set; } public int MenuOnly { get; private set; } public int Pending => _pending.Count + _rebuild.Count; public int PatchedTargets => _seenTargets.Count; public int GameMethods => _all.Count((SourceInfo s) => s.Mod.IsGame); public int LastPatchedMethodCount { get; private set; } = -1; public bool FullRound { get; private set; } public Instrumenter(Harmony harmony, ModRegistry registry, ManualLogSource log, Assembly self, IEnumerable<string> excludedGuids, SafetyGuard guard) { _guard = guard; _harmony = harmony; _registry = registry; _log = log; _self = self; _excludedGuids = new HashSet<string>(excludedGuids, StringComparer.OrdinalIgnoreCase); } public int Scan(bool includeUnityCallbacks, bool includeGame) { int count = _all.Count; List<MethodBase> list; try { list = Harmony.GetAllPatchedMethods().ToList(); } catch (Exception ex) { _log.LogError((object)("Could not list Harmony patches: " + ex.Message)); return 0; } foreach (MethodBase item in list) { Patches patchInfo; try { patchInfo = Harmony.GetPatchInfo(item); } catch { continue; } if (patchInfo == null) { continue; } bool flag = false; foreach (Patch prefix in patchInfo.Prefixes) { flag |= Consider(prefix, "Prefix", item); } foreach (Patch postfix in patchInfo.Postfixes) { flag |= Consider(postfix, "Postfix", item); } foreach (Patch finalizer in patchInfo.Finalizers) { flag |= Consider(finalizer, "Finalizer", item); } foreach (Patch transpiler in patchInfo.Transpilers) { if (!(transpiler.owner == _harmony.Id) && !(transpiler.PatchMethod == null)) { flag = true; if (_seenTranspilers.Add((transpiler.PatchMethod, item))) { _registry.Resolve(transpiler.PatchMethod.DeclaringType?.Assembly, transpiler.owner).Transpilers++; } } } if (flag) { _seenTargets.Add(item); } } LastPatchedMethodCount = list.Count; _includeUnity = includeUnityCallbacks; _includeGame = includeGame; if (includeGame && !_scannedGameExtras) { _scannedGameExtras = true; ScanGameExtras(); } if (includeUnityCallbacks && IncludeCoroutines && !_scannedCoroutines) { _scannedCoroutines = true; ScanCoroutines(); } if (!_scannedExtraMethods) { _scannedExtraMethods = true; ScanExtraMethods(); } ScanLiveBehaviours(); return _all.Count - count; } public bool PatchSetChanged() { try { return Harmony.GetAllPatchedMethods().Count() != LastPatchedMethodC
patchers/ValPerfMonitor.Preloader.dll
Decompiled a day agousing System; using System.Collections.Generic; using System.Diagnostics; using System.Reflection; using System.Runtime.CompilerServices; using System.Runtime.Versioning; using BepInEx.Logging; using Microsoft.CodeAnalysis; using Mono.Cecil; [assembly: CompilationRelaxations(8)] [assembly: RuntimeCompatibility(WrapNonExceptionThrows = true)] [assembly: Debuggable(DebuggableAttribute.DebuggingModes.IgnoreSymbolStoreSequencePoints)] [assembly: TargetFramework(".NETFramework,Version=v4.7.2", FrameworkDisplayName = ".NET Framework 4.7.2")] [assembly: AssemblyCompany("ValPerfMonitor.Preloader")] [assembly: AssemblyConfiguration("Release")] [assembly: AssemblyFileVersion("1.0.4.0")] [assembly: AssemblyInformationalVersion("1.0.4+48297f7891792c5f310383ce0d3f9e1b7f69c6d0")] [assembly: AssemblyProduct("ValPerfMonitor.Preloader")] [assembly: AssemblyTitle("ValPerfMonitor.Preloader")] [assembly: AssemblyVersion("1.0.4.0")] [module: RefSafetyRules(11)] namespace Microsoft.CodeAnalysis { [CompilerGenerated] [Embedded] internal sealed class EmbeddedAttribute : Attribute { } } namespace System.Runtime.CompilerServices { [CompilerGenerated] [Embedded] [AttributeUsage(AttributeTargets.Module, AllowMultiple = false, Inherited = false)] internal sealed class RefSafetyRulesAttribute : Attribute { public readonly int Version; public RefSafetyRulesAttribute(int P_0) { Version = P_0; } } } namespace ValPerfMonitor.Preloader { public static class LoadTimer { public const string Key = "ValPerfMonitor.LoadEvents"; public static IEnumerable<string> TargetDLLs { get; } = new string[0]; public static void Patch(AssemblyDefinition assembly) { } public static void Initialize() { List<string> list = new List<string>(); Listener.Record(list, "Preloader patcher started"); AppDomain.CurrentDomain.SetData("ValPerfMonitor.LoadEvents", list); Logger.Listeners.Add((ILogListener)(object)new Listener(list)); } } internal sealed class Listener : ILogListener, IDisposable { private readonly List<string> _events; private bool _done; public Listener(List<string> events) { _events = events; } public void LogEvent(object sender, LogEventArgs e) { if (_done || e.Source == null || e.Source.SourceName != "BepInEx") { return; } string text = (e.Data as string) ?? e.Data?.ToString(); if (text != null) { if (text.StartsWith("Loading [") || text.StartsWith("Chainloader started") || text.StartsWith("Preloader finished")) { Record(_events, text); } else if (text.StartsWith("Chainloader startup complete")) { Record(_events, text); _done = true; } } } internal static void Record(List<string> events, string text) { lock (events) { events.Add(Stopwatch.GetTimestamp() + "\t" + DateTime.UtcNow.Ticks + "\t" + text); } } public void Dispose() { } } }