Decompiled source of ValPerfMonitor v1.0.4

plugins/ValPerfMonitor.dll

Decompiled a day ago
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 ago
using 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()
		{
		}
	}
}