Please disclose if any significant portion of your mod was created using AI tools by adding the 'AI Generated' category. Failing to do so may result in the mod being removed from Thunderstore.
Decompiled source of TickProfiler v1.0.3
MagiCorp.ValheimTickProfiler.dll
Decompiled a month agousing 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 agousing 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; } }