Decompiled source of TickProfiler v1.0.3

MagiCorp.ValheimTickProfiler.dll

Decompiled a month ago
using System;
using System.Collections;
using System.Collections.Generic;
using System.Diagnostics;
using System.Globalization;
using System.IO;
using System.Linq;
using System.Reflection;
using System.Runtime.CompilerServices;
using System.Runtime.Versioning;
using System.Text;
using System.Threading;
using BepInEx;
using BepInEx.Bootstrap;
using BepInEx.Configuration;
using BepInEx.Logging;
using HarmonyLib;
using UnityEngine;

[assembly: CompilationRelaxations(8)]
[assembly: RuntimeCompatibility(WrapNonExceptionThrows = true)]
[assembly: Debuggable(DebuggableAttribute.DebuggingModes.IgnoreSymbolStoreSequencePoints)]
[assembly: TargetFramework(".NETFramework,Version=v4.8", FrameworkDisplayName = ".NET Framework 4.8")]
[assembly: AssemblyCompany("MagiCorp.ValheimTickProfiler")]
[assembly: AssemblyConfiguration("Release")]
[assembly: AssemblyFileVersion("1.0.0.0")]
[assembly: AssemblyInformationalVersion("1.0.0+883f730d6cd20b9ea16971cc978a0cc5035abddb")]
[assembly: AssemblyProduct("MagiCorp.ValheimTickProfiler")]
[assembly: AssemblyTitle("MagiCorp.ValheimTickProfiler")]
[assembly: AssemblyVersion("1.0.0.0")]
namespace MagiCorp.ValheimTickProfiler;

public static class ProfilerCore
{
	public sealed class ResettableGapStats
	{
		private readonly List<double> _samples = new List<double>();

		private long _count;

		private double _sum;

		private double _max;

		public void Add(double ms)
		{
			_count++;
			_sum += ms;
			if (ms > _max)
			{
				_max = ms;
			}
			_samples.Add(ms);
		}

		public GapSnapshot SnapshotAndReset()
		{
			double p95Ms = Percentile(_samples, 0.95);
			double p99Ms = Percentile(_samples, 0.99);
			GapSnapshot result = new GapSnapshot(_count, (_count == 0L) ? 0.0 : (_sum / (double)_count), p95Ms, p99Ms, _max);
			_samples.Clear();
			_count = 0L;
			_sum = 0.0;
			_max = 0.0;
			return result;
		}

		private static double Percentile(List<double> values, double p)
		{
			if (values.Count == 0)
			{
				return 0.0;
			}
			double[] array = values.ToArray();
			Array.Sort(array);
			int num = Math.Max(0, Math.Min(array.Length - 1, (int)Math.Ceiling((double)array.Length * p) - 1));
			return array[num];
		}
	}

	public readonly struct GapSnapshot
	{
		public readonly long Count;

		public readonly double AvgMs;

		public readonly double P95Ms;

		public readonly double P99Ms;

		public readonly double MaxMs;

		public GapSnapshot(long count, double avgMs, double p95Ms, double p99Ms, double maxMs)
		{
			Count = count;
			AvgMs = avgMs;
			P95Ms = p95Ms;
			P99Ms = p99Ms;
			MaxMs = maxMs;
		}
	}

	public static long CountMissedFixedDeadlines(double gapMs, double fixedDeltaMs)
	{
		if (fixedDeltaMs <= 0.0 || gapMs < fixedDeltaMs * 2.0)
		{
			return 0L;
		}
		long num = (long)Math.Floor((gapMs + 0.001) / fixedDeltaMs);
		return Math.Max(0L, num - 1);
	}

	public static long CountNewPendingMisses(double gapMs, double fixedDeltaMs, long alreadyReported)
	{
		long num = CountMissedFixedDeadlines(gapMs, fixedDeltaMs);
		return Math.Max(0L, num - Math.Max(0L, alreadyReported));
	}

	public static string JsonString(string value)
	{
		if (value == null)
		{
			return "null";
		}
		StringBuilder stringBuilder = new StringBuilder(value.Length + 8);
		stringBuilder.Append('"');
		foreach (char c in value)
		{
			switch (c)
			{
			case '"':
				stringBuilder.Append("\\\"");
				continue;
			case '\\':
				stringBuilder.Append("\\\\");
				continue;
			case '\n':
				stringBuilder.Append("\\n");
				continue;
			case '\r':
				stringBuilder.Append("\\r");
				continue;
			case '\t':
				stringBuilder.Append("\\t");
				continue;
			}
			if (c < ' ')
			{
				StringBuilder stringBuilder2 = stringBuilder.Append("\\u");
				int num = c;
				stringBuilder2.Append(num.ToString("x4", CultureInfo.InvariantCulture));
			}
			else
			{
				stringBuilder.Append(c);
			}
		}
		stringBuilder.Append('"');
		return stringBuilder.ToString();
	}

	public static int HitchBucketIndex(double gapMs)
	{
		if (gapMs < 100.0)
		{
			return 0;
		}
		if (gapMs < 250.0)
		{
			return 1;
		}
		if (gapMs < 500.0)
		{
			return 2;
		}
		if (gapMs < 1000.0)
		{
			return 3;
		}
		return 4;
	}
}
[BepInPlugin("magicorp.valheim.tickprofiler", "TickProfiler", "1.0.3")]
public sealed class TickProfilerPlugin : BaseUnityPlugin
{
	private readonly struct TargetSpec
	{
		public readonly string TypeName;

		public readonly string MethodName;

		public readonly string Label;

		public readonly bool Deep;

		public TargetSpec(string typeName, string methodName, string label, bool deep)
		{
			TypeName = typeName;
			MethodName = methodName;
			Label = label;
			Deep = deep;
		}
	}

	private struct ScopeFrame
	{
		public Metric Metric;

		public long StartTicks;

		public long ChildTicks;
	}

	private sealed class Metric
	{
		public readonly string Name;

		public readonly string Kind;

		public readonly int SampleEveryN;

		public readonly string PatchOwners;

		private long _sampleCounter;

		private long _calls;

		private long _selfTicks;

		private long _inclusiveTicks;

		private long _maxInclusiveTicks;

		public Metric(string name, string kind, int sampleEveryN, string patchOwners)
		{
			Name = name;
			Kind = kind;
			SampleEveryN = Math.Max(1, sampleEveryN);
			PatchOwners = patchOwners ?? string.Empty;
		}

		public bool ShouldSample()
		{
			if (SampleEveryN > 1)
			{
				return Interlocked.Increment(ref _sampleCounter) % SampleEveryN == 0;
			}
			return true;
		}

		public void Add(long selfTicks, long inclusiveTicks)
		{
			Interlocked.Increment(ref _calls);
			Interlocked.Add(ref _selfTicks, selfTicks);
			Interlocked.Add(ref _inclusiveTicks, inclusiveTicks);
			long num = Volatile.Read(in _maxInclusiveTicks);
			while (inclusiveTicks > num)
			{
				long num2 = Interlocked.CompareExchange(ref _maxInclusiveTicks, inclusiveTicks, num);
				if (num2 != num)
				{
					num = num2;
					continue;
				}
				break;
			}
		}

		public MetricRaw SnapshotAndReset()
		{
			return new MetricRaw(Interlocked.Exchange(ref _calls, 0L), Interlocked.Exchange(ref _selfTicks, 0L), Interlocked.Exchange(ref _inclusiveTicks, 0L), Interlocked.Exchange(ref _maxInclusiveTicks, 0L));
		}
	}

	private readonly struct MetricRaw
	{
		public readonly long Calls;

		public readonly long SelfTicks;

		public readonly long InclusiveTicks;

		public readonly long MaxInclusiveTicks;

		public MetricRaw(long calls, long selfTicks, long inclusiveTicks, long maxInclusiveTicks)
		{
			Calls = calls;
			SelfTicks = selfTicks;
			InclusiveTicks = inclusiveTicks;
			MaxInclusiveTicks = maxInclusiveTicks;
		}
	}

	private sealed class MetricSnapshot
	{
		public string Name;

		public string Kind;

		public double SelfMsPerSec;

		public double InclusiveMsPerSec;

		public double CpuPct;

		public double CallsPerSec;

		public double AvgSelfUs;

		public double MaxInclusiveMs;

		public int SampleEveryN;

		public string PatchOwners;

		public static MetricSnapshot From(Metric metric, MetricRaw raw, double seconds)
		{
			double num = Math.Max(1, metric.SampleEveryN);
			double num2 = ToMilliseconds(raw.SelfTicks);
			double num3 = num2 * num;
			return new MetricSnapshot
			{
				Name = metric.Name,
				Kind = metric.Kind,
				SelfMsPerSec = num3 / seconds,
				InclusiveMsPerSec = ToMilliseconds(raw.InclusiveTicks) * num / seconds,
				CpuPct = num3 / (seconds * 1000.0) * 100.0,
				CallsPerSec = (double)raw.Calls * num / seconds,
				AvgSelfUs = ((raw.Calls > 0) ? (num2 * 1000.0 / (double)raw.Calls) : 0.0),
				MaxInclusiveMs = ToMilliseconds(raw.MaxInclusiveTicks),
				SampleEveryN = metric.SampleEveryN,
				PatchOwners = metric.PatchOwners
			};
		}
	}

	private sealed class GapStats
	{
		private readonly double[] _samples;

		private int _sampleCount;

		private int _next;

		private long _count;

		private double _sum;

		private double _max;

		private long _over20;

		private long _over40;

		private long _over100;

		public GapStats(int capacity)
		{
			_samples = new double[capacity];
		}

		public void Add(double ms)
		{
			_count++;
			_sum += ms;
			if (ms > _max)
			{
				_max = ms;
			}
			if (ms > 20.0)
			{
				_over20++;
			}
			if (ms > 40.0)
			{
				_over40++;
			}
			if (ms > 100.0)
			{
				_over100++;
			}
			_samples[_next++] = ms;
			if (_next == _samples.Length)
			{
				_next = 0;
			}
			if (_sampleCount < _samples.Length)
			{
				_sampleCount++;
			}
		}

		public GapSnapshot SnapshotAndReset()
		{
			double p95Ms = 0.0;
			double p99Ms = 0.0;
			if (_sampleCount > 0)
			{
				double[] array = new double[_sampleCount];
				Array.Copy(_samples, array, _sampleCount);
				Array.Sort(array);
				int val = Math.Min(array.Length - 1, (int)Math.Ceiling((double)array.Length * 0.95) - 1);
				p95Ms = array[Math.Max(0, val)];
				int val2 = Math.Min(array.Length - 1, (int)Math.Ceiling((double)array.Length * 0.99) - 1);
				p99Ms = array[Math.Max(0, val2)];
			}
			GapSnapshot result = new GapSnapshot
			{
				Count = _count,
				AvgMs = ((_count > 0) ? (_sum / (double)_count) : 0.0),
				P95Ms = p95Ms,
				P99Ms = p99Ms,
				MaxMs = _max,
				Over20Ms = _over20,
				Over40Ms = _over40,
				Over100Ms = _over100
			};
			_sampleCount = 0;
			_next = 0;
			_count = 0L;
			_sum = 0.0;
			_max = 0.0;
			_over20 = 0L;
			_over40 = 0L;
			_over100 = 0L;
			return result;
		}
	}

	private struct GapSnapshot
	{
		public long Count;

		public double AvgMs;

		public double P95Ms;

		public double P99Ms;

		public double MaxMs;

		public long Over20Ms;

		public long Over40Ms;

		public long Over100Ms;
	}

	private sealed class StallStats
	{
		private readonly double[] _samples;

		private int _sampleCount;

		private int _next;

		private long _count;

		private double _maxGapMs;

		private double _totalGapMs;

		private double _totalExcessMs;

		private string _worstCause;

		public StallStats(int capacity)
		{
			_samples = new double[capacity];
		}

		public void Add(double gapMs, double expectedMs, string cause)
		{
			_count++;
			_totalGapMs += gapMs;
			_totalExcessMs += Math.Max(0.0, gapMs - expectedMs);
			if (gapMs > _maxGapMs)
			{
				_maxGapMs = gapMs;
				_worstCause = cause;
			}
			_samples[_next++] = gapMs;
			if (_next == _samples.Length)
			{
				_next = 0;
			}
			if (_sampleCount < _samples.Length)
			{
				_sampleCount++;
			}
		}

		public StallSnapshot SnapshotAndReset()
		{
			StallSnapshot result = new StallSnapshot
			{
				Count = _count,
				MaxGapMs = _maxGapMs,
				TotalGapMs = _totalGapMs,
				TotalExcessMs = _totalExcessMs,
				WorstCause = _worstCause
			};
			_sampleCount = 0;
			_next = 0;
			_count = 0L;
			_maxGapMs = 0.0;
			_totalGapMs = 0.0;
			_totalExcessMs = 0.0;
			_worstCause = null;
			return result;
		}
	}

	private struct StallSnapshot
	{
		public long Count;

		public double MaxGapMs;

		public double TotalGapMs;

		public double TotalExcessMs;

		public string WorstCause;
	}

	private sealed class LongCallEvent
	{
		public string Name;

		public string Kind;

		public string PatchOwners;

		public long StartTicks;

		public long EndTicks;

		public double ElapsedMs;
	}

	private readonly struct StallAttribution
	{
		public readonly string Text;

		public readonly string Confidence;

		public readonly string ShortCause;

		public StallAttribution(string text, string confidence, string shortCause)
		{
			Text = text;
			Confidence = confidence;
			ShortCause = shortCause;
		}
	}

	private struct RuntimeContext
	{
		public int Players;

		public int ZdoCount;

		public int ActiveObjects;

		public int LoadedZones;

		public int GeneratedZones;

		public int Threads;

		public double NetworkRxKiBps;

		public double NetworkTxKiBps;
	}

	private sealed class MiniSample
	{
		public DateTime TimeUtc;

		public double TickHz;

		public double UpdateHz;

		public double Cpu;

		public double RssMiB;

		public int Zdos;

		public int Players;
	}

	private sealed class PeerPingState
	{
		public string Player = string.Empty;

		public double PingMs;

		public long UpdatedTicks;
	}

	private sealed class OutputWork
	{
		public string TextPath;

		public string Text;

		public string JsonPath;

		public string Json;

		public string ExtraPath;

		public string Extra;
	}

	private sealed class ReportSnapshot
	{
		public DateTime TimestampUtc;

		public bool Requested;

		public double WallSeconds;

		public double UpdateHz;

		public double FixedHz;

		public double RawFixedThroughputHz;

		public double NominalFixedHz;

		public double FixedDeltaMs;

		public double TimeScale;

		public long MissedFixedDeadlines;

		public GapSnapshot UpdateGap;

		public GapSnapshot FixedGap;

		public StallSnapshot MainThreadStall;

		public double CpuOneCorePct;

		public double CpuVmPct;

		public double ManagedMiB;

		public double RssMiB;

		public double PrivateMiB;

		public double ProfilerOverheadPct;

		public double ProfiledCallsPerSec;

		public long ScopeRecoveries;

		public int OutputQueueDepth;

		public RuntimeContext Runtime;

		public long[] HitchBuckets;

		public int Gc0;

		public int Gc1;

		public int Gc2;

		public int PatchedMethods;

		public List<MetricSnapshot> Metrics;

		public string[] PatchFailures;
	}

	public const string PluginGuid = "magicorp.valheim.tickprofiler";

	public const string PluginName = "TickProfiler";

	public const string PluginVersion = "1.0.3";

	private const int ProtocolVersion = 2;

	private static readonly long Frequency = Stopwatch.Frequency;

	private static readonly Dictionary<MethodBase, Metric> MethodMetrics = new Dictionary<MethodBase, Metric>();

	private static readonly List<Metric> Metrics = new List<Metric>();

	private static readonly object RecentLongCallLock = new object();

	private static readonly List<LongCallEvent> RecentLongCalls = new List<LongCallEvent>(256);

	private static Harmony _harmony;

	private static ManualLogSource _log;

	private static bool _profilingEnabled;

	private bool _serverMode;

	private bool _rpcRegistered;

	private readonly HashSet<long> _overlayPeers = new HashSet<long>();

	private readonly Dictionary<long, string> _v2OverlayPeers = new Dictionary<long, string>();

	private readonly Dictionary<long, PeerPingState> _peerPings = new Dictionary<long, PeerPingState>();

	private long _lastPingProbeTicks;

	private string _latestOverlayText = string.Empty;

	private string _latestLegacyOverlayText = string.Empty;

	private const string RpcHello = "MagiCorp_TickProfiler_Hello_v1";

	private const string RpcSnapshot = "MagiCorp_TickProfiler_Snapshot_v1";

	private const string RpcHitchNotice = "MagiCorp_TickProfiler_HitchNotice_v1";

	private const string RpcHelloV2 = "MagiCorp_TickProfiler_Hello_v2";

	private const string RpcSnapshotV2 = "MagiCorp_TickProfiler_Snapshot_v2";

	private const string RpcHitchNoticeV2 = "MagiCorp_TickProfiler_HitchNotice_v2";

	private const string RpcHitchEventV2 = "MagiCorp_TickProfiler_HitchEvent_v2";

	private const string RpcPingSnapshotV2 = "MagiCorp_TickProfiler_PingSnapshot_v2";

	[ThreadStatic]
	private static ScopeFrame[] _scopeStack;

	[ThreadStatic]
	private static int _scopeDepth;

	private ConfigEntry<bool> _enabled;

	private ConfigEntry<float> _reportInterval;

	private ConfigEntry<int> _topConsumers;

	private ConfigEntry<bool> _deepGameProfiling;

	private ConfigEntry<bool> _profilePluginLoops;

	private ConfigEntry<bool> _legacyProfileModPatches;

	private ConfigEntry<int> _hotMethodSampleEveryN;

	private ConfigEntry<float> _warmupSeconds;

	private ConfigEntry<float> _overlayIntervalSeconds;

	private ConfigEntry<float> _pingIntervalSeconds;

	private ConfigEntry<float> _requestPollSeconds;

	private ConfigEntry<bool> _writeForensics;

	private ConfigEntry<float> _forensicThresholdMs;

	private ConfigEntry<bool> _logPeriodicSummary;

	private ConfigEntry<bool> _writeStatusJson;

	private ConfigEntry<float> _spikeThresholdMs;

	private ConfigEntry<float> _mainThreadStallThresholdMs;

	private ConfigEntry<bool> _announceHitchesInGame;

	private ConfigEntry<bool> _announceHitchesInChat;

	private ConfigEntry<string> _hitchChatTitle;

	private ConfigEntry<float> _announceHitchThresholdMs;

	private ConfigEntry<float> _announceHitchCooldownSeconds;

	private readonly GapStats _updateGaps = new GapStats(1024);

	private readonly GapStats _fixedGaps = new GapStats(1024);

	private readonly StallStats _mainThreadStalls = new StallStats(256);

	private readonly List<string> _patchFailures = new List<string>();

	private Process _process;

	private TimeSpan _lastCpu;

	private long _lastReportTicks;

	private long _lastUpdateTicks;

	private long _lastFixedTicks;

	private long _updates;

	private long _fixedUpdates;

	private long _missedFixedDeadlines;

	private long _updatesTotal;

	private long _fixedUpdatesTotal;

	private long _missedFixedDeadlinesTotal;

	private long _pendingGapReportedMisses;

	private long _lastOverlayTicks;

	private long _lastOverlayUpdatesTotal;

	private long _lastOverlayFixedTotal;

	private long _lastOverlayMissedTotal;

	private long _lastRequestPollTicks;

	private long _warmupUntilTicks;

	private bool _warmupComplete;

	private long _lastHitchAnnouncementTicks;

	private long _networkRxBytes;

	private long _networkTxBytes;

	private long _lastNetworkTicks;

	private static long _profilerOverheadTicks;

	private static long _profileCallsObserved;

	private static long _scopeRecoveries;

	private readonly long[] _hitchBuckets = new long[5];

	private readonly List<MiniSample> _recentSamples = new List<MiniSample>(64);

	private readonly object _outputLock = new object();

	private readonly Queue<OutputWork> _outputQueue = new Queue<OutputWork>();

	private bool _outputWorkerRunning;

	private int _outputQueueDepth;

	private int _lastEventGc0;

	private int _lastEventGc1;

	private int _lastEventGc2;

	private int _lastGc0;

	private int _lastGc1;

	private int _lastGc2;

	private string _statusPath;

	private string _networkStatusPath;

	private string _textPath;

	private string _requestPath;

	private string _forensicsPath;

	private static readonly TargetSpec[] CoreTargets = new TargetSpec[41]
	{
		new TargetSpec("Game", "Update", "Game.Update", deep: false),
		new TargetSpec("Game", "FixedUpdate", "Game.FixedUpdate", deep: false),
		new TargetSpec("Game", "CollectResources", "World/Game.CollectResources", deep: false),
		new TargetSpec("ZNet", "Update", "Network/ZNet.Update", deep: false),
		new TargetSpec("ZNet", "FixedUpdate", "Network/ZNet.FixedUpdate", deep: false),
		new TargetSpec("ZNet", "LateUpdate", "Network/ZNet.LateUpdate", deep: false),
		new TargetSpec("ZNet", "UpdatePeers", "Network/ZNet.UpdatePeers", deep: false),
		new TargetSpec("ZNet", "UpdateSave", "World/ZNet.UpdateSave", deep: false),
		new TargetSpec("ZNet", "SaveWorldAndPlayerProfiles", "World/Save.SaveWorldAndPlayerProfiles", deep: false),
		new TargetSpec("ZDOExtraData", "PrepareSave", "World/Save.PrepareSave", deep: false),
		new TargetSpec("ZDOMan", "Update", "Network/ZDOMan.Update", deep: false),
		new TargetSpec("ZDOMan", "SendZDOToPeers2", "Network/ZDOMan.SendZDOToPeers2", deep: false),
		new TargetSpec("ZDOMan", "SendZDOs", "Network/ZDOMan.SendZDOs", deep: false),
		new TargetSpec("ZDOMan", "CreateSyncList", "Network/ZDOMan.CreateSyncList", deep: false),
		new TargetSpec("ZDOMan", "RPC_ZDOData", "Network/ZDOMan.RPC_ZDOData", deep: false),
		new TargetSpec("ZNetScene", "Update", "World/ZNetScene.Update", deep: false),
		new TargetSpec("ZNetScene", "CreateDestroyObjects", "World/ZNetScene.CreateDestroyObjects", deep: false),
		new TargetSpec("ZoneSystem", "Update", "World/ZoneSystem.Update", deep: false),
		new TargetSpec("SpawnSystem", "UpdateSpawning", "World/SpawnSystem.UpdateSpawning", deep: false),
		new TargetSpec("EnvMan", "Update", "World/EnvMan.Update", deep: false),
		new TargetSpec("EnvMan", "FixedUpdate", "World/EnvMan.FixedUpdate", deep: false),
		new TargetSpec("Character", "CustomFixedUpdate", "Entities/Character.CustomFixedUpdate", deep: true),
		new TargetSpec("MonsterAI", "UpdateAI", "AI/MonsterAI.UpdateAI", deep: true),
		new TargetSpec("BaseAI", "UpdateAI", "AI/BaseAI.UpdateAI", deep: true),
		new TargetSpec("AnimalAI", "UpdateAI", "AI/AnimalAI.UpdateAI", deep: true),
		new TargetSpec("ZSyncTransform", "CustomFixedUpdate", "Network/ZSyncTransform.CustomFixedUpdate", deep: true),
		new TargetSpec("ZSyncTransform", "OwnerSync", "Network/ZSyncTransform.OwnerSync", deep: true),
		new TargetSpec("ZSyncTransform", "ClientSync", "Network/ZSyncTransform.ClientSync", deep: true),
		new TargetSpec("ZSyncAnimation", "CustomFixedUpdate", "Network/ZSyncAnimation.CustomFixedUpdate", deep: true),
		new TargetSpec("Projectile", "FixedUpdate", "Entities/Projectile.FixedUpdate", deep: true),
		new TargetSpec("Ship", "CustomFixedUpdate", "Entities/Ship.CustomFixedUpdate", deep: true),
		new TargetSpec("Fish", "CustomFixedUpdate", "Entities/Fish.CustomFixedUpdate", deep: true),
		new TargetSpec("ItemDrop", "SlowUpdate", "Items/ItemDrop.SlowUpdate", deep: true),
		new TargetSpec("ItemDrop", "PickupUpdate", "Items/ItemDrop.PickupUpdate", deep: true),
		new TargetSpec("ItemDrop", "EatUpdate", "Items/ItemDrop.EatUpdate", deep: true),
		new TargetSpec("WearNTear", "UpdateWear", "Build/WearNTear.UpdateWear", deep: true),
		new TargetSpec("WearNTear", "UpdateSupport", "Build/WearNTear.UpdateSupport", deep: true),
		new TargetSpec("Smelter", "UpdateSmelter", "Production/Smelter.UpdateSmelter", deep: true),
		new TargetSpec("LootSpawner", "UpdateSpawner", "World/LootSpawner.UpdateSpawner", deep: true),
		new TargetSpec("Heightmap", "CustomLateUpdate", "World/Heightmap.CustomLateUpdate", deep: true),
		new TargetSpec("ClutterSystem", "LateUpdate", "World/ClutterSystem.LateUpdate", deep: true)
	};

	private void Awake()
	{
		//IL_04df: Unknown result type (might be due to invalid IL or missing references)
		//IL_04e9: Expected O, but got Unknown
		_log = ((BaseUnityPlugin)this).Logger;
		_serverMode = IsDedicatedServerProcess();
		if (!_serverMode)
		{
			_profilingEnabled = false;
			((BaseUnityPlugin)this).Logger.LogInfo((object)"TickProfiler 1.0.3: client process detected; server profiler disabled. Install the companion client overlay from the same package for F8 UI.");
			return;
		}
		_enabled = ((BaseUnityPlugin)this).Config.Bind<bool>("General", "Enabled", true, "Enable collection. Patches remain installed but become a fast no-op when disabled.");
		_reportInterval = ((BaseUnityPlugin)this).Config.Bind<float>("General", "ReportIntervalSeconds", 10f, "Snapshot/report interval in seconds.");
		_topConsumers = ((BaseUnityPlugin)this).Config.Bind<int>("General", "TopConsumers", 12, "Number of consumers written to the BepInEx summary.");
		_deepGameProfiling = ((BaseUnityPlugin)this).Config.Bind<bool>("Profiling", "DeepGameProfiling", true, "Profile per-entity AI, sync, item, building and production hot paths.");
		_profilePluginLoops = ((BaseUnityPlugin)this).Config.Bind<bool>("Profiling", "ProfilePluginLoops", true, "Profile other BepInEx plugins' Update/FixedUpdate/LateUpdate methods. Harmony patch methods are never instrumented.");
		_legacyProfileModPatches = ((BaseUnityPlugin)this).Config.Bind<bool>("Profiling", "ProfileModHarmonyMethods", true, "DEPRECATED: migrated to ProfilePluginLoops. Kept for 0.x config compatibility.");
		if (!_legacyProfileModPatches.Value && _profilePluginLoops.Value)
		{
			_profilePluginLoops.Value = false;
		}
		_hotMethodSampleEveryN = ((BaseUnityPlugin)this).Config.Bind<int>("Profiling", "HotMethodSampleEveryN", 4, "Sample one in N calls for deep high-frequency game methods. 1 disables sampling. Aggregate time/call rates are scaled; max call time remains observed-only.");
		_warmupSeconds = ((BaseUnityPlugin)this).Config.Bind<float>("Profiling", "StartupWarmupSeconds", 45f, "Ignore startup/world-load stalls and tick misses for this many seconds after plugin startup.");
		_overlayIntervalSeconds = ((BaseUnityPlugin)this).Config.Bind<float>("Output", "OverlayIntervalSeconds", 2f, "Lightweight client overlay/tick graph refresh interval, independent of full report/log interval.");
		_pingIntervalSeconds = ((BaseUnityPlugin)this).Config.Bind<float>("Output", "PlayerPingIntervalSeconds", 1f, "Steam transport sampling interval for connected players. Used by the overlay and dashboard.");
		_requestPollSeconds = ((BaseUnityPlugin)this).Config.Bind<float>("Output", "RequestPollSeconds", 1f, "How often to check for BepInEx/tickprofiler.request instead of hitting the filesystem every frame.");
		_writeForensics = ((BaseUnityPlugin)this).Config.Bind<bool>("Forensics", "WriteHitchForensics", true, "Write a forensic JSON file for significant hitches on a background writer thread.");
		_forensicThresholdMs = ((BaseUnityPlugin)this).Config.Bind<float>("Forensics", "ForensicHitchThresholdMs", 100f, "Minimum hitch duration that creates a forensic capture.");
		_logPeriodicSummary = ((BaseUnityPlugin)this).Config.Bind<bool>("Output", "LogPeriodicSummary", true, "Write a compact summary to BepInEx/LogOutput.log each interval.");
		_writeStatusJson = ((BaseUnityPlugin)this).Config.Bind<bool>("Output", "WriteStatusJson", true, "Refresh BepInEx/tickprofiler-status.json each interval.");
		_spikeThresholdMs = ((BaseUnityPlugin)this).Config.Bind<float>("Output", "SpikeThresholdMs", 20f, "Mark a consumer as spiking when its worst observed call exceeds this many milliseconds.");
		_mainThreadStallThresholdMs = ((BaseUnityPlugin)this).Config.Bind<float>("Profiling", "MainThreadStallThresholdMs", 75f, "Record any Update-to-Update wall-clock gap at or above this threshold as a main-thread stall, regardless of cause or attribution.");
		_announceHitchesInGame = ((BaseUnityPlugin)this).Config.Bind<bool>("Notifications", "AnnounceHitchesInGame", false, "Also show the hitch as a top-left HUD notification. Chat broadcast is the default player-visible alert.");
		_announceHitchesInChat = ((BaseUnityPlugin)this).Config.Bind<bool>("Notifications", "AnnounceHitchesInChat", true, "Broadcast significant server hitches into the normal Valheim chat feed for every connected TickProfiler client.");
		_hitchChatTitle = ((BaseUnityPlugin)this).Config.Bind<string>("Notifications", "HitchChatTitle", "Server", "Name shown as the sender/title for hitch messages in Valheim chat.");
		_announceHitchThresholdMs = ((BaseUnityPlugin)this).Config.Bind<float>("Notifications", "AnnounceHitchThresholdMs", 250f, "Minimum main-thread hitch duration before an in-game server message is sent.");
		_announceHitchCooldownSeconds = ((BaseUnityPlugin)this).Config.Bind<float>("Notifications", "AnnounceHitchCooldownSeconds", 10f, "Minimum time between in-game hitch messages to avoid spam.");
		_profilingEnabled = _enabled.Value;
		_enabled.SettingChanged += delegate
		{
			_profilingEnabled = _enabled.Value;
		};
		_statusPath = Path.Combine(Paths.BepInExRootPath, "tickprofiler-status.json");
		_networkStatusPath = Path.Combine(Paths.BepInExRootPath, "tickprofiler-network.json");
		_textPath = Path.Combine(Paths.BepInExRootPath, "tickprofiler-latest.txt");
		_requestPath = Path.Combine(Paths.BepInExRootPath, "tickprofiler.request");
		_forensicsPath = Path.Combine(Paths.BepInExRootPath, "tickprofiler-forensics");
		_process = Process.GetCurrentProcess();
		_lastCpu = _process.TotalProcessorTime;
		_lastGc0 = GC.CollectionCount(0);
		_lastGc1 = GC.CollectionCount(1);
		_lastGc2 = GC.CollectionCount(2);
		_lastEventGc0 = _lastGc0;
		_lastEventGc1 = _lastGc1;
		_lastEventGc2 = _lastGc2;
		_lastReportTicks = Stopwatch.GetTimestamp();
		_lastUpdateTicks = _lastReportTicks;
		_lastFixedTicks = _lastReportTicks;
		_lastOverlayTicks = _lastReportTicks;
		_lastRequestPollTicks = _lastReportTicks;
		_lastNetworkTicks = _lastReportTicks;
		_warmupUntilTicks = _lastReportTicks + (long)(Math.Max(0.0, _warmupSeconds.Value) * (double)Frequency);
		ReadProcNetwork(out _networkRxBytes, out _networkTxBytes);
		_harmony = new Harmony("magicorp.valheim.tickprofiler");
		PatchGameTargets();
		((BaseUnityPlugin)this).Logger.LogInfo((object)string.Format("{0} {1}: patched {2} game methods; deep={3}", "TickProfiler", "1.0.3", MethodMetrics.Count, _deepGameProfiling.Value));
	}

	private void Start()
	{
		if (_serverMode)
		{
			TryRegisterProfilerRpc();
			if (_profilePluginLoops.Value)
			{
				ProfileLoadedMods();
				((BaseUnityPlugin)this).Logger.LogInfo((object)string.Format("{0}: total instrumented methods after mod scan: {1}", "TickProfiler", MethodMetrics.Count));
			}
		}
	}

	private void OnDestroy()
	{
		try
		{
			Harmony harmony = _harmony;
			if (harmony != null)
			{
				harmony.UnpatchSelf();
			}
		}
		catch
		{
		}
	}

	private void Update()
	{
		if (!_serverMode)
		{
			return;
		}
		TryRegisterProfilerRpc();
		UpdatePlayerPings();
		if (!_profilingEnabled)
		{
			return;
		}
		long timestamp = Stopwatch.GetTimestamp();
		long num = timestamp;
		_updates++;
		_updatesTotal++;
		if (!_warmupComplete && num >= _warmupUntilTicks)
		{
			_warmupComplete = true;
			ResetMeasurementWindow(num);
			((BaseUnityPlugin)this).Logger.LogInfo((object)"TickProfiler warmup complete; live measurements started.");
		}
		if (_warmupComplete && _lastUpdateTicks != 0L)
		{
			long lastUpdateTicks = _lastUpdateTicks;
			double num2 = ToMilliseconds(num - lastUpdateTicks);
			_updateGaps.Add(num2);
			int num3 = GC.CollectionCount(0);
			int num4 = GC.CollectionCount(1);
			int num5 = GC.CollectionCount(2);
			bool gcDuringWindow = num3 != _lastEventGc0 || num4 != _lastEventGc1 || num5 != _lastEventGc2;
			_lastEventGc0 = num3;
			_lastEventGc1 = num4;
			_lastEventGc2 = num5;
			if (num2 >= (double)_mainThreadStallThresholdMs.Value)
			{
				StallAttribution attribution = ResolveStallCause(lastUpdateTicks, num, num2, gcDuringWindow);
				string text = attribution.Text;
				((BaseUnityPlugin)this).Logger.LogInfo((object)("HITCH EVENT  " + F(num2, 1) + " ms | " + attribution.Confidence + " | cause " + text));
				_mainThreadStalls.Add(num2, ExpectedUpdateMs(), text + " [" + attribution.Confidence + "]");
				int num6 = ProfilerCore.HitchBucketIndex(num2);
				Interlocked.Increment(ref _hitchBuckets[num6]);
				RecordHitchEvent(num, num2, attribution);
				MaybeAnnounceHitch(num, num2, text);
				BroadcastHitchEvent(num2, attribution);
			}
		}
		_lastUpdateTicks = num;
		bool flag = false;
		if (ToSeconds(num - _lastRequestPollTicks) >= Math.Max(0.25, _requestPollSeconds.Value))
		{
			_lastRequestPollTicks = num;
			try
			{
				if (File.Exists(_requestPath))
				{
					File.Delete(_requestPath);
					flag = true;
				}
			}
			catch (Exception ex)
			{
				((BaseUnityPlugin)this).Logger.LogWarning((object)("Unable to consume tickprofiler.request: " + ex.Message));
			}
		}
		if (_warmupComplete && ToSeconds(num - _lastOverlayTicks) >= Math.Max(0.5, _overlayIntervalSeconds.Value))
		{
			BroadcastLightweightOverlay(num);
		}
		double num7 = ToSeconds(num - _lastReportTicks);
		double num8 = Math.Max(1.0, _reportInterval.Value);
		if (_warmupComplete && (flag || num7 >= num8))
		{
			Report(num, flag);
		}
		Interlocked.Add(ref _profilerOverheadTicks, Stopwatch.GetTimestamp() - timestamp);
	}

	private void FixedUpdate()
	{
		if (!_serverMode || !_profilingEnabled)
		{
			return;
		}
		long timestamp = Stopwatch.GetTimestamp();
		long num = timestamp;
		_fixedUpdatesTotal++;
		if (!_warmupComplete)
		{
			_lastFixedTicks = num;
			Interlocked.Add(ref _profilerOverheadTicks, Stopwatch.GetTimestamp() - timestamp);
			return;
		}
		_fixedUpdates++;
		if (_lastFixedTicks != 0L)
		{
			double num2 = ToMilliseconds(num - _lastFixedTicks);
			_fixedGaps.Add(num2);
			double fixedDeltaMs = ((Time.fixedDeltaTime > 0f) ? ((double)Time.fixedDeltaTime * 1000.0) : 20.0);
			long num3 = ProfilerCore.CountMissedFixedDeadlines(num2, fixedDeltaMs);
			long num4 = Math.Max(0L, num3 - _pendingGapReportedMisses);
			_missedFixedDeadlines += num4;
			_missedFixedDeadlinesTotal += num4;
			_pendingGapReportedMisses = 0L;
		}
		_lastFixedTicks = num;
		Interlocked.Add(ref _profilerOverheadTicks, Stopwatch.GetTimestamp() - timestamp);
	}

	private void PatchGameTargets()
	{
		TargetSpec[] coreTargets = CoreTargets;
		for (int i = 0; i < coreTargets.Length; i++)
		{
			TargetSpec spec = coreTargets[i];
			if (spec.Deep && !_deepGameProfiling.Value)
			{
				continue;
			}
			Type type = AccessTools.TypeByName(spec.TypeName);
			if (type == null)
			{
				AddPatchFailure(spec.Label + ": type not found");
				continue;
			}
			MethodInfo[] array;
			try
			{
				array = (from m in type.GetMethods(BindingFlags.Instance | BindingFlags.Static | BindingFlags.Public | BindingFlags.NonPublic)
					where m.Name == spec.MethodName && !m.IsAbstract && !m.ContainsGenericParameters
					select m).ToArray();
			}
			catch (Exception ex)
			{
				AddPatchFailure(spec.Label + ": enumeration failed: " + ex.Message);
				continue;
			}
			if (array.Length == 0)
			{
				AddPatchFailure(spec.Label + ": method not found");
				continue;
			}
			MethodInfo[] array2 = array;
			foreach (MethodInfo method in array2)
			{
				InstrumentMethod(method, spec.Label, "game", (!spec.Deep) ? 1 : Math.Max(1, _hotMethodSampleEveryN.Value), spec.Label == "Game.Update" || spec.Label == "Game.FixedUpdate");
			}
		}
	}

	private void ProfileLoadedMods()
	{
		foreach (KeyValuePair<string, PluginInfo> pluginInfo in Chainloader.PluginInfos)
		{
			PluginInfo value = pluginInfo.Value;
			if (value == null || (Object)(object)value.Instance == (Object)null)
			{
				continue;
			}
			Assembly assembly = ((object)value.Instance).GetType().Assembly;
			if (assembly == typeof(TickProfilerPlugin).Assembly)
			{
				continue;
			}
			string text = ((value.Metadata != null && !string.IsNullOrEmpty(value.Metadata.Name)) ? value.Metadata.Name : pluginInfo.Key);
			foreach (Type item in SafeGetTypes(assembly))
			{
				if (!typeof(BaseUnityPlugin).IsAssignableFrom(item))
				{
					continue;
				}
				MethodInfo[] methods;
				try
				{
					methods = item.GetMethods(BindingFlags.DeclaredOnly | BindingFlags.Instance | BindingFlags.Static | BindingFlags.Public | BindingFlags.NonPublic);
				}
				catch
				{
					continue;
				}
				MethodInfo[] array = methods;
				foreach (MethodInfo methodInfo in array)
				{
					if (!methodInfo.IsAbstract && !methodInfo.ContainsGenericParameters && methodInfo.GetMethodBody() != null && (methodInfo.Name == "Update" || methodInfo.Name == "FixedUpdate" || methodInfo.Name == "LateUpdate"))
					{
						string label = "Mod/" + text + "/" + item.Name + "." + methodInfo.Name;
						InstrumentMethod(methodInfo, label, "mod", 1, exceptionSafe: true);
					}
				}
			}
		}
	}

	private static IEnumerable<Type> SafeGetTypes(Assembly assembly)
	{
		try
		{
			return assembly.GetTypes();
		}
		catch (ReflectionTypeLoadException ex)
		{
			return ex.Types.Where((Type t) => t != null);
		}
		catch
		{
			return Array.Empty<Type>();
		}
	}

	private static bool IsOneShotPatchType(string typeName)
	{
		if (string.IsNullOrEmpty(typeName))
		{
			return false;
		}
		if (typeName.IndexOf("Awake", StringComparison.OrdinalIgnoreCase) < 0 && typeName.IndexOf("Start_Patch", StringComparison.OrdinalIgnoreCase) < 0)
		{
			return typeName.IndexOf("OnDestroy", StringComparison.OrdinalIgnoreCase) >= 0;
		}
		return true;
	}

	private static bool HasAttributeNamed(object[] attrs, string name)
	{
		for (int i = 0; i < attrs.Length; i++)
		{
			Type type = attrs[i]?.GetType();
			if (type != null && (type.Name == name || type.FullName == "HarmonyLib." + name))
			{
				return true;
			}
		}
		return false;
	}

	private void InstrumentMethod(MethodBase method, string label, string kind, int sampleEveryN, bool exceptionSafe)
	{
		//IL_0056: Unknown result type (might be due to invalid IL or missing references)
		//IL_005b: Unknown result type (might be due to invalid IL or missing references)
		//IL_0067: Expected O, but got Unknown
		//IL_0077: Unknown result type (might be due to invalid IL or missing references)
		//IL_007c: Unknown result type (might be due to invalid IL or missing references)
		//IL_0084: Expected O, but got Unknown
		//IL_009b: 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)
		if (method == null || MethodMetrics.ContainsKey(method))
		{
			return;
		}
		string patchOwners = GetPatchOwners(method);
		Metric metric = new Metric(label, kind, Math.Max(1, sampleEveryN), patchOwners);
		MethodMetrics.Add(method, metric);
		Metrics.Add(metric);
		try
		{
			HarmonyMethod val = new HarmonyMethod(typeof(TickProfilerPlugin), "ProfilePrefix", (Type[])null)
			{
				priority = 800
			};
			HarmonyMethod val2 = new HarmonyMethod(typeof(TickProfilerPlugin), "ProfilePostfix", (Type[])null)
			{
				priority = 0
			};
			HarmonyMethod val3 = ((!exceptionSafe) ? ((HarmonyMethod)null) : new HarmonyMethod(typeof(TickProfilerPlugin), "ProfileFinalizer", (Type[])null)
			{
				priority = 0
			});
			_harmony.Patch(method, val, val2, (HarmonyMethod)null, val3, (HarmonyMethod)null);
		}
		catch (Exception ex)
		{
			MethodMetrics.Remove(method);
			Metrics.Remove(metric);
			AddPatchFailure(label + ": " + ex.GetType().Name + " " + ex.Message);
		}
	}

	private static void ProfilePrefix(MethodBase __originalMethod, out int __state)
	{
		long timestamp = Stopwatch.GetTimestamp();
		__state = -1;
		if (!_profilingEnabled)
		{
			Interlocked.Add(ref _profilerOverheadTicks, Stopwatch.GetTimestamp() - timestamp);
			return;
		}
		if (!MethodMetrics.TryGetValue(__originalMethod, out var value))
		{
			Interlocked.Add(ref _profilerOverheadTicks, Stopwatch.GetTimestamp() - timestamp);
			return;
		}
		Interlocked.Increment(ref _profileCallsObserved);
		if (!value.ShouldSample())
		{
			Interlocked.Add(ref _profilerOverheadTicks, Stopwatch.GetTimestamp() - timestamp);
			return;
		}
		if (_scopeStack == null)
		{
			_scopeStack = new ScopeFrame[64];
		}
		if (_scopeDepth >= _scopeStack.Length)
		{
			Array.Resize(ref _scopeStack, _scopeStack.Length * 2);
		}
		int num = _scopeDepth++;
		_scopeStack[num].Metric = value;
		_scopeStack[num].StartTicks = Stopwatch.GetTimestamp();
		_scopeStack[num].ChildTicks = 0L;
		__state = num;
		Interlocked.Add(ref _profilerOverheadTicks, Stopwatch.GetTimestamp() - timestamp);
	}

	private static void ProfilePostfix(ref int __state)
	{
		long timestamp = Stopwatch.GetTimestamp();
		CompleteProfile(ref __state);
		Interlocked.Add(ref _profilerOverheadTicks, Stopwatch.GetTimestamp() - timestamp);
	}

	private static Exception ProfileFinalizer(Exception __exception, ref int __state)
	{
		long timestamp = Stopwatch.GetTimestamp();
		if (__state >= 0)
		{
			Interlocked.Increment(ref _scopeRecoveries);
		}
		CompleteProfile(ref __state);
		Interlocked.Add(ref _profilerOverheadTicks, Stopwatch.GetTimestamp() - timestamp);
		return __exception;
	}

	private static void CompleteProfile(ref int __state)
	{
		if (__state < 0 || _scopeStack == null || __state >= _scopeStack.Length)
		{
			__state = -1;
			return;
		}
		long num = Stopwatch.GetTimestamp() - _scopeStack[__state].StartTicks;
		long childTicks = _scopeStack[__state].ChildTicks;
		long num2 = num - childTicks;
		if (num2 < 0)
		{
			num2 = 0L;
		}
		Metric metric = _scopeStack[__state].Metric;
		metric?.Add(num2, num);
		if (metric != null)
		{
			long timestamp = Stopwatch.GetTimestamp();
			RecordLongCall(metric, _scopeStack[__state].StartTicks, timestamp, num);
		}
		int num3 = (_scopeDepth = __state);
		if (num3 > 0)
		{
			_scopeStack[num3 - 1].ChildTicks += num;
		}
		__state = -1;
	}

	private void Report(long nowTicks, bool requested)
	{
		double num = Math.Max(0.001, ToSeconds(nowTicks - _lastReportTicks));
		long updates = _updates;
		long fixedUpdates = _fixedUpdates;
		long num2 = _missedFixedDeadlines;
		double fixedDeltaMs = ((Time.fixedDeltaTime > 0f) ? ((double)Time.fixedDeltaTime * 1000.0) : 20.0);
		if (_lastFixedTicks != 0L)
		{
			long num3 = ProfilerCore.CountNewPendingMisses(ToMilliseconds(nowTicks - _lastFixedTicks), fixedDeltaMs, _pendingGapReportedMisses);
			if (num3 > 0)
			{
				num2 += num3;
				_pendingGapReportedMisses += num3;
				_missedFixedDeadlinesTotal += num3;
			}
		}
		_updates = 0L;
		_fixedUpdates = 0L;
		_missedFixedDeadlines = 0L;
		_lastReportTicks = nowTicks;
		double updateHz = (double)updates / num;
		double rawFixedThroughputHz = (double)fixedUpdates / num;
		double num4 = ((Time.fixedDeltaTime > 0f) ? (1.0 / (double)Time.fixedDeltaTime) : 0.0);
		double num5 = num4 * num;
		double val = Math.Max(0.0, num5 - (double)num2);
		double fixedHz = Math.Min(fixedUpdates, val) / num;
		GapSnapshot updateGap = _updateGaps.SnapshotAndReset();
		GapSnapshot fixedGap = _fixedGaps.SnapshotAndReset();
		StallSnapshot mainThreadStall = _mainThreadStalls.SnapshotAndReset();
		TimeSpan totalProcessorTime = _process.TotalProcessorTime;
		double totalMilliseconds = (totalProcessorTime - _lastCpu).TotalMilliseconds;
		_lastCpu = totalProcessorTime;
		double num6 = totalMilliseconds / (num * 1000.0) * 100.0;
		double cpuVmPct = num6 / (double)Math.Max(1, Environment.ProcessorCount);
		int num7 = GC.CollectionCount(0);
		int num8 = GC.CollectionCount(1);
		int num9 = GC.CollectionCount(2);
		int gc = num7 - _lastGc0;
		int gc2 = num8 - _lastGc1;
		int gc3 = num9 - _lastGc2;
		_lastGc0 = num7;
		_lastGc1 = num8;
		_lastGc2 = num9;
		List<MetricSnapshot> list = new List<MetricSnapshot>(Metrics.Count);
		foreach (Metric metric in Metrics)
		{
			MetricRaw raw = metric.SnapshotAndReset();
			if (raw.Calls != 0L)
			{
				list.Add(MetricSnapshot.From(metric, raw, num));
			}
		}
		list.Sort((MetricSnapshot a, MetricSnapshot b) => b.SelfMsPerSec.CompareTo(a.SelfMsPerSec));
		_process.Refresh();
		double managedMiB = (double)GC.GetTotalMemory(forceFullCollection: false) / 1048576.0;
		double rssMiB = (double)_process.WorkingSet64 / 1048576.0;
		double privateMiB = (double)_process.PrivateMemorySize64 / 1048576.0;
		long ticks = Interlocked.Exchange(ref _profilerOverheadTicks, 0L);
		long num10 = Interlocked.Exchange(ref _profileCallsObserved, 0L);
		double profilerOverheadPct = ToMilliseconds(ticks) / (num * 1000.0) * 100.0;
		RuntimeContext runtime = CaptureRuntimeContext(nowTicks, num);
		ReportSnapshot s = new ReportSnapshot
		{
			TimestampUtc = DateTime.UtcNow,
			Requested = requested,
			WallSeconds = num,
			UpdateHz = updateHz,
			FixedHz = fixedHz,
			RawFixedThroughputHz = rawFixedThroughputHz,
			MissedFixedDeadlines = num2,
			NominalFixedHz = num4,
			FixedDeltaMs = (double)Time.fixedDeltaTime * 1000.0,
			TimeScale = Time.timeScale,
			UpdateGap = updateGap,
			FixedGap = fixedGap,
			MainThreadStall = mainThreadStall,
			CpuOneCorePct = num6,
			CpuVmPct = cpuVmPct,
			ManagedMiB = managedMiB,
			RssMiB = rssMiB,
			PrivateMiB = privateMiB,
			ProfilerOverheadPct = profilerOverheadPct,
			ProfiledCallsPerSec = (double)num10 / num,
			ScopeRecoveries = Interlocked.Read(in _scopeRecoveries),
			OutputQueueDepth = Volatile.Read(in _outputQueueDepth),
			Runtime = runtime,
			HitchBuckets = _hitchBuckets.ToArray(),
			Gc0 = gc,
			Gc1 = gc2,
			Gc2 = gc3,
			Metrics = list,
			PatchFailures = _patchFailures.ToArray(),
			PatchedMethods = MethodMetrics.Count
		};
		string text = BuildText(s);
		_latestOverlayText = BuildOverlayText(s, v2: true);
		_latestLegacyOverlayText = BuildOverlayText(s, v2: false);
		BroadcastOverlaySnapshot();
		string json = (_writeStatusJson.Value ? BuildJson(s) : null);
		QueueOutput(new OutputWork
		{
			TextPath = _textPath,
			Text = text,
			JsonPath = (_writeStatusJson.Value ? _statusPath : null),
			Json = json
		});
		AddRecentSample(s);
		if (_logPeriodicSummary.Value || requested)
		{
			string text2 = BuildConsoleText(s);
			((BaseUnityPlugin)this).Logger.LogInfo((object)("\n" + text2));
		}
	}

	private static double ExpectedUpdateMs()
	{
		int targetFrameRate = Application.targetFrameRate;
		if (targetFrameRate <= 0)
		{
			return 33.333333333333336;
		}
		return 1000.0 / (double)targetFrameRate;
	}

	private string LikelyStallCause(ReportSnapshot s)
	{
		if (s.MainThreadStall.Count == 0L)
		{
			return "none";
		}
		if (!string.IsNullOrEmpty(s.MainThreadStall.WorstCause))
		{
			return s.MainThreadStall.WorstCause;
		}
		return "unattributed (Unity/native/GC/I/O/OS)";
	}

	private StallAttribution ResolveStallCause(long windowStartTicks, long windowEndTicks, double gapMs, bool gcDuringWindow)
	{
		LongCallEvent longCallEvent = FindBestLongCall(windowStartTicks, windowEndTicks);
		if (longCallEvent != null)
		{
			double num = longCallEvent.ElapsedMs / Math.Max(1.0, gapMs);
			double num2 = Math.Max(20.0, gapMs * 0.4);
			if (longCallEvent.ElapsedMs >= num2)
			{
				string confidence = ((num >= 0.8) ? "high confidence" : "medium confidence");
				string text = (string.IsNullOrEmpty(longCallEvent.PatchOwners) ? string.Empty : (" | patched by " + longCallEvent.PatchOwners + " (involved, not proven cause)"));
				return new StallAttribution(longCallEvent.Name + " (" + F(longCallEvent.ElapsedMs, 1) + " ms)" + text, confidence, longCallEvent.Name);
			}
		}
		if (gcDuringWindow)
		{
			return new StallAttribution("GC observed during hitch; causation not proven", "low confidence", "GC observed");
		}
		return new StallAttribution("unattributed (Unity/native/I/O/OS)", "unknown confidence", "unattributed");
	}

	private static LongCallEvent FindBestLongCall(long windowStartTicks, long windowEndTicks)
	{
		lock (RecentLongCallLock)
		{
			LongCallEvent longCallEvent = null;
			for (int num = RecentLongCalls.Count - 1; num >= 0; num--)
			{
				LongCallEvent longCallEvent2 = RecentLongCalls[num];
				if (longCallEvent2.EndTicks < windowStartTicks)
				{
					break;
				}
				if (longCallEvent2.StartTicks <= windowEndTicks && longCallEvent2.EndTicks >= windowStartTicks && (longCallEvent == null || longCallEvent2.ElapsedMs > longCallEvent.ElapsedMs))
				{
					longCallEvent = longCallEvent2;
				}
			}
			return longCallEvent;
		}
	}

	private static void RecordLongCall(Metric metric, long startTicks, long endTicks, long elapsedTicks)
	{
		if (metric == null)
		{
			return;
		}
		double num = ToMilliseconds(elapsedTicks);
		if (num < 20.0)
		{
			return;
		}
		lock (RecentLongCallLock)
		{
			RecentLongCalls.Add(new LongCallEvent
			{
				Name = metric.Name,
				Kind = metric.Kind,
				PatchOwners = metric.PatchOwners,
				StartTicks = startTicks,
				EndTicks = endTicks,
				ElapsedMs = num
			});
			long num2 = endTicks - Frequency * 15;
			while (RecentLongCalls.Count > 0 && (RecentLongCalls[0].EndTicks < num2 || RecentLongCalls.Count > 512))
			{
				RecentLongCalls.RemoveAt(0);
			}
		}
	}

	private void MaybeAnnounceHitch(long nowTicks, double gapMs, string cause)
	{
		if (gapMs < (double)_announceHitchThresholdMs.Value || (!_announceHitchesInGame.Value && !_announceHitchesInChat.Value) || ((_lastHitchAnnouncementTicks == 0L) ? double.MaxValue : ToSeconds(nowTicks - _lastHitchAnnouncementTicks)) < Math.Max(0.0, _announceHitchCooldownSeconds.Value))
		{
			return;
		}
		ZRoutedRpc instance = ZRoutedRpc.instance;
		ZNet instance2 = ZNet.instance;
		if (instance == null || (Object)(object)instance2 == (Object)null)
		{
			return;
		}
		string text = "Hitch " + F(gapMs, 0) + " ms | " + cause;
		bool flag = false;
		try
		{
			if (_announceHitchesInGame.Value)
			{
				instance.InvokeRoutedRPC(0L, "ShowMessage", new object[2]
				{
					1,
					"Server " + text
				});
				flag = true;
			}
			if (_announceHitchesInChat.Value)
			{
				string text2 = (string.IsNullOrWhiteSpace(_hitchChatTitle.Value) ? "Server" : _hitchChatTitle.Value.Trim());
				long[] array = _overlayPeers.ToArray();
				foreach (long num in array)
				{
					try
					{
						if (instance2.GetPeer(num) == null)
						{
							_overlayPeers.Remove(num);
							continue;
						}
						instance.InvokeRoutedRPC(num, "MagiCorp_TickProfiler_HitchNotice_v1", new object[2] { text2, text });
						flag = true;
					}
					catch
					{
						_overlayPeers.Remove(num);
						_v2OverlayPeers.Remove(num);
					}
				}
			}
			if (flag)
			{
				_lastHitchAnnouncementTicks = nowTicks;
				((BaseUnityPlugin)this).Logger.LogInfo((object)("HITCH NOTICE  " + (_hitchChatTitle.Value ?? "Server") + ": " + text + " | recipients=" + _overlayPeers.Count));
			}
		}
		catch (Exception ex)
		{
			((BaseUnityPlugin)this).Logger.LogWarning((object)("Unable to send hitch notice: " + ex.Message));
		}
	}

	private static bool IsDedicatedServerProcess()
	{
		try
		{
			return (Process.GetCurrentProcess().ProcessName ?? string.Empty).IndexOf("valheim_server", StringComparison.OrdinalIgnoreCase) >= 0;
		}
		catch
		{
			return false;
		}
	}

	private void TryRegisterProfilerRpc()
	{
		if (_rpcRegistered)
		{
			return;
		}
		ZRoutedRpc instance = ZRoutedRpc.instance;
		if (instance == null)
		{
			return;
		}
		try
		{
			instance.Register("MagiCorp_TickProfiler_Hello_v1", (Action<long>)OnProfilerHello);
			instance.Register<string>("MagiCorp_TickProfiler_Hello_v2", (Action<long, string>)OnProfilerHelloV2);
			_rpcRegistered = true;
			((BaseUnityPlugin)this).Logger.LogInfo((object)"TickProfiler client-overlay RPC v1+v2 and Steam transport telemetry registered.");
		}
		catch (Exception ex)
		{
			((BaseUnityPlugin)this).Logger.LogWarning((object)("TickProfiler RPC registration failed: " + ex.Message));
		}
	}

	private void OnProfilerHello(long sender)
	{
		if (!_serverMode || sender == 0L)
		{
			return;
		}
		if (_overlayPeers.Add(sender))
		{
			((BaseUnityPlugin)this).Logger.LogInfo((object)("TickProfiler legacy overlay client registered: peer=" + sender + " clients=" + _overlayPeers.Count));
		}
		if (string.IsNullOrEmpty(_latestLegacyOverlayText) || _v2OverlayPeers.ContainsKey(sender))
		{
			return;
		}
		try
		{
			ZRoutedRpc instance = ZRoutedRpc.instance;
			if (instance != null)
			{
				instance.InvokeRoutedRPC(sender, "MagiCorp_TickProfiler_Snapshot_v1", new object[1] { _latestLegacyOverlayText });
			}
		}
		catch
		{
		}
	}

	private void OnProfilerHelloV2(long sender, string clientInfo)
	{
		if (!_serverMode || sender == 0L)
		{
			return;
		}
		_overlayPeers.Add(sender);
		_v2OverlayPeers[sender] = clientInfo ?? "unknown";
		string text = ParseClientInfoValue(clientInfo, "player");
		if (!_peerPings.TryGetValue(sender, out var value))
		{
			value = (_peerPings[sender] = new PeerPingState());
		}
		if (!string.IsNullOrWhiteSpace(text))
		{
			value.Player = text;
		}
		((BaseUnityPlugin)this).Logger.LogInfo((object)("TickProfiler v2 overlay registered: peer=" + sender + " client=" + (clientInfo ?? "unknown")));
		if (string.IsNullOrEmpty(_latestOverlayText))
		{
			return;
		}
		try
		{
			ZRoutedRpc instance = ZRoutedRpc.instance;
			if (instance != null)
			{
				instance.InvokeRoutedRPC(sender, "MagiCorp_TickProfiler_Snapshot_v2", new object[1] { _latestOverlayText });
			}
		}
		catch
		{
		}
	}

	private void UpdatePlayerPings()
	{
		if (!_serverMode || !_rpcRegistered || ZRoutedRpc.instance == null || (Object)(object)ZNet.instance == (Object)null)
		{
			return;
		}
		long timestamp = Stopwatch.GetTimestamp();
		if (_lastPingProbeTicks != 0L && ToSeconds(timestamp - _lastPingProbeTicks) < Math.Max(0.25, _pingIntervalSeconds.Value))
		{
			return;
		}
		_lastPingProbeTicks = timestamp;
		HashSet<long> seen = new HashSet<long>();
		float num = default(float);
		float num2 = default(float);
		int num3 = default(int);
		float num4 = default(float);
		float num5 = default(float);
		foreach (ZNetPeer peer in ZNet.instance.GetPeers())
		{
			if (peer == null || !peer.IsReady() || peer.m_socket == null || peer.m_uid == 0L)
			{
				continue;
			}
			long uid = peer.m_uid;
			seen.Add(uid);
			if (!_peerPings.TryGetValue(uid, out var value))
			{
				value = (_peerPings[uid] = new PeerPingState());
			}
			if (!string.IsNullOrWhiteSpace(peer.m_playerName))
			{
				value.Player = peer.m_playerName;
			}
			try
			{
				peer.m_socket.GetConnectionQuality(ref num, ref num2, ref num3, ref num4, ref num5);
				if (num3 >= 0)
				{
					value.PingMs = num3;
					value.UpdatedTicks = timestamp;
				}
			}
			catch
			{
			}
		}
		long[] array = _peerPings.Keys.Where((long x) => !seen.Contains(x)).ToArray();
		foreach (long key in array)
		{
			_peerPings.Remove(key);
		}
		string text = BuildPingWire(timestamp);
		array = _v2OverlayPeers.Keys.ToArray();
		foreach (long num7 in array)
		{
			try
			{
				if (ZNet.instance.GetPeer(num7) == null)
				{
					_overlayPeers.Remove(num7);
					_v2OverlayPeers.Remove(num7);
				}
				else
				{
					ZRoutedRpc.instance.InvokeRoutedRPC(num7, "MagiCorp_TickProfiler_PingSnapshot_v2", new object[1] { text });
				}
			}
			catch
			{
				_overlayPeers.Remove(num7);
				_v2OverlayPeers.Remove(num7);
			}
		}
		QueueOutput(new OutputWork
		{
			ExtraPath = _networkStatusPath,
			Extra = BuildNetworkJson(timestamp)
		});
	}

	private static string ParseClientInfoValue(string info, string wanted)
	{
		if (string.IsNullOrEmpty(info))
		{
			return string.Empty;
		}
		string[] array = info.Split(new char[1] { '|' });
		foreach (string text in array)
		{
			int num = text.IndexOf('=');
			if (num > 0 && text.Substring(0, num).Equals(wanted, StringComparison.OrdinalIgnoreCase))
			{
				try
				{
					return Uri.UnescapeDataString(text.Substring(num + 1));
				}
				catch
				{
					return text.Substring(num + 1);
				}
			}
		}
		return string.Empty;
	}

	private string BuildPingWire(long nowTicks)
	{
		StringBuilder stringBuilder = new StringBuilder(512);
		stringBuilder.Append("@TPPING|players=");
		bool flag = true;
		foreach (KeyValuePair<long, PeerPingState> item in _peerPings.OrderBy<KeyValuePair<long, PeerPingState>, string>((KeyValuePair<long, PeerPingState> x) => x.Value.Player ?? string.Empty, StringComparer.OrdinalIgnoreCase))
		{
			PeerPingState value = item.Value;
			if (value.UpdatedTicks != 0L && !(ToSeconds(nowTicks - value.UpdatedTicks) > 10.0))
			{
				if (!flag)
				{
					stringBuilder.Append(',');
				}
				flag = false;
				string stringToEscape = (string.IsNullOrWhiteSpace(value.Player) ? ("peer-" + item.Key.ToString(CultureInfo.InvariantCulture)) : value.Player);
				stringBuilder.Append(item.Key.ToString(CultureInfo.InvariantCulture)).Append('~').Append(Uri.EscapeDataString(stringToEscape))
					.Append('~')
					.Append(F(value.PingMs, 1))
					.Append('~')
					.Append(F(ToSeconds(nowTicks - value.UpdatedTicks), 2));
			}
		}
		return stringBuilder.ToString();
	}

	private string BuildNetworkJson(long nowTicks)
	{
		StringBuilder stringBuilder = new StringBuilder(1024);
		stringBuilder.Append('{');
		JsonProp(stringBuilder, "version", "1.0.3", first: true);
		JsonProp(stringBuilder, "timestampUtc", DateTime.UtcNow.ToString("O"), first: false);
		JsonProp(stringBuilder, "source", "steam-transport", first: false);
		stringBuilder.Append(",\"players\":[");
		bool flag = true;
		foreach (KeyValuePair<long, PeerPingState> item in _peerPings.OrderBy<KeyValuePair<long, PeerPingState>, string>((KeyValuePair<long, PeerPingState> x) => x.Value.Player ?? string.Empty, StringComparer.OrdinalIgnoreCase))
		{
			PeerPingState value = item.Value;
			if (value.UpdatedTicks != 0L && !(ToSeconds(nowTicks - value.UpdatedTicks) > 10.0))
			{
				if (!flag)
				{
					stringBuilder.Append(',');
				}
				flag = false;
				stringBuilder.Append('{');
				JsonProp(stringBuilder, "peer", item.Key, first: true);
				JsonProp(stringBuilder, "player", value.Player ?? string.Empty, first: false);
				JsonProp(stringBuilder, "pingMs", value.PingMs, first: false);
				JsonProp(stringBuilder, "source", "steam-transport", first: false);
				JsonProp(stringBuilder, "ageSeconds", ToSeconds(nowTicks - value.UpdatedTicks), first: false);
				stringBuilder.Append('}');
			}
		}
		stringBuilder.Append("]}");
		return stringBuilder.ToString();
	}

	private void BroadcastOverlaySnapshot()
	{
		if (string.IsNullOrEmpty(_latestOverlayText) || _overlayPeers.Count == 0)
		{
			return;
		}
		ZRoutedRpc instance = ZRoutedRpc.instance;
		ZNet instance2 = ZNet.instance;
		if (instance == null || (Object)(object)instance2 == (Object)null)
		{
			return;
		}
		long[] array = _overlayPeers.ToArray();
		foreach (long num in array)
		{
			try
			{
				if (instance2.GetPeer(num) == null)
				{
					_overlayPeers.Remove(num);
				}
				else if (_v2OverlayPeers.ContainsKey(num))
				{
					instance.InvokeRoutedRPC(num, "MagiCorp_TickProfiler_Snapshot_v2", new object[1] { _latestOverlayText });
				}
				else
				{
					instance.InvokeRoutedRPC(num, "MagiCorp_TickProfiler_Snapshot_v1", new object[1] { _latestLegacyOverlayText });
				}
			}
			catch
			{
				_overlayPeers.Remove(num);
				_v2OverlayPeers.Remove(num);
				_peerPings.Remove(num);
			}
		}
	}

	private string BuildOverlayText(ReportSnapshot s, bool v2)
	{
		StringBuilder stringBuilder = new StringBuilder(3072);
		if (v2)
		{
			stringBuilder.Append("@TPPROTO|protocol=").Append(2).Append("|server=")
				.Append("1.0.3")
				.Append("|features=hitch-events,forensics,runtime,sampling,steam-transport\n");
		}
		double value = ((s.NominalFixedHz > 0.0) ? Math.Max(0.0, (s.NominalFixedHz - s.FixedHz) / s.NominalFixedHz * 100.0) : 0.0);
		double value2 = ((s.WallSeconds > 0.0) ? (s.MainThreadStall.TotalExcessMs / (s.WallSeconds * 1000.0) * 100.0) : 0.0);
		stringBuilder.Append("SERVER TICK PROFILER\n");
		stringBuilder.Append("Tick rate ").Append(F(s.FixedHz, 1)).Append('/')
			.Append(F(s.NominalFixedHz, 1))
			.Append(" Hz   ")
			.Append("Update ")
			.Append(F(s.UpdateHz, 1))
			.Append(" Hz   ")
			.Append("Behind ")
			.Append(F(value, 1))
			.Append("%\n");
		stringBuilder.Append("Main-thread stalls: ").Append(s.MainThreadStall.Count).Append("   worst ")
			.Append(F(s.MainThreadStall.MaxGapMs, 1))
			.Append(" ms   lost ")
			.Append(F(s.MainThreadStall.TotalExcessMs, 1))
			.Append(" ms (")
			.Append(F(value2, 2))
			.Append("%)\n");
		stringBuilder.Append("Update gaps: p95 ").Append(F(s.UpdateGap.P95Ms, 1)).Append(" ms   p99 ")
			.Append(F(s.UpdateGap.P99Ms, 1))
			.Append(" ms   worst ")
			.Append(F(s.UpdateGap.MaxMs, 1))
			.Append(" ms   cause ")
			.Append(LikelyStallCause(s))
			.Append("\n");
		stringBuilder.Append("CPU ").Append(F(s.CpuOneCorePct, 1)).Append("% core   VM ")
			.Append(F(s.CpuVmPct, 1))
			.Append("%   managed ")
			.Append("RSS ")
			.Append(F(s.RssMiB, 0))
			.Append(" MiB   managed ")
			.Append(F(s.ManagedMiB, 0))
			.Append(" MiB   GC ")
			.Append(s.Gc0)
			.Append('/')
			.Append(s.Gc1)
			.Append('/')
			.Append(s.Gc2)
			.Append("\n");
		stringBuilder.Append("World players ").Append(s.Runtime.Players).Append("   ZDOs ")
			.Append(s.Runtime.ZdoCount)
			.Append("   objects ")
			.Append(s.Runtime.ActiveObjects)
			.Append("   zones ")
			.Append(s.Runtime.LoadedZones)
			.Append('/')
			.Append(s.Runtime.GeneratedZones)
			.Append("   threads ")
			.Append(s.Runtime.Threads)
			.Append("\n");
		stringBuilder.Append("Profiler ").Append(F(s.ProfilerOverheadPct, 3)).Append("% core   calls ")
			.Append(F(s.ProfiledCallsPerSec, 0))
			.Append("/s   outputQ ")
			.Append(s.OutputQueueDepth)
			.Append("   recoveries ")
			.Append(s.ScopeRecoveries)
			.Append("\n\n");
		stringBuilder.Append("TOP CONSUMERS\n");
		int num = Math.Min(8, s.Metrics.Count);
		for (int i = 0; i < num; i++)
		{
			MetricSnapshot metricSnapshot = s.Metrics[i];
			stringBuilder.Append(i + 1).Append(". ").Append(metricSnapshot.Name)
				.Append("  ")
				.Append(F(metricSnapshot.SelfMsPerSec, 2))
				.Append(" ms/s")
				.Append("  max ")
				.Append(F(metricSnapshot.MaxInclusiveMs, 1))
				.Append(" ms");
			if (metricSnapshot.MaxInclusiveMs >= (double)_spikeThresholdMs.Value)
			{
				stringBuilder.Append("  SPIKE");
			}
			stringBuilder.Append('\n');
		}
		stringBuilder.Append("\n@TPDATA|tick=").Append(F(s.FixedHz, 3)).Append("|stall=")
			.Append(F(s.MainThreadStall.MaxGapMs, 3))
			.Append("|lost=")
			.Append(F(s.MainThreadStall.TotalExcessMs, 3));
		stringBuilder.Append('\n').Append(BuildPingWire(Stopwatch.GetTimestamp()));
		return stringBuilder.ToString().TrimEnd(Array.Empty<char>());
	}

	private string BuildConsoleText(ReportSnapshot s)
	{
		StringBuilder stringBuilder = new StringBuilder(1024);
		string value = DateTime.Now.ToString("HH:mm:ss", CultureInfo.InvariantCulture);
		stringBuilder.Append("========== TICK PROFILE ").Append(value).Append(" ==========")
			.AppendLine();
		stringBuilder.Append("TICK   ").Append(F(s.FixedHz, 1)).Append('/')
			.Append(F(s.NominalFixedHz, 1))
			.Append(" Hz")
			.Append(" | missed ")
			.Append(s.MissedFixedDeadlines)
			.Append(" | CPU ")
			.Append(F(s.CpuOneCorePct, 1))
			.Append('%')
			.Append(" | RSS ")
			.Append(F(s.RssMiB, 0))
			.Append(" MiB")
			.Append(" | managed ")
			.Append(F(s.ManagedMiB, 0))
			.Append(" MiB")
			.Append(" | GC ")
			.Append(s.Gc0)
			.Append('/')
			.Append(s.Gc1)
			.Append('/')
			.Append(s.Gc2)
			.AppendLine();
		if (s.MainThreadStall.Count == 0L)
		{
			stringBuilder.Append("STALL  none | p99 ").Append(F(s.UpdateGap.P99Ms, 1)).Append(" ms | lost 0 ms")
				.AppendLine();
		}
		else
		{
			stringBuilder.Append("STALL  ").Append(s.MainThreadStall.Count).Append(" | worst ")
				.Append(F(s.MainThreadStall.MaxGapMs, 1))
				.Append(" ms")
				.Append(" | lost ")
				.Append(F(s.MainThreadStall.TotalExcessMs, 1))
				.Append(" ms")
				.Append(" | cause ")
				.Append(LikelyStallCause(s))
				.AppendLine();
		}
		stringBuilder.AppendLine();
		stringBuilder.Append("TOP CONSUMERS").AppendLine();
		stringBuilder.Append("#  Method                            ms/s   max    /s").AppendLine();
		stringBuilder.Append("-  --------------------------------  -----  -----  ---").AppendLine();
		int num = Math.Min(5, s.Metrics.Count);
		for (int i = 0; i < num; i++)
		{
			MetricSnapshot metricSnapshot = s.Metrics[i];
			string text = metricSnapshot.Name ?? "unknown";
			if (text.StartsWith("World/", StringComparison.Ordinal))
			{
				text = text.Substring(6);
			}
			else if (text.StartsWith("Game/", StringComparison.Ordinal))
			{
				text = text.Substring(5);
			}
			if (text.Length > 32)
			{
				text = text.Substring(0, 29) + "...";
			}
			string text2 = F(metricSnapshot.MaxInclusiveMs, 1) + ((metricSnapshot.MaxInclusiveMs >= (double)_spikeThresholdMs.Value) ? "!" : "");
			stringBuilder.Append((i + 1).ToString(CultureInfo.InvariantCulture)).Append("  ").Append(text.PadRight(32))
				.Append("  ")
				.Append(F(metricSnapshot.SelfMsPerSec, 1).PadLeft(5))
				.Append("  ")
				.Append(text2.PadLeft(5))
				.Append("  ")
				.Append(F(metricSnapshot.CallsPerSec, 0).PadLeft(3))
				.AppendLine();
		}
		stringBuilder.Append("SELF   ").Append(F(s.ProfilerOverheadPct, 3)).Append("% core | calls ")
			.Append(F(s.ProfiledCallsPerSec, 0))
			.Append("/s | outputQ ")
			.Append(s.OutputQueueDepth)
			.Append(" | recoveries ")
			.Append(s.ScopeRecoveries)
			.Append(" | players ")
			.Append(s.Runtime.Players)
			.Append(" | ZDOs ")
			.Append(s.Runtime.ZdoCount)
			.AppendLine();
		if (_patchFailures.Count > 0)
		{
			stringBuilder.Append("WARN   patch failures ").Append(_patchFailures.Count).Append(" | instrumented ")
				.Append(s.PatchedMethods)
				.AppendLine();
		}
		stringBuilder.Append("============================================");
		return stringBuilder.ToString();
	}

	private string BuildText(ReportSnapshot s)
	{
		StringBuilder stringBuilder = new StringBuilder(4096);
		double value = ((s.NominalFixedHz > 0.0) ? Math.Max(0.0, (s.NominalFixedHz - s.FixedHz) / s.NominalFixedHz * 100.0) : 0.0);
		stringBuilder.Append("[TickProfiler] ").Append(s.TimestampUtc.ToString("O")).Append(" tickRate=")
			.Append(F(s.FixedHz, 1))
			.Append('/')
			.Append(F(s.NominalFixedHz, 1))
			.Append("Hz")
			.Append(" rawFixedThroughput=")
			.Append(F(s.RawFixedThroughputHz, 1))
			.Append("Hz")
			.Append(" missedFixedTicks=")
			.Append(s.MissedFixedDeadlines)
			.Append(" behind=")
			.Append(F(value, 1))
			.Append('%')
			.Append(" update=")
			.Append(F(s.UpdateHz, 1))
			.Append("Hz")
			.Append(" CPU=")
			.Append(F(s.CpuOneCorePct, 1))
			.Append("% of one core")
			.Append(" VMCPU=")
			.Append(F(s.CpuVmPct, 1))
			.Append('%')
			.Append(" rss=")
			.Append(F(s.RssMiB, 1))
			.Append("MiB")
			.Append(" managed=")
			.Append(F(s.ManagedMiB, 1))
			.Append("MiB")
			.Append(" GC=")
			.Append(s.Gc0)
			.Append('/')
			.Append(s.Gc1)
			.Append('/')
			.Append(s.Gc2)
			.AppendLine();
		stringBuilder.Append("[TickProfiler] mainThreadStalls=").Append(s.MainThreadStall.Count).Append(" worst=")
			.Append(F(s.MainThreadStall.MaxGapMs, 2))
			.Append("ms lost=")
			.Append(F(s.MainThreadStall.TotalExcessMs, 2))
			.Append("ms")
			.Append(" | update p95=")
			.Append(F(s.UpdateGap.P95Ms, 2))
			.Append("ms p99=")
			.Append(F(s.UpdateGap.P99Ms, 2))
			.Append("ms worst=")
			.Append(F(s.UpdateGap.MaxMs, 2))
			.Append("ms")
			.Append(" | cause=")
			.Append(LikelyStallCause(s))
			.Append(" | players=")
			.Append(s.Runtime.Players)
			.Append(" zdo=")
			.Append(s.Runtime.ZdoCount)
			.Append(" objects=")
			.Append(s.Runtime.ActiveObjects)
			.Append(" zones=")
			.Append(s.Runtime.LoadedZones)
			.Append('/')
			.Append(s.Runtime.GeneratedZones)
			.AppendLine();
		int num = Math.Min(Math.Max(1, _topConsumers.Value), s.Metrics.Count);
		for (int i = 0; i < num; i++)
		{
			MetricSnapshot metricSnapshot = s.Metrics[i];
			stringBuilder.Append("[TickProfiler] #").Append(i + 1).Append(' ')
				.Append(metricSnapshot.Name)
				.Append(" self=")
				.Append(F(metricSnapshot.SelfMsPerSec, 3))
				.Append("ms/s")
				.Append(" (")
				.Append(F(metricSnapshot.CpuPct, 3))
				.Append("% core)")
				.Append(" incl=")
				.Append(F(metricSnapshot.InclusiveMsPerSec, 3))
				.Append("ms/s")
				.Append(" calls=")
				.Append(F(metricSnapshot.CallsPerSec, 1))
				.Append("/s")
				.Append(" avg=")
				.Append(F(metricSnapshot.AvgSelfUs, 2))
				.Append("us")
				.Append(" max=")
				.Append(F(metricSnapshot.MaxInclusiveMs, 3))
				.Append("ms")
				.Append(" sample=1/")
				.Append(metricSnapshot.SampleEveryN);
			if (!string.IsNullOrEmpty(metricSnapshot.PatchOwners))
			{
				stringBuilder.Append(" patchedBy=").Append(metricSnapshot.PatchOwners);
			}
			if (metricSnapshot.MaxInclusiveMs >= (double)_spikeThresholdMs.Value)
			{
				stringBuilder.Append(" SPIKE");
			}
			stringBuilder.AppendLine();
		}
		if (_patchFailures.Count > 0)
		{
			stringBuilder.Append("[TickProfiler] patchFailures=").Append(_patchFailures.Count).Append(" patchedMethods=")
				.Append(s.PatchedMethods)
				.AppendLine();
		}
		else
		{
			stringBuilder.Append("[TickProfiler] patchedMethods=").Append(s.PatchedMethods).AppendLine();
		}
		return stringBuilder.ToString();
	}

	private string BuildJson(ReportSnapshot s)
	{
		StringBuilder stringBuilder = new StringBuilder(16384);
		stringBuilder.Append('{');
		JsonProp(stringBuilder, "plugin", "TickProfiler", first: true);
		JsonProp(stringBuilder, "version", "1.0.3", first: false);
		JsonProp(stringBuilder, "timestampUtc", s.TimestampUtc.ToString("O"), first: false);
		JsonProp(stringBuilder, "requested", s.Requested, first: false);
		JsonProp(stringBuilder, "wallSeconds", s.WallSeconds, first: false);
		JsonProp(stringBuilder, "updateHz", s.UpdateHz, first: false);
		JsonProp(stringBuilder, "fixedHz", s.FixedHz, first: false);
		JsonProp(stringBuilder, "rawFixedThroughputHz", s.RawFixedThroughputHz, first: false);
		JsonProp(stringBuilder, "missedFixedDeadlines", s.MissedFixedDeadlines, first: false);
		JsonProp(stringBuilder, "nominalFixedHz", s.NominalFixedHz, first: false);
		JsonProp(stringBuilder, "fixedDeltaMs", s.FixedDeltaMs, first: false);
		JsonProp(stringBuilder, "timeScale", s.TimeScale, first: false);
		JsonProp(stringBuilder, "processCpuOneCorePct", s.CpuOneCorePct, first: false);
		JsonProp(stringBuilder, "processCpuVmPct", s.CpuVmPct, first: false);
		JsonProp(stringBuilder, "managedMemoryMiB", s.ManagedMiB, first: false);
		JsonProp(stringBuilder, "rssMemoryMiB", s.RssMiB, first: false);
		JsonProp(stringBuilder, "privateMemoryMiB", s.PrivateMiB, first: false);
		JsonProp(stringBuilder, "profilerOverheadPctOneCore", s.ProfilerOverheadPct, first: false);
		JsonProp(stringBuilder, "profiledCallsPerSec", s.ProfiledCallsPerSec, first: false);
		JsonProp(stringBuilder, "scopeRecoveries", s.ScopeRecoveries, first: false);
		JsonProp(stringBuilder, "outputQueueDepth", s.OutputQueueDepth, first: false);
		stringBuilder.Append(",\"runtime\":{");
		JsonProp(stringBuilder, "players", s.Runtime.Players, first: true);
		JsonProp(stringBuilder, "zdoCount", s.Runtime.ZdoCount, first: false);
		JsonProp(stringBuilder, "activeObjects", s.Runtime.ActiveObjects, first: false);
		JsonProp(stringBuilder, "loadedZones", s.Runtime.LoadedZones, first: false);
		JsonProp(stringBuilder, "generatedZones", s.Runtime.GeneratedZones, first: false);
		JsonProp(stringBuilder, "threads", s.Runtime.Threads, first: false);
		JsonProp(stringBuilder, "vmNetworkRxKiBps", s.Runtime.NetworkRxKiBps, first: false);
		JsonProp(stringBuilder, "vmNetworkTxKiBps", s.Runtime.NetworkTxKiBps, first: false);
		stringBuilder.Append('}');
		stringBuilder.Append(",\"hitchBucketsCumulative\":{");
		JsonProp(stringBuilder, "lt100ms", s.HitchBuckets[0], first: true);
		JsonProp(stringBuilder, "100to249ms", s.HitchBuckets[1], first: false);
		JsonProp(stringBuilder, "250to499ms", s.HitchBuckets[2], first: false);
		JsonProp(stringBuilder, "500to999ms", s.HitchBuckets[3], first: false);
		JsonProp(stringBuilder, "gte1000ms", s.HitchBuckets[4], first: false);
		stringBuilder.Append('}');
		stringBuilder.Append(",\"gcCollections\":{");
		JsonProp(stringBuilder, "gen0", s.Gc0, first: true);
		JsonProp(stringBuilder, "gen1", s.Gc1, first: false);
		JsonProp(stringBuilder, "gen2", s.Gc2, first: false);
		stringBuilder.Append('}');
		stringBuilder.Append(",\"fixedGap\":");
		AppendGapJson(stringBuilder, s.FixedGap);
		stringBuilder.Append(",\"updateGap\":");
		AppendGapJson(stringBuilder, s.UpdateGap);
		stringBuilder.Append(",\"mainThreadStalls\":{");
		JsonProp(stringBuilder, "count", s.MainThreadStall.Count, first: true);
		JsonProp(stringBuilder, "maxGapMs", s.MainThreadStall.MaxGapMs, first: false);
		JsonProp(stringBuilder, "totalGapMs", s.MainThreadStall.TotalGapMs, first: false);
		JsonProp(stringBuilder, "totalExcessMs", s.MainThreadStall.TotalExcessMs, first: false);
		JsonProp(stringBuilder, "likelyCause", LikelyStallCause(s), first: false);
		stringBuilder.Append('}');
		JsonProp(stringBuilder, "patchedMethods", s.PatchedMethods, first: false);
		stringBuilder.Append(",\"patchFailures\":[");
		for (int i = 0; i < s.PatchFailures.Length; i++)
		{
			if (i > 0)
			{
				stringBuilder.Append(',');
			}
			stringBuilder.Append(JsonString(s.PatchFailures[i]));
		}
		stringBuilder.Append(']');
		stringBuilder.Append(",\"playerPings\":[");
		bool flag = true;
		long timestamp = Stopwatch.GetTimestamp();
		foreach (KeyValuePair<long, PeerPingState> item in _peerPings.OrderBy<KeyValuePair<long, PeerPingState>, string>((KeyValuePair<long, PeerPingState> x) => x.Value.Player ?? string.Empty, StringComparer.OrdinalIgnoreCase))
		{
			PeerPingState value = item.Value;
			if (value.UpdatedTicks != 0L && !(ToSeconds(timestamp - value.UpdatedTicks) > 10.0))
			{
				if (!flag)
				{
					stringBuilder.Append(',');
				}
				flag = false;
				stringBuilder.Append('{');
				JsonProp(stringBuilder, "peer", item.Key, first: true);
				JsonProp(stringBuilder, "player", value.Player ?? string.Empty, first: false);
				JsonProp(stringBuilder, "pingMs", value.PingMs, first: false);
				JsonProp(stringBuilder, "source", "steam-transport", first: false);
				JsonProp(stringBuilder, "ageSeconds", ToSeconds(timestamp - value.UpdatedTicks), first: false);
				stringBuilder.Append('}');
			}
		}
		stringBuilder.Append(']');
		stringBuilder.Append(",\"consumers\":[");
		int num = Math.Min(Math.Max(_topConsumers.Value, 30), s.Metrics.Count);
		for (int num2 = 0; num2 < num; num2++)
		{
			if (num2 > 0)
			{
				stringBuilder.Append(',');
			}
			MetricSnapshot metricSnapshot = s.Metrics[num2];
			stringBuilder.Append('{');
			JsonProp(stringBuilder, "name", metricSnapshot.Name, first: true);
			JsonProp(stringBuilder, "kind", metricSnapshot.Kind, first: false);
			JsonProp(stringBuilder, "selfMsPerSec", metricSnapshot.SelfMsPerSec, first: false);
			JsonProp(stringBuilder, "inclusiveMsPerSec", metricSnapshot.InclusiveMsPerSec, first: false);
			JsonProp(stringBuilder, "cpuPctOneCore", metricSnapshot.CpuPct, first: false);
			JsonProp(stringBuilder, "callsPerSec", metricSnapshot.CallsPerSec, first: false);
			JsonProp(stringBuilder, "avgSelfUs", metricSnapshot.AvgSelfUs, first: false);
			JsonProp(stringBuilder, "maxInclusiveMs", metricSnapshot.MaxInclusiveMs, first: false);
			JsonProp(stringBuilder, "spike", metricSnapshot.MaxInclusiveMs >= (double)_spikeThresholdMs.Value, first: false);
			JsonProp(stringBuilder, "sampleEveryN", metricSnapshot.SampleEveryN, first: false);
			JsonProp(stringBuilder, "patchOwners", metricSnapshot.PatchOwners, first: false);
			stringBuilder.Append('}');
		}
		stringBuilder.Append("]}");
		return stringBuilder.ToString();
	}

	private static void AppendGapJson(StringBuilder b, GapSnapshot g)
	{
		b.Append('{');
		JsonProp(b, "count", g.Count, first: true);
		JsonProp(b, "avgMs", g.AvgMs, first: false);
		JsonProp(b, "p95Ms", g.P95Ms, first: false);
		JsonProp(b, "p99Ms", g.P99Ms, first: false);
		JsonProp(b, "maxMs", g.MaxMs, first: false);
		JsonProp(b, "over20Ms", g.Over20Ms, first: false);
		JsonProp(b, "over40Ms", g.Over40Ms, first: false);
		JsonProp(b, "over100Ms", g.Over100Ms, first: false);
		b.Append('}');
	}

	private static void JsonProp(StringBuilder b, string name, string value, bool first)
	{
		if (!first)
		{
			b.Append(',');
		}
		b.Append(JsonString(name)).Append(':').Append(JsonString(value));
	}

	private static void JsonProp(StringBuilder b, string name, bool value, bool first)
	{
		if (!first)
		{
			b.Append(',');
		}
		b.Append(JsonString(name)).Append(':').Append(value ? "true" : "false");
	}

	private static void JsonProp(StringBuilder b, string name, int value, bool first)
	{
		if (!first)
		{
			b.Append(',');
		}
		b.Append(JsonString(name)).Append(':').Append(value.ToString(CultureInfo.InvariantCulture));
	}

	private static void JsonProp(StringBuilder b, string name, long value, bool first)
	{
		if (!first)
		{
			b.Append(',');
		}
		b.Append(JsonString(name)).Append(':').Append(value.ToString(CultureInfo.InvariantCulture));
	}

	private static void JsonProp(StringBuilder b, string name, double value, bool first)
	{
		if (!first)
		{
			b.Append(',');
		}
		b.Append(JsonString(name)).Append(':').Append(value.ToString("0.######", CultureInfo.InvariantCulture));
	}

	private static string JsonString(string value)
	{
		return ProfilerCore.JsonString(value);
	}

	private static void WriteAtomic(string path, string content)
	{
		string text = path + ".tmp";
		File.WriteAllText(text, content);
		if (File.Exists(path))
		{
			File.Delete(path);
		}
		File.Move(text, path);
	}

	private void ResetMeasurementWindow(long now)
	{
		_updates = 0L;
		_fixedUpdates = 0L;
		_missedFixedDeadlines = 0L;
		_pendingGapReportedMisses = 0L;
		_lastReportTicks = now;
		_lastUpdateTicks = now;
		_lastFixedTicks = now;
		_lastOverlayTicks = now;
		_lastOverlayUpdatesTotal = _updatesTotal;
		_lastOverlayFixedTotal = _fixedUpdatesTotal;
		_lastOverlayMissedTotal = _missedFixedDeadlinesTotal;
		_updateGaps.SnapshotAndReset();
		_fixedGaps.SnapshotAndReset();
		_mainThreadStalls.SnapshotAndReset();
		foreach (Metric metric in Metrics)
		{
			metric.SnapshotAndReset();
		}
		try
		{
			_process.Refresh();
			_lastCpu = _process.TotalProcessorTime;
		}
		catch
		{
		}
		_lastGc0 = (_lastEventGc0 = GC.CollectionCount(0));
		_lastGc1 = (_lastEventGc1 = GC.CollectionCount(1));
		_lastGc2 = (_lastEventGc2 = GC.CollectionCount(2));
		ReadProcNetwork(out _networkRxBytes, out _networkTxBytes);
		_lastNetworkTicks = now;
		Interlocked.Exchange(ref _profilerOverheadTicks, 0L);
		Interlocked.Exchange(ref _profileCallsObserved, 0L);
	}

	private static string GetPatchOwners(MethodBase method)
	{
		try
		{
			Patches patchInfo = Harmony.GetPatchInfo(method);
			if (patchInfo == null || patchInfo.Owners == null)
			{
				return string.Empty;
			}
			return string.Join(",", (from x in patchInfo.Owners.Where((string x) => !string.IsNullOrEmpty(x) && x != "magicorp.valheim.tickprofiler").Distinct()
				orderby x
				select x).ToArray());
		}
		catch
		{
			return string.Empty;
		}
	}

	private RuntimeContext CaptureRuntimeContext(long nowTicks, double wallSeconds)
	{
		int players = -1;
		try
		{
			if ((Object)(object)ZNet.instance != (Object)null)
			{
				players = ZNet.instance.GetNrOfPlayers();
			}
		}
		catch
		{
		}
		int zdoCount = CountCollectionField(ZDOMan.instance, "m_objectsByID", "m_objects", "m_zdos");
		int activeObjects = CountCollectionField(ZNetScene.instance, "m_instances", "m_namedPrefabs");
		int loadedZones = CountCollectionField(ZoneSystem.instance, "m_zones", "m_activeZones");
		int generatedZones = CountCollectionField(ZoneSystem.instance, "m_generatedZones");
		int threads = 0;
		try
		{
			_process.Refresh();
			threads = _process.Threads.Count;
		}
		catch
		{
		}
		double networkRxKiBps = 0.0;
		double networkTxKiBps = 0.0;
		if (ReadProcNetwork(out var rx, out var tx))
		{
			double num = Math.Max(0.001, ToSeconds(nowTicks - _lastNetworkTicks));
			networkRxKiBps = (double)Math.Max(0L, rx - _networkRxBytes) / 1024.0 / num;
			networkTxKiBps = (double)Math.Max(0L, tx - _networkTxBytes) / 1024.0 / num;
			_networkRxBytes = rx;
			_networkTxBytes = tx;
			_lastNetworkTicks = nowTicks;
		}
		return new RuntimeContext
		{
			Players = players,
			ZdoCount = zdoCount,
			ActiveObjects = activeObjects,
			LoadedZones = loadedZones,
			GeneratedZones = generatedZones,
			Threads = threads,
			NetworkRxKiBps = networkRxKiBps,
			NetworkTxKiBps = networkTxKiBps
		};
	}

	private static int CountCollectionField(object instance, params string[] names)
	{
		if (instance == null)
		{
			return -1;
		}
		Type type = instance.GetType();
		foreach (string text in names)
		{
			try
			{
				FieldInfo fieldInfo = AccessTools.Field(type, text);
				if (!(fieldInfo == null))
				{
					object value = fieldInfo.GetValue(instance);
					if (value is ICollection { Count: var count })
					{
						return count;
					}
					PropertyInfo propertyInfo = value?.GetType().GetProperty("Count");
					if (propertyInfo != null)
					{
						return Convert.ToInt32(propertyInfo.GetValue(value, null), CultureInfo.InvariantCulture);
					}
				}
			}
			catch
			{
			}
		}
		return -1;
	}

	private static bool ReadProcNetwork(out long rx, out long tx)
	{
		rx = 0L;
		tx = 0L;
		try
		{
			string[] array = File.ReadAllLines("/proc/net/dev");
			foreach (string text in array)
			{
				int num = text.IndexOf(':');
				if (num < 0 || text.Substring(0, num).Trim() == "lo")
				{
					continue;
				}
				string[] array2 = text.Substring(num + 1).Split(new char[1] { ' ' }, StringSplitOptions.RemoveEmptyEntries);
				if (array2.Length >= 9)
				{
					if (long.TryParse(array2[0], out var result))
					{
						rx += result;
					}
					if (long.TryParse(array2[8], out result))
					{
						tx += result;
					}
				}
			}
			return true;
		}
		catch
		{
			return false;
		}
	}

	private void BroadcastLightweightOverlay(long nowTicks)
	{
		double num = Math.Max(0.001, ToSeconds(nowTicks - _lastOverlayTicks));
		_ = _updatesTotal;
		_ = _lastOverlayUpdatesTotal;
		long num2 = _fixedUpdatesTotal - _lastOverlayFixedTotal;
		long num3 = _missedFixedDeadlinesTotal - _lastOverlayMissedTotal;
		double num4 = ((Time.fixedDeltaTime > 0f) ? (1.0 / (double)Time.fixedDeltaTime) : 50.0);
		double value = Math.Min(num2, Math.Max(0.0, num4 * num - (double)num3)) / num;
		_lastOverlayTicks = nowTicks;
		_lastOverlayUpdatesTotal = _updatesTotal;
		_lastOverlayFixedTotal = _fixedUpdatesTotal;
		_lastOverlayMissedTotal = _missedFixedDeadlinesTotal;
		string text = (string.IsNullOrEmpty(_latestOverlayText) ? "@TPPROTO|protocol=2|server=1.0.3|features=hitch-events,forensics,runtime,sampling,steam-transport\nSERVER TICK PROFILER\nCollecting full report..." : StripDataLine(_latestOverlayText));
		_latestOverlayText = text + "\n@TPDATA|tick=" + F(value, 3) + "|stall=0|lost=0\n" + BuildPingWire(nowTicks);
		string text2 = (string.IsNullOrEmpty(_latestLegacyOverlayText) ? "SERVER TICK PROFILER\nCollecting full report..." : StripDataLine(_latestLegacyOverlayText));
		_latestLegacyOverlayText = text2 + "\n@TPDATA|tick=" + F(value, 3) + "|stall=0|lost=0";
		BroadcastOverlaySnapshot();
	}

	private static string StripDataLine(string text)
	{
		if (string.IsNullOrEmpty(text))
		{
			return string.Empty;
		}
		int num = text.LastIndexOf("\n@TPDATA|", StringComparison.Ordinal);
		if (num < 0)
		{
			return text;
		}
		return text.Substring(0, num);
	}

	private void BroadcastHitchEvent(double gapMs, StallAttribution attribution)
	{
		if (_v2OverlayPeers.Count == 0 || ZRoutedRpc.instance == null || (Object)(object)ZNet.instance == (Object)null)
		{
			return;
		}
		string text = DateTime.UtcNow.Ticks.ToString(CultureInfo.InvariantCulture) + "|" + F(gapMs, 3) + "|" + attribution.Confidence + "|" + attribution.ShortCause;
		long[] array = _v2OverlayPeers.Keys.ToArray();
		foreach (long num in array)
		{
			try
			{
				if (ZNet.instance.GetPeer(num) != null)
				{
					ZRoutedRpc.instance.InvokeRoutedRPC(num, "MagiCorp_TickProfiler_HitchEvent_v2", new object[1] { text });
				}
			}
			catch
			{
				_overlayPeers.Remove(num);
				_v2OverlayPeers.Remove(num);
			}
		}
	}

	private void AddRecentSample(ReportSnapshot s)
	{
		_recentSamples.Add(new MiniSample
		{
			TimeUtc = s.TimestampUtc,
			TickHz = s.FixedHz,
			UpdateHz = s.UpdateHz,
			Cpu = s.CpuOneCorePct,
			RssMiB = s.RssMiB,
			Zdos = s.Runtime.ZdoCount,
			Players = s.Runtime.Players
		});
		DateTime dateTime = DateTime.UtcNow.AddSeconds(-60.0);
		while (_recentSamples.Count > 0 && _recentSamples[0].TimeUtc < dateTime)
		{
			_recentSamples.RemoveAt(0);
		}
	}

	private void RecordHitchEvent(long nowTicks, double gapMs, StallAttribution attribution)
	{
		if (!_writeForensics.Value || gapMs < (double)_forensicThresholdMs.Value)
		{
			return;
		}
		RuntimeContext runtimeContext = CaptureRuntimeContext(nowTicks, Math.Max(0.001, ToSeconds(nowTicks - _lastReportTicks)));
		StringBuilder stringBuilder = new StringBuilder(8192);
		stringBuilder.Append('{');
		JsonProp(stringBuilder, "pluginVersion", "1.0.3", first: true);
		JsonProp(stringBuilder, "timestampUtc", DateTime.UtcNow.ToString("O"), first: false);
		JsonProp(stringBuilder, "gapMs", gapMs, first: false);
		JsonProp(stringBuilder, "expectedMissedFixedDeadlines", ProfilerCore.CountMissedFixedDeadlines(gapMs, (Time.fixedDeltaTime > 0f) ? ((double)Time.fixedDeltaTime * 1000.0) : 20.0), first: false);
		JsonProp(stringBuilder, "cause", attribution.Text, first: false);
		JsonProp(stringBuilder, "confidence", attribution.Confidence, first: false);
		stringBuilder.Append(",\"runtime\":{");
		JsonProp(stringBuilder, "players", runtimeContext.Players, first: true);
		JsonProp(stringBuilder, "zdoCount", runtimeContext.ZdoCount, first: false);
		JsonProp(stringBuilder, "activeObjects", runtimeContext.ActiveObjects, first: false);
		JsonProp(stringBuilder, "loadedZones", runtimeContext.LoadedZones, first: false);
		JsonProp(stringBuilder, "generatedZones", runtimeContext.GeneratedZones, first: false);
		JsonProp(stringBuilder, "threads", runtimeContext.Threads, first: false);
		stringBuilder.Append('}');
		stringBuilder.Append(",\"previousSamples\":[");
		for (int i = 0; i < _recentSamples.Count; i++)
		{
			if (i > 0)
			{
				stringBuilder.Append(',');
			}
			MiniSample miniSample = _recentSamples[i];
			stringBuilder.Append('{');
			JsonProp(stringBuilder, "timeUtc", miniSample.TimeUtc.ToString("O"), first: true);
			JsonProp(stringBuilder, "tickHz", miniSample.TickHz, first: false);
			JsonProp(stringBuilder, "updateHz", miniSample.UpdateHz, first: false);
			JsonProp(stringBuilder, "cpu", miniSample.Cpu, first: false);
			JsonProp(stringBuilder, "rssMiB", miniSample.RssMiB, first: false);
			JsonProp(stringBuilder, "zdos", miniSample.Zdos, first: false);
			JsonProp(stringBuilder, "players", miniSample.Players, first: false);
			stringBuilder.Append('}');
		}
		stringBuilder.Append("]}");
		string extraPath = Path.Combine(_forensicsPath, "hitch-" + DateTime.UtcNow.ToString("yyyyMMdd-HHmmssfff", CultureInfo.InvariantCulture) + ".json");
		QueueOutput(new OutputWork
		{
			ExtraPath = extraPath,
			Extra = stringBuilder.ToString()
		});
	}

	private void QueueOutput(OutputWork work)
	{
		lock (_outputLock)
		{
			while (_outputQueue.Count >= 3)
			{
				_outputQueue.Dequeue();
			}
			_outputQueue.Enqueue(work);
			_outputQueueDepth = _outputQueue.Count;
			if (_outputWorkerRunning)
			{
				return;
			}
			_outputWorkerRunning = true;
		}
		ThreadPool.QueueUserWorkItem(delegate
		{
			OutputWorker();
		});
	}

	private void OutputWorker()
	{
		while (true)
		{
			OutputWork outputWork;
			lock (_outputLock)
			{
				if (_outputQueue.Count == 0)
				{
					_outputWorkerRunning = false;
					_outputQueueDepth = 0;
					break;
				}
				outputWork = _outputQueue.Dequeue();
				_outputQueueDepth = _outputQueue.Count;
			}
			try
			{
				if (!string.IsNullOrEmpty(outputWork.TextPath) && outputWork.Text != null)
				{
					WriteAtomic(outputWork.TextPath, outputWork.Text);
				}
				if (!string.IsNullOrEmpty(outputWork.JsonPath) && outputWork.Json != null)
				{
					WriteAtomic(outputWork.JsonPath, outputWork.Json);
				}
				if (!string.IsNullOrEmpty(outputWork.ExtraPath) && outputWork.Extra != null)
				{
					Directory.CreateDirectory(Path.GetDirectoryName(outputWork.ExtraPath));
					WriteAtomic(outputWork.ExtraPath, outputWork.Extra);
				}
			}
			catch (Exception ex)
			{
				ManualLogSource log = _log;
				if (log != null)
				{
					log.LogWarning((object)("TickProfiler background output failed: " + ex.Message));
				}
			}
		}
	}

	private void AddPatchFailure(string message)
	{
		_patchFailures.Add(message);
		((BaseUnityPlugin)this).Logger.LogWarning((object)("TickProfiler patch skipped: " + message));
	}

	private static string F(double value, int decimals)
	{
		return value.ToString("F" + decimals, CultureInfo.InvariantCulture);
	}

	private static double ToMilliseconds(long ticks)
	{
		return (double)ticks * 1000.0 / (double)Frequency;
	}

	private static double ToSeconds(long ticks)
	{
		return (double)ticks / (double)Frequency;
	}
}

MagiCorp.ValheimTickProfiler.Client.dll

Decompiled a month ago
using System;
using System.Collections.Generic;
using System.Diagnostics;
using System.Globalization;
using System.Linq;
using System.Reflection;
using System.Runtime.CompilerServices;
using System.Runtime.Versioning;
using BepInEx;
using BepInEx.Bootstrap;
using BepInEx.Configuration;
using HarmonyLib;
using UnityEngine;

[assembly: CompilationRelaxations(8)]
[assembly: RuntimeCompatibility(WrapNonExceptionThrows = true)]
[assembly: Debuggable(DebuggableAttribute.DebuggingModes.IgnoreSymbolStoreSequencePoints)]
[assembly: TargetFramework(".NETFramework,Version=v4.8", FrameworkDisplayName = ".NET Framework 4.8")]
[assembly: AssemblyCompany("MagiCorp.ValheimTickProfiler.Client")]
[assembly: AssemblyConfiguration("Release")]
[assembly: AssemblyFileVersion("1.0.0.0")]
[assembly: AssemblyInformationalVersion("1.0.0+883f730d6cd20b9ea16971cc978a0cc5035abddb")]
[assembly: AssemblyProduct("MagiCorp.ValheimTickProfiler.Client")]
[assembly: AssemblyTitle("MagiCorp.ValheimTickProfiler.Client")]
[assembly: AssemblyVersion("1.0.0.0")]
namespace MagiCorp.ValheimTickProfiler.Client;

[BepInPlugin("magicorp.valheim.tickprofiler.client", "MagiCorp Valheim Tick Profiler Client Overlay", "1.0.3")]
[BepInProcess("valheim.exe")]
public sealed class TickProfilerClientOverlay : BaseUnityPlugin
{
	private sealed class GapStats
	{
		private readonly double[] _samples;

		private int _sampleCount;

		private int _next;

		private long _count;

		private double _sum;

		private double _max;

		public GapStats(int capacity)
		{
			_samples = new double[capacity];
		}

		public void Add(double ms)
		{
			_count++;
			_sum += ms;
			if (ms > _max)
			{
				_max = ms;
			}
			_samples[_next++] = ms;
			if (_next == _samples.Length)
			{
				_next = 0;
			}
			if (_sampleCount < _samples.Length)
			{
				_sampleCount++;
			}
		}

		public GapSnapshot SnapshotAndReset()
		{
			double p95Ms = 0.0;
			double p99Ms = 0.0;
			if (_sampleCount > 0)
			{
				double[] array = new double[_sampleCount];
				Array.Copy(_samples, array, _sampleCount);
				Array.Sort(array);
				p95Ms = array[Math.Max(0, Math.Min(array.Length - 1, (int)Math.Ceiling((double)array.Length * 0.95) - 1))];
				p99Ms = array[Math.Max(0, Math.Min(array.Length - 1, (int)Math.Ceiling((double)array.Length * 0.99) - 1))];
			}
			GapSnapshot result = new GapSnapshot
			{
				Count = _count,
				AvgMs = ((_count > 0) ? (_sum / (double)_count) : 0.0),
				P95Ms = p95Ms,
				P99Ms = p99Ms,
				MaxMs = _max
			};
			_sampleCount = 0;
			_next = 0;
			_count = 0L;
			_sum = 0.0;
			_max = 0.0;
			return result;
		}
	}

	private struct GapSnapshot
	{
		public long Count;

		public double AvgMs;

		public double P95Ms;

		public double P99Ms;

		public double MaxMs;
	}

	private sealed class StallStats
	{
		private long _count;

		private double _maxGapMs;

		private double _totalGapMs;

		private double _totalExcessMs;

		private string _worstCause;

		public void Add(double gapMs, double expectedMs, string cause)
		{
			_count++;
			_totalGapMs += gapMs;
			_totalExcessMs += Math.Max(0.0, gapMs - expectedMs);
			if (gapMs > _maxGapMs)
			{
				_maxGapMs = gapMs;
				_worstCause = cause;
			}
		}

		public StallSnapshot SnapshotAndReset()
		{
			StallSnapshot result = new StallSnapshot
			{
				Count = _count,
				MaxGapMs = _maxGapMs,
				TotalGapMs = _totalGapMs,
				TotalExcessMs = _totalExcessMs,
				WorstCause = _worstCause
			};
			_count = 0L;
			_maxGapMs = 0.0;
			_totalGapMs = 0.0;
			_totalExcessMs = 0.0;
			_worstCause = null;
			return result;
		}
	}

	private struct StallSnapshot
	{
		public long Count;

		public double MaxGapMs;

		public double TotalGapMs;

		public double TotalExcessMs;

		public string WorstCause;
	}

	private sealed class HistoryPoint
	{
		public DateTime TimeUtc;

		public double Rate;

		public double StallMs;
	}

	private sealed class Metric
	{
		public readonly string Name;

		public Metric(string name)
		{
			Name = name;
		}
	}

	private struct ScopeFrame
	{
		public Metric Metric;

		public long StartTicks;
	}

	private sealed class LongCallEvent
	{
		public string Name;

		public long StartTicks;

		public long EndTicks;

		public double ElapsedMs;
	}

	public const string PluginGuid = "magicorp.valheim.tickprofiler.client";

	public const string PluginName = "MagiCorp Valheim Tick Profiler Client Overlay";

	public const string PluginVersion = "1.0.3";

	private const int ProtocolVersion = 2;

	private const string RpcHello = "MagiCorp_TickProfiler_Hello_v1";

	private const string RpcSnapshot = "MagiCorp_TickProfiler_Snapshot_v1";

	private const string RpcHitchNotice = "MagiCorp_TickProfiler_HitchNotice_v1";

	private const string RpcHelloV2 = "MagiCorp_TickProfiler_Hello_v2";

	private const string RpcSnapshotV2 = "MagiCorp_TickProfiler_Snapshot_v2";

	private const string RpcHitchNoticeV2 = "MagiCorp_TickProfiler_HitchNotice_v2";

	private const string RpcHitchEventV2 = "MagiCorp_TickProfiler_HitchEvent_v2";

	private const string RpcPingProbeV2 = "MagiCorp_TickProfiler_PingProbe_v2";

	private const string RpcPingReplyV2 = "MagiCorp_TickProfiler_PingReply_v2";

	private const string RpcPingSnapshotV2 = "MagiCorp_TickProfiler_PingSnapshot_v2";

	private static readonly long Frequency = Stopwatch.Frequency;

	private static readonly Dictionary<MethodBase, Metric> MethodMetrics = new Dictionary<MethodBase, Metric>();

	private static readonly List<Metric> Metrics = new List<Metric>();

	private static readonly object RecentLongCallLock = new object();

	private static readonly List<LongCallEvent> RecentLongCalls = new List<LongCallEvent>(256);

	private static Harmony _harmony;

	private static float _longCallThresholdMs = 20f;

	[ThreadStatic]
	private static ScopeFrame[] _scopeStack;

	[ThreadStatic]
	private static int _scopeDepth;

	private ConfigEntry<KeyCode> _toggleKey;

	private ConfigEntry<bool> _visibleByDefault;

	private ConfigEntry<int> _fontSize;

	private ConfigEntry<float> _x;

	private ConfigEntry<float> _y;

	private ConfigEntry<float> _width;

	private ConfigEntry<float> _uiScale;

	private ConfigEntry<bool> _localProfilingEnabled;

	private ConfigEntry<float> _clientReportIntervalSeconds;

	private ConfigEntry<float> _clientStallThresholdMs;

	private ConfigEntry<bool> _logClientSummary;

	private ConfigEntry<bool> _profilePluginMethods;

	private ConfigEntry<float> _pluginAttributionThresholdMs;

	private bool _visible;

	private bool _rpcRegistered;

	private ZRoutedRpc _registeredRpcInstance;

	private long _lastHelloTicks;

	private string _snapshot = string.Empty;

	private DateTime _snapshotUtc = DateTime.MinValue;

	private string _serverVersion = "unknown";

	private int _serverProtocol;

	private bool _v2SnapshotSeen;

	private double _lastServerTickRate;

	private string _cachedDisplayText = string.Empty;

	private long _cachedDisplayTicks;

	private float _cachedPingMs = -1f;

	private string _playerPingDisplay = "Steam transport  waiting for samples...";

	private GUIStyle _textStyle;

	private GUIStyle _graphStyle;

	private bool _loggedFirstSnapshot;

	private bool _loggedPeerIdFailure;

	private static readonly MethodInfo GetServerPeerIdMethod = typeof(ZRoutedRpc).GetMethod("GetServerPeerID", BindingFlags.Instance | BindingFlags.Public | BindingFlags.NonPublic);

	private readonly GapStats _frameGaps = new GapStats(4096);

	private readonly StallStats _clientStalls = new StallStats();

	private Process _process;

	private TimeSpan _lastCpu;

	private long _lastFrameTicks;

	private long _lastLocalReportTicks;

	private long _frames;

	private int _lastGc0;

	private int _lastGc1;

	private int _lastGc2;

	private int _lastEventGc0;

	private int _lastEventGc1;

	private int _lastEventGc2;

	private readonly List<HistoryPoint> _clientHistory = new List<HistoryPoint>(64);

	private readonly List<HistoryPoint> _serverHistory = new List<HistoryPoint>(64);

	private string _clientSnapshot = "CLIENT MAIN THREAD\nCollecting local frame timing...";

	private void Awake()
	{
		//IL_027a: Unknown result type (might be due to invalid IL or missing references)
		//IL_0284: Expected O, but got Unknown
		//IL_02bb: Unknown result type (might be due to invalid IL or missing references)
		_toggleKey = ((BaseUnityPlugin)this).Config.Bind<KeyCode>("Client UI", "ToggleKey", (KeyCode)289, "Show or hide the TickProfiler overlay.");
		_visibleByDefault = ((BaseUnityPlugin)this).Config.Bind<bool>("Client UI", "VisibleByDefault", false, "Show the overlay when Valheim starts.");
		_fontSize = ((BaseUnityPlugin)this).Config.Bind<int>("Client UI", "FontSize", 14, "Overlay font size.");
		_x = ((BaseUnityPlugin)this).Config.Bind<float>("Client UI", "X", 16f, "Overlay X position in pixels.");
		_y = ((BaseUnityPlugin)this).Config.Bind<float>("Client UI", "Y", 16f, "Overlay Y position in pixels.");
		_width = ((BaseUnityPlugin)this).Config.Bind<float>("Client UI", "Width", 820f, "Overlay width at 1080p before UI scaling.");
		_uiScale = ((BaseUnityPlugin)this).Config.Bind<float>("Client UI", "UIScale", 0f, "Overlay scale. 0 = automatic based on vertical resolution (1x at 1080p, 2x at 4K).");
		_localProfilingEnabled = ((BaseUnityPlugin)this).Config.Bind<bool>("Client Profiling", "Enabled", true, "Measure local client main-thread/frame stalls independently of the server profiler.");
		_clientReportIntervalSeconds = ((BaseUnityPlugin)this).Config.Bind<float>("Client Profiling", "ReportIntervalSeconds", 10f, "Local client timing report interval.");
		_clientStallThresholdMs = ((BaseUnityPlugin)this).Config.Bind<float>("Client Profiling", "MainThreadStallThresholdMs", 50f, "Record any frame-to-frame wall-clock gap at or above this threshold as a client main-thread stall.");
		_logClientSummary = ((BaseUnityPlugin)this).Config.Bind<bool>("Client Profiling", "LogSummary", true, "Write compact local client stall summaries to the BepInEx log.");
		_profilePluginMethods = ((BaseUnityPlugin)this).Config.Bind<bool>("Client Profiling", "ProfilePluginMethods", true, "Instrument only client BepInEx plugin Update/FixedUpdate/LateUpdate methods. TickProfiler never patches another mod's Harmony Prefix/Postfix/Finalizer methods.");
		_pluginAttributionThresholdMs = ((BaseUnityPlugin)this).Config.Bind<float>("Client Profiling", "PluginAttributionThresholdMs", 20f, "Minimum plugin method duration retained for hitch attribution.");
		_longCallThresholdMs = _pluginAttributionThresholdMs.Value;
		_pluginAttributionThresholdMs.SettingChanged += delegate
		{
			_longCallThresholdMs = _pluginAttributionThresholdMs.Value;
		};
		_visible = _visibleByDefault.Value;
		_process = Process.GetCurrentProcess();
		_lastCpu = _process.TotalProcessorTime;
		_lastGc0 = GC.CollectionCount(0);
		_lastGc1 = GC.CollectionCount(1);
		_lastGc2 = GC.CollectionCount(2);
		_lastEventGc0 = _lastGc0;
		_lastEventGc1 = _lastGc1;
		_lastEventGc2 = _lastGc2;
		_lastLocalReportTicks = Stopwatch.GetTimestamp();
		_harmony = new Harmony("magicorp.valheim.tickprofiler.client");
		((BaseUnityPlugin)this).Logger.LogInfo((object)string.Format("{0} {1} loaded in process {2}. Toggle: {3}", "MagiCorp Valheim Tick Profiler Client Overlay", "1.0.3", _process.ProcessName, _toggleKey.Value));
	}

	private void Start()
	{
		if (_profilePluginMethods.Value)
		{
			ProfileLoadedMods();
			((BaseUnityPlugin)this).Logger.LogInfo((object)("Client TickProfiler instrumented " + MethodMetrics.Count + " safe plugin loop methods for hitch attribution."));
		}
	}

	private void OnDestroy()
	{
		try
		{
			Harmony harmony = _harmony;
			if (harmony != null)
			{
				harmony.UnpatchSelf();
			}
		}
		catch
		{
		}
	}

	private void Update()
	{
		//IL_005e: Unknown result type (might be due to invalid IL or missing references)
		//IL_0063: Unknown result type (might be due to invalid IL or missing references)
		RecordLocalFrame();
		TryRegisterRpc();
		if (TogglePressed())
		{
			_visible = !_visible;
			((BaseUnityPlugin)this).Logger.LogInfo((object)("TickProfiler overlay " + (_visible ? "shown" : "hidden") + " via " + ((object)_toggleKey.Value/*cast due to .constrained prefix*/).ToString() + "."));
		}
		long timestamp = Stopwatch.GetTimestamp();
		if (_rpcRegistered && (double)(timestamp - _lastHelloTicks) / (double)Frequency >= 10.0)
		{
			SendHello();
		}
	}

	private void RecordLocalFrame()
	{
		if (!_localProfilingEnabled.Value)
		{
			return;
		}
		long timestamp = Stopwatch.GetTimestamp();
		_frames++;
		if (_lastFrameTicks != 0L)
		{
			long lastFrameTicks = _lastFrameTicks;
			double num = ToMilliseconds(timestamp - lastFrameTicks);
			_frameGaps.Add(num);
			int num2 = GC.CollectionCount(0);
			int num3 = GC.CollectionCount(1);
			int num4 = GC.CollectionCount(2);
			bool gcDuringWindow = num2 != _lastEventGc0 || num3 != _lastEventGc1 || num4 != _lastEventGc2;
			_lastEventGc0 = num2;
			_lastEventGc1 = num3;
			_lastEventGc2 = num4;
			if (num >= (double)_clientStallThresholdMs.Value)
			{
				string cause = ResolveLocalStallCause(lastFrameTicks, timestamp, num, gcDuringWindow);
				_clientStalls.Add(num, ExpectedFrameMs(), cause);
			}
		}
		_lastFrameTicks = timestamp;
		double num5 = ToSeconds(timestamp - _lastLocalReportTicks);
		if (num5 >= Math.Max(1.0, _clientReportIntervalSeconds.Value))
		{
			ReportLocalClient(timestamp, num5);
		}
	}

	private void ReportLocalClient(long now, double wallSeconds)
	{
		GapSnapshot gapSnapshot = _frameGaps.SnapshotAndReset();
		StallSnapshot stallSnapshot = _clientStalls.SnapshotAndReset();
		double num = (double)_frames / Math.Max(0.001, wallSeconds);
		_frames = 0L;
		_lastLocalReportTicks = now;
		TimeSpan totalProcessorTime = _process.TotalProcessorTime;
		double totalMilliseconds = (totalProcessorTime - _lastCpu).TotalMilliseconds;
		_lastCpu = totalProcessorTime;
		double value = totalMilliseconds / (Math.Max(0.001, wallSeconds) * 1000.0) * 100.0;
		int num2 = GC.CollectionCount(0);
		int num3 = GC.CollectionCount(1);
		int num4 = GC.CollectionCount(2);
		int num5 = num2 - _lastGc0;
		int num6 = num3 - _lastGc1;
		int num7 = num4 - _lastGc2;
		_lastGc0 = num2;
		_lastGc1 = num3;
		_lastGc2 = num4;
		double value2 = stallSnapshot.TotalExcessMs / (Math.Max(0.001, wallSeconds) * 1000.0) * 100.0;
		string text = ((stallSnapshot.Count == 0L) ? "none" : (string.IsNullOrEmpty(stallSnapshot.WorstCause) ? "unattributed (render/Unity/native/I/O/OS)" : stallSnapshot.WorstCause));
		_clientSnapshot = "CLIENT MAIN THREAD\nFrame throughput " + F(num, 1) + " FPS   p95 " + F(gapSnapshot.P95Ms, 1) + " ms   p99 " + F(gapSnapshot.P99Ms, 1) + " ms\nStalls " + stallSnapshot.Count + "   worst " + F(stallSnapshot.MaxGapMs, 1) + " ms   lost " + F(stallSnapshot.TotalExcessMs, 1) + " ms (" + F(value2, 2) + "%)\nCPU " + F(value, 1) + "% core   RSS " + F(GetWorkingSetMiB(), 0) + " MiB   managed " + F((double)GC.GetTotalMemory(forceFullCollection: false) / 1048576.0, 0) + " MiB   GC " + num5 + "/" + num6 + "/" + num7 + "\nCause " + text;
		AddHistory(_clientHistory, num, stallSnapshot.MaxGapMs);
		if (_logClientSummary.Value)
		{
			((BaseUnityPlugin)this).Logger.LogInfo((object)("CLIENT STALLS  frames " + F(num, 1) + "/s | p99 " + F(gapSnapshot.P99Ms, 1) + " ms | stalls " + stallSnapshot.Count + " | worst " + F(stallSnapshot.MaxGapMs, 1) + " ms | lost " + F(stallSnapshot.TotalExcessMs, 1) + " ms | " + text));
		}
	}

	private static double ExpectedFrameMs()
	{
		//IL_0018: Unknown result type (might be due to invalid IL or missing references)
		//IL_001d: 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)
		//IL_0025: Unknown result type (might be due to invalid IL or missing references)
		int targetFrameRate = Application.targetFrameRate;
		if (targetFrameRate > 0)
		{
			return 1000.0 / (double)targetFrameRate;
		}
		try
		{
			Resolution currentResolution = Screen.currentResolution;
			RefreshRate refreshRateRatio = ((Resolution)(ref currentResolution)).refreshRateRatio;
			double value = ((RefreshRate)(ref refreshRateRatio)).value;
			int vSyncCount = QualitySettings.vSyncCount;
			if (value > 0.0 && vSyncCount > 0)
			{
				return 1000.0 * (double)vSyncCount / value;
			}
		}
		catch
		{
		}
		return 16.666666666666668;
	}

	private void TryRegisterRpc()
	{
		ZRoutedRpc instance = ZRoutedRpc.instance;
		if (instance == null)
		{
			_rpcRegistered = false;
			_registeredRpcInstance = null;
		}
		else if (!_rpcRegistered || _registeredRpcInstance != instance)
		{
			_rpcRegistered = false;
			_registeredRpcInstance = null;
			try
			{
				instance.Register<string>("MagiCorp_TickProfiler_Snapshot_v1", (Action<long, string>)OnSnapshotLegacy);
				instance.Register<string, string>("MagiCorp_TickProfiler_HitchNotice_v1", (Action<long, string, string>)OnHitchNotice);
				instance.Register<string>("MagiCorp_TickProfiler_Snapshot_v2", (Action<long, string>)OnSnapshotV2);
				instance.Register<string, string>("MagiCorp_TickProfiler_HitchNotice_v2", (Action<long, string, string>)OnHitchNotice);
				instance.Register<string>("MagiCorp_TickProfiler_HitchEvent_v2", (Action<long, string>)OnHitchEvent);
				instance.Register<string>("MagiCorp_TickProfiler_PingProbe_v2", (Action<long, string>)OnPingProbeV2);
				instance.Register<string>("MagiCorp_TickProfiler_PingSnapshot_v2", (Action<long, string>)OnPingSnapshotV2);
				_registeredRpcInstance = instance;
				_rpcRegistered = true;
				_lastHelloTicks = 0L;
				((BaseUnityPlugin)this).Logger.LogInfo((object)"TickProfiler snapshot RPC registered on current ZRoutedRpc instance.");
				SendHello();
			}
			catch (Exception ex)
			{
				((BaseUnityPlugin)this).Logger.LogWarning((object)("TickProfiler overlay RPC registration failed: " + ex.Message));
			}
		}
	}

	private void SendHello()
	{
		ZRoutedRpc instance = ZRoutedRpc.instance;
		if (instance == null)
		{
			return;
		}
		long serverPeerId = GetServerPeerId(instance);
		if (serverPeerId == 0L)
		{
			return;
		}
		try
		{
			instance.InvokeRoutedRPC(serverPeerId, "MagiCorp_TickProfiler_Hello_v2", new object[1] { "version=1.0.3|protocol=" + 2 + "|features=hitch-events,steam-transport|player=" + Uri.EscapeDataString(GetLocalPlayerName()) });
			instance.InvokeRoutedRPC(serverPeerId, "MagiCorp_TickProfiler_Hello_v1", Array.Empty<object>());
			_lastHelloTicks = Stopwatch.GetTimestamp();
			((BaseUnityPlugin)this).Logger.LogDebug((object)("TickProfiler v2+legacy hello sent to server peer " + serverPeerId + "."));
		}
		catch
		{
		}
	}

	private long GetServerPeerId(ZRoutedRpc routed)
	{
		if (routed == null || GetServerPeerIdMethod == null)
		{
			return 0L;
		}
		try
		{
			return (GetServerPeerIdMethod.Invoke(routed, null) is long num) ? num : 0;
		}
		catch (Exception ex)
		{
			if (!_loggedPeerIdFailure)
			{
				_loggedPeerIdFailure = true;
				((BaseUnityPlugin)this).Logger.LogWarning((object)("TickProfiler could not resolve the server peer ID: " + ex.GetBaseException().Message));
			}
			return 0L;
		}
	}

	private void OnPingProbeV2(long sender, string sequence)
	{
		if (string.IsNullOrEmpty(sequence))
		{
			return;
		}
		ZRoutedRpc instance = ZRoutedRpc.instance;
		if (instance == null)
		{
			return;
		}
		long serverPeerId = GetServerPeerId(instance);
		if (serverPeerId != 0L && sender != serverPeerId)
		{
			return;
		}
		try
		{
			instance.InvokeRoutedRPC(sender, "MagiCorp_TickProfiler_PingReply_v2", new object[1] { sequence + "|" + Uri.EscapeDataString(GetLocalPlayerName()) });
		}
		catch
		{
		}
	}

	private void OnPingSnapshotV2(long sender, string payload)
	{
		ZRoutedRpc instance = ZRoutedRpc.instance;
		if (instance != null)
		{
			long serverPeerId = GetServerPeerId(instance);
			if (serverPeerId != 0L && sender != serverPeerId)
			{
				return;
			}
		}
		ParsePingData(payload);
	}

	private static string GetLocalPlayerName()
	{
		try
		{
			if ((Object)(object)Player.m_localPlayer != (Object)null && !string.IsNullOrWhiteSpace(Player.m_localPlayer.GetPlayerName()))
			{
				return Player.m_localPlayer.GetPlayerName();
			}
		}
		catch
		{
		}
		try
		{
			if ((Object)(object)Game.instance != (Object)null && Game.instance.GetPlayerProfile() != null)
			{
				return Game.instance.GetPlayerProfile().GetName() ?? "unknown";
			}
		}
		catch
		{
		}
		return "unknown";
	}

	private void ParsePingData(string line)
	{
		if (string.IsNullOrEmpty(line))
		{
			return;
		}
		int num = line.IndexOf("players=", StringComparison.Ordinal);
		if (num < 0)
		{
			return;
		}
		string text = line.Substring(num + 8);
		string localPlayerName = GetLocalPlayerName();
		List<string> list = new List<string>();
		float cachedPingMs = -1f;
		string[] array = text.Split(new char[1] { ',' }, StringSplitOptions.RemoveEmptyEntries);
		for (int i = 0; i < array.Length; i++)
		{
			string[] array2 = array[i].Split(new char[1] { '~' });
			if (array2.Length < 3)
			{
				continue;
			}
			string text2;
			try
			{
				text2 = Uri.UnescapeDataString(array2[1]);
			}
			catch
			{
				text2 = array2[1];
			}
			if (float.TryParse(array2[2], NumberStyles.Float, CultureInfo.InvariantCulture, out var result))
			{
				bool flag = text2.Equals(localPlayerName, StringComparison.OrdinalIgnoreCase);
				if (flag)
				{
					cachedPingMs = result;
				}
				list.Add(text2 + " " + result.ToString("F0", CultureInfo.InvariantCulture) + "ms" + (flag ? " (you)" : string.Empty));
			}
		}
		_cachedPingMs = cachedPingMs;
		_playerPingDisplay = ((list.Count == 0) ? "Steam transport  waiting for samples..." : ("Steam transport  " + string.Join("   ", list.ToArray())));
		_cachedDisplayTicks = 0L;
	}

	private void OnHitchNotice(long sender, string title, string text)
	{
		if (string.IsNullOrEmpty(text))
		{
			return;
		}
		ZRoutedRpc instance = ZRoutedRpc.instance;
		if (instance != null)
		{
			long serverPeerId = GetServerPeerId(instance);
			if (serverPeerId != 0L && sender != serverPeerId)
			{
				return;
			}
		}
		try
		{
			if ((Object)(object)Chat.instance != (Object)null)
			{
				((Terminal)Chat.instance).AddString(string.IsNullOrWhiteSpace(title) ? "Server" : title, text, (Type)1, false);
			}
			else
			{
				((BaseUnityPlugin)this).Logger.LogInfo((object)((string.IsNullOrWhiteSpace(title) ? "Server" : title) + ": " + text));
			}
		}
		catch (Exception ex)
		{
			((BaseUnityPlugin)this).Logger.LogWarning((object)("TickProfiler hitch chat display failed: " + ex.Message));
		}
	}

	private void OnSnapshotLegacy(long sender, string text)
	{
		if (!_v2SnapshotSeen)
		{
			_serverProtocol = 1;
			_serverVersion = "0.x legacy";
			OnSnapshot(sender, text);
		}
	}

	private void OnSnapshotV2(long sender, string text)
	{
		_v2SnapshotSeen = true;
		OnSnapshot(sender, text);
	}

	private void OnSnapshot(long sender, string text)
	{
		if (string.IsNullOrEmpty(text))
		{
			return;
		}
		ZRoutedRpc instance = ZRoutedRpc.instance;
		if (instance != null)
		{
			long serverPeerId = GetServerPeerId(instance);
			if (serverPeerId != 0L && sender != serverPeerId)
			{
				return;
			}
		}
		string[] array = text.Replace("\r", string.Empty).Split(new char[1] { '\n' });
		List<string> list = new List<string>(array.Length);
		for (int i = 0; i < array.Length; i++)
		{
			if (array[i].StartsWith("@TPPROTO|", StringComparison.Ordinal))
			{
				ParseProtocol(array[i]);
			}
			else if (array[i].StartsWith("@TPDATA|", StringComparison.Ordinal))
			{
				ParseServerHistory(array[i]);
			}
			else if (array[i].StartsWith("@TPPING|", StringComparison.Ordinal))
			{
				ParsePingData(array[i]);
			}
			else
			{
				list.Add(array[i]);
			}
		}
		_snapshot = string.Join("\n", list.ToArray()).TrimEnd(Array.Empty<char>());
		_snapshotUtc = DateTime.UtcNow;
		if (!_loggedFirstSnapshot)
		{
			_loggedFirstSnapshot = true;
			((BaseUnityPlugin)this).Logger.LogInfo((object)"TickProfiler first server snapshot received.");
		}
	}

	private bool TogglePressed()
	{
		//IL_0006: 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)
		try
		{
			return ZInput.GetKeyDown(_toggleKey.Value, true);
		}
		catch (Exception ex)
		{
			try
			{
				return Input.GetKeyDown(_toggleKey.Value);
			}
			catch
			{
				((BaseUnityPlugin)this).Logger.LogWarning((object)("TickProfiler toggle input failed: " + ex.Message));
				return false;
			}
		}
	}

	private void OnGUI()
	{
		//IL_0081: 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)
		//IL_008d: Unknown result type (might be due to invalid IL or missing references)
		//IL_0094: Unknown result type (might be due to invalid IL or missing references)
		//IL_00a0: Expected O, but got Unknown
		//IL_00ab: Unknown result type (might be due to invalid IL or missing references)
		//IL_025a: Unknown result type (might be due to invalid IL or missing references)
		//IL_025f: Unknown result type (might be due to invalid IL or missing references)
		//IL_0365: Unknown result type (might be due to invalid IL or missing references)
		//IL_03a8: Unknown result type (might be due to invalid IL or missing references)
		//IL_03e8: Unknown result type (might be due to invalid IL or missing references)
		//IL_0435: Unknown result type (might be due to invalid IL or missing references)
		if (!_visible)
		{
			return;
		}
		float num = ((_uiScale.Value > 0f) ? Mathf.Clamp(_uiScale.Value, 0.75f, 4f) : Mathf.Clamp((float)Screen.height / 1080f, 1f, 3f));
		int fontSize = Math.Max(10, Mathf.RoundToInt((float)_fontSize.Value * num));
		if (_textStyle == null)
		{
			_textStyle = new GUIStyle(GUI.skin.label)
			{
				richText = false,
				wordWrap = false,
				alignment = (TextAnchor)0
			};
			_textStyle.normal.textColor = Color.white;
		}
		_textStyle.fontSize = fontSize;
		long timestamp = Stopwatch.GetTimestamp();
		if (string.IsNullOrEmpty(_cachedDisplayText) || ToSeconds(timestamp - _cachedDisplayTicks) >= 0.25)
		{
			string text;
			if (string.IsNullOrEmpty(_snapshot))
			{
				text = "SERVER TICK PROFILER\nWaiting for TickProfiler data from this server...";
			}
			else
			{
				double num2 = Math.Max(0.0, (DateTime.UtcNow - _snapshotUtc).TotalSeconds);
				string text2 = ((_serverProtocol == 0) ? "protocol unknown" : ((_serverProtocol == 2) ? ("protocol v" + _serverProtocol) : ("PROTOCOL MISMATCH server v" + _serverProtocol + " client v" + 2)));
				text = _snapshot + "\nServer " + _serverVersion + "   " + text2 + "\nAge " + num2.ToString("F1", CultureInfo.InvariantCulture) + "s" + ((_cachedPingMs >= 0f) ? ("   Steam " + _cachedPingMs.ToString("F0", CultureInfo.InvariantCulture) + "ms") : string.Empty) + "\n" + _playerPingDisplay;
			}
			_cachedDisplayText = _clientSnapshot + "\n\n" + text + "\nToggle " + ((object)_toggleKey.Value/*cast due to .constrained prefix*/).ToString();
			_cachedDisplayTicks = timestamp;
		}
		string cachedDisplayText = _cachedDisplayText;
		int num3 = cachedDisplayText.Count((char c) => c == '\n') + 1;
		float num4 = 112f * num;
		float num5 = 8f * num;
		float num6 = _x.Value * num;
		float num7 = _y.Value * num;
		float val = 620f * num;
		float num8 = Math.Min(Math.Max(val, _width.Value * num), Math.Max(val, (float)Screen.width - num6 - 12f * num));
		float num9 = 20f * num + (float)num3 * ((float)_textStyle.fontSize + 5f * num);
		float num10 = num9 + num5 + num4 * 2f + 18f * num;
		Rect val2 = default(Rect);
		((Rect)(ref val2))..ctor(num6, num7, num8, num10);
		GUI.Box(val2, GUIContent.none);
		GUI.Label(new Rect(((Rect)(ref val2)).x + 10f * num, ((Rect)(ref val2)).y + 8f * num, ((Rect)(ref val2)).width - 20f * num, num9 - 12f * num), cachedDisplayText, _textStyle);
		float num11 = ((Rect)(ref val2)).y + num9;
		DrawHistoryGraph(new Rect(((Rect)(ref val2)).x + 10f * num, num11, ((Rect)(ref val2)).width - 20f * num, num4), _clientHistory, "CLIENT 5 MIN", "FPS", 60.0, num);
		DrawHistoryGraph(new Rect(((Rect)(ref val2)).x + 10f * num, num11 + num4 + num5, ((Rect)(ref val2)).width - 20f * num, num4), _serverHistory, "SERVER 5 MIN", "Hz", 50.0, num);
	}

	private void ParseProtocol(string line)
	{
		string[] array = line.Split(new char[1] { '|' });
		for (int i = 1; i < array.Length; i++)
		{
			int num = array[i].IndexOf('=');
			if (num > 0)
			{
				string text = array[i].Substring(0, num);
				string text2 = array[i].Substring(num + 1);
				if (text == "protocol")
				{
					int.TryParse(text2, NumberStyles.Integer, CultureInfo.InvariantCulture, out _serverProtocol);
				}
				else if (text == "server")
				{
					_serverVersion = text2;
				}
			}
		}
	}

	private void OnHitchEvent(long sender, string payload)
	{
		if (string.IsNullOrEmpty(payload))
		{
			return;
		}
		ZRoutedRpc instance = ZRoutedRpc.instance;
		if (instance != null)
		{
			long serverPeerId = GetServerPeerId(instance);
			if (serverPeerId != 0L && sender != serverPeerId)
			{
				return;
			}
		}
		try
		{
			string[] array = payload.Split(new char[1] { '|' });
			if (array.Length >= 2)
			{
				long result;
				DateTime now = (long.TryParse(array[0], NumberStyles.Integer, CultureInfo.InvariantCulture, out result) ? new DateTime(result, DateTimeKind.Utc) : DateTime.UtcNow);
				if (double.TryParse(array[1], NumberStyles.Float, CultureInfo.InvariantCulture, out var result2))
				{
					AddHistory(_serverHistory, _lastServerTickRate, result2, now);
				}
			}
		}
		catch
		{
		}
	}

	private void ParseServerHistory(string line)
	{
		double result = 0.0;
		double result2 = 0.0;
		string[] array = line.Split(new char[1] { '|' });
		for (int i = 1; i < array.Length; i++)
		{
			int num = array[i].IndexOf('=');
			if (num > 0)
			{
				string text = array[i].Substring(0, num);
				string s = array[i].Substring(num + 1);
				if (text == "tick")
				{
					double.TryParse(s, NumberStyles.Float, CultureInfo.InvariantCulture, out result);
				}
				else if (text == "stall")
				{
					double.TryParse(s, NumberStyles.Float, CultureInfo.InvariantCulture, out result2);
				}
			}
		}
		_lastServerTickRate = result;
		AddHistory(_serverHistory, result, result2);
	}

	private static void AddHistory(List<HistoryPoint> history, double rate, double stallMs)
	{
		AddHistory(history, rate, stallMs, DateTime.UtcNow);
	}

	private static void AddHistory(List<HistoryPoint> history, double rate, double stallMs, DateTime now)
	{
		history.Add(new HistoryPoint
		{
			TimeUtc = now,
			Rate = rate,
			StallMs = stallMs
		});
		DateTime dateTime = now.AddMinutes(-5.0);
		while (history.Count > 0 && history[0].TimeUtc < dateTime)
		{
			history.RemoveAt(0);
		}
		while (history.Count > 180)
		{
			history.RemoveAt(0);
		}
	}

	private void DrawHistoryGraph(Rect rect, List<HistoryPoint> history, string title, string rateLabel, double minimumRateScale, float scale)
	{
		//IL_0000: Unknown result type (might be due to invalid IL or missing references)
		//IL_009b: Unknown result type (might be due to invalid IL or missing references)
		//IL_001e: Unknown result type (might be due to invalid IL or missing references)
		//IL_0023: Unknown result type (might be due to invalid IL or missing references)
		//IL_002f: Expected O, but got Unknown
		//IL_003a: Unknown result type (might be due to invalid IL or missing references)
		//IL_00fb: Unknown result type (might be due to invalid IL or missing references)
		//IL_0362: Unknown result type (might be due to invalid IL or missing references)
		//IL_03ed: Unknown result type (might be due to invalid IL or missing references)
		//IL_043e: Unknown result type (might be due to invalid IL or missing references)
		//IL_026b: Unknown result type (might be due to invalid IL or missing references)
		//IL_024c: Unknown result type (might be due to invalid IL or missing references)
		//IL_0251: Unknown result type (might be due to invalid IL or missing references)
		//IL_025f: Unknown result type (might be due to invalid IL or missing references)
		//IL_02bc: Unknown result type (might be due to invalid IL or missing references)
		//IL_02d5: Unknown result type (might be due to invalid IL or missing references)
		//IL_02f9: Unknown result type (might be due to invalid IL or missing references)
		GUI.Box(rect, GUIContent.none);
		if (_graphStyle == null)
		{
			_graphStyle = new GUIStyle(GUI.skin.label)
			{
				alignment = (TextAnchor)0
			};
			_graphStyle.normal.textColor = Color.white;
		}
		_graphStyle.fontSize = Math.Max(9, Mathf.RoundToInt(10f * scale));
		GUI.Label(new Rect(((Rect)(ref rect)).x + 6f * scale, ((Rect)(ref rect)).y + 2f * scale, ((Rect)(ref rect)).width - 12f * scale, 18f * scale), title, _graphStyle);
		Rect val = default(Rect);
		((Rect)(ref val))..ctor(((Rect)(ref rect)).x + 6f * scale, ((Rect)(ref rect)).y + 21f * scale, ((Rect)(ref rect)).width - 12f * scale, ((Rect)(ref rect)).height - 27f * scale);
		if (history.Count == 0)
		{
			GUI.Label(val, "Collecting history...", _graphStyle);
			return;
		}
		DateTime dateTime = DateTime.UtcNow.AddMinutes(-5.0);
		double num = minimumRateScale;
		double num2 = 100.0;
		for (int i = 0; i < history.Count; i++)
		{
			if (history[i].Rate > num)
			{
				num = history[i].Rate;
			}
			if (history[i].StallMs > num2)
			{
				num2 = history[i].StallMs;
			}
		}
		num *= 1.05;
		Vector2? val2 = null;
		Vector2 val3 = default(Vector2);
		for (int j = 0; j < history.Count; j++)
		{
			HistoryPoint historyPoint = history[j];
			double totalSeconds = (historyPoint.TimeUtc - dateTime).TotalSeconds;
			float num3 = ((Rect)(ref val)).x + (float)Math.Max(0.0, Math.Min(1.0, totalSeconds / 300.0)) * ((Rect)(ref val)).width;
			float num4 = ((Rect)(ref val)).yMax - (float)Math.Max(0.0, Math.Min(1.0, historyPoint.Rate / num)) * ((Rect)(ref val)).height;
			((Vector2)(ref val3))..ctor(num3, num4);
			if (val2.HasValue)
			{
				DrawLine(val2.Value, val3, Math.Max(1f, scale), Color.green);
			}
			val2 = val3;
			if (historyPoint.StallMs > 0.0)
			{
				float num5 = (float)Math.Max(2.0 * (double)scale, Math.Min(((Rect)(ref val)).height, historyPoint.StallMs / num2 * (double)((Rect)(ref val)).height));
				Color color = GUI.color;
				GUI.color = new Color(1f, 0.55f, 0.15f, 0.85f);
				GUI.DrawTexture(new Rect(num3, ((Rect)(ref val)).yMax - num5, Math.Max(1f, scale), num5), (Texture)(object)Texture2D.whiteTexture);
				GUI.color = color;
			}
		}
		HistoryPoint historyPoint2 = history[history.Count - 1];
		_graphStyle.alignment = (TextAnchor)2;
		GUI.Label(new Rect(((Rect)(ref val)).x, ((Rect)(ref rect)).y + 2f * scale, ((Rect)(ref val)).width, 18f * scale), F(historyPoint2.Rate, 1) + " " + rateLabel + "   hitch " + F(historyPoint2.StallMs, 0) + " ms", _graphStyle);
		_graphStyle.alignment = (TextAnchor)0;
		GUI.Label(new Rect(((Rect)(ref val)).x, ((Rect)(ref val)).yMax - 15f * scale, 70f * scale, 15f * scale), "-5m", _graphStyle);
		_graphStyle.alignment = (TextAnchor)2;
		GUI.Label(new Rect(((Rect)(ref val)).xMax - 70f * scale, ((Rect)(ref val)).yMax - 15f * scale, 70f * scale, 15f * scale), "now", _graphStyle);
		_graphStyle.alignment = (TextAnchor)0;
	}

	private static void DrawLine(Vector2 a, Vector2 b, float width, Color color)
	{
		//IL_0000: Unknown result type (might be due to invalid IL or missing references)
		//IL_0001: Unknown result type (might be due to invalid IL or missing references)
		//IL_0002: Unknown result type (might be due to invalid IL or missing references)
		//IL_0007: Unknown result type (might be due to invalid IL or missing references)
		//IL_0019: Unknown result type (might be due to invalid IL or missing references)
		//IL_001e: Unknown result type (might be due to invalid IL or missing references)
		//IL_001f: Unknown result type (might be due to invalid IL or missing references)
		//IL_0024: Unknown result type (might be due to invalid IL or missing references)
		//IL_002a: Unknown result type (might be due to invalid IL or missing references)
		//IL_003b: Unknown result type (might be due to invalid IL or missing references)
		//IL_0041: Unknown result type (might be due to invalid IL or missing references)
		//IL_0047: Unknown result type (might be due to invalid IL or missing references)
		//IL_004d: Unknown result type (might be due to invalid IL or missing references)
		//IL_005d: Unknown result type (might be due to invalid IL or missing references)
		//IL_006c: Unknown result type (might be due to invalid IL or missing references)
		Vector2 val = b - a;
		float magnitude = ((Vector2)(ref val)).magnitude;
		if (!(magnitude < 0.1f))
		{
			Matrix4x4 matrix = GUI.matrix;
			Color color2 = GUI.color;
			GUIUtility.RotateAroundPivot(Mathf.Atan2(val.y, val.x) * 57.29578f, a);
			GUI.color = color;
			GUI.DrawTexture(new Rect(a.x, a.y - width * 0.5f, magnitude, width), (Texture)(object)Texture2D.whiteTexture);
			GUI.matrix = matrix;
			GUI.color = color2;
		}
	}

	private string ResolveLocalStallCause(long windowStartTicks, long windowEndTicks, double gapMs, bool gcDuringWindow)
	{
		LongCallEvent longCallEvent = FindBestLongCall(windowStartTicks, windowEndTicks);
		if (longCallEvent != null)
		{
			double num = Math.Max(_pluginAttributionThresholdMs.Value, gapMs * 0.4);
			if (longCallEvent.ElapsedMs >= num)
			{
				return longCallEvent.Name + " (" + F(longCallEvent.ElapsedMs, 1) + " ms)";
			}
		}
		if (gcDuringWindow)
		{
			return "GC observed during hitch (causation not proven)";
		}
		return "unattributed (render/Unity/native/I/O/OS)";
	}

	private static LongCallEvent FindBestLongCall(long windowStartTicks, long windowEndTicks)
	{
		lock (RecentLongCallLock)
		{
			LongCallEvent longCallEvent = null;
			for (int num = RecentLongCalls.Count - 1; num >= 0; num--)
			{
				LongCallEvent longCallEvent2 = RecentLongCalls[num];
				if (longCallEvent2.EndTicks < windowStartTicks)
				{
					break;
				}
				if (longCallEvent2.StartTicks <= windowEndTicks && longCallEvent2.EndTicks >= windowStartTicks && (longCallEvent == null || longCallEvent2.ElapsedMs > longCallEvent.ElapsedMs))
				{
					longCallEvent = longCallEvent2;
				}
			}
			return longCallEvent;
		}
	}

	private void ProfileLoadedMods()
	{
		foreach (KeyValuePair<string, PluginInfo> pluginInfo in Chainloader.PluginInfos)
		{
			PluginInfo value = pluginInfo.Value;
			if (value == null || (Object)(object)value.Instance == (Object)null)
			{
				continue;
			}
			Assembly assembly = ((object)value.Instance).GetType().Assembly;
			string text = assembly.GetName().Name ?? string.Empty;
			if (assembly == typeof(TickProfilerClientOverlay).Assembly || text.StartsWith("MagiCorp.ValheimTickProfiler", StringComparison.OrdinalIgnoreCase))
			{
				continue;
			}
			string text2 = ((value.Metadata != null && !string.IsNullOrEmpty(value.Metadata.Name)) ? value.Metadata.Name : pluginInfo.Key);
			foreach (Type item in SafeGetTypes(assembly))
			{
				if (!typeof(BaseUnityPlugin).IsAssignableFrom(item))
				{
					continue;
				}
				MethodInfo[] methods;
				try
				{
					methods = item.GetMethods(BindingFlags.DeclaredOnly | BindingFlags.Instance | BindingFlags.Static | BindingFlags.Public | BindingFlags.NonPublic);
				}
				catch
				{
					continue;
				}
				MethodInfo[] array = methods;
				foreach (MethodInfo methodInfo in array)
				{
					if (!methodInfo.IsAbstract && !methodInfo.ContainsGenericParameters && methodInfo.GetMethodBody() != null && (methodInfo.Name == "Update" || methodInfo.Name == "FixedUpdate" || methodInfo.Name == "LateUpdate"))
					{
						InstrumentMethod(methodInfo, "Plugin/" + text2 + "/" + item.Name + "." + methodInfo.Name);
					}
				}
			}
		}
	}

	private static IEnumerable<Type> SafeGetTypes(Assembly assembly)
	{
		try
		{
			return assembly.GetTypes();
		}
		catch (ReflectionTypeLoadException ex)
		{
			return ex.Types.Where((Type t) => t != null);
		}
		catch
		{
			return Array.Empty<Type>();
		}
	}

	private static bool HasAttributeNamed(object[] attrs, string name)
	{
		for (int i = 0; i < attrs.Length; i++)
		{
			Type type = attrs[i]?.GetType();
			if (type != null && (type.Name == name || type.FullName == "HarmonyLib." + name))
			{
				return true;
			}
		}
		return false;
	}

	private void InstrumentMethod(MethodBase method, string label)
	{
		//IL_004b: Unknown result type (might be due to invalid IL or missing references)
		//IL_0050: Unknown result type (might be due to invalid IL or missing references)
		//IL_006b: Unknown result type (might be due to invalid IL or missing references)
		//IL_0070: Unknown result type (might be due to invalid IL or missing references)
		//IL_0088: Unknown result type (might be due to invalid IL or missing references)
		//IL_008d: Unknown result type (might be due to invalid IL or missing references)
		//IL_009a: Expected O, but got Unknown
		//IL_009a: Expected O, but got Unknown
		//IL_009a: Expected O, but got Unknown
		if (method == null || MethodMetrics.ContainsKey(method))
		{
			return;
		}
		Metric metric = new Metric(label);
		MethodMetrics.Add(method, metric);
		Metrics.Add(metric);
		try
		{
			_harmony.Patch(method, new HarmonyMethod(typeof(TickProfilerClientOverlay), "ProfilePrefix", (Type[])null)
			{
				priority = 800
			}, new HarmonyMethod(typeof(TickProfilerClientOverlay), "ProfilePostfix", (Type[])null)
			{
				priority = 0
			}, (HarmonyMethod)null, new HarmonyMethod(typeof(TickProfilerClientOverlay), "ProfileFinalizer", (Type[])null)
			{
				priority = 0
			}, (HarmonyMethod)null);
		}
		catch (Exception ex)
		{
			MethodMetrics.Remove(method);
			Metrics.Remove(metric);
			((BaseUnityPlugin)this).Logger.LogWarning((object)("Client plugin profiling skipped " + label + ": " + ex.Message));
		}
	}

	private static void ProfilePrefix(MethodBase __originalMethod, out int __state)
	{
		__state = -1;
		if (MethodMetrics.TryGetValue(__originalMethod, out var value))
		{
			if (_scopeStack == null)
			{
				_scopeStack = new ScopeFrame[64];
			}
			if (_scopeDepth >= _scopeStack.Length)
			{
				Array.Resize(ref _scopeStack, _scopeStack.Length * 2);
			}
			int num = _scopeDepth++;
			_scopeStack[num].Metric = value;
			_scopeStack[num].StartTicks = Stopwatch.GetTimestamp();
			__state = num;
		}
	}

	private static void ProfilePostfix(ref int __state)
	{
		CompleteProfile(ref __state);
	}

	private static Exception ProfileFinalizer(Exception __exception, ref int __state)
	{
		CompleteProfile(ref __state);
		return __exception;
	}

	private static void CompleteProfile(ref int __state)
	{
		if (__state < 0 || _scopeStack == null || __state >= _scopeStack.Length)
		{
			__state = -1;
			return;
		}
		long timestamp = Stopwatch.GetTimestamp();
		long ticks = timestamp - _scopeStack[__state].StartTicks;
		Metric metric = _scopeStack[__state].Metric;
		if (metric != null)
		{
			double num = ToMilliseconds(ticks);
			if (num >= Math.Max(1.0, _longCallThresholdMs))
			{
				lock (RecentLongCallLock)
				{
					RecentLongCalls.Add(new LongCallEvent
					{
						Name = metric.Name,
						StartTicks = _scopeStack[__state].StartTicks,
						EndTicks = timestamp,
						ElapsedMs = num
					});
					long num2 = timestamp - Frequency * 15;
					while (RecentLongCalls.Count > 0 && (RecentLongCalls[0].EndTicks < num2 || RecentLongCalls.Count > 512))
					{
						RecentLongCalls.RemoveAt(0);
					}
				}
			}
		}
		_scopeDepth = __state;
		__state = -1;
	}

	private double GetWorkingSetMiB()
	{
		try
		{
			_process.Refresh();
			return (double)_process.WorkingSet64 / 1048576.0;
		}
		catch
		{
			return 0.0;
		}
	}

	private static string F(double value, int decimals)
	{
		return value.ToString("F" + decimals, CultureInfo.InvariantCulture);
	}

	private static double ToMilliseconds(long ticks)
	{
		return (double)ticks * 1000.0 / (double)Frequency;
	}

	private static double ToSeconds(long ticks)
	{
		return (double)ticks / (double)Frequency;
	}
}