using System; using System.Collections.Generic; using System.Diagnostics; using System.Globalization; using System.IO; using System.Linq; using System.Reflection; using System.Reflection.Emit; using System.Runtime.CompilerServices; using System.Runtime.InteropServices; using System.Runtime.Versioning; using System.Text; using BepInEx; using BepInEx.Bootstrap; using BepInEx.Configuration; using BepInEx.Logging; using HarmonyLib; using Microsoft.CodeAnalysis; using Mono.Cecil; using Mono.Cecil.Cil; using UnityEngine; [assembly: CompilationRelaxations(8)] [assembly: RuntimeCompatibility(WrapNonExceptionThrows = true)] [assembly: Debuggable(DebuggableAttribute.DebuggingModes.IgnoreSymbolStoreSequencePoints)] [assembly: AssemblyTitle("LoadTimeProfiler")] [assembly: AssemblyDescription("Valheim preloader startup and connection profiler")] [assembly: AssemblyConfiguration("")] [assembly: AssemblyCompany("sighsorry")] [assembly: AssemblyProduct("LoadTimeProfiler")] [assembly: AssemblyCopyright("Copyright 2026")] [assembly: AssemblyTrademark("")] [assembly: ComVisible(false)] [assembly: Guid("1E310F27-0F47-41D7-94D7-4A3FE40B8539")] [assembly: AssemblyFileVersion("1.0.0")] [assembly: TargetFramework(".NETFramework,Version=v4.8", FrameworkDisplayName = ".NET Framework 4.8")] [assembly: AssemblyVersion("1.0.0.0")] [module: RefSafetyRules(11)] namespace Microsoft.CodeAnalysis { [CompilerGenerated] [Embedded] internal sealed class EmbeddedAttribute : Attribute { } } namespace System.Runtime.CompilerServices { [CompilerGenerated] [Embedded] [AttributeUsage(AttributeTargets.Class | AttributeTargets.Property | AttributeTargets.Field | AttributeTargets.Event | AttributeTargets.Parameter | AttributeTargets.ReturnValue | AttributeTargets.GenericParameter, AllowMultiple = false, Inherited = false)] internal sealed class NullableAttribute : Attribute { public readonly byte[] NullableFlags; public NullableAttribute(byte P_0) { NullableFlags = new byte[1] { P_0 }; } public NullableAttribute(byte[] P_0) { NullableFlags = P_0; } } [CompilerGenerated] [Embedded] [AttributeUsage(AttributeTargets.Class | AttributeTargets.Struct | AttributeTargets.Method | AttributeTargets.Interface | AttributeTargets.Delegate, AllowMultiple = false, Inherited = false)] internal sealed class NullableContextAttribute : Attribute { public readonly byte Flag; public NullableContextAttribute(byte P_0) { Flag = P_0; } } [CompilerGenerated] [Embedded] [AttributeUsage(AttributeTargets.Module, AllowMultiple = false, Inherited = false)] internal sealed class RefSafetyRulesAttribute : Attribute { public readonly int Version; public RefSafetyRulesAttribute(int P_0) { Version = P_0; } } } namespace LoadTimeProfiler { public static class LoadTimeProfilerPatcher { internal const string ModName = "LoadTimeProfiler"; internal const string ModVersion = "1.0.0"; internal const string Author = "sighsorry"; internal const string ModGUID = "sighsorry.LoadTimeProfiler"; private static ConfigEntry? _enabled; private static bool _initialized; public static IEnumerable TargetDLLs { get; } = new string[1] { "UnityEngine.CoreModule.dll" }; internal static ManualLogSource? Log { get; private set; } internal static bool ProfilingEnabled { get; private set; } internal static bool IsDedicatedServer { get; private set; } public static void Patch(AssemblyDefinition assembly) { TimelineProfiler.CapturePatcherStart(); try { InjectRuntimeEntrypoint(assembly); } catch (Exception arg) { Console.Error.WriteLine(string.Format("[{0}] Could not inject runtime entrypoint: {1}", "LoadTimeProfiler", arg)); } } public static void Finish() { } private static void InjectRuntimeEntrypoint(AssemblyDefinition assembly) { //IL_00c4: Unknown result type (might be due to invalid IL or missing references) //IL_00e2: Unknown result type (might be due to invalid IL or missing references) MethodDefinition obj = ((IEnumerable)((IEnumerable)assembly.MainModule.Types).First((TypeDefinition type) => ((TypeReference)type).Namespace == "UnityEngine" && ((MemberReference)type).Name == "GameObject").Methods).First((MethodDefinition val2) => val2.IsConstructor && val2.IsStatic); Instruction val = ((IEnumerable)obj.Body.Instructions).First(delegate(Instruction instruction) { //IL_0001: Unknown result type (might be due to invalid IL or missing references) //IL_0006: Unknown result type (might be due to invalid IL or missing references) if (instruction.OpCode == OpCodes.Call) { object operand = instruction.Operand; MethodReference val2 = (MethodReference)((operand is MethodReference) ? operand : null); if (val2 != null && ((MemberReference)((MemberReference)val2).DeclaringType).FullName == "BepInEx.Bootstrap.Chainloader") { return ((MemberReference)val2).Name == "Start"; } } return false; }); MethodInfo method = typeof(RuntimeEntrypoint).GetMethod("BeforeChainloaderStart", BindingFlags.Static | BindingFlags.Public); MethodInfo method2 = typeof(RuntimeEntrypoint).GetMethod("AfterChainloaderStart", BindingFlags.Static | BindingFlags.Public); ILProcessor iLProcessor = obj.Body.GetILProcessor(); iLProcessor.InsertBefore(val, iLProcessor.Create(OpCodes.Call, assembly.MainModule.ImportReference((MethodBase)method))); iLProcessor.InsertAfter(val, iLProcessor.Create(OpCodes.Call, assembly.MainModule.ImportReference((MethodBase)method2))); } internal static void InitializeProfiler() { //IL_0028: Unknown result type (might be due to invalid IL or missing references) if (!_initialized) { _initialized = true; IsDedicatedServer = DetectDedicatedServer(); try { _enabled = new ConfigFile(Path.Combine(Paths.ConfigPath, "sighsorry.LoadTimeProfiler.cfg"), true).Bind("General", "Enabled", true, "Profile client loading or dedicated server startup. Changes apply on the next launch."); ProfilingEnabled = _enabled.Value; } catch (Exception ex) { ProfilingEnabled = true; Console.Error.WriteLine("[LoadTimeProfiler] Could not read config; profiling remains enabled: " + ex.Message); } ProfilerLog.Initialize(); if (!ProfilingEnabled) { ProfilerLog.WriteLine("Profiling is disabled in config."); return; } TimelineProfiler.BeginStartup(IsDedicatedServer); ProfilerLog.WriteLine(IsDedicatedServer ? "Preloader profiler active. Waiting for dedicated server startup." : "Preloader profiler active. Waiting for BepInEx chainloader startup."); } } internal static void AttachBepInExLogger() { if (Log != null) { return; } try { Log = Logger.CreateLogSource("LoadTimeProfiler"); if (ProfilingEnabled) { string text = (IsDedicatedServer ? "dedicated server startup" : "startup and connection"); Log.LogInfo((object)("Writing " + text + " profiles to " + ProfilerLog.FilePath + ".")); } else { Log.LogInfo((object)("Profiler disabled. Session log reset at " + ProfilerLog.FilePath + ".")); } } catch (Exception ex) { Console.Error.WriteLine("[LoadTimeProfiler] Could not create BepInEx log source: " + ex.Message); } } internal static void LogInfo(string message) { ManualLogSource? log = Log; if (log != null) { log.LogInfo((object)message); } } internal static void LogWarning(string message) { if (Log != null) { Log.LogWarning((object)message); } else { Console.Error.WriteLine("[LoadTimeProfiler] " + message); } } internal static void LogError(string message) { if (Log != null) { Log.LogError((object)message); } else { Console.Error.WriteLine("[LoadTimeProfiler] " + message); } } private static bool DetectDedicatedServer() { return (Paths.ProcessName ?? string.Empty).IndexOf("valheim_server", StringComparison.OrdinalIgnoreCase) >= 0; } } internal static class ProfilerLog { private const int MaximumRetainedLogs = 10; private static readonly object Lock = new object(); private static StreamWriter? _writer; internal static string FilePath { get; private set; } = Path.Combine(Paths.ConfigPath, "LoadTimeProfiler", "pending.log"); internal static void Initialize() { lock (Lock) { DisposeWriter(); DateTime now = DateTime.Now; string text = Path.Combine(Paths.ConfigPath, "LoadTimeProfiler"); try { Directory.CreateDirectory(text); RetainNewestLogs(text, 9); FilePath = CreateUniqueLogPath(text, now); _writer = new StreamWriter(new FileStream(FilePath, FileMode.CreateNew, FileAccess.Write, FileShare.ReadWrite), new UTF8Encoding(encoderShouldEmitUTF8Identifier: false)) { AutoFlush = true }; _writer.WriteLine("LoadTimeProfiler 1.0.0"); _writer.WriteLine($"Session: {now:yyyy-MM-dd HH:mm:ss zzz}"); _writer.WriteLine("Process: " + Paths.ProcessName); _writer.WriteLine("Mode: " + (LoadTimeProfilerPatcher.IsDedicatedServer ? "Dedicated server" : "Client")); _writer.WriteLine($"BepInEx: {typeof(BaseUnityPlugin).Assembly.GetName().Version}"); _writer.WriteLine(LoadTimeProfilerPatcher.IsDedicatedServer ? "Coverage: BepInEx plugin construction/Awake/OnEnable, plugin Start methods, dedicated server lifecycle execution, and milestone intervals." : "Coverage: BepInEx plugin construction/Awake/OnEnable, plugin Start methods, selected client lifecycle execution, and milestone intervals."); _writer.WriteLine("Deep attribution instruments existing synchronous Harmony callbacks in ObjectDB.Awake and ZNetScene.Awake."); _writer.WriteLine(); } catch (Exception ex) { DisposeWriter(); LoadTimeProfilerPatcher.LogError("Could not create profiler log '" + FilePath + "': " + ex.Message); } } } private static string CreateUniqueLogPath(string directory, DateTime timestamp) { string text = timestamp.ToString("yyyy-MM-dd_HH-mm-ss"); string text2 = Path.Combine(directory, text + ".log"); int num = 2; while (File.Exists(text2)) { text2 = Path.Combine(directory, text + "_" + num + ".log"); num++; } return text2; } private static void RetainNewestLogs(string directory, int maximumExistingLogs) { FileInfo[] array = (from file in new DirectoryInfo(directory).GetFiles("*.log", SearchOption.TopDirectoryOnly) orderby file.LastWriteTimeUtc select file).ThenBy((FileInfo file) => file.Name, StringComparer.Ordinal).ToArray(); int num = Math.Max(0, array.Length - maximumExistingLogs); for (int num2 = 0; num2 < num; num2++) { try { array[num2].Delete(); } catch (IOException) { } catch (UnauthorizedAccessException) { } } } internal static void WriteLine(string text) { lock (Lock) { if (_writer == null) { return; } try { _writer.WriteLine(text); } catch (Exception ex) { LoadTimeProfilerPatcher.LogWarning("Could not write profiler log: " + ex.Message); DisposeWriter(); } } } internal static void WriteBlock(string text) { lock (Lock) { if (_writer == null) { return; } try { _writer.WriteLine(text.TrimEnd(Array.Empty())); _writer.WriteLine(); } catch (Exception ex) { LoadTimeProfilerPatcher.LogWarning("Could not write profiler log: " + ex.Message); DisposeWriter(); } } } private static void DisposeWriter() { try { _writer?.Dispose(); } catch { } _writer = null; } } internal enum ProfileSession { Startup, Connection } [Flags] internal enum ProfileSessionMask { None = 0, Startup = 1, Connection = 2 } internal static class TimelineProfiler { private sealed class SessionState { private readonly List _milestones = new List(); private readonly HashSet _seenMilestones = new HashSet(StringComparer.Ordinal); internal ProfileSession Session { get; } internal string Name { get; private set; } internal bool Active { get; private set; } private double StartMilliseconds { get; set; } internal SessionState(ProfileSession session, string name) { Session = session; Name = name; } internal void Begin(double startMilliseconds, string? name = null) { if (!string.IsNullOrEmpty(name)) { Name = name; } Active = true; StartMilliseconds = startMilliseconds; _milestones.Clear(); _seenMilestones.Clear(); } internal void AddMilestoneOnce(string label, double absoluteMilliseconds) { if (_seenMilestones.Add(label)) { AddMilestone(label, absoluteMilliseconds); } } internal void AddMilestone(string label, double absoluteMilliseconds) { _seenMilestones.Add(label); _milestones.Add(new Milestone(label, Math.Max(0.0, absoluteMilliseconds - StartMilliseconds))); } internal SessionSnapshot Complete(double absoluteMilliseconds, string result) { Active = false; double totalMilliseconds = Math.Max(0.0, absoluteMilliseconds - StartMilliseconds); return new SessionSnapshot(Session, Name, result, totalMilliseconds, _milestones.ToArray()); } } private readonly struct SessionSnapshot { internal ProfileSession Session { get; } internal string Name { get; } internal string Result { get; } internal double TotalMilliseconds { get; } internal Milestone[] Milestones { get; } internal SessionSnapshot(ProfileSession session, string name, string result, double totalMilliseconds, Milestone[] milestones) { Session = session; Name = name; Result = result; TotalMilliseconds = totalMilliseconds; Milestones = milestones; } } private readonly struct Milestone { internal string Label { get; } internal double ElapsedMilliseconds { get; } internal Milestone(string label, double elapsedMilliseconds) { Label = label; ElapsedMilliseconds = elapsedMilliseconds; } } private static readonly object Lock = new object(); private static readonly SessionState Startup = new SessionState(ProfileSession.Startup, "Start To Lobby"); private static readonly SessionState Connection = new SessionState(ProfileSession.Connection, "Lobby To World"); private static double _patcherStartMilliseconds; internal static void CapturePatcherStart() { lock (Lock) { if (_patcherStartMilliseconds <= 0.0) { _patcherStartMilliseconds = NowMilliseconds(); } } } internal static void BeginStartup(bool dedicatedServer) { if (!LoadTimeProfilerPatcher.ProfilingEnabled) { return; } LifecyclePhaseProfiler.ResetSession(ProfileSession.Startup); ChainloaderProfiler.ResetSession(); if (dedicatedServer) { DeepLobbyAttributionProfiler.ResetSession(); } lock (Lock) { double num = NowMilliseconds(); double num2 = ((_patcherStartMilliseconds > 0.0) ? _patcherStartMilliseconds : num); Startup.Begin(num2, dedicatedServer ? "Server Startup" : "Start To Lobby"); Startup.AddMilestone("LoadTimeProfiler.Patcher.Finish", num2); Startup.AddMilestone("LoadTimeProfiler.Patcher initialized", num); } } internal static void BeginConnection(string label, bool restartActive) { if (!LoadTimeProfilerPatcher.ProfilingEnabled) { return; } if (restartActive) { AbortConnection("superseded by " + label); } lock (Lock) { if (Connection.Active) { Connection.AddMilestoneOnce(label, NowMilliseconds()); return; } } LifecyclePhaseProfiler.ResetSession(ProfileSession.Connection); DeepLobbyAttributionProfiler.ResetSession(); lock (Lock) { double num = NowMilliseconds(); if (Connection.Active) { Connection.AddMilestoneOnce(label, num); return; } Connection.Begin(num); Connection.AddMilestone(label, num); } } internal static void MarkStartup(string label) { Mark(Startup, label); } internal static void MarkConnection(string label) { Mark(Connection, label); } internal static bool IsActive(ProfileSession session) { lock (Lock) { return GetState(session).Active; } } internal static ProfileSessionMask GetActiveSessionMask() { lock (Lock) { ProfileSessionMask profileSessionMask = ProfileSessionMask.None; if (Startup.Active) { profileSessionMask |= ProfileSessionMask.Startup; } if (Connection.Active) { profileSessionMask |= ProfileSessionMask.Connection; } return profileSessionMask; } } internal static double GetElapsedSincePatcherStart() { lock (Lock) { return (_patcherStartMilliseconds <= 0.0) ? 0.0 : Math.Max(0.0, NowMilliseconds() - _patcherStartMilliseconds); } } internal static void CompleteStartup(string label) { Finish(Startup, label, "completed"); } internal static void AbortStartup(string label) { Finish(Startup, label, "aborted"); } internal static void CompleteConnection(string label) { Finish(Connection, label, "completed"); } internal static void AbortConnection(string label) { Finish(Connection, label, "aborted"); } private static void Mark(SessionState state, string label) { if (!LoadTimeProfilerPatcher.ProfilingEnabled) { return; } lock (Lock) { if (state.Active) { state.AddMilestoneOnce(label, NowMilliseconds()); } } } private static void Finish(SessionState state, string label, string result) { SessionSnapshot sessionSnapshot; lock (Lock) { if (!state.Active) { return; } double absoluteMilliseconds = NowMilliseconds(); state.AddMilestone(label, absoluteMilliseconds); sessionSnapshot = state.Complete(absoluteMilliseconds, result); } StringBuilder stringBuilder = new StringBuilder(); stringBuilder.AppendLine("=== " + sessionSnapshot.Name + " ==="); stringBuilder.AppendLine("Result: " + sessionSnapshot.Result); stringBuilder.Append("Total: ").AppendLine(FormatDuration(sessionSnapshot.TotalMilliseconds)); Dictionary lifecycleExecutionTimes = LifecyclePhaseProfiler.SnapshotSingleExecutionTimes(sessionSnapshot.Session); AppendMilestoneIntervals(stringBuilder, sessionSnapshot.Milestones, lifecycleExecutionTimes); if (sessionSnapshot.Session == ProfileSession.Startup) { ChainloaderProfiler.AppendStartupReport(stringBuilder); } LifecyclePhaseProfiler.AppendSessionReport(stringBuilder, sessionSnapshot.Session); if (sessionSnapshot.Session == ProfileSession.Connection || (sessionSnapshot.Session == ProfileSession.Startup && LoadTimeProfilerPatcher.IsDedicatedServer)) { DeepLobbyAttributionProfiler.AppendReport(stringBuilder); } ProfilerLog.WriteBlock(stringBuilder.ToString()); LoadTimeProfilerPatcher.LogInfo(sessionSnapshot.Name + " profile " + sessionSnapshot.Result + ": " + FormatDuration(sessionSnapshot.TotalMilliseconds) + ". See " + ProfilerLog.FilePath + "."); } private static void AppendMilestoneIntervals(StringBuilder builder, Milestone[] milestones, IReadOnlyDictionary lifecycleExecutionTimes) { builder.AppendLine("Milestone intervals:"); builder.AppendLine(" Breakdown: lifecycle execution + remaining time until the next milestone."); if (milestones.Length < 2) { builder.AppendLine(" No milestone interval completed."); return; } for (int i = 0; i < milestones.Length - 1; i++) { Milestone milestone = milestones[i]; Milestone milestone2 = milestones[i + 1]; double num = Math.Max(0.0, milestone2.ElapsedMilliseconds - milestone.ElapsedMilliseconds); double num2 = Math.Truncate(num); builder.Append(" ").Append(FormatSeconds(num2)); if (lifecycleExecutionTimes.TryGetValue(milestone.Label, out var value) && value <= num) { double num3 = Math.Min(num2, Math.Truncate(Math.Max(0.0, value))); double milliseconds = num2 - num3; builder.Append(" (").Append(FormatSeconds(num3)).Append(" + ") .Append(FormatSeconds(milliseconds)) .Append(')'); } builder.Append(": ").AppendLine(milestone.Label); } } private static SessionState GetState(ProfileSession session) { if (session != ProfileSession.Startup) { return Connection; } return Startup; } private static double NowMilliseconds() { return (double)Stopwatch.GetTimestamp() * 1000.0 / (double)Stopwatch.Frequency; } internal static string FormatDuration(double milliseconds) { if (milliseconds >= 60000.0) { int num = (int)(milliseconds / 60000.0); double num2 = (milliseconds - (double)num * 60000.0) / 1000.0; return $"{num} min {num2:00.000} s"; } if (milliseconds >= 1000.0) { return $"{milliseconds / 1000.0:0.000} s"; } return $"{milliseconds:0.###} ms"; } internal static string FormatSeconds(double milliseconds) { return (Math.Truncate(Math.Max(0.0, milliseconds)) / 1000.0).ToString("0.000", CultureInfo.InvariantCulture) + " s"; } } public static class RuntimeEntrypoint { private static bool _chainloaderStarted; public static void BeforeChainloaderStart() { try { LoadTimeProfilerPatcher.InitializeProfiler(); LoadTimeProfilerPatcher.AttachBepInExLogger(); if (LoadTimeProfilerPatcher.ProfilingEnabled) { ProfilerLog.WriteLine("Chainloader runtime entrypoint reached. Installing Unity and Valheim hooks."); RuntimeHookInstaller.Install(); ChainloaderProfiler.BeginChainloader(); _chainloaderStarted = true; } } catch (Exception ex) { RuntimeHookInstaller.RemovePluginConstructionHook(); ProfilerLog.WriteLine("Runtime hook initialization failed: " + ex); LoadTimeProfilerPatcher.LogError("Runtime hook initialization failed: " + ex.Message); } } public static void AfterChainloaderStart() { bool flag = _chainloaderStarted && LoadTimeProfilerPatcher.ProfilingEnabled && LoadTimeProfilerPatcher.IsDedicatedServer; try { if (_chainloaderStarted) { ChainloaderProfiler.EndChainloader(); } } catch (Exception ex) { ProfilerLog.WriteLine("Chainloader completion measurement failed: " + ex); } finally { _chainloaderStarted = false; RuntimeHookInstaller.RemovePluginConstructionHook(); } if (flag) { try { DeepLobbyAttributionProfiler.PrepareForActiveSession(); } catch (Exception ex2) { ProfilerLog.WriteLine("Dedicated server attribution preparation failed: " + ex2); } } } } internal static class RuntimeHookInstaller { private static readonly object Lock = new object(); private static readonly Harmony Harmony = new Harmony("sighsorry.LoadTimeProfiler.runtime"); private static bool _installed; private static MethodBase? _pluginConstructionTarget; private static bool _pluginConstructionPatched; internal static void Install() { lock (Lock) { if (!_installed) { _installed = true; int installed = 0; int failed = 0; if (TryPatchPluginConstruction()) { installed++; } else { failed++; } PatchLifecycleTargets(ref installed, ref failed); ProfilerLog.WriteLine($"Unity and Valheim runtime hook installation completed: installed={installed}, failed={failed}."); } } } internal static void RemovePluginConstructionHook() { lock (Lock) { if (!_pluginConstructionPatched || _pluginConstructionTarget == null) { return; } try { Harmony.Unpatch(_pluginConstructionTarget, (HarmonyPatchType)0, Harmony.Id); ProfilerLog.WriteLine("Removed the startup-only GameObject.AddComponent profiling hook."); } catch (Exception ex) { ProfilerLog.WriteLine("Runtime hook warning: could not remove the plugin construction hook: " + ex.Message); } finally { _pluginConstructionPatched = false; _pluginConstructionTarget = null; } } } private static bool TryPatchPluginConstruction() { try { MethodBase methodBase = AccessTools.Method(typeof(GameObject), "AddComponent", new Type[1] { typeof(Type) }, (Type[])null); if (methodBase == null) { throw new MissingMethodException(typeof(GameObject).FullName, "AddComponent"); } Harmony.Patch(methodBase, Highest(typeof(LoadTimeProfilerPluginInitializationPatch), "Prefix"), Lowest(typeof(LoadTimeProfilerPluginInitializationPatch), "Postfix"), (HarmonyMethod)null, Lowest(typeof(LoadTimeProfilerPluginInitializationPatch), "Finalizer"), (HarmonyMethod)null); _pluginConstructionTarget = methodBase; _pluginConstructionPatched = true; return true; } catch (Exception ex) { ProfilerLog.WriteLine("Runtime hook warning: could not profile plugin construction: " + ex.Message); return false; } } private static void PatchLifecycleTargets(ref int installed, ref int failed) { HarmonyMethod val; HarmonyMethod val2; try { val = Highest(typeof(LoadTimeProfilerLifecyclePatch), "Prefix"); val2 = Lowest(typeof(LoadTimeProfilerLifecyclePatch), "Finalizer"); } catch (Exception ex) { failed++; ProfilerLog.WriteLine("Runtime hook warning: could not prepare lifecycle hooks: " + ex.Message); return; } foreach (MethodBase target in LifecyclePatches.GetTargets()) { try { Harmony.Patch(target, val, (HarmonyMethod)null, (HarmonyMethod)null, val2, (HarmonyMethod)null); installed++; } catch (Exception ex2) { failed++; string text = (target.DeclaringType?.FullName ?? "") + "." + target.Name; ProfilerLog.WriteLine("Runtime hook warning: could not patch " + text + ": " + ex2.Message); } } } private static HarmonyMethod Highest(Type type, string methodName) { //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_0030: Expected O, but got Unknown MethodInfo methodInfo = AccessTools.Method(type, methodName, (Type[])null, (Type[])null); if (methodInfo == null) { throw new MissingMethodException(type.FullName, methodName); } return new HarmonyMethod(methodInfo) { priority = int.MaxValue }; } private static HarmonyMethod Lowest(Type type, string methodName) { //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_0030: Expected O, but got Unknown MethodInfo methodInfo = AccessTools.Method(type, methodName, (Type[])null, (Type[])null); if (methodInfo == null) { throw new MissingMethodException(type.FullName, methodName); } return new HarmonyMethod(methodInfo) { priority = int.MinValue }; } } internal static class ChainloaderProfiler { internal sealed class PluginInitializationState { internal Type ComponentType { get; } internal PluginIdentity Identity { get; } internal long StartTimestamp { get; } internal bool Completed { get; set; } internal PluginInitializationState(Type componentType, PluginIdentity identity, long startTimestamp) { ComponentType = componentType; Identity = identity; StartTimestamp = startTimestamp; } } internal readonly struct PluginIdentity { internal string Key { get; } internal string DisplayName { get; } internal PluginIdentity(string key, string displayName) { Key = key; DisplayName = displayName; } } private sealed class MutableTiming { private PluginIdentity Identity { get; } private int Count { get; set; } private double ElapsedMilliseconds { get; set; } internal MutableTiming(PluginIdentity identity) { Identity = identity; } internal void Add(double elapsedMilliseconds) { Count++; ElapsedMilliseconds += elapsedMilliseconds; } internal TimingSnapshot Snapshot() { return new TimingSnapshot(Identity, Count, ElapsedMilliseconds); } } private readonly struct TimingSnapshot { internal PluginIdentity Identity { get; } internal int Count { get; } internal double ElapsedMilliseconds { get; } internal TimingSnapshot(PluginIdentity identity, int count, double elapsedMilliseconds) { Identity = identity; Count = count; ElapsedMilliseconds = elapsedMilliseconds; } } private static readonly object Lock = new object(); private static readonly Harmony PluginStartHarmony = new Harmony("sighsorry.LoadTimeProfiler.plugin-start"); private static readonly Dictionary Initializations = new Dictionary(StringComparer.Ordinal); private static readonly Dictionary Starts = new Dictionary(StringComparer.Ordinal); private static readonly Dictionary StartMethods = new Dictionary(); private static readonly HashSet InstrumentedStartMethods = new HashSet(); private static bool _chainloaderActive; private static long _chainloaderStarted; private static double _preChainloaderMilliseconds; private static double _chainloaderMilliseconds; internal static void ResetSession() { lock (Lock) { Initializations.Clear(); Starts.Clear(); _chainloaderActive = false; _chainloaderStarted = 0L; _preChainloaderMilliseconds = 0.0; _chainloaderMilliseconds = 0.0; } } internal static void BeginChainloader() { if (LoadTimeProfilerPatcher.ProfilingEnabled) { lock (Lock) { _chainloaderActive = true; _chainloaderStarted = Stopwatch.GetTimestamp(); _preChainloaderMilliseconds = TimelineProfiler.GetElapsedSincePatcherStart(); } TimelineProfiler.MarkStartup("BepInEx.Chainloader.Start"); } } internal static void EndChainloader() { long timestamp = Stopwatch.GetTimestamp(); lock (Lock) { if (_chainloaderStarted > 0) { _chainloaderMilliseconds = TicksToMilliseconds(timestamp - _chainloaderStarted); _chainloaderStarted = 0L; } _chainloaderActive = false; } TimelineProfiler.MarkStartup("BepInEx.Chainloader.Start complete"); } internal static PluginInitializationState? BeginPlugin(GameObject gameObject, Type componentType) { if (!LoadTimeProfilerPatcher.ProfilingEnabled || !typeof(BaseUnityPlugin).IsAssignableFrom(componentType)) { return null; } lock (Lock) { if (!_chainloaderActive || gameObject != Chainloader.ManagerObject) { return null; } } return new PluginInitializationState(componentType, ResolveIdentity(componentType), Stopwatch.GetTimestamp()); } internal static void CompletePlugin(PluginInitializationState? state) { if (state != null && !state.Completed) { state.Completed = true; double elapsedMilliseconds = TicksToMilliseconds(Stopwatch.GetTimestamp() - state.StartTimestamp); lock (Lock) { Add(Initializations, state.Identity.Key, state.Identity, elapsedMilliseconds); } InstrumentStart(state.ComponentType, state.Identity); } } internal static void AppendStartupReport(StringBuilder builder) { TimingSnapshot[] array; TimingSnapshot[] timings; double preChainloaderMilliseconds; double chainloaderMilliseconds; lock (Lock) { array = Initializations.Values.Select((MutableTiming value) => value.Snapshot()).ToArray(); timings = Starts.Values.Select((MutableTiming value) => value.Snapshot()).ToArray(); preChainloaderMilliseconds = _preChainloaderMilliseconds; chainloaderMilliseconds = _chainloaderMilliseconds; } builder.AppendLine("BepInEx startup:"); builder.Append(" Before Chainloader.Start (includes Chainloader.Initialize): ").AppendLine(TimelineProfiler.FormatDuration(preChainloaderMilliseconds)); builder.Append(" Chainloader.Start: ").AppendLine(TimelineProfiler.FormatDuration(chainloaderMilliseconds)); builder.AppendLine("Plugin construction/Awake/OnEnable:"); AppendTimings(builder, array); builder.AppendLine("Plugin Start methods:"); AppendTimings(builder, timings); double num = array.Sum((TimingSnapshot value) => value.ElapsedMilliseconds); double milliseconds = Math.Max(0.0, chainloaderMilliseconds - num); builder.Append(" Chainloader scan/load/dependency remainder: ").AppendLine(TimelineProfiler.FormatDuration(milliseconds)); } private static void AppendTimings(StringBuilder builder, TimingSnapshot[] timings) { if (timings.Length == 0) { builder.AppendLine(" No callbacks were measured."); return; } foreach (TimingSnapshot item in timings.OrderByDescending((TimingSnapshot value) => value.ElapsedMilliseconds)) { builder.Append(" ").Append(TimelineProfiler.FormatSeconds(item.ElapsedMilliseconds)); if (item.Count > 1) { builder.Append(" x").Append(item.Count); } builder.Append(": ").AppendLine(item.Identity.DisplayName); } } private static void InstrumentStart(Type componentType, PluginIdentity identity) { //IL_007a: Unknown result type (might be due to invalid IL or missing references) //IL_007f: Unknown result type (might be due to invalid IL or missing references) //IL_008b: 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) //IL_00ad: Expected O, but got Unknown MethodInfo method = componentType.GetMethod("Start", BindingFlags.DeclaredOnly | BindingFlags.Instance | BindingFlags.Public | BindingFlags.NonPublic, null, Type.EmptyTypes, null); if (method == null || method.IsAbstract || method.ContainsGenericParameters) { return; } lock (Lock) { if (!InstrumentedStartMethods.Add(method)) { return; } StartMethods[method] = identity; } try { HarmonyMethod val = new HarmonyMethod(typeof(ChainloaderProfiler), "ProfiledStartPrefix", (Type[])null) { priority = int.MaxValue }; HarmonyMethod val2 = new HarmonyMethod(typeof(ChainloaderProfiler), "ProfiledStartFinalizer", (Type[])null) { priority = int.MinValue }; PluginStartHarmony.Patch((MethodBase)method, val, (HarmonyMethod)null, (HarmonyMethod)null, val2, (HarmonyMethod)null); } catch (Exception ex) { lock (Lock) { InstrumentedStartMethods.Remove(method); StartMethods.Remove(method); } ProfilerLog.WriteLine("Instrumentation warning: could not profile plugin Start '" + componentType.FullName + "': " + ex.Message); } } private static void ProfiledStartPrefix(out long __state) { __state = 0L; if (LoadTimeProfilerPatcher.ProfilingEnabled && TimelineProfiler.IsActive(ProfileSession.Startup)) { __state = Stopwatch.GetTimestamp(); } } private static Exception? ProfiledStartFinalizer(MethodBase __originalMethod, long __state, Exception? __exception) { if (__state <= 0) { return __exception; } double elapsedMilliseconds = TicksToMilliseconds(Stopwatch.GetTimestamp() - __state); lock (Lock) { if (StartMethods.TryGetValue(__originalMethod, out var value)) { Add(Starts, value.Key, value, elapsedMilliseconds); } } return __exception; } private static PluginIdentity ResolveIdentity(Type componentType) { BepInPlugin? obj = componentType.GetCustomAttributes(typeof(BepInPlugin), inherit: false).OfType().FirstOrDefault(); string key = ((obj != null) ? obj.GUID : null) ?? componentType.FullName ?? componentType.Name; string displayName = ((obj != null) ? obj.Name : null) ?? componentType.Name; return new PluginIdentity(key, displayName); } private static void Add(Dictionary timings, string key, PluginIdentity identity, double elapsedMilliseconds) { if (!timings.TryGetValue(key, out MutableTiming value)) { value = (timings[key] = new MutableTiming(identity)); } value.Add(elapsedMilliseconds); } private static double TicksToMilliseconds(long ticks) { return (double)ticks * 1000.0 / (double)Stopwatch.Frequency; } } internal static class LoadTimeProfilerPluginInitializationPatch { internal static void Prefix(GameObject __instance, Type componentType, out ChainloaderProfiler.PluginInitializationState? __state) { __state = ChainloaderProfiler.BeginPlugin(__instance, componentType); } internal static void Postfix(ChainloaderProfiler.PluginInitializationState? __state) { ChainloaderProfiler.CompletePlugin(__state); } internal static Exception? Finalizer(ChainloaderProfiler.PluginInitializationState? __state, Exception? __exception) { ChainloaderProfiler.CompletePlugin(__state); return __exception; } } internal static class LifecyclePhaseProfiler { private sealed class PhaseContext { internal MethodBase Target { get; } internal string Label { get; } internal ProfileSessionMask Sessions { get; } internal long StartTimestamp { get; } internal PhaseContext(MethodBase target, string label, ProfileSessionMask sessions, long startTimestamp) { Target = target; Label = label; Sessions = sessions; StartTimestamp = startTimestamp; } } private sealed class SessionAggregate { private readonly Dictionary _timings = new Dictionary(StringComparer.Ordinal); internal void Clear() { _timings.Clear(); } internal void Add(PhaseContext context, double elapsedMilliseconds) { if (!_timings.TryGetValue(context.Label, out MutableTiming value)) { value = new MutableTiming(); _timings[context.Label] = value; } value.Count++; value.ElapsedMilliseconds += elapsedMilliseconds; } internal PhaseTiming[] Snapshot() { return _timings.Select, PhaseTiming>((KeyValuePair pair) => new PhaseTiming(pair.Key, pair.Value.Count, pair.Value.ElapsedMilliseconds)).ToArray(); } internal Dictionary SnapshotSingleExecutionTimes() { return _timings.Where>((KeyValuePair pair) => pair.Value.Count == 1).ToDictionary, string, double>((KeyValuePair pair) => pair.Key, (KeyValuePair pair) => pair.Value.ElapsedMilliseconds, StringComparer.Ordinal); } } private sealed class MutableTiming { internal int Count { get; set; } internal double ElapsedMilliseconds { get; set; } } private readonly struct PhaseTiming { internal string Label { get; } internal int Count { get; } internal double ElapsedMilliseconds { get; } internal PhaseTiming(string label, int count, double elapsedMilliseconds) { Label = label; Count = count; ElapsedMilliseconds = elapsedMilliseconds; } } private static readonly object Lock = new object(); private static readonly List ActivePhases = new List(); private static readonly Dictionary Sessions = new Dictionary { [ProfileSession.Startup] = new SessionAggregate(), [ProfileSession.Connection] = new SessionAggregate() }; internal static void BeginTarget(MethodBase target, string label) { if (!LoadTimeProfilerPatcher.ProfilingEnabled) { return; } ProfileSessionMask activeSessionMask = TimelineProfiler.GetActiveSessionMask(); if (activeSessionMask == ProfileSessionMask.None) { return; } lock (Lock) { ActivePhases.Add(new PhaseContext(target, label, activeSessionMask, Stopwatch.GetTimestamp())); } } internal static void EndTarget(MethodBase target) { if (!LoadTimeProfilerPatcher.ProfilingEnabled) { return; } long timestamp = Stopwatch.GetTimestamp(); lock (Lock) { for (int num = ActivePhases.Count - 1; num >= 0; num--) { PhaseContext phaseContext = ActivePhases[num]; if (!(phaseContext.Target != target)) { ActivePhases.RemoveAt(num); double elapsedMilliseconds = TicksToMilliseconds(timestamp - phaseContext.StartTimestamp); if ((phaseContext.Sessions & ProfileSessionMask.Startup) != ProfileSessionMask.None) { Sessions[ProfileSession.Startup].Add(phaseContext, elapsedMilliseconds); } if ((phaseContext.Sessions & ProfileSessionMask.Connection) != ProfileSessionMask.None) { Sessions[ProfileSession.Connection].Add(phaseContext, elapsedMilliseconds); } break; } } } } internal static void ResetSession(ProfileSession session) { lock (Lock) { Sessions[session].Clear(); } } internal static Dictionary SnapshotSingleExecutionTimes(ProfileSession session) { lock (Lock) { return Sessions[session].SnapshotSingleExecutionTimes(); } } internal static void AppendSessionReport(StringBuilder builder, ProfileSession session) { PhaseTiming[] array; lock (Lock) { array = Sessions[session].Snapshot(); } builder.AppendLine("Measured lifecycle execution times (prefix -> finalizer, inclusive):"); if (array.Length == 0) { builder.AppendLine(" No profiled lifecycle phase completed."); return; } foreach (PhaseTiming item in array.OrderByDescending((PhaseTiming value) => value.ElapsedMilliseconds)) { builder.Append(" ").Append(TimelineProfiler.FormatSeconds(item.ElapsedMilliseconds)); if (item.Count > 1) { builder.Append(" x").Append(item.Count); } builder.Append(": ").AppendLine(item.Label); } } private static double TicksToMilliseconds(long ticks) { return (double)ticks * 1000.0 / (double)Stopwatch.Frequency; } } internal static class LifecyclePatches { private sealed class LifecycleTarget { internal MethodBase Method { get; } internal string Label { get; } internal bool StartupMilestone { get; } internal bool ConnectionMilestone { get; } internal bool BeginsConnection { get; } internal bool RestartsConnection { get; } internal bool PreparesDeepLobbyAttribution { get; } internal bool CompletesStartup { get; } internal bool DedicatedStartupCompletion { get; } internal bool CompletesConnection { get; } internal bool AbortsConnection { get; } internal LifecycleTarget(MethodBase method, string label, bool startupMilestone, bool connectionMilestone, bool beginsConnection, bool restartsConnection, bool preparesDeepLobbyAttribution, bool completesStartup, bool dedicatedStartupCompletion, bool completesConnection, bool abortsConnection) { Method = method; Label = label; StartupMilestone = startupMilestone; ConnectionMilestone = connectionMilestone; BeginsConnection = beginsConnection; RestartsConnection = restartsConnection; PreparesDeepLobbyAttribution = preparesDeepLobbyAttribution; CompletesStartup = completesStartup; DedicatedStartupCompletion = dedicatedStartupCompletion; CompletesConnection = completesConnection; AbortsConnection = abortsConnection; } } private static readonly List Targets = BuildTargets(); private static readonly Dictionary TargetsByMethod = BuildTargetLookup(); internal static IEnumerable GetTargets() { foreach (LifecycleTarget target in Targets) { if (!target.DedicatedStartupCompletion || LoadTimeProfilerPatcher.IsDedicatedServer) { yield return target.Method; } } } internal static void Enter(MethodBase method) { if (LoadTimeProfilerPatcher.ProfilingEnabled && TargetsByMethod.TryGetValue(method, out LifecycleTarget value)) { bool isDedicatedServer = LoadTimeProfilerPatcher.IsDedicatedServer; if (!isDedicatedServer && value.BeginsConnection) { TimelineProfiler.BeginConnection(value.Label, value.RestartsConnection); } if (value.StartupMilestone || (isDedicatedServer && (value.ConnectionMilestone || value.DedicatedStartupCompletion))) { TimelineProfiler.MarkStartup(value.Label); } if (!isDedicatedServer && value.ConnectionMilestone) { TimelineProfiler.MarkConnection(value.Label); } if (!isDedicatedServer && value.PreparesDeepLobbyAttribution) { DeepLobbyAttributionProfiler.PrepareForActiveSession(); } LifecyclePhaseProfiler.BeginTarget(method, value.Label); DeepLobbyAttributionProfiler.BeginTarget(method); } } internal static void Exit(MethodBase method, Exception? exception) { //IL_00cf: Unknown result type (might be due to invalid IL or missing references) //IL_00d4: Unknown result type (might be due to invalid IL or missing references) if (!LoadTimeProfilerPatcher.ProfilingEnabled || !TargetsByMethod.TryGetValue(method, out LifecycleTarget value)) { return; } DeepLobbyAttributionProfiler.EndTarget(method); LifecyclePhaseProfiler.EndTarget(method); bool isDedicatedServer = LoadTimeProfilerPatcher.IsDedicatedServer; if ((!isDedicatedServer && value.CompletesStartup) || (isDedicatedServer && value.DedicatedStartupCompletion)) { if (exception == null) { TimelineProfiler.CompleteStartup(value.Label + " complete"); } else { TimelineProfiler.AbortStartup(value.Label + " failed: " + exception.GetType().Name); } } if (!isDedicatedServer && value.CompletesConnection) { if (exception == null) { TimelineProfiler.CompleteConnection(value.Label + " local player ready"); } else { TimelineProfiler.AbortConnection(value.Label + " failed: " + exception.GetType().Name); } } if (!isDedicatedServer && value.AbortsConnection) { string text; try { text = ((object)ZNet.GetConnectionStatus()/*cast due to .constrained prefix*/).ToString(); } catch { text = "unknown"; } TimelineProfiler.AbortConnection(value.Label + " status=" + text); } } private static List BuildTargets() { List list = new List(); Add(list, typeof(FejdStartup), "Awake", "FejdStartup.Awake", startup: true); Add(list, typeof(FejdStartup), "SetupGui", "FejdStartup.SetupGui", startup: true); Add(list, typeof(FejdStartup), "SetupObjectDB", "FejdStartup.SetupObjectDB", startup: true); Add(list, typeof(FejdStartup), "Start", "FejdStartup.Start", startup: true, connection: false, beginsConnection: false, restartsConnection: false, preparesDeepLobbyAttribution: false, completesStartup: true); Add(list, typeof(FejdStartup), "JoinServer", "FejdStartup.JoinServer", startup: false, connection: true, beginsConnection: true, restartsConnection: true); Add(list, typeof(FejdStartup), "OnWorldStart", "FejdStartup.OnWorldStart", startup: false, connection: true, beginsConnection: true, restartsConnection: false, preparesDeepLobbyAttribution: true); Add(list, typeof(FejdStartup), "TransitionToMainScene", "FejdStartup.TransitionToMainScene", startup: false, connection: true); Add(list, typeof(FejdStartup), "LoadMainScene", "FejdStartup.LoadMainScene", startup: false, connection: true); Add(list, typeof(FejdStartup), "ShowConnectError", "FejdStartup.ShowConnectError", startup: false, connection: true, beginsConnection: false, restartsConnection: false, preparesDeepLobbyAttribution: false, completesStartup: false, dedicatedStartupCompletion: false, completesConnection: false, abortsConnection: true); Add(list, typeof(Game), "Awake", "Game.Awake", startup: false, connection: true); Add(list, typeof(ZoneSystem), "Awake", "ZoneSystem.Awake", startup: false, connection: true); Add(list, typeof(ZNet), "Awake", "ZNet.Awake", startup: false, connection: true); Add(list, typeof(ZNetScene), "Awake", "ZNetScene.Awake", startup: false, connection: true); Add(list, typeof(ObjectDB), "Awake", "ObjectDB.Awake", startup: false, connection: true); Add(list, typeof(Game), "Start", "Game.Start", startup: false, connection: true); Add(list, typeof(ZoneSystem), "Start", "ZoneSystem.Start", startup: false, connection: true); Add(list, typeof(DungeonDB), "Start", "DungeonDB.Start", startup: false, connection: true); Add(list, typeof(ZNet), "Start", "ZNet.Start", startup: false, connection: true); Add(list, typeof(ZNet), "OnGenerationFinished", "ZNet.OnGenerationFinished", startup: false, connection: false, beginsConnection: false, restartsConnection: false, preparesDeepLobbyAttribution: false, completesStartup: false, dedicatedStartupCompletion: true); Add(list, typeof(ZNet), "ClientConnect", "ZNet.ClientConnect", startup: false, connection: true); Add(list, typeof(ZNet), "OnNewConnection", "ZNet.OnNewConnection", startup: false, connection: true); Add(list, typeof(ZNet), "RPC_PeerInfo", "ZNet.RPC_PeerInfo", startup: false, connection: true); Add(list, typeof(Game), "RequestRespawn", "Game.RequestRespawn", startup: false, connection: true); Add(list, typeof(Game), "SpawnPlayer", "Game.SpawnPlayer", startup: false, connection: true, beginsConnection: false, restartsConnection: false, preparesDeepLobbyAttribution: false, completesStartup: false, dedicatedStartupCompletion: false, completesConnection: true); return list; } private static Dictionary BuildTargetLookup() { Dictionary dictionary = new Dictionary(); foreach (LifecycleTarget target in Targets) { dictionary[target.Method] = target; } return dictionary; } private static void Add(List targets, Type type, string methodName, string label, bool startup = false, bool connection = false, bool beginsConnection = false, bool restartsConnection = false, bool preparesDeepLobbyAttribution = false, bool completesStartup = false, bool dedicatedStartupCompletion = false, bool completesConnection = false, bool abortsConnection = false) { MethodBase methodBase = AccessTools.Method(type, methodName, (Type[])null, (Type[])null); if (methodBase == null) { ProfilerLog.WriteLine("Target warning: could not find " + type.FullName + "." + methodName + "."); } else { targets.Add(new LifecycleTarget(methodBase, label, startup, connection, beginsConnection, restartsConnection, preparesDeepLobbyAttribution, completesStartup, dedicatedStartupCompletion, completesConnection, abortsConnection)); } } } internal static class LoadTimeProfilerLifecyclePatch { internal static void Prefix(MethodBase __originalMethod) { LifecyclePatches.Enter(__originalMethod); } internal static Exception? Finalizer(MethodBase __originalMethod, Exception? __exception) { LifecyclePatches.Exit(__originalMethod, __exception); return __exception; } } internal static class DeepLobbyAttributionProfiler { private sealed class PluginResolver { private readonly Dictionary> _byAssembly; private readonly Dictionary _byGuid; private PluginResolver(Dictionary> byAssembly, Dictionary byGuid) { _byAssembly = byAssembly; _byGuid = byGuid; } internal static PluginResolver Create() { Dictionary> dictionary = new Dictionary>(); Dictionary dictionary2 = new Dictionary(StringComparer.Ordinal); foreach (PluginInfo value2 in Chainloader.PluginInfos.Values) { try { string gUID = value2.Metadata.GUID; string name = value2.Metadata.Name; PluginIdentity item = (dictionary2[gUID] = new PluginIdentity(gUID, name)); BaseUnityPlugin instance = value2.Instance; if (!((Object)(object)instance == (Object)null)) { Assembly assembly = ((object)instance).GetType().Assembly; if (!dictionary.TryGetValue(assembly, out var value)) { value = (dictionary[assembly] = new List()); } value.Add(item); } } catch { } } return new PluginResolver(dictionary, dictionary2); } internal PluginIdentity Resolve(MethodBase method, string? owner) { string normalizedOwner = owner ?? string.Empty; if (string.IsNullOrWhiteSpace(normalizedOwner)) { normalizedOwner = string.Empty; } Assembly assembly = method.Module.Assembly; if (_byAssembly.TryGetValue(assembly, out List value)) { if (value.Count == 1) { return value[0]; } PluginIdentity result = value.FirstOrDefault((PluginIdentity identity) => string.Equals(identity.Key, normalizedOwner, StringComparison.Ordinal)); if (!string.IsNullOrEmpty(result.Key)) { return result; } string displayName = assembly.GetName().Name ?? ""; return new PluginIdentity("assembly:" + assembly.FullName, displayName); } if (!string.IsNullOrEmpty(normalizedOwner) && _byGuid.TryGetValue(normalizedOwner, out var value2)) { return value2; } string text = assembly.GetName().Name ?? ""; if (!string.IsNullOrEmpty(normalizedOwner)) { return new PluginIdentity("owner:" + normalizedOwner + "|assembly:" + text, normalizedOwner); } return new PluginIdentity("assembly:" + assembly.FullName, text); } } private sealed class CallbackState { internal PluginIdentity Identity { get; } internal string PhaseLabel { get; } internal long StartTimestamp { get; } internal long ChildTicks { get; set; } internal CallbackState(PluginIdentity identity, string phaseLabel, long startTimestamp) { Identity = identity; PhaseLabel = phaseLabel; StartTimestamp = startTimestamp; } } private sealed class PhaseState { internal MethodBase Target { get; } internal string Label { get; } internal PhaseState(MethodBase target, string label) { Target = target; Label = label; } } private sealed class PluginAggregate { private readonly Dictionary _phases = new Dictionary(StringComparer.Ordinal); private PluginIdentity Identity { get; } internal PluginAggregate(PluginIdentity identity) { Identity = identity; } internal void Add(string phaseLabel, double elapsedMilliseconds) { _phases.TryGetValue(phaseLabel, out var value); _phases[phaseLabel] = value + elapsedMilliseconds; } internal PluginSnapshot Snapshot() { return new PluginSnapshot(Identity, _phases.ToDictionary, string, double>((KeyValuePair pair) => pair.Key, (KeyValuePair pair) => pair.Value, StringComparer.Ordinal)); } } private readonly struct PluginIdentity { internal string Key { get; } internal string DisplayName { get; } internal PluginIdentity(string key, string displayName) { Key = key; DisplayName = displayName; } } private readonly struct PluginSnapshot { private readonly IReadOnlyDictionary _phases; internal PluginIdentity Identity { get; } internal double TotalMilliseconds { get; } internal PluginSnapshot(PluginIdentity identity, IReadOnlyDictionary phases) { Identity = identity; _phases = phases; TotalMilliseconds = phases.Values.Sum(); } internal double GetPhaseMilliseconds(string phaseLabel) { if (!_phases.TryGetValue(phaseLabel, out var value)) { return 0.0; } return value; } } private readonly struct PreparationSummary { internal int Installed { get; } internal int AlreadyInstrumented { get; } internal int Skipped { get; } internal int Failed { get; } internal double ElapsedMilliseconds { get; } internal PreparationSummary(int installed, int alreadyInstrumented, int skipped, int failed, double elapsedMilliseconds) { Installed = installed; AlreadyInstrumented = alreadyInstrumented; Skipped = skipped; Failed = failed; ElapsedMilliseconds = elapsedMilliseconds; } } private const string ObjectDbLabel = "ObjectDB.Awake"; private const string ZNetSceneLabel = "ZNetScene.Awake"; private const int MaximumReportedPlugins = 60; private static readonly object Lock = new object(); private static readonly Harmony InstrumentationHarmony = new Harmony("sighsorry.LoadTimeProfiler.deep-lobby-attribution"); private static readonly Dictionary TargetLabels = BuildTargetLabels(); private static readonly HashSet InstrumentedMethods = new HashSet(); private static readonly Dictionary CallbackOwners = new Dictionary(); private static readonly Dictionary PluginTimings = new Dictionary(StringComparer.Ordinal); [ThreadStatic] private static Stack? _activePhases; [ThreadStatic] private static Stack? _activeCallbacks; private static PreparationSummary _lastPreparation; internal static void ResetSession() { lock (Lock) { PluginTimings.Clear(); _lastPreparation = default(PreparationSummary); } _activePhases?.Clear(); _activeCallbacks?.Clear(); } internal static void PrepareForActiveSession() { //IL_016c: Unknown result type (might be due to invalid IL or missing references) //IL_0171: Unknown result type (might be due to invalid IL or missing references) //IL_018e: Unknown result type (might be due to invalid IL or missing references) //IL_0193: Unknown result type (might be due to invalid IL or missing references) //IL_01a4: Expected O, but got Unknown //IL_01a4: Expected O, but got Unknown if (!LoadTimeProfilerPatcher.ProfilingEnabled) { return; } long timestamp = Stopwatch.GetTimestamp(); HashSet blockedMethods = (LoadTimeProfilerPatcher.IsDedicatedServer ? new HashSet() : GetCurrentMethodBlocklist()); PluginResolver pluginResolver = PluginResolver.Create(); HashSet hashSet = new HashSet(); int num = 0; int num2 = 0; int num3 = 0; int num4 = 0; foreach (MethodBase key in TargetLabels.Keys) { Patches patchInfo; try { patchInfo = Harmony.GetPatchInfo(key); } catch (Exception ex) { num4++; ProfilerLog.WriteLine("Deep attribution warning: could not inspect " + FormatMethodName(key) + ": " + ex.Message); continue; } if (patchInfo == null) { continue; } foreach (Patch item in EnumerateInvocationPatches(patchInfo)) { MethodBase patchMethod = item.PatchMethod; if (patchMethod == null || !hashSet.Add(patchMethod)) { continue; } if (!CanInstrument(patchMethod, blockedMethods)) { num3++; continue; } PluginIdentity value = pluginResolver.Resolve(patchMethod, item.owner); lock (Lock) { if (InstrumentedMethods.Contains(patchMethod)) { num2++; continue; } InstrumentedMethods.Add(patchMethod); CallbackOwners[patchMethod] = value; } try { InstrumentationHarmony.Patch(patchMethod, new HarmonyMethod(typeof(DeepLobbyAttributionProfiler), "ProfiledCallbackPrefix", (Type[])null) { priority = int.MaxValue }, (HarmonyMethod)null, (HarmonyMethod)null, new HarmonyMethod(typeof(DeepLobbyAttributionProfiler), "ProfiledCallbackFinalizer", (Type[])null) { priority = int.MinValue }, (HarmonyMethod)null); num++; } catch (Exception ex2) { num4++; lock (Lock) { InstrumentedMethods.Remove(patchMethod); CallbackOwners.Remove(patchMethod); } ProfilerLog.WriteLine("Deep attribution warning: could not instrument " + FormatMethodName(patchMethod) + ": " + ex2.Message); } } } double num5 = TicksToMilliseconds(Stopwatch.GetTimestamp() - timestamp); lock (Lock) { _lastPreparation = new PreparationSummary(num, num2, num3, num4, num5); } string arg = (LoadTimeProfilerPatcher.IsDedicatedServer ? "server startup" : "deep lobby"); ProfilerLog.WriteLine($"Scoped {arg} attribution prepared: installed={num}, existing={num2}, " + $"skipped={num3}, failed={num4}, setup={TimelineProfiler.FormatSeconds(num5)}."); } internal static void BeginTarget(MethodBase target) { ProfileSession session = ((!LoadTimeProfilerPatcher.IsDedicatedServer) ? ProfileSession.Connection : ProfileSession.Startup); if (LoadTimeProfilerPatcher.ProfilingEnabled && TargetLabels.TryGetValue(target, out string value) && TimelineProfiler.IsActive(session)) { (_activePhases ?? (_activePhases = new Stack())).Push(new PhaseState(target, value)); } } internal static void EndTarget(MethodBase target) { if (_activePhases != null && _activePhases.Count != 0) { if (_activePhases.Peek().Target != target) { _activePhases.Clear(); _activeCallbacks?.Clear(); } else { _activePhases.Pop(); _activeCallbacks?.Clear(); } } } internal static void AppendReport(StringBuilder builder) { builder.AppendLine(LoadTimeProfilerPatcher.IsDedicatedServer ? "Scoped server startup attribution:" : "Scoped deep lobby attribution:"); PluginSnapshot[] source; PreparationSummary lastPreparation; lock (Lock) { source = PluginTimings.Values.Select((PluginAggregate value) => value.Snapshot()).ToArray(); lastPreparation = _lastPreparation; } builder.AppendLine(" Exclusive synchronous Harmony callback time in ObjectDB.Awake and ZNetScene.Awake."); builder.AppendLine(" Breakdown order: ObjectDB + ZNetScene."); builder.Append(" Prepared callbacks: installed=").Append(lastPreparation.Installed).Append(", existing=") .Append(lastPreparation.AlreadyInstrumented) .Append(", skipped=") .Append(lastPreparation.Skipped) .Append(", failed=") .Append(lastPreparation.Failed) .Append(", setup=") .AppendLine(TimelineProfiler.FormatSeconds(lastPreparation.ElapsedMilliseconds)); PluginSnapshot[] array = (from value in source where value.TotalMilliseconds > 0.0 orderby value.TotalMilliseconds descending select value).ThenBy((PluginSnapshot value) => value.Identity.DisplayName, StringComparer.Ordinal).ToArray(); if (array.Length == 0) { builder.AppendLine(" No instrumented callbacks were invoked in the two scoped phases."); return; } int num = Math.Min(array.Length, 60); for (int num2 = 0; num2 < num; num2++) { PluginSnapshot pluginSnapshot = array[num2]; double phaseMilliseconds = pluginSnapshot.GetPhaseMilliseconds("ObjectDB.Awake"); double phaseMilliseconds2 = pluginSnapshot.GetPhaseMilliseconds("ZNetScene.Awake"); builder.Append(" ").Append(TimelineProfiler.FormatSeconds(pluginSnapshot.TotalMilliseconds)).Append(" (") .Append(TimelineProfiler.FormatSeconds(phaseMilliseconds)) .Append(" + ") .Append(TimelineProfiler.FormatSeconds(phaseMilliseconds2)) .Append("): ") .AppendLine(pluginSnapshot.Identity.DisplayName); } if (array.Length > num) { builder.Append(" ... ").Append(array.Length - num).AppendLine(" more plugins omitted."); } } private static void ProfiledCallbackPrefix(MethodBase __originalMethod, out CallbackState? __state) { __state = null; if (_activePhases == null || _activePhases.Count == 0) { return; } PluginIdentity value; lock (Lock) { if (!CallbackOwners.TryGetValue(__originalMethod, out value)) { return; } } PhaseState phaseState = _activePhases.Peek(); CallbackState callbackState = new CallbackState(value, phaseState.Label, Stopwatch.GetTimestamp()); (_activeCallbacks ?? (_activeCallbacks = new Stack())).Push(callbackState); __state = callbackState; } private static Exception? ProfiledCallbackFinalizer(CallbackState? __state, Exception? __exception) { if (__state == null) { return __exception; } long num = Math.Max(0L, Stopwatch.GetTimestamp() - __state.StartTimestamp); if (TryRemoveActiveCallback(__state) && _activeCallbacks != null && _activeCallbacks.Count > 0) { _activeCallbacks.Peek().ChildTicks += num; } double elapsedMilliseconds = TicksToMilliseconds(Math.Max(0L, num - __state.ChildTicks)); lock (Lock) { if (!PluginTimings.TryGetValue(__state.Identity.Key, out PluginAggregate value)) { value = new PluginAggregate(__state.Identity); PluginTimings[__state.Identity.Key] = value; } value.Add(__state.PhaseLabel, elapsedMilliseconds); return __exception; } } private static bool TryRemoveActiveCallback(CallbackState state) { if (_activeCallbacks == null || _activeCallbacks.Count == 0) { return false; } if (_activeCallbacks.Peek() == state) { _activeCallbacks.Pop(); return true; } CallbackState[] array = _activeCallbacks.ToArray(); int num = Array.FindIndex(array, (CallbackState candidate) => candidate == state); if (num < 0) { return false; } _activeCallbacks.Clear(); for (int num2 = array.Length - 1; num2 > num; num2--) { _activeCallbacks.Push(array[num2]); } return true; } private static Dictionary BuildTargetLabels() { Dictionary dictionary = new Dictionary(); AddTarget(dictionary, typeof(ObjectDB), "Awake", "ObjectDB.Awake"); AddTarget(dictionary, typeof(ZNetScene), "Awake", "ZNetScene.Awake"); return dictionary; } private static void AddTarget(Dictionary targets, Type type, string methodName, string label) { MethodBase methodBase = AccessTools.Method(type, methodName, (Type[])null, (Type[])null); if (methodBase != null) { targets[methodBase] = label; } } private static HashSet GetCurrentMethodBlocklist() { HashSet hashSet = new HashSet(); MethodBase methodBase = AccessTools.Method(typeof(FejdStartup), "OnWorldStart", (Type[])null, (Type[])null); if (methodBase == null) { return hashSet; } hashSet.Add(methodBase); try { Patches patchInfo = Harmony.GetPatchInfo(methodBase); if (patchInfo != null) { foreach (Patch item in EnumerateInvocationPatches(patchInfo)) { if (item.PatchMethod != null) { hashSet.Add(item.PatchMethod); } } } } catch { } return hashSet; } private static IEnumerable EnumerateInvocationPatches(Patches patches) { foreach (Patch prefix in patches.Prefixes) { yield return prefix; } foreach (Patch postfix in patches.Postfixes) { yield return postfix; } foreach (Patch finalizer in patches.Finalizers) { yield return finalizer; } } private static bool CanInstrument(MethodBase method, ISet blockedMethods) { if (blockedMethods.Contains(method) || !(method is MethodInfo methodInfo) || method is DynamicMethod || method.DeclaringType == null || method.DeclaringType.ContainsGenericParameters || method.ContainsGenericParameters || method.IsAbstract) { return false; } Assembly assembly = method.Module.Assembly; if (assembly.IsDynamic || assembly == typeof(DeepLobbyAttributionProfiler).Assembly) { return false; } string text = assembly.GetName().Name ?? string.Empty; if (text.Equals("0Harmony", StringComparison.OrdinalIgnoreCase) || text.StartsWith("BepInEx", StringComparison.OrdinalIgnoreCase) || text.StartsWith("MonoMod", StringComparison.OrdinalIgnoreCase) || text.StartsWith("Mono.Cecil", StringComparison.OrdinalIgnoreCase)) { return false; } string text2 = method.DeclaringType.FullName ?? string.Empty; if (text2.StartsWith("HarmonyLib.", StringComparison.Ordinal) || text2.StartsWith("BepInEx.", StringComparison.Ordinal) || text2.StartsWith("MonoMod.", StringComparison.Ordinal) || text2.StartsWith("Mono.Cecil.", StringComparison.Ordinal) || methodInfo.Name.StartsWith("DMD<", StringComparison.Ordinal)) { return false; } try { return methodInfo.GetMethodBody() != null; } catch { return false; } } private static string FormatMethodName(MethodBase method) { return (method.DeclaringType?.FullName ?? "") + "." + method.Name; } private static double TicksToMilliseconds(long ticks) { return (double)ticks * 1000.0 / (double)Stopwatch.Frequency; } } }