From 9107409253e0a39870d75e4db50602539d7b8d4c Mon Sep 17 00:00:00 2001 From: Cole Thompson Date: Mon, 14 Dec 2020 14:35:15 -0600 Subject: [PATCH 01/23] Use correct filename case for building on Linux --- source/BattletechPerformanceFix.csproj | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/source/BattletechPerformanceFix.csproj b/source/BattletechPerformanceFix.csproj index 4c46833..2d6cb21 100644 --- a/source/BattletechPerformanceFix.csproj +++ b/source/BattletechPerformanceFix.csproj @@ -67,7 +67,7 @@ - + @@ -120,4 +120,4 @@ yes | cp "$(TargetDir)$(TargetName).dll" "$(SolutionDir)../../Mods/BattletechPerformanceFix/$(TargetName).dll" yes | cp "$(TargetDir)$(TargetName).dll" "$(SolutionDir)../../Mods/BattletechPerformanceFix/$(TargetName).dll" - \ No newline at end of file + From 0d9f42c7b9cf72311fbdfa180bb21348a5fd5bc8 Mon Sep 17 00:00:00 2001 From: Cole Thompson Date: Fri, 22 Jan 2021 20:38:30 -0600 Subject: [PATCH 02/23] First set of memory leak fixes, mostly involving save file loading --- source/BattletechPerformanceFix.csproj | 1 + source/Main.cs | 1 + source/MemoryLeakFix.cs | 220 +++++++++++++++++++++++++ 3 files changed, 222 insertions(+) create mode 100644 source/MemoryLeakFix.cs diff --git a/source/BattletechPerformanceFix.csproj b/source/BattletechPerformanceFix.csproj index 2d6cb21..ea5767a 100644 --- a/source/BattletechPerformanceFix.csproj +++ b/source/BattletechPerformanceFix.csproj @@ -73,6 +73,7 @@ + diff --git a/source/Main.cs b/source/Main.cs index 0231c9e..8d2c582 100644 --- a/source/Main.cs +++ b/source/Main.cs @@ -99,6 +99,7 @@ public static void Start(string modDirectory, string json) { typeof(DisableSimAnimations), false }, { typeof(RemovedContractsFix), true }, { typeof(VersionManifestPatches), true }, + { typeof(MemoryLeakFix), true }, { typeof(EnableConsole), false }, }; diff --git a/source/MemoryLeakFix.cs b/source/MemoryLeakFix.cs new file mode 100644 index 0000000..434b1d3 --- /dev/null +++ b/source/MemoryLeakFix.cs @@ -0,0 +1,220 @@ +using System; +using System.Reflection; +using System.Reflection.Emit; +using System.Collections.Generic; +using System.Linq; +using Harmony; +using static BattletechPerformanceFix.Extensions; +using BattleTech; +using BattleTech.Analytics.Sim; +using BattleTech.Framework; +using BattleTech.UI; +using Localize; +using HBS.Collections; + +namespace BattletechPerformanceFix +{ + class MemoryLeakFix: Feature + { + private static Type self = typeof(MemoryLeakFix); + + public void Activate() { + // fixes group 1: occurs on save file load + // fix 1.1: allow the BattleTechSimAnalytics class to properly remove its message subscriptions + "BeginSession".Transpile("_SessionTranspiler"); + "EndSession".Transpile("_SessionTranspiler"); + // fix 1.2: add a RemoveSubscriber() for a message type that never had one to begin with + "OnSimGameInitializeComplete".Post("_OnSimGameInitializeComplete_Post"); + // fix 1.3.1: clear InterpolatedText objects that aren't supposed to live forever + "ClearSimulation".Pre("_ClearSimulation_Pre"); + "ClearSimulation".Post("_ClearSimulation_Post"); + // fix 1.3.2: patch methods making an InterpolatedText object and doesn't store it anywhere + // FIXME may also need to patch calls to LocalizableText.UpdateTMPText() [this uses LocalizableText objects as well] [untested] + // TODO/FIXME also patch over the class' finalizers with a nop so we don't have doubled calls to RemoveSubscriber() + "RunMadLibs".Transpile("_MadLib_TagSet_Transpiler"); + "RunMadLibsOnLanceDef".Transpile("_MadLib_TagSet_Transpiler"); + // this method uses both overloads of RunMadLib, so it needs two transpiler passes + "RunMadLib".Transpile("_MadLib_TagSet_Transpiler"); + "RunMadLib".Transpile("_MadLib_string_Transpiler"); + } + + private static IEnumerable _SessionTranspiler(IEnumerable ins) + { + var meth = AccessTools.Method(self, "_UpdateMessageSubscriptions"); + return TranspileReplaceCall(ins, "UpdateMessageSubscriptions", meth); + } + + private static void _UpdateMessageSubscriptions(BattleTechSimAnalytics __instance, bool subscribe) + { + LogSpam("_UpdateMessageSubscriptions called"); + var mc = __instance.messageCenter; + if (mc != null) { + mc.Subscribe(MessageCenterMessageType.OnReportMechwarriorSkillUp, + new ReceiveMessageCenterMessage(__instance.ReportMechWarriorSkilledUp), subscribe); + mc.Subscribe(MessageCenterMessageType.OnReportMechwarriorHired, + new ReceiveMessageCenterMessage(__instance.ReportMechWarriorHired), subscribe); + mc.Subscribe(MessageCenterMessageType.OnReportMechWarriorKilled, + new ReceiveMessageCenterMessage(__instance.ReportMechWarriorKilled), subscribe); + mc.Subscribe(MessageCenterMessageType.OnReportShipUpgradePurchased, + new ReceiveMessageCenterMessage(__instance.ReportShipUpgradePurchased), subscribe); + mc.Subscribe(MessageCenterMessageType.OnSimGameContractComplete, + new ReceiveMessageCenterMessage(__instance.ReportContractComplete), subscribe); + mc.Subscribe(MessageCenterMessageType.OnSimRoomStateChanged, + new ReceiveMessageCenterMessage(__instance.ReportSimGameRoomChange), subscribe); + } + } + + private static void _OnSimGameInitializeComplete_Post(SimGameUXCreator __instance) + { + LogSpam("_OnSimGameInitializeComplete_Post called"); + __instance.sim.MessageCenter.RemoveSubscriber( + MessageCenterMessageType.OnSimGameInitialized, + new ReceiveMessageCenterMessage(__instance.OnSimGameInitializeComplete)); + } + + private static void _ClearSimulation_Pre(GameInstance __instance, ref SimGameState __state) + { + LogSpam("_ClearSimulation_Pre called"); + __state = __instance.Simulation; + } + + private static void _ClearSimulation_Post(GameInstance __instance, ref SimGameState __state) + { + LogSpam("_ClearSimulation_Post called"); + if (__state == null) { + LogSpam("SimGameState was null (ok if first load)"); + return; + } + + var mc = __instance.MessageCenter; + + foreach (var contract in __state.GetAllCurrentlySelectableContracts()) { + mc.RemoveSubscriber(MessageCenterMessageType.OnLanguageChanged, + new ReceiveMessageCenterMessage(contract.OnLanguageChanged)); + + if (contract.Override == null) { + LogDebug("contract.Override is null!"); + continue; + } + + foreach (var dialogue in contract.Override.dialogueList) { + foreach (var content in dialogue.dialogueContent) { + RemoveTextSubscriber(mc, (InterpolatedText) content.GetWords()); + mc.RemoveSubscriber(MessageCenterMessageType.OnLanguageChanged, + new ReceiveMessageCenterMessage(content.OnLanguageChanged)); + } + } + + foreach (var objective in contract.Override.contractObjectiveList) { + RemoveTextSubscriber(mc, (InterpolatedText) objective.GetTitle()); + RemoveTextSubscriber(mc, (InterpolatedText) objective.GetDescription()); + mc.RemoveSubscriber(MessageCenterMessageType.OnLanguageChanged, + new ReceiveMessageCenterMessage(objective.OnLanguageChanged)); + } + + foreach (var objective in contract.Override.objectiveList) { + RemoveTextSubscriber(mc, (InterpolatedText) objective._Title); + RemoveTextSubscriber(mc, (InterpolatedText) objective._Description); + mc.RemoveSubscriber(MessageCenterMessageType.OnLanguageChanged, + new ReceiveMessageCenterMessage(objective.OnLanguageChanged)); + } + } + } + + private static void RemoveTextSubscriber(MessageCenter mc, InterpolatedText text) { + if (text != null) { + mc.RemoveSubscriber(MessageCenterMessageType.OnLanguageChanged, + new ReceiveMessageCenterMessage(text.OnLanguageChanged)); + } + } + + private static IEnumerable _MadLib_string_Transpiler(IEnumerable ins) + { + var meth = AccessTools.Method(self, "_Contract_RunMadLib", + new Type[]{typeof(Contract), typeof(string)}); + return TranspileReplaceOverloadedCall(ins, typeof(Contract), "RunMadLib", + new Type[]{typeof(string)}, meth); + } + + private static IEnumerable _MadLib_TagSet_Transpiler(IEnumerable ins) + { + var meth = AccessTools.Method(self, "_Contract_RunMadLib", + new Type[]{typeof(Contract), typeof(TagSet)}); + return TranspileReplaceOverloadedCall(ins, typeof(Contract), "RunMadLib", + new Type[]{typeof(TagSet)}, meth); + } + + private static IEnumerable + TranspileReplaceCall(IEnumerable ins, string originalMethodName, + MethodInfo replacementMethod) + { + LogInfo($"TranspileReplaceCall: {originalMethodName} -> {replacementMethod.ToString()}"); + return ins.SelectMany(i => { + if (i.opcode == OpCodes.Call && + (i.operand as MethodInfo).Name.StartsWith(originalMethodName)) { + i.operand = replacementMethod; + } + return Sequence(i); + }); + } + + private static IEnumerable + TranspileReplaceOverloadedCall(IEnumerable ins, Type originalMethodClass, + string originalMethodName, Type[] originalParamTypes, + MethodInfo replacementMethod) + { + LogInfo($"TranspileReplaceOverloadedCall: {originalMethodClass.ToString()}.{originalMethodName}" + + $"({String.Concat(originalParamTypes.Select(x => x.ToString()))}) -> {replacementMethod.ToString()}"); + return ins.SelectMany(i => { + var methInfo = i.operand as MethodInfo; + if (i.opcode == OpCodes.Callvirt && + methInfo.DeclaringType == originalMethodClass && + methInfo.Name.StartsWith(originalMethodName) && + Enumerable.SequenceEqual(methInfo.GetParameters().Select(x => x.ParameterType), originalParamTypes)) + { + i.operand = replacementMethod; + } + return Sequence(i); + }); + } + + private static string _Contract_RunMadLib(Contract __instance, string text) + { + LogSpam("_Contract_RunMadLib(string) called"); + if (string.IsNullOrEmpty(text)) + { + return ""; + } + InterpolatedText iText = __instance.Interpolate(text); + text = iText.ToString(false); + __instance.messageCenter.RemoveSubscriber(MessageCenterMessageType.OnLanguageChanged, + new ReceiveMessageCenterMessage(iText.OnLanguageChanged)); + return text; + } + + private static void _Contract_RunMadLib(Contract __instance, TagSet tagSet) + { + LogSpam("_Contract_RunMadLib(tagSet) called"); + if (tagSet == null) + { + return; + } + string[] array = tagSet.ToArray(); + if (array == null) + { + return; + } + for (int i = 0; i < array.Length; i++) + { + string text = array[i]; + InterpolatedText iText = __instance.Interpolate(text); + text = iText.ToString(false); + __instance.messageCenter.RemoveSubscriber(MessageCenterMessageType.OnLanguageChanged, + new ReceiveMessageCenterMessage(iText.OnLanguageChanged)); + array[i] = text.ToLower(); + } + tagSet.Clear(); + tagSet.AddRange(array); + } + } +} From d90f37e1c8246733edeacba2b4bcaa8da8d5fd24 Mon Sep 17 00:00:00 2001 From: Cole Thompson Date: Sat, 6 Feb 2021 09:07:47 -0600 Subject: [PATCH 03/23] cleanup & expand on message subscription fixes --- source/MemoryLeakFix.cs | 328 ++++++++++++++++++++++++++++++++-------- 1 file changed, 262 insertions(+), 66 deletions(-) diff --git a/source/MemoryLeakFix.cs b/source/MemoryLeakFix.cs index 434b1d3..f6d6aaa 100644 --- a/source/MemoryLeakFix.cs +++ b/source/MemoryLeakFix.cs @@ -1,4 +1,5 @@ using System; +using System.Diagnostics; using System.Reflection; using System.Reflection.Emit; using System.Collections.Generic; @@ -7,10 +8,14 @@ using static BattletechPerformanceFix.Extensions; using BattleTech; using BattleTech.Analytics.Sim; +using BattleTech.Data; using BattleTech.Framework; using BattleTech.UI; +using BattleTech.UI.Tooltips; +using BattleTech.UI.TMProWrapper; using Localize; using HBS.Collections; +using HBS.Util; namespace BattletechPerformanceFix { @@ -21,24 +26,65 @@ class MemoryLeakFix: Feature public void Activate() { // fixes group 1: occurs on save file load // fix 1.1: allow the BattleTechSimAnalytics class to properly remove its message subscriptions - "BeginSession".Transpile("_SessionTranspiler"); - "EndSession".Transpile("_SessionTranspiler"); + "BeginSession".Transpile("Session_Transpile"); + "EndSession".Transpile("Session_Transpile"); // fix 1.2: add a RemoveSubscriber() for a message type that never had one to begin with - "OnSimGameInitializeComplete".Post("_OnSimGameInitializeComplete_Post"); + "OnSimGameInitializeComplete".Post(); // fix 1.3.1: clear InterpolatedText objects that aren't supposed to live forever - "ClearSimulation".Pre("_ClearSimulation_Pre"); - "ClearSimulation".Post("_ClearSimulation_Post"); + "Destroy".Post(); // fix 1.3.2: patch methods making an InterpolatedText object and doesn't store it anywhere - // FIXME may also need to patch calls to LocalizableText.UpdateTMPText() [this uses LocalizableText objects as well] [untested] - // TODO/FIXME also patch over the class' finalizers with a nop so we don't have doubled calls to RemoveSubscriber() - "RunMadLibs".Transpile("_MadLib_TagSet_Transpiler"); - "RunMadLibsOnLanceDef".Transpile("_MadLib_TagSet_Transpiler"); - // this method uses both overloads of RunMadLib, so it needs two transpiler passes - "RunMadLib".Transpile("_MadLib_TagSet_Transpiler"); - "RunMadLib".Transpile("_MadLib_string_Transpiler"); + "RunMadLibs".Transpile("MadLib_Transpile"); + "RunMadLibsOnLanceDef".Transpile("MadLib_Transpile"); + "RunMadLib".Transpile("MadLib_Transpile"); + "UpdateTMPText".Transpile("ToTMP_Transpile"); + // these finalizers could never run to begin with, and they only did RemoveSubscriber; nop them + "Finalize".Transpile("Nop_Transpile"); + "Finalize".Transpile("Nop_Transpile"); + "Finalize".Transpile("Nop_Transpile"); + "Finalize".Transpile("Nop_Transpile"); + "Finalize".Transpile("Nop_Transpile"); + // fix 1.4: when a savefile is created, two copies of each contract are created and stored; + // when that savefile is loaded, one set of copies overwrites the other, and both sets register subs. + // this patch unregisters subs for the about-to-be-overwritten contracts + "Rehydrate".Pre(); + // fix 1.5: TODO explain this + // FIXME unable to attach to a generic method with generic parameter (how???) + //var paramTypes = new Type[]{typeof(ContractOverride), typeof(string)}; + //var genericsTypes = new Type[]{typeof(ContractOverride)}; + //var meth = AccessTools.Method(typeof(JSONSerializationUtility), "FromJSON", paramTypes, genericsTypes); + //var patch = new HarmonyMethod(AccessTools.Method(self, "FromJSON_Post")); + // NOTE ideally we would patch FromJSON() for T : ContractOverride, + // but Harmony has trouble dealing with generic methods, so we patch this instead. + var paramTypes = new Type[]{typeof(object), typeof(Dictionary), typeof(string), typeof(HBS.Stopwatch), + typeof(HBS.Stopwatch), typeof(JSONSerializationUtility.RehydrationFilteringMode), + typeof(Func[])}; + var meth = AccessTools.Method(typeof(JSONSerializationUtility), "RehydrateObjectFromDictionary", paramTypes); + var patch = new HarmonyMethod(AccessTools.Method(self, "RehydrateObjectFromDictionary_Post")); + Main.harmony.Patch(meth, null, patch); + + // fixes group 2: occurs on entering/exiting a contract + // fix 2.2: when a contract completes, remove its OnLanguageChanged subs + "ResolveCompleteContract".Pre(); + + // fixes group 3: occurs on transiting between star systems + // fix 3.1: when a star system removes its contracts, remove those contracts' OnLanguageChanged subs + "ResetContracts".Pre(); + // fix 3.2: when traveling to a new star system, remove the current system's contracts' OnLanguageChanged subs + "StartGeneratePotentialContractsRoutine".Pre(); + + // fixes group 4: occurs on accepting & completing a travel contract + // fix 4.1: don't let the Contract constructor make a copy of its given ContractOverride, let the caller handle it + paramTypes = new Type[]{ typeof(string), typeof(string), typeof(string), typeof(ContractTypeValue), + typeof(GameInstance), typeof(ContractOverride), typeof(GameContext), + typeof(bool), typeof(int), typeof(int), typeof(int?)}; + var ctor = AccessTools.Constructor(typeof(Contract), paramTypes); + patch = new HarmonyMethod(AccessTools.Method(self, "Contract_Transpile")); + Main.harmony.Patch(ctor, null, null, patch); + "StartGeneratePotentialContractsRoutine".Transpile(); + } - private static IEnumerable _SessionTranspiler(IEnumerable ins) + private static IEnumerable Session_Transpile(IEnumerable ins) { var meth = AccessTools.Method(self, "_UpdateMessageSubscriptions"); return TranspileReplaceCall(ins, "UpdateMessageSubscriptions", meth); @@ -46,7 +92,7 @@ private static IEnumerable _SessionTranspiler(IEnumerable _MadLib_string_Transpiler(IEnumerable ins) - { - var meth = AccessTools.Method(self, "_Contract_RunMadLib", - new Type[]{typeof(Contract), typeof(string)}); - return TranspileReplaceOverloadedCall(ins, typeof(Contract), "RunMadLib", - new Type[]{typeof(string)}, meth); - } - - private static IEnumerable _MadLib_TagSet_Transpiler(IEnumerable ins) + private static IEnumerable MadLib_Transpile(IEnumerable ins) { - var meth = AccessTools.Method(self, "_Contract_RunMadLib", - new Type[]{typeof(Contract), typeof(TagSet)}); - return TranspileReplaceOverloadedCall(ins, typeof(Contract), "RunMadLib", - new Type[]{typeof(TagSet)}, meth); + var methString = AccessTools.Method(self, "_Contract_RunMadLib", + new Type[]{typeof(Contract), typeof(string)}); + var methTagSet = AccessTools.Method(self, "_Contract_RunMadLib", + new Type[]{typeof(Contract), typeof(TagSet)}); + var firstPass = TranspileReplaceOverloadedCall(ins, typeof(Contract), "RunMadLib", + new Type[]{typeof(string)}, methString); + return TranspileReplaceOverloadedCall(firstPass, typeof(Contract), "RunMadLib", + new Type[]{typeof(TagSet)}, methTagSet); } private static IEnumerable @@ -180,7 +228,6 @@ private static IEnumerable private static string _Contract_RunMadLib(Contract __instance, string text) { - LogSpam("_Contract_RunMadLib(string) called"); if (string.IsNullOrEmpty(text)) { return ""; @@ -194,7 +241,6 @@ private static string _Contract_RunMadLib(Contract __instance, string text) private static void _Contract_RunMadLib(Contract __instance, TagSet tagSet) { - LogSpam("_Contract_RunMadLib(tagSet) called"); if (tagSet == null) { return; @@ -216,5 +262,155 @@ private static void _Contract_RunMadLib(Contract __instance, TagSet tagSet) tagSet.Clear(); tagSet.AddRange(array); } + + private static IEnumerable ToTMP_Transpile(IEnumerable ins) + { + var originalTypes = new Type[]{typeof(GameContext), typeof(TextTooltipFormatOptions)}; + var replacementTypes = new Type[]{typeof(TextTooltipParser), typeof(GameContext), typeof(TextTooltipFormatOptions)}; + var meth = AccessTools.Method(self, "_TextTooltipParser_ToTMP", replacementTypes); + return TranspileReplaceOverloadedCall(ins, typeof(TextTooltipParser), "ToTMP", originalTypes, meth); + } + + private static Text + _TextTooltipParser_ToTMP(TextTooltipParser __instance, GameContext gameContext, TextTooltipFormatOptions formatOptions) + { + var mc = HBS.SceneSingletonBehavior.Instance.Game.MessageCenter; + return __instance.GenerateFinalString((TextTooltipData x) => { + InterpolatedText iText = x.ToTMP(gameContext, formatOptions); + mc.RemoveSubscriber(MessageCenterMessageType.OnLanguageChanged, + new ReceiveMessageCenterMessage(iText.OnLanguageChanged)); + return iText; + }); + } + + private static IEnumerable Nop_Transpile(IEnumerable ins) + { + return ins.SelectMany(i => { + i.opcode = OpCodes.Nop; + i.operand = null; + return Sequence(i); + }); + } + + private static void Rehydrate_Pre(StarSystem __instance, SimGameState sim) + { + if (__instance.activeSystemContracts != null) { + foreach (var contract in __instance.activeSystemContracts) { + RemoveContractSubscriptions(sim.MessageCenter, contract); + } + } + if (__instance.activeSystemBreadcrumbs != null) { + foreach (var contract in __instance.activeSystemBreadcrumbs) { + RemoveContractSubscriptions(sim.MessageCenter, contract); + } + } + } + + private static void RehydrateObjectFromDictionary_Post(object target) + { + if (target.GetType() != typeof(ContractOverride)) return; + //LogSpam($"{new StackTrace().GetFrame(0).GetMethod()} called with target type ContractOverride"); + var mc = HBS.SceneSingletonBehavior.Instance.Game.MessageCenter; + RemoveContractOverrideSubscriptions(mc, (ContractOverride) target); + } + + private static void ResolveCompleteContract_Pre(SimGameState __instance) + { + LogSpam($"{new StackTrace().GetFrame(0).GetMethod()} called"); + if (__instance.CompletedContract != null) { + RemoveContractSubscriptions(__instance.MessageCenter, __instance.CompletedContract); + } + } + + private static void ResetContracts_Pre(StarSystem __instance) + { + LogSpam($"{new StackTrace().GetFrame(0).GetMethod()} called"); + foreach (var contract in __instance.activeSystemContracts) { + RemoveContractSubscriptions(__instance.Sim.MessageCenter, contract); + } + foreach (var contract in __instance.activeSystemBreadcrumbs) { + RemoveContractSubscriptions(__instance.Sim.MessageCenter, contract); + } + } + + private static void + StartGeneratePotentialContractsRoutine_Pre(SimGameState __instance, bool clearExistingContracts, + StarSystem systemOverride) + { + LogSpam($"{new StackTrace().GetFrame(0).GetMethod()} called"); + if (clearExistingContracts) { + List contracts = (systemOverride != null) ? __instance.CurSystem.SystemBreadcrumbs : + __instance.CurSystem.SystemContracts; + LogSpam($"clearExistingContracts is set; removing subscriptions for {contracts.Count} contracts"); + foreach(var contract in contracts) { + RemoveContractSubscriptions(__instance.MessageCenter, contract); + } + } + } + + private static IEnumerable Contract_Transpile(IEnumerable ins) + { + LogInfo($"Contract_Transpile: nopping call to Copy()"); + var meth = AccessTools.Method(typeof(ContractOverride), "Copy"); + CodeInstruction toBeNopped = new CodeInstruction(OpCodes.Callvirt, meth); + return ins.SelectMany(i => { + if (i.opcode == toBeNopped.opcode && i.operand == toBeNopped.operand) { + + i.opcode = OpCodes.Nop; + i.operand = null; + } + return Sequence(i); + }); + } + + private static IEnumerable + StartGeneratePotentialContractsRoutine_Transpile(IEnumerable ins) + { + LogSpam($"{new StackTrace().GetFrame(0).GetMethod()} called"); + var types = new Type[]{typeof(SimGameState), typeof(StarSystem), typeof(bool), typeof(MapAndEncounters), + typeof(SimGameState.MapEncounterContractData), typeof(GameContext)}; + var meth = AccessTools.Method(self, "_CreateProceduralContract", types); + return TranspileReplaceCall(ins, "CreateProceduralContract", meth); + } + + private Contract + _CreateProceduralContract(SimGameState __instance, StarSystem system, bool usingBreadcrumbs, MapAndEncounters level, + SimGameState.MapEncounterContractData MapEncounterContractData, GameContext gameContext) + { + LogSpam($"{new StackTrace().GetFrame(0).GetMethod()} called"); + WeightedList flatContracts = MapEncounterContractData.FlatContracts; + __instance.FilterContracts(flatContracts); + SimGameState.PotentialContract next = flatContracts.GetNext(true); + int id = next.contractOverride.ContractTypeValue.ID; + MapEncounterContractData.Encounters[id].Shuffle(); + string encounterLayerGUID = MapEncounterContractData.Encounters[id][0].EncounterLayerGUID; + ContractOverride contractOverride = next.contractOverride; + FactionValue employer = next.employer; + FactionValue target = next.target; + FactionValue employerAlly = next.employerAlly; + FactionValue targetAlly = next.targetAlly; + FactionValue neutralToAll = next.NeutralToAll; + FactionValue hostileToAll = next.HostileToAll; + int difficulty = next.difficulty; + Contract contract; + if (usingBreadcrumbs) { + contract = __instance.CreateTravelContract(level.Map.MapName, level.Map.MapPath, encounterLayerGUID, + next.contractOverride.ContractTypeValue, contractOverride, + gameContext, employer, target, targetAlly, employerAlly, + neutralToAll, hostileToAll, false, difficulty); + } else { + LogSpam("copying contractOverride"); + contractOverride = contractOverride.Copy(); + contract = new Contract(level.Map.MapName, level.Map.MapPath, encounterLayerGUID, + next.contractOverride.ContractTypeValue, __instance.BattleTechGame, + contractOverride, gameContext, true, difficulty, 0, null); + } + __instance.mapDiscardPile.Add(level.Map.MapID); + __instance.contractDiscardPile.Add(contractOverride.ID); + __instance.PrepContract(contract, employer, employerAlly, target, targetAlly, neutralToAll, + hostileToAll, level.Map.BiomeSkinEntry.BiomeSkin, contract.Override.travelSeed, system); + return contract; + } } } +// vim: ts=4:sw=4 From 4791c14e233828604ef8704487b746435c1cba8f Mon Sep 17 00:00:00 2001 From: Cole Thompson Date: Fri, 12 Feb 2021 13:46:24 -0600 Subject: [PATCH 04/23] add more fixes for OnLanguageChanged subs including travel contract handling --- source/MemoryLeakFix.cs | 426 +++++++++++++++++++++++++++++++++++----- 1 file changed, 376 insertions(+), 50 deletions(-) diff --git a/source/MemoryLeakFix.cs b/source/MemoryLeakFix.cs index f6d6aaa..550461f 100644 --- a/source/MemoryLeakFix.cs +++ b/source/MemoryLeakFix.cs @@ -2,6 +2,7 @@ using System.Diagnostics; using System.Reflection; using System.Reflection.Emit; +using System.Collections; using System.Collections.Generic; using System.Linq; using Harmony; @@ -10,12 +11,17 @@ using BattleTech.Analytics.Sim; using BattleTech.Data; using BattleTech.Framework; +using BattleTech.Framework.Save; +using BattleTech.Save; +using BattleTech.Save.Test; using BattleTech.UI; using BattleTech.UI.Tooltips; using BattleTech.UI.TMProWrapper; using Localize; using HBS.Collections; +using HBS.FSM; using HBS.Util; +using UnityEngine; namespace BattletechPerformanceFix { @@ -46,42 +52,62 @@ public void Activate() { // fix 1.4: when a savefile is created, two copies of each contract are created and stored; // when that savefile is loaded, one set of copies overwrites the other, and both sets register subs. // this patch unregisters subs for the about-to-be-overwritten contracts - "Rehydrate".Pre(); - // fix 1.5: TODO explain this - // FIXME unable to attach to a generic method with generic parameter (how???) - //var paramTypes = new Type[]{typeof(ContractOverride), typeof(string)}; - //var genericsTypes = new Type[]{typeof(ContractOverride)}; - //var meth = AccessTools.Method(typeof(JSONSerializationUtility), "FromJSON", paramTypes, genericsTypes); - //var patch = new HarmonyMethod(AccessTools.Method(self, "FromJSON_Post")); - // NOTE ideally we would patch FromJSON() for T : ContractOverride, - // but Harmony has trouble dealing with generic methods, so we patch this instead. - var paramTypes = new Type[]{typeof(object), typeof(Dictionary), typeof(string), typeof(HBS.Stopwatch), - typeof(HBS.Stopwatch), typeof(JSONSerializationUtility.RehydrationFilteringMode), - typeof(Func[])}; - var meth = AccessTools.Method(typeof(JSONSerializationUtility), "RehydrateObjectFromDictionary", paramTypes); - var patch = new HarmonyMethod(AccessTools.Method(self, "RehydrateObjectFromDictionary_Post")); - Main.harmony.Patch(meth, null, patch); + "Rehydrate".Pre("SS_Rehydrate_Pre"); + // fix 1.5: when the ContractOverrides are read from JSON, subs are created, but these COs get Copy()ed + // before getting attached to a Contract, and Copy also creates subs. the first set of subs + // are never seen by the user, so modify the JSON deserialization process to immediately unsub + "FullRehydrate".Post("CO_JSON_Post"); + "FromJSON".Post("CO_JSON_Post"); + // fix 1.6: when loading a campaign save, contracts & subs are created for all previously completed + // story missions (why? dunno) and then overwritten later on (ugh...) with the globalContracts + // defined in the file. completed contract subs were created and must be removed + "Rehydrate".Pre("SGS_Rehydrate_Pre"); + "Rehydrate".Post("SGS_Rehydrate_Post"); + "Rehydrate".Transpile("SGS_Rehydrate_Transpile"); // fixes group 2: occurs on entering/exiting a contract - // fix 2.2: when a contract completes, remove its OnLanguageChanged subs + // fix 2.1: when a contract completes, remove its OnLanguageChanged subs "ResolveCompleteContract".Pre(); + // fix 2.2: none of these classes need to store a CombatGameState + "ContractInitialize".Post("DialogueContent_ContractInitialize_Post"); + "ContractInitialize".Post("ConversationContent_ContractInitialize_Post"); + "ContractInitialize".Post("DialogBucketDef_ContractInitialize_Post"); // fixes group 3: occurs on transiting between star systems // fix 3.1: when a star system removes its contracts, remove those contracts' OnLanguageChanged subs "ResetContracts".Pre(); - // fix 3.2: when traveling to a new star system, remove the current system's contracts' OnLanguageChanged subs - "StartGeneratePotentialContractsRoutine".Pre(); + // fix 3.2: see below // fixes group 4: occurs on accepting & completing a travel contract - // fix 4.1: don't let the Contract constructor make a copy of its given ContractOverride, let the caller handle it - paramTypes = new Type[]{ typeof(string), typeof(string), typeof(string), typeof(ContractTypeValue), + // fix 4.1: don't let the Contract constructor make a copy of its given ContractOverride, + // let the caller handle it... + var paramTypes = new Type[]{ typeof(string), typeof(string), typeof(string), typeof(ContractTypeValue), typeof(GameInstance), typeof(ContractOverride), typeof(GameContext), typeof(bool), typeof(int), typeof(int), typeof(int?)}; var ctor = AccessTools.Constructor(typeof(Contract), paramTypes); - patch = new HarmonyMethod(AccessTools.Method(self, "Contract_Transpile")); + var patch = new HarmonyMethod(AccessTools.Method(self, "Contract_Transpile")); Main.harmony.Patch(ctor, null, null, patch); - "StartGeneratePotentialContractsRoutine".Transpile(); - + // ...and perform a Copy() of the ObjectiveOverride in the one spot that needs it + // NOTE there's no clear way for Harmony to transpile an IEnumerator-returning method directly + // (something about having to patch *all* instances of IEnumerator.MoveNext() [lol no]), + // so instead just reimplement the method and patch its callers to use the reimplementation + "GeneratePotentialContracts".Transpile(); + + // fixes group 5: occurs when completing and/or cancelling a travel contract + // fix 5.1: when arriving at a travel contract's destination, + // remove the breadcrumb's ("pointer" to destination contract's) subs + "FinishCompleteBreadcrumbProcess".Pre(); + "FinishCompleteBreadcrumbProcess".Post(); + // fix 5.2.1: when backing out of a travel contract proper (ie not breadcrumb), remove its subs + "OnLanceConfigurationCancelled".Pre(); + // fix 5.2.2: same but for campaign mission + "CancelStoryOrConsecutiveLanceConfiguration".Pre(); + // fix 5.3: when cancelling a travel contract, remove its breadcrumb's subs + "FailBreadcrumb".Pre(); + + // fixes group 6: occurs on creating a new savefile + // fix 6.1: clean up the GameInstanceSave.references after serialization is complete + "PostSerialization".Post(); } private static IEnumerable Session_Transpile(IEnumerable ins) @@ -141,7 +167,6 @@ private static void RemoveContractSubscriptions(MessageCenter mc, Contract contr new ReceiveMessageCenterMessage(contract.OnLanguageChanged)); if (contract.Override != null) { - LogSpam(" contract.Override is set, removing its subs"); RemoveContractOverrideSubscriptions(mc, contract.Override); } } @@ -292,7 +317,7 @@ private static IEnumerable Nop_Transpile(IEnumerable.Instance.Game.MessageCenter; - RemoveContractOverrideSubscriptions(mc, (ContractOverride) target); + RemoveContractOverrideSubscriptions(mc, __instance); + } + + private static void SGS_Rehydrate_Pre(SimGameState __instance, ref List __state) + { + LogSpam($"{new StackTrace().GetFrame(0).GetMethod()} called"); + __state = __instance.globalContracts; + } + + private static void SGS_Rehydrate_Post(SimGameState __instance, ref List __state) + { + LogSpam($"{new StackTrace().GetFrame(0).GetMethod()} called"); + LogSpam($"{__state?.Count} contracts"); + foreach (var contract in __state) { + RemoveContractSubscriptions(__instance.MessageCenter, contract); + } + } + + private static IEnumerable SGS_Rehydrate_Transpile(IEnumerable ins) + { + LogSpam($"{new StackTrace().GetFrame(0).GetMethod()} called"); + var types = new Type[]{typeof(SimGameState), typeof(SimGameState.AddContractData)}; + var meth = AccessTools.Method(self, "_AddContract", types); + return TranspileReplaceCall(ins, "AddContract", meth); + } + + private static Contract _AddContract(SimGameState __instance, SimGameState.AddContractData contractData) + { + StarSystem starSystem; + if (!string.IsNullOrEmpty(contractData.TargetSystem)) + { + string validatedSystemString = __instance.GetValidatedSystemString(contractData.TargetSystem); + if (!__instance.starDict.ContainsKey(validatedSystemString)) + { + return null; + } + starSystem = __instance.starDict[validatedSystemString]; + } + else + { + starSystem = __instance.CurSystem; + } + FactionValue factionValueFromString = __instance.GetFactionValueFromString(contractData.Target); + FactionValue factionValueFromString2 = __instance.GetFactionValueFromString(contractData.Employer); + FactionValue factionValue = __instance.GetFactionValueFromString(contractData.TargetAlly); + FactionValue factionValue2 = __instance.GetFactionValueFromString(contractData.EmployerAlly); + FactionValue factionValueFromString3 = __instance.GetFactionValueFromString(contractData.NeutralToAll); + FactionValue factionValueFromString4 = __instance.GetFactionValueFromString(contractData.HostileToAll); + if (factionValueFromString.IsInvalidUnset || factionValueFromString2.IsInvalidUnset) + { + return null; + } + factionValue = (factionValue.IsInvalidUnset ? factionValueFromString : factionValue); + factionValue2 = (factionValue2.IsInvalidUnset ? factionValueFromString2 : factionValue2); + ContractOverride contractOverride = __instance.DataManager.ContractOverrides.Get(contractData.ContractName).Copy(); + RemoveContractOverrideSubscriptions(__instance.MessageCenter, contractOverride); + ContractTypeValue contractTypeValue = contractOverride.ContractTypeValue; + if (contractTypeValue.IsTravelOnly) + { + return __instance.AddTravelContract(contractOverride, starSystem, factionValueFromString2); + } + List releasedMapsAndEncountersByContractTypeAndOwnership = MetadataDatabase.Instance.GetReleasedMapsAndEncountersByContractTypeAndOwnership(contractTypeValue.ID, false); + if (releasedMapsAndEncountersByContractTypeAndOwnership == null || releasedMapsAndEncountersByContractTypeAndOwnership.Count == 0) + { + UnityEngine.Debug.LogError(string.Format("There are no playable maps for __instance contract type[{0}]. Was your map published?", contractTypeValue.Name)); + } + MapAndEncounters mapAndEncounters = releasedMapsAndEncountersByContractTypeAndOwnership[0]; + List list = new List(); + foreach (EncounterLayer_MDD encounterLayer_MDD in mapAndEncounters.Encounters) + { + if (encounterLayer_MDD.ContractTypeRow.ContractTypeID == (long)contractTypeValue.ID) + { + list.Add(encounterLayer_MDD); + } + } + if (list.Count <= 0) + { + throw new Exception("Map does not contain any encounters of type: " + contractTypeValue.Name); + } + string encounterLayerGUID = list[__instance.NetworkRandom.Int(0, list.Count)].EncounterLayerGUID; + GameContext gameContext = new GameContext(__instance.Context); + gameContext.SetObject(GameContextObjectTagEnum.TargetStarSystem, starSystem); + if (contractData.IsGlobal) + { + Contract contract = __instance.CreateTravelContract(mapAndEncounters.Map.MapName, mapAndEncounters.Map.MapPath, encounterLayerGUID, contractTypeValue, contractOverride, gameContext, factionValueFromString2, factionValueFromString, factionValue, factionValue2, factionValueFromString3, factionValueFromString4, contractData.IsGlobal, contractOverride.difficulty); + __instance.PrepContract(contract, factionValueFromString2, factionValue2, factionValueFromString, factionValue, factionValueFromString3, factionValueFromString4, mapAndEncounters.Map.BiomeSkinEntry.BiomeSkin, contract.Override.travelSeed, starSystem); + __instance.GlobalContracts.Add(contract); + return contract; + } + Contract contract2 = new Contract(mapAndEncounters.Map.MapName, mapAndEncounters.Map.MapPath, encounterLayerGUID, contractTypeValue, __instance.BattleTechGame, contractOverride, gameContext, true, contractOverride.difficulty, 0, null); + if (!contractData.FromSave) + { + ContractData contractData2 = new ContractData(contractData.ContractName, contractData.Target, contractData.Employer, contractData.TargetSystem, contractData.TargetAlly, contractData.EmployerAlly); + contractData2.SetGuid(Guid.NewGuid().ToString()); + contract2.SetGuid(contractData2.GUID); + __instance.contractBits.Add(contractData2); + } + if (contractData.FromSave) + { + contract2.SetGuid(contractData.SaveGuid); + } + __instance.PrepContract(contract2, factionValueFromString2, factionValue2, factionValueFromString, factionValue, factionValueFromString3, factionValueFromString4, mapAndEncounters.Map.BiomeSkinEntry.BiomeSkin, contract2.Override.travelSeed, starSystem); + starSystem.SystemContracts.Add(contract2); + return contract2; } private static void ResolveCompleteContract_Pre(SimGameState __instance) @@ -322,6 +449,24 @@ private static void ResolveCompleteContract_Pre(SimGameState __instance) } } + private static void DialogueContent_ContractInitialize_Post(DialogueContent __instance) + { + LogSpam($"{new StackTrace().GetFrame(0).GetMethod()} called"); + __instance.combat = null; + } + + private static void ConversationContent_ContractInitialize_Post(ConversationContent __instance) + { + LogSpam($"{new StackTrace().GetFrame(0).GetMethod()} called"); + __instance.combat = null; + } + + private static void DialogBucketDef_ContractInitialize_Post(DialogBucketDef __instance) + { + LogSpam($"{new StackTrace().GetFrame(0).GetMethod()} called"); + __instance.combat = null; + } + private static void ResetContracts_Pre(StarSystem __instance) { LogSpam($"{new StackTrace().GetFrame(0).GetMethod()} called"); @@ -333,21 +478,6 @@ private static void ResetContracts_Pre(StarSystem __instance) } } - private static void - StartGeneratePotentialContractsRoutine_Pre(SimGameState __instance, bool clearExistingContracts, - StarSystem systemOverride) - { - LogSpam($"{new StackTrace().GetFrame(0).GetMethod()} called"); - if (clearExistingContracts) { - List contracts = (systemOverride != null) ? __instance.CurSystem.SystemBreadcrumbs : - __instance.CurSystem.SystemContracts; - LogSpam($"clearExistingContracts is set; removing subscriptions for {contracts.Count} contracts"); - foreach(var contract in contracts) { - RemoveContractSubscriptions(__instance.MessageCenter, contract); - } - } - } - private static IEnumerable Contract_Transpile(IEnumerable ins) { LogInfo($"Contract_Transpile: nopping call to Copy()"); @@ -364,16 +494,113 @@ private static IEnumerable Contract_Transpile(IEnumerable - StartGeneratePotentialContractsRoutine_Transpile(IEnumerable ins) + GeneratePotentialContracts_Transpile(IEnumerable ins) + { + LogSpam($"{new StackTrace().GetFrame(0).GetMethod()} called"); + var types = new Type[]{typeof(SimGameState), typeof(bool), typeof(Action), typeof(StarSystem), typeof(bool)}; + var meth = AccessTools.Method(self, "_StartGeneratePotentialContractsRoutine", types); + return TranspileReplaceCall(ins, "StartGeneratePotentialContractsRoutine", meth); + } + + private static IEnumerator + _StartGeneratePotentialContractsRoutine(SimGameState __instance, bool clearExistingContracts, + Action onContractGenComplete, StarSystem systemOverride, bool useWait) { LogSpam($"{new StackTrace().GetFrame(0).GetMethod()} called"); - var types = new Type[]{typeof(SimGameState), typeof(StarSystem), typeof(bool), typeof(MapAndEncounters), - typeof(SimGameState.MapEncounterContractData), typeof(GameContext)}; - var meth = AccessTools.Method(self, "_CreateProceduralContract", types); - return TranspileReplaceCall(ins, "CreateProceduralContract", meth); + int debugCount = 0; + bool usingBreadcrumbs = systemOverride != null; + if (useWait) + { + yield return new WaitForSeconds(0.2f); + } + StarSystem system; + List contractList; + int maxContracts; + if (usingBreadcrumbs) + { + system = systemOverride; + contractList = __instance.CurSystem.SystemBreadcrumbs; + maxContracts = __instance.CurSystem.CurMaxBreadcrumbs; + } + else + { + system = __instance.CurSystem; + contractList = __instance.CurSystem.SystemContracts; + maxContracts = Mathf.CeilToInt(system.CurMaxContracts); + } + if (clearExistingContracts) + { + // fix 3.2: when traveling to a new star system, remove the current system's contracts' OnLanguageChanged subs + LogSpam($"clearExistingContracts is set; removing subscriptions for {contractList.Count} contractList"); + foreach(var contract in contractList) { + RemoveContractSubscriptions(__instance.MessageCenter, contract); + } + contractList.Clear(); + } + SimGameState.ContractDifficultyRange difficultyRange = __instance.GetContractRangeDifficultyRange(system, __instance.SimGameMode, __instance.GlobalDifficulty); + Dictionary> potentialContracts = __instance.GetSinglePlayerProceduralContractOverrides(difficultyRange); + WeightedList playableMaps = SimGameState.GetSinglePlayerProceduralPlayableMaps(system); + Dictionary> validParticipants = __instance.GetValidParticipants(system); + if (!__instance.HasValidMaps(system, playableMaps) || !__instance.HasValidContracts(difficultyRange, potentialContracts) || !__instance.HasValidParticipants(system, validParticipants)) + { + if (onContractGenComplete != null) + { + onContractGenComplete(); + } + yield break; + } + __instance.ClearUsedBiomeFromDiscardPile(playableMaps); + while (contractList.Count < maxContracts && debugCount < 1000) + { + int num = debugCount; + debugCount = num + 1; + IEnumerable source = from map in playableMaps + select map.Map.Weight; + WeightedList weightedList = new WeightedList(WeightedListType.WeightedRandom, playableMaps.ToList(), source.ToList(), 0); + __instance.FilterActiveMaps(weightedList, contractList); + weightedList.Reset(false); + MapAndEncounters next = weightedList.GetNext(false); + SimGameState.MapEncounterContractData mapEncounterContractData = __instance.FillMapEncounterContractData(system, difficultyRange, potentialContracts, validParticipants, next); + while (!mapEncounterContractData.HasContracts && weightedList.ActiveListCount > 0) + { + next = weightedList.GetNext(false); + mapEncounterContractData = __instance.FillMapEncounterContractData(system, difficultyRange, potentialContracts, validParticipants, next); + } + system.SetCurrentContractFactions(null, null); + if (mapEncounterContractData == null || mapEncounterContractData.Contracts.Count == 0) + { + if (__instance.mapDiscardPile.Count > 0) + { + __instance.mapDiscardPile.Clear(); + } + else + { + debugCount = 1000; + SimGameState.logger.LogError(string.Format("[CONTRACT] Unable to find any valid contracts for available map pool. Alert designers."/*, Array.Empty()*/)); + } + } + GameContext gameContext = new GameContext(__instance.Context); + gameContext.SetObject(GameContextObjectTagEnum.TargetStarSystem, system); + // see fix 4.1 + Contract item = _CreateProceduralContract(__instance, system, usingBreadcrumbs, next, mapEncounterContractData, gameContext); + contractList.Add(item); + if (useWait) + { + yield return new WaitForSeconds(0.2f); + } + } + if (debugCount >= 1000) + { + SimGameState.logger.LogError("Unable to fill contract list. Please inform AJ Immediately"); + } + if (onContractGenComplete != null) + { + onContractGenComplete(); + } + yield break; } - private Contract + private static Contract _CreateProceduralContract(SimGameState __instance, StarSystem system, bool usingBreadcrumbs, MapAndEncounters level, SimGameState.MapEncounterContractData MapEncounterContractData, GameContext gameContext) { @@ -399,6 +626,7 @@ private Contract gameContext, employer, target, targetAlly, employerAlly, neutralToAll, hostileToAll, false, difficulty); } else { + // see fix 4.1 LogSpam("copying contractOverride"); contractOverride = contractOverride.Copy(); contract = new Contract(level.Map.MapName, level.Map.MapPath, encounterLayerGUID, @@ -411,6 +639,104 @@ private Contract hostileToAll, level.Map.BiomeSkinEntry.BiomeSkin, contract.Override.travelSeed, system); return contract; } + + private static void FinishCompleteBreadcrumbProcess_Pre(SimGameState __instance, ref Contract __state) + { + LogSpam($"{new StackTrace().GetFrame(0).GetMethod()} called"); + if (__instance.activeBreadcrumb != null) { + LogSpam($"activeBreadcrumb: {__instance.activeBreadcrumb.GetHashCode()}"); + } + if (__instance.pendingBreadcrumb != null) { + LogSpam($"pendingBreadcrumb: {__instance.pendingBreadcrumb.GetHashCode()}"); + } + __state = __instance.activeBreadcrumb; + } + + private static void FinishCompleteBreadcrumbProcess_Post(SimGameState __instance, ref Contract __state) + { + LogSpam($"{new StackTrace().GetFrame(0).GetMethod()} called"); + if (__instance.pendingBreadcrumb == null && __state != null) { + LogSpam($"activeBreadcrumb and pendingBreadcrumb are both null, __state isn't null; contract was removed"); + RemoveContractSubscriptions(__instance.MessageCenter, __state); + } + } + + private static void OnLanceConfigurationCancelled_Pre(SimGameState __instance) + { + LogSpam($"{new StackTrace().GetFrame(0).GetMethod()} called"); + if (__instance.SelectedContract != null && !__instance.SelectedContract.IsPriorityContract && + __instance.pendingBreadcrumb != null && __instance.IsSelectedContractForced) { + if (__instance.CurSystem.SystemContracts.Contains(__instance.SelectedContract) || + __instance.GlobalContracts.Contains(__instance.SelectedContract)) { + RemoveContractSubscriptions(__instance.MessageCenter, __instance.SelectedContract); + } + } + } + + private static void CancelStoryOrConsecutiveLanceConfiguration_Pre(SimGameState __instance) + { + LogSpam($"{new StackTrace().GetFrame(0).GetMethod()} called"); + if (__instance.SelectedContract != null) { + LogSpam($"SelectedContract: {__instance.SelectedContract.GetHashCode()}"); + RemoveContractSubscriptions(__instance.MessageCenter, __instance.SelectedContract); + } + } + + private static void FailBreadcrumb_Pre(SimGameState __instance) + { + LogSpam($"{new StackTrace().GetFrame(0).GetMethod()} called"); + if (__instance.activeBreadcrumb != null) { + RemoveContractSubscriptions(__instance.MessageCenter, __instance.activeBreadcrumb); + } + } + + private static void ClearMessageMemory(MessageMemory msgMemory) + { + LogSpam($"{new StackTrace().GetFrame(0).GetMethod()} called"); + if (msgMemory.subscribedMessages != null) { + LogSpam($" {msgMemory.subscribedMessages.Count} subscribed "); + foreach (var key in msgMemory.subscribedMessages.Keys) { + foreach (var sub in msgMemory.subscribedMessages[key]) { + msgMemory.messageCenter.RemoveSubscriber(key, sub); + } + msgMemory.subscribedMessages[key].Clear(); + } + msgMemory.subscribedMessages.Clear(); + } + if (msgMemory.trackedMessages != null) { + LogSpam($" {msgMemory.trackedMessages.Count} tracked "); + foreach (var trackList in msgMemory.trackedMessages.Values) { + trackList.Clear(); + } + msgMemory.trackedMessages.Clear(); + } + } + + private static void ClearBasicMachine(BasicMachine machine) + { + LogSpam($"{new StackTrace().GetFrame(0).GetMethod()} called"); + machine.OnChange = null; + if (machine.stateList != null) { + LogSpam($" clearing state machine, {machine.stateList.Count} states"); + foreach (var state in machine.stateList) { + state.CanEnter = null; + state.OnEnter = null; + state.OnExit = null; + } + } + } + + // TODO test this more thoroughly, i think it caused problems in the past... + private static void PostSerialization_Post(GameInstanceSave __instance) + { + LogSpam($"{new StackTrace().GetFrame(0).GetMethod()} called"); + //LogSpam($"{__instance.serializePassOne.ToString()}"); + //if (!__instance.serializePassOne) { + // TODO test this on a combat save (might break in that case) + LogSpam("setting references to an empty SerializableReferenceContainer"); + __instance.references = new SerializableReferenceContainer("the one and only"); + //} + } } } // vim: ts=4:sw=4 From d9f97f171b1feea4aae5786ed647efad952ddae1 Mon Sep 17 00:00:00 2001 From: Cole Thompson Date: Mon, 15 Feb 2021 11:06:02 -0600 Subject: [PATCH 05/23] replace existing OnLanguageChanged fixes with a set of much simpler fixes (dont even create subs in the first place) --- source/MemoryLeakFix.cs | 677 ++++++---------------------------------- 1 file changed, 104 insertions(+), 573 deletions(-) diff --git a/source/MemoryLeakFix.cs b/source/MemoryLeakFix.cs index 550461f..2edf885 100644 --- a/source/MemoryLeakFix.cs +++ b/source/MemoryLeakFix.cs @@ -36,77 +36,52 @@ public void Activate() { "EndSession".Transpile("Session_Transpile"); // fix 1.2: add a RemoveSubscriber() for a message type that never had one to begin with "OnSimGameInitializeComplete".Post(); - // fix 1.3.1: clear InterpolatedText objects that aren't supposed to live forever - "Destroy".Post(); - // fix 1.3.2: patch methods making an InterpolatedText object and doesn't store it anywhere - "RunMadLibs".Transpile("MadLib_Transpile"); - "RunMadLibsOnLanceDef".Transpile("MadLib_Transpile"); - "RunMadLib".Transpile("MadLib_Transpile"); - "UpdateTMPText".Transpile("ToTMP_Transpile"); + // fix 1.3: remove OnLanguageChanged subscriptions for these objects, which never unsub and therefore leak. + // b/c the user must drop back to main menu to change the language, there's no reason + // to use these in the first place (objects are created in-game and never on the main menu) + // Contract + var contractCtorTypes = new Type[]{typeof(string), typeof(string), typeof(string), typeof(ContractTypeValue), + typeof(GameInstance), typeof(ContractOverride), typeof(GameContext), + typeof(bool), typeof(int), typeof(int), typeof(int)}; + Main.harmony.Patch(AccessTools.Constructor(typeof(Contract), contractCtorTypes), + null, null, new HarmonyMethod(self, "Contract_ctor_Transpile")); + "PostDeserialize".Transpile(); + // ContractObjectiveOverride + Main.harmony.Patch(AccessTools.Constructor(typeof(ContractObjectiveOverride), new Type[]{}), + null, null, new HarmonyMethod(self, "ContractObjectiveOverride_ctor_Transpile")); + var cooCtorTypes = new Type[]{typeof(ContractObjectiveGameLogic)}; + Main.harmony.Patch(AccessTools.Constructor(typeof(ContractObjectiveOverride), cooCtorTypes), + null, null, new HarmonyMethod(self, "ContractObjectiveOverride_ctor_cogl_Transpile")); + // ObjectiveOverride + Main.harmony.Patch(AccessTools.Constructor(typeof(ObjectiveOverride), new Type[]{}), + null, null, new HarmonyMethod(self, "ObjectiveOverride_ctor_Transpile")); + var ooCtorTypes = new Type[]{typeof(ObjectiveGameLogic)}; + Main.harmony.Patch(AccessTools.Constructor(typeof(ObjectiveOverride), ooCtorTypes), + null, null, new HarmonyMethod(self, "ObjectiveOverride_ctor_ogl_Transpile")); + // DialogueContentOverride + Main.harmony.Patch(AccessTools.Constructor(typeof(DialogueContentOverride), new Type[]{}), + null, null, new HarmonyMethod(self, "DialogueContentOverride_ctor_Transpile")); + var dcoCtorTypes = new Type[]{typeof(DialogueContent)}; + Main.harmony.Patch(AccessTools.Constructor(typeof(DialogueContentOverride), dcoCtorTypes), + null, null, new HarmonyMethod(self, "DialogueContentOverride_ctor_dc_Transpile")); + // InterpolatedText + "Init".Transpile(); // these finalizers could never run to begin with, and they only did RemoveSubscriber; nop them - "Finalize".Transpile("Nop_Transpile"); - "Finalize".Transpile("Nop_Transpile"); - "Finalize".Transpile("Nop_Transpile"); - "Finalize".Transpile("Nop_Transpile"); - "Finalize".Transpile("Nop_Transpile"); - // fix 1.4: when a savefile is created, two copies of each contract are created and stored; - // when that savefile is loaded, one set of copies overwrites the other, and both sets register subs. - // this patch unregisters subs for the about-to-be-overwritten contracts - "Rehydrate".Pre("SS_Rehydrate_Pre"); - // fix 1.5: when the ContractOverrides are read from JSON, subs are created, but these COs get Copy()ed - // before getting attached to a Contract, and Copy also creates subs. the first set of subs - // are never seen by the user, so modify the JSON deserialization process to immediately unsub - "FullRehydrate".Post("CO_JSON_Post"); - "FromJSON".Post("CO_JSON_Post"); - // fix 1.6: when loading a campaign save, contracts & subs are created for all previously completed - // story missions (why? dunno) and then overwritten later on (ugh...) with the globalContracts - // defined in the file. completed contract subs were created and must be removed - "Rehydrate".Pre("SGS_Rehydrate_Pre"); - "Rehydrate".Post("SGS_Rehydrate_Post"); - "Rehydrate".Transpile("SGS_Rehydrate_Transpile"); + // FIXME? may need to nop out specifically the call to RemoveSubscriber (test this) + "Finalize".Transpile("TranspileNopAll"); + "Finalize".Transpile("TranspileNopAll"); + "Finalize".Transpile("TranspileNopAll"); + "Finalize".Transpile("TranspileNopAll"); + "Finalize".Transpile("TranspileNopAll"); // fixes group 2: occurs on entering/exiting a contract - // fix 2.1: when a contract completes, remove its OnLanguageChanged subs - "ResolveCompleteContract".Pre(); - // fix 2.2: none of these classes need to store a CombatGameState + // fix 2.1: none of these classes need to store a CombatGameState "ContractInitialize".Post("DialogueContent_ContractInitialize_Post"); "ContractInitialize".Post("ConversationContent_ContractInitialize_Post"); "ContractInitialize".Post("DialogBucketDef_ContractInitialize_Post"); - // fixes group 3: occurs on transiting between star systems - // fix 3.1: when a star system removes its contracts, remove those contracts' OnLanguageChanged subs - "ResetContracts".Pre(); - // fix 3.2: see below - - // fixes group 4: occurs on accepting & completing a travel contract - // fix 4.1: don't let the Contract constructor make a copy of its given ContractOverride, - // let the caller handle it... - var paramTypes = new Type[]{ typeof(string), typeof(string), typeof(string), typeof(ContractTypeValue), - typeof(GameInstance), typeof(ContractOverride), typeof(GameContext), - typeof(bool), typeof(int), typeof(int), typeof(int?)}; - var ctor = AccessTools.Constructor(typeof(Contract), paramTypes); - var patch = new HarmonyMethod(AccessTools.Method(self, "Contract_Transpile")); - Main.harmony.Patch(ctor, null, null, patch); - // ...and perform a Copy() of the ObjectiveOverride in the one spot that needs it - // NOTE there's no clear way for Harmony to transpile an IEnumerator-returning method directly - // (something about having to patch *all* instances of IEnumerator.MoveNext() [lol no]), - // so instead just reimplement the method and patch its callers to use the reimplementation - "GeneratePotentialContracts".Transpile(); - - // fixes group 5: occurs when completing and/or cancelling a travel contract - // fix 5.1: when arriving at a travel contract's destination, - // remove the breadcrumb's ("pointer" to destination contract's) subs - "FinishCompleteBreadcrumbProcess".Pre(); - "FinishCompleteBreadcrumbProcess".Post(); - // fix 5.2.1: when backing out of a travel contract proper (ie not breadcrumb), remove its subs - "OnLanceConfigurationCancelled".Pre(); - // fix 5.2.2: same but for campaign mission - "CancelStoryOrConsecutiveLanceConfiguration".Pre(); - // fix 5.3: when cancelling a travel contract, remove its breadcrumb's subs - "FailBreadcrumb".Pre(); - - // fixes group 6: occurs on creating a new savefile - // fix 6.1: clean up the GameInstanceSave.references after serialization is complete + // fixes group 3: occurs on creating a new savefile + // fix 3.1: clean up the GameInstanceSave.references after serialization is complete "PostSerialization".Post(); } @@ -144,77 +119,87 @@ private static void OnSimGameInitializeComplete_Post(SimGameUXCreator __instance new ReceiveMessageCenterMessage(__instance.OnSimGameInitializeComplete)); } - private static void Destroy_Post(SimGameState __instance) + private static IEnumerable Contract_ctor_Transpile(IEnumerable ins) { LogSpam($"{new StackTrace().GetFrame(0).GetMethod()} called"); - if (__instance == null) { - LogSpam("SimGameState was null (ok if first load)"); - return; - } + return TranspileNopIndicesRange(ins, 125, 134); + } - var contracts = __instance.GetAllCurrentlySelectableContracts(); - LogSpam($"removing subscriptions for {contracts.Count} contracts"); - foreach (var contract in contracts) { - RemoveContractSubscriptions(__instance.MessageCenter, contract); - } + private static IEnumerable PostDeserialize_Transpile(IEnumerable ins) + { + LogSpam($"{new StackTrace().GetFrame(0).GetMethod()} called"); + return TranspileNopIndicesRange(ins, 21, 27); } - private static void RemoveContractSubscriptions(MessageCenter mc, Contract contract) + private static IEnumerable + ContractObjectiveOverride_ctor_Transpile(IEnumerable ins) { LogSpam($"{new StackTrace().GetFrame(0).GetMethod()} called"); - LogSpam($"removing subs for contract {contract.GetHashCode()}"); - mc.RemoveSubscriber(MessageCenterMessageType.OnLanguageChanged, - new ReceiveMessageCenterMessage(contract.OnLanguageChanged)); + return TranspileNopIndicesRange(ins, 5, 14); + } - if (contract.Override != null) { - RemoveContractOverrideSubscriptions(mc, contract.Override); - } + private static IEnumerable + ContractObjectiveOverride_ctor_cogl_Transpile(IEnumerable ins) + { + LogSpam($"{new StackTrace().GetFrame(0).GetMethod()} called"); + return TranspileNopIndicesRange(ins, 9, 18); } - private static void - RemoveContractOverrideSubscriptions(MessageCenter mc, ContractOverride contractOverride) + private static IEnumerable + ObjectiveOverride_ctor_Transpile(IEnumerable ins) { - LogSpam($"removing subs for contract override {contractOverride.GetHashCode()}"); - foreach (var dialogue in contractOverride.dialogueList) { - foreach (var content in dialogue.dialogueContent) { - RemoveTextSubscriber(mc, (InterpolatedText) content._Words); - mc.RemoveSubscriber(MessageCenterMessageType.OnLanguageChanged, - new ReceiveMessageCenterMessage(content.OnLanguageChanged)); - } - } + LogSpam($"{new StackTrace().GetFrame(0).GetMethod()} called"); + return TranspileNopIndicesRange(ins, 8, 17); + } - foreach (var objective in contractOverride.contractObjectiveList) { - RemoveTextSubscriber(mc, (InterpolatedText) objective._Title); - RemoveTextSubscriber(mc, (InterpolatedText) objective._Description); - mc.RemoveSubscriber(MessageCenterMessageType.OnLanguageChanged, - new ReceiveMessageCenterMessage(objective.OnLanguageChanged)); - } + private static IEnumerable + ObjectiveOverride_ctor_ogl_Transpile(IEnumerable ins) + { + LogSpam($"{new StackTrace().GetFrame(0).GetMethod()} called"); + return TranspileNopIndicesRange(ins, 12, 21); + } - foreach (var objective in contractOverride.objectiveList) { - RemoveTextSubscriber(mc, (InterpolatedText) objective._Title); - RemoveTextSubscriber(mc, (InterpolatedText) objective._Description); - mc.RemoveSubscriber(MessageCenterMessageType.OnLanguageChanged, - new ReceiveMessageCenterMessage(objective.OnLanguageChanged)); - } + private static IEnumerable + DialogueContentOverride_ctor_Transpile(IEnumerable ins) + { + LogSpam($"{new StackTrace().GetFrame(0).GetMethod()} called"); + return TranspileNopIndicesRange(ins, 23, 32); } - private static void RemoveTextSubscriber(MessageCenter mc, InterpolatedText text) { - if (text != null) { - mc.RemoveSubscriber(MessageCenterMessageType.OnLanguageChanged, - new ReceiveMessageCenterMessage(text.OnLanguageChanged)); - } + private static IEnumerable + DialogueContentOverride_ctor_dc_Transpile(IEnumerable ins) + { + LogSpam($"{new StackTrace().GetFrame(0).GetMethod()} called"); + return TranspileNopIndicesRange(ins, 60, 69); + } + + private static IEnumerable Init_Transpile(IEnumerable ins) + { + LogSpam($"{new StackTrace().GetFrame(0).GetMethod()} called"); + return TranspileNopIndicesRange(ins, 3, 10); } - private static IEnumerable MadLib_Transpile(IEnumerable ins) + private static IEnumerable + TranspileNopIndicesRange(IEnumerable ins, int startIndex, int endIndex) { - var methString = AccessTools.Method(self, "_Contract_RunMadLib", - new Type[]{typeof(Contract), typeof(string)}); - var methTagSet = AccessTools.Method(self, "_Contract_RunMadLib", - new Type[]{typeof(Contract), typeof(TagSet)}); - var firstPass = TranspileReplaceOverloadedCall(ins, typeof(Contract), "RunMadLib", - new Type[]{typeof(string)}, methString); - return TranspileReplaceOverloadedCall(firstPass, typeof(Contract), "RunMadLib", - new Type[]{typeof(TagSet)}, methTagSet); + LogDebug($"TranspileNopIndicesRange: nopping indices {startIndex}-{endIndex}"); + if (endIndex < startIndex || startIndex < 0) { + LogError($"TranspileNopIndicesRange: invalid use with startIndex = {startIndex}," + + $" endIndex = {endIndex} (transpiled method remains unmodified)"); + return ins; + } + + var code = ins.ToList(); + try { + for (int i = startIndex; i <= endIndex; i++) { + code[i].opcode = OpCodes.Nop; + code[i].operand = null; + } + return code.AsEnumerable(); + } catch (ArgumentOutOfRangeException ex) { + LogError($"TranspileNopIndicesRange: {ex.Message} (transpiled method remains unmodified)"); + return ins; + } } private static IEnumerable @@ -251,64 +236,7 @@ private static IEnumerable }); } - private static string _Contract_RunMadLib(Contract __instance, string text) - { - if (string.IsNullOrEmpty(text)) - { - return ""; - } - InterpolatedText iText = __instance.Interpolate(text); - text = iText.ToString(false); - __instance.messageCenter.RemoveSubscriber(MessageCenterMessageType.OnLanguageChanged, - new ReceiveMessageCenterMessage(iText.OnLanguageChanged)); - return text; - } - - private static void _Contract_RunMadLib(Contract __instance, TagSet tagSet) - { - if (tagSet == null) - { - return; - } - string[] array = tagSet.ToArray(); - if (array == null) - { - return; - } - for (int i = 0; i < array.Length; i++) - { - string text = array[i]; - InterpolatedText iText = __instance.Interpolate(text); - text = iText.ToString(false); - __instance.messageCenter.RemoveSubscriber(MessageCenterMessageType.OnLanguageChanged, - new ReceiveMessageCenterMessage(iText.OnLanguageChanged)); - array[i] = text.ToLower(); - } - tagSet.Clear(); - tagSet.AddRange(array); - } - - private static IEnumerable ToTMP_Transpile(IEnumerable ins) - { - var originalTypes = new Type[]{typeof(GameContext), typeof(TextTooltipFormatOptions)}; - var replacementTypes = new Type[]{typeof(TextTooltipParser), typeof(GameContext), typeof(TextTooltipFormatOptions)}; - var meth = AccessTools.Method(self, "_TextTooltipParser_ToTMP", replacementTypes); - return TranspileReplaceOverloadedCall(ins, typeof(TextTooltipParser), "ToTMP", originalTypes, meth); - } - - private static Text - _TextTooltipParser_ToTMP(TextTooltipParser __instance, GameContext gameContext, TextTooltipFormatOptions formatOptions) - { - var mc = HBS.SceneSingletonBehavior.Instance.Game.MessageCenter; - return __instance.GenerateFinalString((TextTooltipData x) => { - InterpolatedText iText = x.ToTMP(gameContext, formatOptions); - mc.RemoveSubscriber(MessageCenterMessageType.OnLanguageChanged, - new ReceiveMessageCenterMessage(iText.OnLanguageChanged)); - return iText; - }); - } - - private static IEnumerable Nop_Transpile(IEnumerable ins) + private static IEnumerable TranspileNopAll(IEnumerable ins) { return ins.SelectMany(i => { i.opcode = OpCodes.Nop; @@ -317,138 +245,6 @@ private static IEnumerable Nop_Transpile(IEnumerable.Instance.Game.MessageCenter; - RemoveContractOverrideSubscriptions(mc, __instance); - } - - private static void SGS_Rehydrate_Pre(SimGameState __instance, ref List __state) - { - LogSpam($"{new StackTrace().GetFrame(0).GetMethod()} called"); - __state = __instance.globalContracts; - } - - private static void SGS_Rehydrate_Post(SimGameState __instance, ref List __state) - { - LogSpam($"{new StackTrace().GetFrame(0).GetMethod()} called"); - LogSpam($"{__state?.Count} contracts"); - foreach (var contract in __state) { - RemoveContractSubscriptions(__instance.MessageCenter, contract); - } - } - - private static IEnumerable SGS_Rehydrate_Transpile(IEnumerable ins) - { - LogSpam($"{new StackTrace().GetFrame(0).GetMethod()} called"); - var types = new Type[]{typeof(SimGameState), typeof(SimGameState.AddContractData)}; - var meth = AccessTools.Method(self, "_AddContract", types); - return TranspileReplaceCall(ins, "AddContract", meth); - } - - private static Contract _AddContract(SimGameState __instance, SimGameState.AddContractData contractData) - { - StarSystem starSystem; - if (!string.IsNullOrEmpty(contractData.TargetSystem)) - { - string validatedSystemString = __instance.GetValidatedSystemString(contractData.TargetSystem); - if (!__instance.starDict.ContainsKey(validatedSystemString)) - { - return null; - } - starSystem = __instance.starDict[validatedSystemString]; - } - else - { - starSystem = __instance.CurSystem; - } - FactionValue factionValueFromString = __instance.GetFactionValueFromString(contractData.Target); - FactionValue factionValueFromString2 = __instance.GetFactionValueFromString(contractData.Employer); - FactionValue factionValue = __instance.GetFactionValueFromString(contractData.TargetAlly); - FactionValue factionValue2 = __instance.GetFactionValueFromString(contractData.EmployerAlly); - FactionValue factionValueFromString3 = __instance.GetFactionValueFromString(contractData.NeutralToAll); - FactionValue factionValueFromString4 = __instance.GetFactionValueFromString(contractData.HostileToAll); - if (factionValueFromString.IsInvalidUnset || factionValueFromString2.IsInvalidUnset) - { - return null; - } - factionValue = (factionValue.IsInvalidUnset ? factionValueFromString : factionValue); - factionValue2 = (factionValue2.IsInvalidUnset ? factionValueFromString2 : factionValue2); - ContractOverride contractOverride = __instance.DataManager.ContractOverrides.Get(contractData.ContractName).Copy(); - RemoveContractOverrideSubscriptions(__instance.MessageCenter, contractOverride); - ContractTypeValue contractTypeValue = contractOverride.ContractTypeValue; - if (contractTypeValue.IsTravelOnly) - { - return __instance.AddTravelContract(contractOverride, starSystem, factionValueFromString2); - } - List releasedMapsAndEncountersByContractTypeAndOwnership = MetadataDatabase.Instance.GetReleasedMapsAndEncountersByContractTypeAndOwnership(contractTypeValue.ID, false); - if (releasedMapsAndEncountersByContractTypeAndOwnership == null || releasedMapsAndEncountersByContractTypeAndOwnership.Count == 0) - { - UnityEngine.Debug.LogError(string.Format("There are no playable maps for __instance contract type[{0}]. Was your map published?", contractTypeValue.Name)); - } - MapAndEncounters mapAndEncounters = releasedMapsAndEncountersByContractTypeAndOwnership[0]; - List list = new List(); - foreach (EncounterLayer_MDD encounterLayer_MDD in mapAndEncounters.Encounters) - { - if (encounterLayer_MDD.ContractTypeRow.ContractTypeID == (long)contractTypeValue.ID) - { - list.Add(encounterLayer_MDD); - } - } - if (list.Count <= 0) - { - throw new Exception("Map does not contain any encounters of type: " + contractTypeValue.Name); - } - string encounterLayerGUID = list[__instance.NetworkRandom.Int(0, list.Count)].EncounterLayerGUID; - GameContext gameContext = new GameContext(__instance.Context); - gameContext.SetObject(GameContextObjectTagEnum.TargetStarSystem, starSystem); - if (contractData.IsGlobal) - { - Contract contract = __instance.CreateTravelContract(mapAndEncounters.Map.MapName, mapAndEncounters.Map.MapPath, encounterLayerGUID, contractTypeValue, contractOverride, gameContext, factionValueFromString2, factionValueFromString, factionValue, factionValue2, factionValueFromString3, factionValueFromString4, contractData.IsGlobal, contractOverride.difficulty); - __instance.PrepContract(contract, factionValueFromString2, factionValue2, factionValueFromString, factionValue, factionValueFromString3, factionValueFromString4, mapAndEncounters.Map.BiomeSkinEntry.BiomeSkin, contract.Override.travelSeed, starSystem); - __instance.GlobalContracts.Add(contract); - return contract; - } - Contract contract2 = new Contract(mapAndEncounters.Map.MapName, mapAndEncounters.Map.MapPath, encounterLayerGUID, contractTypeValue, __instance.BattleTechGame, contractOverride, gameContext, true, contractOverride.difficulty, 0, null); - if (!contractData.FromSave) - { - ContractData contractData2 = new ContractData(contractData.ContractName, contractData.Target, contractData.Employer, contractData.TargetSystem, contractData.TargetAlly, contractData.EmployerAlly); - contractData2.SetGuid(Guid.NewGuid().ToString()); - contract2.SetGuid(contractData2.GUID); - __instance.contractBits.Add(contractData2); - } - if (contractData.FromSave) - { - contract2.SetGuid(contractData.SaveGuid); - } - __instance.PrepContract(contract2, factionValueFromString2, factionValue2, factionValueFromString, factionValue, factionValueFromString3, factionValueFromString4, mapAndEncounters.Map.BiomeSkinEntry.BiomeSkin, contract2.Override.travelSeed, starSystem); - starSystem.SystemContracts.Add(contract2); - return contract2; - } - - private static void ResolveCompleteContract_Pre(SimGameState __instance) - { - LogSpam($"{new StackTrace().GetFrame(0).GetMethod()} called"); - if (__instance.CompletedContract != null) { - RemoveContractSubscriptions(__instance.MessageCenter, __instance.CompletedContract); - } - } - private static void DialogueContent_ContractInitialize_Post(DialogueContent __instance) { LogSpam($"{new StackTrace().GetFrame(0).GetMethod()} called"); @@ -467,275 +263,10 @@ private static void DialogBucketDef_ContractInitialize_Post(DialogBucketDef __in __instance.combat = null; } - private static void ResetContracts_Pre(StarSystem __instance) - { - LogSpam($"{new StackTrace().GetFrame(0).GetMethod()} called"); - foreach (var contract in __instance.activeSystemContracts) { - RemoveContractSubscriptions(__instance.Sim.MessageCenter, contract); - } - foreach (var contract in __instance.activeSystemBreadcrumbs) { - RemoveContractSubscriptions(__instance.Sim.MessageCenter, contract); - } - } - - private static IEnumerable Contract_Transpile(IEnumerable ins) - { - LogInfo($"Contract_Transpile: nopping call to Copy()"); - var meth = AccessTools.Method(typeof(ContractOverride), "Copy"); - CodeInstruction toBeNopped = new CodeInstruction(OpCodes.Callvirt, meth); - return ins.SelectMany(i => { - if (i.opcode == toBeNopped.opcode && i.operand == toBeNopped.operand) { - - i.opcode = OpCodes.Nop; - i.operand = null; - } - return Sequence(i); - }); - } - - private static IEnumerable - GeneratePotentialContracts_Transpile(IEnumerable ins) - { - LogSpam($"{new StackTrace().GetFrame(0).GetMethod()} called"); - var types = new Type[]{typeof(SimGameState), typeof(bool), typeof(Action), typeof(StarSystem), typeof(bool)}; - var meth = AccessTools.Method(self, "_StartGeneratePotentialContractsRoutine", types); - return TranspileReplaceCall(ins, "StartGeneratePotentialContractsRoutine", meth); - } - - private static IEnumerator - _StartGeneratePotentialContractsRoutine(SimGameState __instance, bool clearExistingContracts, - Action onContractGenComplete, StarSystem systemOverride, bool useWait) - { - LogSpam($"{new StackTrace().GetFrame(0).GetMethod()} called"); - int debugCount = 0; - bool usingBreadcrumbs = systemOverride != null; - if (useWait) - { - yield return new WaitForSeconds(0.2f); - } - StarSystem system; - List contractList; - int maxContracts; - if (usingBreadcrumbs) - { - system = systemOverride; - contractList = __instance.CurSystem.SystemBreadcrumbs; - maxContracts = __instance.CurSystem.CurMaxBreadcrumbs; - } - else - { - system = __instance.CurSystem; - contractList = __instance.CurSystem.SystemContracts; - maxContracts = Mathf.CeilToInt(system.CurMaxContracts); - } - if (clearExistingContracts) - { - // fix 3.2: when traveling to a new star system, remove the current system's contracts' OnLanguageChanged subs - LogSpam($"clearExistingContracts is set; removing subscriptions for {contractList.Count} contractList"); - foreach(var contract in contractList) { - RemoveContractSubscriptions(__instance.MessageCenter, contract); - } - contractList.Clear(); - } - SimGameState.ContractDifficultyRange difficultyRange = __instance.GetContractRangeDifficultyRange(system, __instance.SimGameMode, __instance.GlobalDifficulty); - Dictionary> potentialContracts = __instance.GetSinglePlayerProceduralContractOverrides(difficultyRange); - WeightedList playableMaps = SimGameState.GetSinglePlayerProceduralPlayableMaps(system); - Dictionary> validParticipants = __instance.GetValidParticipants(system); - if (!__instance.HasValidMaps(system, playableMaps) || !__instance.HasValidContracts(difficultyRange, potentialContracts) || !__instance.HasValidParticipants(system, validParticipants)) - { - if (onContractGenComplete != null) - { - onContractGenComplete(); - } - yield break; - } - __instance.ClearUsedBiomeFromDiscardPile(playableMaps); - while (contractList.Count < maxContracts && debugCount < 1000) - { - int num = debugCount; - debugCount = num + 1; - IEnumerable source = from map in playableMaps - select map.Map.Weight; - WeightedList weightedList = new WeightedList(WeightedListType.WeightedRandom, playableMaps.ToList(), source.ToList(), 0); - __instance.FilterActiveMaps(weightedList, contractList); - weightedList.Reset(false); - MapAndEncounters next = weightedList.GetNext(false); - SimGameState.MapEncounterContractData mapEncounterContractData = __instance.FillMapEncounterContractData(system, difficultyRange, potentialContracts, validParticipants, next); - while (!mapEncounterContractData.HasContracts && weightedList.ActiveListCount > 0) - { - next = weightedList.GetNext(false); - mapEncounterContractData = __instance.FillMapEncounterContractData(system, difficultyRange, potentialContracts, validParticipants, next); - } - system.SetCurrentContractFactions(null, null); - if (mapEncounterContractData == null || mapEncounterContractData.Contracts.Count == 0) - { - if (__instance.mapDiscardPile.Count > 0) - { - __instance.mapDiscardPile.Clear(); - } - else - { - debugCount = 1000; - SimGameState.logger.LogError(string.Format("[CONTRACT] Unable to find any valid contracts for available map pool. Alert designers."/*, Array.Empty()*/)); - } - } - GameContext gameContext = new GameContext(__instance.Context); - gameContext.SetObject(GameContextObjectTagEnum.TargetStarSystem, system); - // see fix 4.1 - Contract item = _CreateProceduralContract(__instance, system, usingBreadcrumbs, next, mapEncounterContractData, gameContext); - contractList.Add(item); - if (useWait) - { - yield return new WaitForSeconds(0.2f); - } - } - if (debugCount >= 1000) - { - SimGameState.logger.LogError("Unable to fill contract list. Please inform AJ Immediately"); - } - if (onContractGenComplete != null) - { - onContractGenComplete(); - } - yield break; - } - - private static Contract - _CreateProceduralContract(SimGameState __instance, StarSystem system, bool usingBreadcrumbs, MapAndEncounters level, - SimGameState.MapEncounterContractData MapEncounterContractData, GameContext gameContext) - { - LogSpam($"{new StackTrace().GetFrame(0).GetMethod()} called"); - WeightedList flatContracts = MapEncounterContractData.FlatContracts; - __instance.FilterContracts(flatContracts); - SimGameState.PotentialContract next = flatContracts.GetNext(true); - int id = next.contractOverride.ContractTypeValue.ID; - MapEncounterContractData.Encounters[id].Shuffle(); - string encounterLayerGUID = MapEncounterContractData.Encounters[id][0].EncounterLayerGUID; - ContractOverride contractOverride = next.contractOverride; - FactionValue employer = next.employer; - FactionValue target = next.target; - FactionValue employerAlly = next.employerAlly; - FactionValue targetAlly = next.targetAlly; - FactionValue neutralToAll = next.NeutralToAll; - FactionValue hostileToAll = next.HostileToAll; - int difficulty = next.difficulty; - Contract contract; - if (usingBreadcrumbs) { - contract = __instance.CreateTravelContract(level.Map.MapName, level.Map.MapPath, encounterLayerGUID, - next.contractOverride.ContractTypeValue, contractOverride, - gameContext, employer, target, targetAlly, employerAlly, - neutralToAll, hostileToAll, false, difficulty); - } else { - // see fix 4.1 - LogSpam("copying contractOverride"); - contractOverride = contractOverride.Copy(); - contract = new Contract(level.Map.MapName, level.Map.MapPath, encounterLayerGUID, - next.contractOverride.ContractTypeValue, __instance.BattleTechGame, - contractOverride, gameContext, true, difficulty, 0, null); - } - __instance.mapDiscardPile.Add(level.Map.MapID); - __instance.contractDiscardPile.Add(contractOverride.ID); - __instance.PrepContract(contract, employer, employerAlly, target, targetAlly, neutralToAll, - hostileToAll, level.Map.BiomeSkinEntry.BiomeSkin, contract.Override.travelSeed, system); - return contract; - } - - private static void FinishCompleteBreadcrumbProcess_Pre(SimGameState __instance, ref Contract __state) - { - LogSpam($"{new StackTrace().GetFrame(0).GetMethod()} called"); - if (__instance.activeBreadcrumb != null) { - LogSpam($"activeBreadcrumb: {__instance.activeBreadcrumb.GetHashCode()}"); - } - if (__instance.pendingBreadcrumb != null) { - LogSpam($"pendingBreadcrumb: {__instance.pendingBreadcrumb.GetHashCode()}"); - } - __state = __instance.activeBreadcrumb; - } - - private static void FinishCompleteBreadcrumbProcess_Post(SimGameState __instance, ref Contract __state) - { - LogSpam($"{new StackTrace().GetFrame(0).GetMethod()} called"); - if (__instance.pendingBreadcrumb == null && __state != null) { - LogSpam($"activeBreadcrumb and pendingBreadcrumb are both null, __state isn't null; contract was removed"); - RemoveContractSubscriptions(__instance.MessageCenter, __state); - } - } - - private static void OnLanceConfigurationCancelled_Pre(SimGameState __instance) - { - LogSpam($"{new StackTrace().GetFrame(0).GetMethod()} called"); - if (__instance.SelectedContract != null && !__instance.SelectedContract.IsPriorityContract && - __instance.pendingBreadcrumb != null && __instance.IsSelectedContractForced) { - if (__instance.CurSystem.SystemContracts.Contains(__instance.SelectedContract) || - __instance.GlobalContracts.Contains(__instance.SelectedContract)) { - RemoveContractSubscriptions(__instance.MessageCenter, __instance.SelectedContract); - } - } - } - - private static void CancelStoryOrConsecutiveLanceConfiguration_Pre(SimGameState __instance) - { - LogSpam($"{new StackTrace().GetFrame(0).GetMethod()} called"); - if (__instance.SelectedContract != null) { - LogSpam($"SelectedContract: {__instance.SelectedContract.GetHashCode()}"); - RemoveContractSubscriptions(__instance.MessageCenter, __instance.SelectedContract); - } - } - - private static void FailBreadcrumb_Pre(SimGameState __instance) - { - LogSpam($"{new StackTrace().GetFrame(0).GetMethod()} called"); - if (__instance.activeBreadcrumb != null) { - RemoveContractSubscriptions(__instance.MessageCenter, __instance.activeBreadcrumb); - } - } - - private static void ClearMessageMemory(MessageMemory msgMemory) - { - LogSpam($"{new StackTrace().GetFrame(0).GetMethod()} called"); - if (msgMemory.subscribedMessages != null) { - LogSpam($" {msgMemory.subscribedMessages.Count} subscribed "); - foreach (var key in msgMemory.subscribedMessages.Keys) { - foreach (var sub in msgMemory.subscribedMessages[key]) { - msgMemory.messageCenter.RemoveSubscriber(key, sub); - } - msgMemory.subscribedMessages[key].Clear(); - } - msgMemory.subscribedMessages.Clear(); - } - if (msgMemory.trackedMessages != null) { - LogSpam($" {msgMemory.trackedMessages.Count} tracked "); - foreach (var trackList in msgMemory.trackedMessages.Values) { - trackList.Clear(); - } - msgMemory.trackedMessages.Clear(); - } - } - - private static void ClearBasicMachine(BasicMachine machine) - { - LogSpam($"{new StackTrace().GetFrame(0).GetMethod()} called"); - machine.OnChange = null; - if (machine.stateList != null) { - LogSpam($" clearing state machine, {machine.stateList.Count} states"); - foreach (var state in machine.stateList) { - state.CanEnter = null; - state.OnEnter = null; - state.OnExit = null; - } - } - } - - // TODO test this more thoroughly, i think it caused problems in the past... private static void PostSerialization_Post(GameInstanceSave __instance) { LogSpam($"{new StackTrace().GetFrame(0).GetMethod()} called"); - //LogSpam($"{__instance.serializePassOne.ToString()}"); - //if (!__instance.serializePassOne) { - // TODO test this on a combat save (might break in that case) - LogSpam("setting references to an empty SerializableReferenceContainer"); - __instance.references = new SerializableReferenceContainer("the one and only"); - //} + __instance.references = new SerializableReferenceContainer("the one and only"); } } } From d4d9971bcc2ca8a3cba8a3543c599fc4c52e8001 Mon Sep 17 00:00:00 2001 From: Cole Thompson Date: Mon, 15 Feb 2021 11:33:26 -0600 Subject: [PATCH 06/23] cleanup --- source/MemoryLeakFix.cs | 8 -------- 1 file changed, 8 deletions(-) diff --git a/source/MemoryLeakFix.cs b/source/MemoryLeakFix.cs index 2edf885..c132066 100644 --- a/source/MemoryLeakFix.cs +++ b/source/MemoryLeakFix.cs @@ -9,19 +9,11 @@ using static BattletechPerformanceFix.Extensions; using BattleTech; using BattleTech.Analytics.Sim; -using BattleTech.Data; using BattleTech.Framework; -using BattleTech.Framework.Save; using BattleTech.Save; using BattleTech.Save.Test; using BattleTech.UI; -using BattleTech.UI.Tooltips; -using BattleTech.UI.TMProWrapper; using Localize; -using HBS.Collections; -using HBS.FSM; -using HBS.Util; -using UnityEngine; namespace BattletechPerformanceFix { From bdf1c306ac956351ba71e3815c4da7db8dc8facf Mon Sep 17 00:00:00 2001 From: Cole Thompson Date: Wed, 16 Dec 2020 12:46:25 -0600 Subject: [PATCH 07/23] Ignore vim .swp files --- .gitignore | 1 + 1 file changed, 1 insertion(+) diff --git a/.gitignore b/.gitignore index aa4950f..4c47a94 100644 --- a/.gitignore +++ b/.gitignore @@ -267,3 +267,4 @@ __pycache__/ libs/ libs.fixed/ +*/*.swp From 39ec6ea6549a456a2ef1ee0e71f5cb1fa9900fca Mon Sep 17 00:00:00 2001 From: Cole Thompson Date: Wed, 16 Dec 2020 13:44:57 -0600 Subject: [PATCH 08/23] Add NavigationMapFilterLagFix, which saves a few seconds in modpacks with large maps --- source/BattletechPerformanceFix.csproj | 1 + source/Main.cs | 1 + source/NavigationMapFilterLagFix.cs | 63 ++++++++++++++++++++++++++ 3 files changed, 65 insertions(+) create mode 100644 source/NavigationMapFilterLagFix.cs diff --git a/source/BattletechPerformanceFix.csproj b/source/BattletechPerformanceFix.csproj index ea5767a..49c9503 100644 --- a/source/BattletechPerformanceFix.csproj +++ b/source/BattletechPerformanceFix.csproj @@ -53,6 +53,7 @@ + diff --git a/source/Main.cs b/source/Main.cs index 8d2c582..8f97fb2 100644 --- a/source/Main.cs +++ b/source/Main.cs @@ -91,6 +91,7 @@ public static void Start(string modDirectory, string json) { typeof(DataLoaderGetEntryCheck), true }, { typeof(ShopTabLagFix), true }, { typeof(ContractLagFix), true }, + { typeof(NavigationMapFilterLagFix), true }, { typeof(EnableLoggingDuringLoads), true }, { typeof(ExtraLogging), true }, { typeof(ShaderDependencyOverride), true }, diff --git a/source/NavigationMapFilterLagFix.cs b/source/NavigationMapFilterLagFix.cs new file mode 100644 index 0000000..61c0c32 --- /dev/null +++ b/source/NavigationMapFilterLagFix.cs @@ -0,0 +1,63 @@ +using System.Collections.Generic; +using System.Linq; +using BattleTech.UI; +using Harmony; +using System.Reflection.Emit; +using HBS; +using static BattletechPerformanceFix.Extensions; + +namespace BattletechPerformanceFix +{ + /* Removes a redundant call for creating difficulty callouts, saves a few seconds per filter selection */ + class NavigationMapFilterLagFix : Feature + { + public void Activate() + { + var method = AccessTools.Method(typeof(SGNavigationScreen), "OnDifficultySelectionChanged"); + var transpiler = new HarmonyMethod(typeof(NavigationMapFilterLagFix), nameof(Transpiler)); + Main.harmony.Patch(method, null, null, transpiler); + + var cdc = nameof(SGNavigationScreen.CreateDifficultyCallouts); + cdc.Pre(); + cdc.Post(); + var rsi = nameof(SGNavigationScreen.RefreshSystemIndicators); + rsi.Pre(); + rsi.Post(); + } + + // FIXME remove + // TODO write a wrapper to make these quickly & easily + public static void CreateDifficultyCallouts_Pre(ref Stopwatch __state) + { __state = new Stopwatch(); __state.Start(); } + public static void CreateDifficultyCallouts_Post(ref Stopwatch __state) + { __state.Stop(); LogDebug("[PROFILE] " + __state.Elapsed + " CreateDifficultyCallouts"); } + public static void RefreshSystemIndicators_Pre(ref Stopwatch __state) + { __state = new Stopwatch(); __state.Start(); } + public static void RefreshSystemIndicators_Post(ref Stopwatch __state) + { __state.Stop(); LogDebug("[PROFILE] " + __state.Elapsed + " RefreshSystemIndicators"); } + + static IEnumerable Transpiler(IEnumerable instructions) + { + // cuts out the if/else block in CreateDifficultyCallouts() because CreateDifficultyCallouts() + // is called later by RefreshSystemIndicators() anyways just after the block + // FIXME? this technique is v fragile but we don't expect: + // - other mods modifying this chunk of IL + // - updates to the game (1.9.1 is supposedly the final release) + // but this is mostly just for test purposes anyway so YOLO + // FIXME? maybe faster to leave instructions in enumerable form & + // not convert it to a list & back + int startIndex = 7; + int endIndex = 15; + + LogInfo("Overwriting instructions in SGNavigationScreen.OnDifficultySelectionChanged()" + + " at indices " + startIndex + " through " + endIndex + " with nops"); + var codes = new List(instructions); + for(int i = startIndex; i <= endIndex; i++) { + LogSpam("overwriting index " + i + " value " + codes[i].opcode + " with nop"); + codes[i].opcode = OpCodes.Nop; + } + + return codes.AsEnumerable(); + } + } +} From b0d4d675d02cf800709816168b323fc6f12925aa Mon Sep 17 00:00:00 2001 From: Cole Thompson Date: Wed, 16 Dec 2020 15:47:21 -0600 Subject: [PATCH 09/23] Use a more robust & defensive method of finding the code to remove --- source/NavigationMapFilterLagFix.cs | 52 +++++++++++++++++++---------- 1 file changed, 35 insertions(+), 17 deletions(-) diff --git a/source/NavigationMapFilterLagFix.cs b/source/NavigationMapFilterLagFix.cs index 61c0c32..12ae074 100644 --- a/source/NavigationMapFilterLagFix.cs +++ b/source/NavigationMapFilterLagFix.cs @@ -2,6 +2,7 @@ using System.Linq; using BattleTech.UI; using Harmony; +using System.Reflection; using System.Reflection.Emit; using HBS; using static BattletechPerformanceFix.Extensions; @@ -36,28 +37,45 @@ public static void RefreshSystemIndicators_Pre(ref Stopwatch __state) public static void RefreshSystemIndicators_Post(ref Stopwatch __state) { __state.Stop(); LogDebug("[PROFILE] " + __state.Elapsed + " RefreshSystemIndicators"); } + // cuts out the if/else block in CreateDifficultyCallouts() because CreateDifficultyCallouts() + // is called later by RefreshSystemIndicators() anyways just after the block static IEnumerable Transpiler(IEnumerable instructions) { - // cuts out the if/else block in CreateDifficultyCallouts() because CreateDifficultyCallouts() - // is called later by RefreshSystemIndicators() anyways just after the block - // FIXME? this technique is v fragile but we don't expect: - // - other mods modifying this chunk of IL - // - updates to the game (1.9.1 is supposedly the final release) - // but this is mostly just for test purposes anyway so YOLO - // FIXME? maybe faster to leave instructions in enumerable form & - // not convert it to a list & back - int startIndex = 7; - int endIndex = 15; + int startIndex = -1; + int endIndex = -1; - LogInfo("Overwriting instructions in SGNavigationScreen.OnDifficultySelectionChanged()" + - " at indices " + startIndex + " through " + endIndex + " with nops"); - var codes = new List(instructions); - for(int i = startIndex; i <= endIndex; i++) { - LogSpam("overwriting index " + i + " value " + codes[i].opcode + " with nop"); - codes[i].opcode = OpCodes.Nop; + // searches for the if block start & end points, in case some other mod is modifying this function + // (which is why we don't hardcode the indices) + var code = instructions.ToList(); + for (int i = 0; i < code.Count-1; i++) { + if (startIndex == -1) { + if (code[i].opcode == OpCodes.Ldarg_1 && code[i+1].opcode == OpCodes.Brtrue) { + startIndex = i; + } + } else { + if (code[i].opcode == OpCodes.Ldarg_1 && code[i+1].opcode == OpCodes.Call && + (code[i+1].operand as MethodInfo).Name == "CreateDifficultyCallouts") { + endIndex = i+1; + break; + } + } } - return codes.AsEnumerable(); + if (startIndex != -1 && endIndex != -1) { + LogInfo("Applying code changes for NavigationMapFilterLagFix"); + LogDebug("Overwriting instructions in SGNavigationScreen.OnDifficultySelectionChanged()" + + " at indices " + startIndex + "-" + endIndex + " with nops"); + for (int i = startIndex; i <= endIndex; i++) { + code[i].opcode = OpCodes.Nop; + } + } else { + LogError("Failed to find the code to overwrite in " + + "SGNavigationScreen.OnDifficultySelectionChanged(); no changes were made."); + LogError("NavigationMapFilterLagFix has not been applied, report this as a bug"); + } + + //foreach(CodeInstruction c in code) { LogSpam(c.opcode + " | " + c.operand); } + return code.AsEnumerable(); } } } From ef9bc6cc9b2c634269994df95cbde8ddbf98d495 Mon Sep 17 00:00:00 2001 From: Cole Thompson Date: Wed, 16 Dec 2020 15:53:59 -0600 Subject: [PATCH 10/23] Update README.md with NavigationMapFilterLagFix --- README.md | 3 +++ 1 file changed, 3 insertions(+) diff --git a/README.md b/README.md index 5ce6626..cb6c27c 100644 --- a/README.md +++ b/README.md @@ -35,6 +35,9 @@ - Make all simgame room transitions instant. - RemovedContractsFix - This fix removes invalid contracts allowing saves to load if a user created contract was removed from the mods in use. +- NavigationMapFilterLagFix + - This fix removes some unnecessary code in the navigation map's difficulty filter selection. + - The performance gain is negligible in vanilla, but in modpacks using the entire IS map such as RT or BTA, this saves a few seconds. # Experimental patches - MDDB_TagsetQueryInChunks From 9f698497c69e45da598b4b1eb64ef57281afcb61 Mon Sep 17 00:00:00 2001 From: Cole Thompson Date: Thu, 17 Dec 2020 09:45:14 -0600 Subject: [PATCH 11/23] Cleanup --- source/NavigationMapFilterLagFix.cs | 25 ++----------------------- 1 file changed, 2 insertions(+), 23 deletions(-) diff --git a/source/NavigationMapFilterLagFix.cs b/source/NavigationMapFilterLagFix.cs index 12ae074..b3d43ab 100644 --- a/source/NavigationMapFilterLagFix.cs +++ b/source/NavigationMapFilterLagFix.cs @@ -4,7 +4,6 @@ using Harmony; using System.Reflection; using System.Reflection.Emit; -using HBS; using static BattletechPerformanceFix.Extensions; namespace BattletechPerformanceFix @@ -17,29 +16,11 @@ public void Activate() var method = AccessTools.Method(typeof(SGNavigationScreen), "OnDifficultySelectionChanged"); var transpiler = new HarmonyMethod(typeof(NavigationMapFilterLagFix), nameof(Transpiler)); Main.harmony.Patch(method, null, null, transpiler); - - var cdc = nameof(SGNavigationScreen.CreateDifficultyCallouts); - cdc.Pre(); - cdc.Post(); - var rsi = nameof(SGNavigationScreen.RefreshSystemIndicators); - rsi.Pre(); - rsi.Post(); } - // FIXME remove - // TODO write a wrapper to make these quickly & easily - public static void CreateDifficultyCallouts_Pre(ref Stopwatch __state) - { __state = new Stopwatch(); __state.Start(); } - public static void CreateDifficultyCallouts_Post(ref Stopwatch __state) - { __state.Stop(); LogDebug("[PROFILE] " + __state.Elapsed + " CreateDifficultyCallouts"); } - public static void RefreshSystemIndicators_Pre(ref Stopwatch __state) - { __state = new Stopwatch(); __state.Start(); } - public static void RefreshSystemIndicators_Post(ref Stopwatch __state) - { __state.Stop(); LogDebug("[PROFILE] " + __state.Elapsed + " RefreshSystemIndicators"); } - - // cuts out the if/else block in CreateDifficultyCallouts() because CreateDifficultyCallouts() + // cuts out the if/else block in OnDifficultySelectionChanged() because CreateDifficultyCallouts() // is called later by RefreshSystemIndicators() anyways just after the block - static IEnumerable Transpiler(IEnumerable instructions) + private static IEnumerable Transpiler(IEnumerable instructions) { int startIndex = -1; int endIndex = -1; @@ -62,7 +43,6 @@ static IEnumerable Transpiler(IEnumerable inst } if (startIndex != -1 && endIndex != -1) { - LogInfo("Applying code changes for NavigationMapFilterLagFix"); LogDebug("Overwriting instructions in SGNavigationScreen.OnDifficultySelectionChanged()" + " at indices " + startIndex + "-" + endIndex + " with nops"); for (int i = startIndex; i <= endIndex; i++) { @@ -74,7 +54,6 @@ static IEnumerable Transpiler(IEnumerable inst LogError("NavigationMapFilterLagFix has not been applied, report this as a bug"); } - //foreach(CodeInstruction c in code) { LogSpam(c.opcode + " | " + c.operand); } return code.AsEnumerable(); } } From d069ed95f849597dabb103f951af2fd7ec1be22a Mon Sep 17 00:00:00 2001 From: Matthew Spencer Date: Wed, 9 Jan 2019 11:40:25 -0600 Subject: [PATCH 12/23] Merged SimpleMetrics to branch From remotes/origin/feature/PerformanceTracking --- source/BattletechPerformanceFix.csproj | 1 + source/Extensions.cs | 52 ++++-- source/Main.cs | 1 + source/SimpleMetrics.cs | 243 +++++++++++++++++++++++++ 4 files changed, 284 insertions(+), 13 deletions(-) create mode 100644 source/SimpleMetrics.cs diff --git a/source/BattletechPerformanceFix.csproj b/source/BattletechPerformanceFix.csproj index 49c9503..781bb91 100644 --- a/source/BattletechPerformanceFix.csproj +++ b/source/BattletechPerformanceFix.csproj @@ -75,6 +75,7 @@ + diff --git a/source/Extensions.cs b/source/Extensions.cs index 875dba9..80e6f83 100644 --- a/source/Extensions.cs +++ b/source/Extensions.cs @@ -89,6 +89,15 @@ public static IEnumerable> GroupsOf(this IEnumerable items, .GroupBy(kv => kv.idx / count, kv => kv.item) .Select(x => x.Select(v => v)); } + + public static IEnumerable> Zip(this IEnumerable left, IEnumerable right) { + var itl = left.GetEnumerator(); + var itr = right.GetEnumerator(); + + while(itl.MoveNext() && itr.MoveNext()) { + yield return new KeyValuePair(itl.Current, itr.Current); + } + } public static T GetWithDefault(this Dictionary d, K key, Func lazyDefault) => d.TryGetValue(key, out var val) ? val : d[key] = lazyDefault(); @@ -228,33 +237,50 @@ public static void Patch( this MethodBase method Trap(() => Main.harmony.Patch( method, patches[0], patches[1], patches[2])); } + + public static MethodBase FindQualifiedMethod(this string qualifiedName) { + var ss = qualifiedName.Split(Array("::"), StringSplitOptions.None).ToArray(); + if (ss.Length != 2) throw new Exception($"Invalid QualifiedMethod string {qualifiedName}"); + var type = ss[0]; + var meth = ss[1]; + LogSpam($"FindQualifiedMethod for {type} {meth}"); + return AppDomain.CurrentDomain.GetAssemblies() + .Select(asm => asm.GetType(type)) + .Where(t => t != null) + .SingleOrDefault() + ?.GetMethod(meth, AccessTools.all); + } + + public static string QualifiedSignature(this MethodBase method) + => $"{method.DeclaringType.FullName}::{method.ToString()}"; - public static void Patch( this string method - , string premethod = null - , string postmethod = null - , string transpilemethod = null - , int priority = Priority.Normal - ) { + public static void Patch( this string method + , Type t + , string premethod = null + , string postmethod = null + , string transpilemethod = null + , int priority = Priority.Normal + ) { MethodBase meth = null; - if (method.StartsWith("ctor")) meth = (MethodBase)typeof(T).GetConstructors(AccessTools.all)[0]; - else if (method.StartsWith("get_")) meth = (MethodBase)typeof(T).GetProperties(AccessTools.all) + if (method.StartsWith("ctor")) meth = (MethodBase)t.GetConstructors(AccessTools.all)[0]; + else if (method.StartsWith("get_")) meth = (MethodBase)t.GetProperties(AccessTools.all) .FirstOrDefault(mm => { LogDebug($"{mm.Name}"); return method.EndsWith(mm.Name); }) ?.GetGetMethod(); - else meth = (MethodBase)typeof(T).GetMethods(AccessTools.all) + else meth = (MethodBase)t.GetMethods(AccessTools.all) .FirstOrDefault(mm => mm.Name == method && mm.GetMethodBody() != null); - meth.LogIfNull($"Failed to find patchable function {method} on {typeof(T).FullName}"); + meth.LogIfNull($"Failed to find patchable function {method} on {t.FullName}"); meth.Patch(premethod, postmethod, transpilemethod, priority); } public static void Pre(this string method, string patchmethod = null, int priority = Priority.Normal) - => method.Patch(patchmethod ?? $"{method}_Pre", null, null, priority); + => method.Patch(typeof(T), patchmethod ?? $"{method}_Pre", null, null, priority); public static void Post(this string method, string patchmethod = null, int priority = Priority.Normal) - => method.Patch(null, patchmethod ?? $"{method}_Post", null, priority); + => method.Patch(typeof(T), null, patchmethod ?? $"{method}_Post", null, priority); public static void Transpile(this string method, string patchmethod = null, int priority = Priority.Normal) - => method.Patch(null, null, patchmethod ?? $"{method}_Transpile", priority); + => method.Patch(typeof(T), null, null, patchmethod ?? $"{method}_Transpile", priority); // C# macros when... public static void Pre(this string method, Action f) where T : class diff --git a/source/Main.cs b/source/Main.cs index 8f97fb2..6e7b297 100644 --- a/source/Main.cs +++ b/source/Main.cs @@ -102,6 +102,7 @@ public static void Start(string modDirectory, string json) { typeof(VersionManifestPatches), true }, { typeof(MemoryLeakFix), true }, { typeof(EnableConsole), false }, + { typeof(SimpleMetrics), true } }; diff --git a/source/SimpleMetrics.cs b/source/SimpleMetrics.cs new file mode 100644 index 0000000..936dc3a --- /dev/null +++ b/source/SimpleMetrics.cs @@ -0,0 +1,243 @@ +using System; +using System.Reflection; +using System.Reflection.Emit; +using System.Diagnostics; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using Harmony; +using UnityEngine; +using BattleTech.Data; +using static BattletechPerformanceFix.Extensions; + +namespace BattletechPerformanceFix +{ + public static partial class Extensions { + public static void TrackSequence(params string[] qualifiedMethods) { + LogInfo($"SimpleMetrics.TrackSequence {qualifiedMethods.Dump(false)}"); + var methseq = qualifiedMethods.Select(FindQualifiedMethod).ToList(); + methseq.Zip(methseq.Skip(1)) + .ForEach(kv => SimpleMetrics.WithEntryAndExit(kv.Key, kv.Value)); + } + + } + + class SimpleMetrics : Feature + { + public static bool Active = false; + public void Activate() { + var self = typeof(SimpleMetrics); + Main.harmony.Patch( AccessTools.Method(typeof(BattleTech.UI.SimGameOptionsMenu), "OnAddedToHierarchy") + , new HarmonyMethod(AccessTools.Method(self, "Summary"))); + Main.harmony.Patch( AccessTools.Method(typeof(BattleTech.UI.SGLoadSavedGameScreen), "LoadSelectedSlot") + , new HarmonyMethod(AccessTools.Method(self, "Summary"))); + Active = true; + + "Update".Post(); + + TrackSequence( "BattleTech.UI.SGLoadSavedGameScreen::LoadSelectedSlot" + , "BattleTech.SimGameState::_OnBeginDefsLoad" + , "BattleTech.SimGameState::_OnDefsLoadComplete" + , "BattleTech.SimGameState::_OnBeginAttachUX" + , "BattleTech.UI.SimGameUXCreator::Awake" + , "BattleTech.SimGameState::_OnAttachUXComplete"); + } + + public static void Update_Post() { + if (Input.GetKey(KeyCode.LeftShift) && Input.GetKeyDown(KeyCode.M)) { + Summary(); + } + } + + // This will be broken if entry method is in the stack multiple times + public static void WithEntryAndExit(MethodBase entry, MethodBase exit) { + entry.LogIfNull("Cannot instrument null entry"); + exit.LogIfNull("Cannot instrument null exit"); + + LogSpam($"Preparing {entry.ToString()} -> {exit.ToString()}"); + + var entryIndex = MethodEntryIndexMap.GetWithDefault(entry.QualifiedSignature(), () => MethodEntryIndexMap.Count); + while (entryIndex >= EntryTimers.Count) EntryTimers.Add(new Stopwatch()); + + var exitIndex = MethodExitIndexMap.GetWithDefault(exit.QualifiedSignature(), () => MethodExitIndexMap.Count); + while (exitIndex >= ExitTimers.Count) ExitTimers.Add(new List()); + + ExitTimers[exitIndex].Add(entryIndex); + + var cindex = ((uint)entryIndex << 16) | (uint)exitIndex; + IdPairToMethods[cindex] = new KeyValuePair(entry, exit); + + var transpileEntry = new HarmonyMethod(typeof(SimpleMetrics), "TranspileEntry"); + transpileEntry.prioritiy = Priority.First; + var transpileExit = new HarmonyMethod(typeof(SimpleMetrics), "TranspileExit"); + transpileExit.prioritiy = Priority.Last; + + + LogSpam($"Patching {entry.ToString()} -> {exit.ToString()}"); + + entry.Patch(null, null, "TranspileEntry", Priority.First); + exit.Patch(null, null, "TranspileExit", Priority.Last); + + LogSpam($"Patched {entry.ToString()} -> {exit.ToString()}"); + } + + + public static Dictionary> IdPairToMethods = new Dictionary>(); + public static void EntryStart(int index) { + var timer = EntryTimers[index]; + timer.Reset(); + timer.Start(); + } + + public static void EntryStop(int index) { + ExitTimers[index] + .ForEach(startIndex => { + var timer = EntryTimers[startIndex]; + var cindex = ((uint)startIndex << 16) | (uint)index; + var meths = IdPairToMethods[cindex]; + LogWarning($"Measured [{meths.Key.QualifiedSignature()} -> {meths.Value.QualifiedSignature()}] in {timer.Elapsed.TotalMilliseconds}"); + }); + } + + public static Dictionary MethodEntryIndexMap = new Dictionary(); + public static List EntryTimers = new List(); + public static IEnumerable TranspileEntry(ILGenerator ilGenerator, MethodBase original, IEnumerable ins) { + LogInfo($"TranspileEntry {original.QualifiedSignature()}"); + var index = MethodEntryIndexMap[original.QualifiedSignature()]; + + // SimpleMetrics.EntryStart(index) + var start = Sequence( new CodeInstruction(OpCodes.Ldc_I4, index) + , new CodeInstruction(OpCodes.Call, AccessTools.Method(typeof(SimpleMetrics), "EntryStart"))); + + return start.Concat(ins); + } + + + + public static Dictionary MethodExitIndexMap = new Dictionary(); + public static List> ExitTimers = new List>(); + public static IEnumerable TranspileExit(ILGenerator ilGenerator, MethodBase original, IEnumerable ins) { + LogInfo($"TranspileExit {original.QualifiedSignature()}"); + var index = MethodExitIndexMap[original.QualifiedSignature()]; + + var retType = (original as MethodInfo)?.ReturnType ?? typeof(void); + + var tmp = retType == typeof(void) ? null : ilGenerator.DeclareLocal(retType); + + return ins.SelectMany(i => { + if (i.opcode == OpCodes.Ret) { + var seq = Sequence( new CodeInstruction(OpCodes.Ldc_I4, index) + , new CodeInstruction(OpCodes.Call, AccessTools.Method(typeof(SimpleMetrics), "EntryStop"))); + + IEnumerable pre; + IEnumerable post; + if (tmp == null) { + pre = Sequence(); + post = Sequence( new CodeInstruction(OpCodes.Ret)); + } else { + pre = Sequence( new CodeInstruction(OpCodes.Stloc, tmp)); + post = Sequence( new CodeInstruction(OpCodes.Ldloc, tmp) + , new CodeInstruction(OpCodes.Ret)); + } + + var all = pre.Concat(seq).Concat(post); + var first = all.First(); + var rest = all.Skip(1); + // Mutate the current instruction to retain jump offsets. + i.opcode = first.opcode; + i.operand = first.operand; + return Sequence(i).Concat(rest); + } else { + return Sequence(i); + } + }); + } + + public static void Instrument(MethodBase meth) { + if (meth == null) + LogError($"Cannot instrument null meth from {new StackTrace().ToString()}"); + + LogDebug($"Instrumenting {meth.DeclaringType.FullName}::{meth.ToString()}"); + + if(PreHook == null || PostHook == null) { + LogInfo("Initializing simple metrics hooks"); + + var self = typeof(SimpleMetrics); + PreHook = new HarmonyMethod(AccessTools.Method(self, nameof(__Pre))); + PreHook.prioritiy = Priority.First; + PostHook = new HarmonyMethod(AccessTools.Method(self, nameof(__Post))); + PostHook.prioritiy = Priority.Last; + } + + if (meth.IsGenericMethod || meth.IsGenericMethodDefinition) { + LogError($"Cannot instrument a generic method {meth.DeclaringType.FullName}::{meth.ToString()}"); + } else if (meth.GetMethodBody() == null) { + LogError($"Cannot instrument a method with no body {meth.DeclaringType.FullName}::{meth.ToString()}"); + } else { + Trap(() => Main.harmony.Patch(meth, PreHook, PostHook)); + } + } + + public static void Track(MethodBase meth) { + if (meth == null) + LogError($"Cannot instrument null meth from {new StackTrace().ToString()}"); + + LogDebug($"Tracking {meth.DeclaringType.FullName}::{meth.ToString()}"); + + if(TrackHook == null) { + LogInfo("Initializing tracking hooks"); + + var self = typeof(SimpleMetrics); + TrackHook = new HarmonyMethod(AccessTools.Method(self, nameof(__Track))); + TrackHook.prioritiy = Priority.First; + } + + if (meth.IsGenericMethod || meth.IsGenericMethodDefinition) { + LogError($"Cannot instrument a generic method {meth.DeclaringType.FullName}::{meth.ToString()}"); + } else if (meth.GetMethodBody() == null) { + LogError($"Cannot instrument a method with no body {meth.DeclaringType.FullName}::{meth.ToString()}"); + } else { + Trap(() => Main.harmony.Patch(meth, TrackHook)); + } + } + + static HarmonyMethod PreHook; + static HarmonyMethod PostHook; + static Dictionary Metrics = new Dictionary(); + public static void __Pre(ref Metric __state) { + // Tons of overhead, but it's good enough for what we're doing. + // FIXME: Change to transpiler doing a lookup into a fixed array of Metrics + var meth = new StackFrame(1).GetMethod(); + var fullname = meth.DeclaringType.Name + "::" + meth.Name; + var metric = Metrics.GetWithDefault(fullname, () => new Metric()); + metric.times++; + metric.timer.Start(); + __state = metric; + } + + public static void __Post(ref Metric __state) { + try { + __state.timer.Stop(); + } catch(Exception e) { LogException(e); } + } + + static HarmonyMethod TrackHook; + public static void __Track(object __instance) { + var meth = new StackFrame(1).GetMethod(); + var hash = __instance.GetHashCode(); + LogDebug($"Tracked[{hash}] {meth.DeclaringType.FullName}::{meth.ToString()}"); + } + + public static void Summary() { + var buf = ""; + foreach (var kv in Metrics) { + buf += $":times {kv.Value.times} :ms {kv.Value.timer.Elapsed.TotalMilliseconds} :method {kv.Key}\n"; + } + Metrics.Clear(); + LogInfo( string.Format("SimpleMetrics -------------------------------------- \n{0}\n\n----------------------------------" + , buf)); + } + } + + class Metric { public long times = 0; public Stopwatch timer = new Stopwatch(); } +} From 1f8626c4a6af8347e0ba37301056d265f32f495a Mon Sep 17 00:00:00 2001 From: Cole Thompson Date: Tue, 22 Dec 2020 13:20:06 -0600 Subject: [PATCH 13/23] Cleanup --- source/Extensions.cs | 1 - source/SimpleMetrics.cs | 8 ++++---- 2 files changed, 4 insertions(+), 5 deletions(-) diff --git a/source/Extensions.cs b/source/Extensions.cs index 80e6f83..2e462d0 100644 --- a/source/Extensions.cs +++ b/source/Extensions.cs @@ -250,7 +250,6 @@ public static MethodBase FindQualifiedMethod(this string qualifiedName) { .SingleOrDefault() ?.GetMethod(meth, AccessTools.all); } - public static string QualifiedSignature(this MethodBase method) => $"{method.DeclaringType.FullName}::{method.ToString()}"; diff --git a/source/SimpleMetrics.cs b/source/SimpleMetrics.cs index 936dc3a..3c3d801 100644 --- a/source/SimpleMetrics.cs +++ b/source/SimpleMetrics.cs @@ -61,7 +61,7 @@ public static void WithEntryAndExit(MethodBase entry, MethodBase exit) { var exitIndex = MethodExitIndexMap.GetWithDefault(exit.QualifiedSignature(), () => MethodExitIndexMap.Count); while (exitIndex >= ExitTimers.Count) ExitTimers.Add(new List()); - + ExitTimers[exitIndex].Add(entryIndex); var cindex = ((uint)entryIndex << 16) | (uint)exitIndex; @@ -143,7 +143,7 @@ public static IEnumerable TranspileExit(ILGenerator ilGenerator var all = pre.Concat(seq).Concat(post); var first = all.First(); var rest = all.Skip(1); - // Mutate the current instruction to retain jump offsets. + // Mutate the current instruction to retain jump offsets. i.opcode = first.opcode; i.operand = first.operand; return Sequence(i).Concat(rest); @@ -152,7 +152,7 @@ public static IEnumerable TranspileExit(ILGenerator ilGenerator } }); } - + public static void Instrument(MethodBase meth) { if (meth == null) LogError($"Cannot instrument null meth from {new StackTrace().ToString()}"); @@ -216,7 +216,7 @@ public static void __Pre(ref Metric __state) { } public static void __Post(ref Metric __state) { - try { + try { __state.timer.Stop(); } catch(Exception e) { LogException(e); } } From 485679a68547da9e22ff328a3afbc4e01beac6a5 Mon Sep 17 00:00:00 2001 From: Cole Thompson Date: Fri, 22 Jan 2021 20:41:46 -0600 Subject: [PATCH 14/23] add UnityHeapDump and add logging for MessageCenter subscriptions --- source/BattletechPerformanceFix.csproj | 1 + source/SimpleMetrics.cs | 39 +- source/UnityHeapDump.cs | 624 +++++++++++++++++++++++++ 3 files changed, 662 insertions(+), 2 deletions(-) create mode 100644 source/UnityHeapDump.cs diff --git a/source/BattletechPerformanceFix.csproj b/source/BattletechPerformanceFix.csproj index 781bb91..c9efcaf 100644 --- a/source/BattletechPerformanceFix.csproj +++ b/source/BattletechPerformanceFix.csproj @@ -76,6 +76,7 @@ + diff --git a/source/SimpleMetrics.cs b/source/SimpleMetrics.cs index 3c3d801..158db1d 100644 --- a/source/SimpleMetrics.cs +++ b/source/SimpleMetrics.cs @@ -41,11 +41,45 @@ public void Activate() { , "BattleTech.SimGameState::_OnBeginAttachUX" , "BattleTech.UI.SimGameUXCreator::Awake" , "BattleTech.SimGameState::_OnAttachUXComplete"); + + "AddSubscriber".Pre("_LogMessageCenterSubscribe"); + "AddFiniteSubscriber".Pre("_LogMessageCenterSubscribe"); + "RemoveSubscriber".Pre("_LogMessageCenterSubscribe"); + "RemoveFiniteSubscriber".Pre("_LogMessageCenterSubscribe"); } public static void Update_Post() { if (Input.GetKey(KeyCode.LeftShift) && Input.GetKeyDown(KeyCode.M)) { Summary(); + } else if (Input.GetKey(KeyCode.LeftShift) && Input.GetKeyDown(KeyCode.D)) { + DumpHeap(); + } + } + + private static void DumpHeap() { + GC.Collect(); + GC.WaitForPendingFinalizers(); + GC.Collect(); + Trap (() => UnityHeapDump.Create()); + } + + private static void _Subscriber_Pre(MessageCenter __instance, MessageCenterMessageType GUID, ReceiveMessageCenterMessage subscriber) { + _LogMessageCenterSubscribe(__instance, GUID, subscriber); + } + + private static void _FiniteSubscriber_Pre(MessageCenter __instance, MessageCenterMessageType GUID, ReceiveMessageCenterMessageAutoDelete subscriber) { + _LogMessageCenterSubscribe(__instance, GUID, subscriber); + } + + private static void _LogMessageCenterSubscribe(MessageCenter __instance, MessageCenterMessageType GUID, Delegate subscriber) { + var meth = new StackFrame(2).GetMethod(); + var hash = __instance.GetHashCode(); + LogSpam($"Tracked[{hash}] {DateTime.Now.ToString("MM/dd/yy HH:mm:ss.ffffff")} " + + $"{meth.ToString().Split("(".ToCharArray())[0]} GUID: {GUID} " + + $"MethodImpl: {subscriber.GetMethodImpl().ReturnType} {subscriber.GetMethodImpl().Name}(...)"); + StackTrace st = new StackTrace(); + foreach (var line in st.ToString().Split(new [] { '\r', '\n' })) { + LogSpam(line); } } @@ -95,7 +129,7 @@ public static void EntryStop(int index) { var timer = EntryTimers[startIndex]; var cindex = ((uint)startIndex << 16) | (uint)index; var meths = IdPairToMethods[cindex]; - LogWarning($"Measured [{meths.Key.QualifiedSignature()} -> {meths.Value.QualifiedSignature()}] in {timer.Elapsed.TotalMilliseconds}"); + LogWarning($"Measured [{meths.Key.QualifiedSignature()} -> {meths.Value.QualifiedSignature()}] in {timer.Elapsed.TotalMilliseconds} ms"); }); } @@ -225,7 +259,8 @@ public static void __Post(ref Metric __state) { public static void __Track(object __instance) { var meth = new StackFrame(1).GetMethod(); var hash = __instance.GetHashCode(); - LogDebug($"Tracked[{hash}] {meth.DeclaringType.FullName}::{meth.ToString()}"); + var timestamp = DateTime.Now.ToString("MM/dd/yy HH:mm:ss.ffffff"); + LogDebug($"Tracked[{hash}] {timestamp} {meth.DeclaringType.FullName}::{meth.ToString()}"); } public static void Summary() { diff --git a/source/UnityHeapDump.cs b/source/UnityHeapDump.cs new file mode 100644 index 0000000..a5aa252 --- /dev/null +++ b/source/UnityHeapDump.cs @@ -0,0 +1,624 @@ +using System; +using System.Collections.Generic; +using System.IO; +using System.Reflection; +using System.Runtime.InteropServices; +using System.Linq; +using UnityEngine; +using UObject = UnityEngine.Object; +using System.Text.RegularExpressions; + +// adapted from https://github.com/Zuntatos/UnityHeapDump + +public class UnityHeapDump +{ + const int TYPE_MIN_SIZE_TO_PRINT = 40*1024*1024; + const int ROOT_MIN_SIZE = TYPE_MIN_SIZE_TO_PRINT; + const int CHILD_MIN_SIZE = 128*1024; + + static string childIndent = " "; + + static int ThreadJobsPrinting = 0; + static int ThreadJobsDone = 0; + + static HashSet genericTypes = new HashSet(); + + static String dumpDir; + + public static void Create () + { + TypeData.Start(); + ThreadJobsPrinting = 0; + ThreadJobsDone = 0; + + genericTypes = new HashSet(); + + dumpDir = $"dump_{DateTime.Now.ToString("yyyyMMdd_HHmmssfff")}"; + Directory.CreateDirectory(dumpDir); + Directory.CreateDirectory(dumpDir + "/sobjects"); + Directory.CreateDirectory(dumpDir + "/uobjects"); + Directory.CreateDirectory(dumpDir + "/statics"); + + Regex assembliesToDump = new Regex("Assembly-CSharp,.*|Unity(Engine)?.*"); + + using (var logger = new StreamWriter(dumpDir + "/log.txt")) + { + Dictionary> assemblyResults = new Dictionary>(); + Dictionary assemblySizes = new Dictionary(); + List> parseErrors = new List>(); + + foreach (var assembly in AppDomain.CurrentDomain.GetAssemblies()) + { + if (!assembliesToDump.IsMatch(assembly.FullName)) { continue; } + + string assemblyFolder; + if (assembly.FullName.Contains("Assembly-CSharp")) + { + assemblyFolder = string.Format(dumpDir + "/statics/{0}/", assembly.FullName.Replace("<", "(").Replace(">", ")").Replace(".", "_")); + } else + { + assemblyFolder = dumpDir + "/statics/misc/"; + } + Directory.CreateDirectory(assemblyFolder); + List types = new List(); + foreach (var type in assembly.GetTypes()) + { + if (type.IsEnum || type.IsGenericType) + { + continue; + } + + // FIXME remove + if (!type.FullName.Contains("ActiveOrDefaultSettings")) { continue; } + + try + { + types.Add(new StructOrClass(type, assemblyFolder)); + } + catch (Exception e) + { + parseErrors.Add (new KeyValuePair(type, e)); + } + } + assemblyResults[assembly] = types; + } + + List unityComponents = new List(); + + /* + foreach (var go in Resources.FindObjectsOfTypeAll()) + { + foreach (var component in go.GetComponents()) + { + if (component == null) + { + continue; + } + try + { + unityComponents.Add(new StructOrClass(component)); + } + catch (Exception e) + { + parseErrors.Add(new KeyValuePair(component.GetType(), e)); + } + } + } + */ + + List unityScriptableObjects = new List(); + + /* + foreach (ScriptableObject scriptableObject in Resources.FindObjectsOfTypeAll()) + { + if (scriptableObject == null) + { + continue; + } + try + { + unityScriptableObjects.Add(new StructOrClass(scriptableObject)); + } + catch (Exception e) + { + parseErrors.Add(new KeyValuePair(scriptableObject.GetType(), e)); + } + } + */ + + /* + foreach (var genericType in genericTypes.ToList()) + { + try + { + assemblyResults[genericType.Assembly].Add(new StructOrClass(genericType, dumpDir + "/statics/misc/")); + } + catch (Exception e) + { + parseErrors.Add(new KeyValuePair(genericType, e)); + } + } + */ + + foreach (var pair in assemblyResults) + { + assemblySizes[pair.Key] = pair.Value.Sum(a => a.Size); + pair.Value.Sort((a, b) => (int) (b.Size - a.Size)); + } + + while (ThreadJobsDone < ThreadJobsPrinting) { + System.Threading.Thread.Sleep(100); + } + + TypeData.Clear(); + + var assemblySizesList = assemblySizes.ToList(); + assemblySizesList.Sort((a, b) => (int) (b.Value - a.Value)); + long assemblySizesListSize = assemblySizesList.Sum(a => a.Value); + + unityComponents.Sort((a, b) => (int) (b.Size - a.Size)); + long unityComponentsSize = unityComponents.Sum(a => a.Size); + bool printedUnityComponents = false; + + unityScriptableObjects.Sort((a, b) => (int) (b.Size - a.Size)); + long unityScriptableObjectsSize = unityScriptableObjects.Sum(a => a.Size); + bool printedUnityScriptableObjects = false; + + logger.WriteLine("Total tracked memory (including duplicates, so too high) = {0}", assemblySizesListSize + unityComponentsSize + unityScriptableObjectsSize); + + + foreach (var pair in assemblySizesList) + { + var assembly = pair.Key; + var size = pair.Value; + + if (!printedUnityComponents && size < unityComponentsSize) + { + printedUnityComponents = true; + logger.WriteLine("Unity components of total size: {0}", unityComponentsSize); + foreach (var instance in unityComponents) + { + if (instance.Size >= TYPE_MIN_SIZE_TO_PRINT) + { + logger.WriteLine(" Type {0} (ID: {1}) of size {2}", instance.ParsedType.FullName, instance.InstanceID, instance.Size); + } + } + } + + if (!printedUnityScriptableObjects && size < unityScriptableObjectsSize) + { + printedUnityScriptableObjects = true; + logger.WriteLine("Unity scriptableobjects of total size: {0}", unityScriptableObjectsSize); + foreach (var instance in unityScriptableObjects) + { + if (instance.Size >= TYPE_MIN_SIZE_TO_PRINT) + { + logger.WriteLine(" Type {0} (ID: {1}) of size {2}", instance.ParsedType.FullName, instance.InstanceID, instance.Size); + } + } + } + + logger.WriteLine("Assembly: {0} of total size: {1}", assembly, size); + foreach (var type in assemblyResults[assembly]) + { + if (type.Size >= TYPE_MIN_SIZE_TO_PRINT) + { + logger.WriteLine(" Type: {0} of size {1}", type.ParsedType.FullName, type.Size); + } + } + } + foreach (var error in parseErrors) + { + logger.WriteLine(error); + } + } + } + + class StructOrClass + { + public long Size { get; private set; } + public Type ParsedType { get; private set; } + public int InstanceID { get; private set; } + int ArraySize { get; set; } + string Identifier { get; set; } + + List Children = new List(); + + /// + /// Parse static types + /// + public StructOrClass (Type type, string assemblyFolder) + { + ParsedType = type; + HashSet seenObjects = new HashSet(); + Identifier = type.FullName; + foreach (var fieldInfo in type.GetFields(BindingFlags.Static | BindingFlags.Public | BindingFlags.NonPublic)) + { + ParseField(fieldInfo, null, seenObjects); + } + if (Size < ROOT_MIN_SIZE) + { + return; + } + + System.Threading.Interlocked.Increment(ref ThreadJobsPrinting); + System.Threading.ThreadPool.QueueUserWorkItem (obj => { + try + { + string fileName = Identifier.Replace("<", "(").Replace(">", ")").Replace(".", "_"); + using (var writer = new StreamWriter(string.Format("{0}{1}-{2}", assemblyFolder, Size, fileName))) + { + writer.WriteLine("Static ({0}): {1} bytes", ParsedType, Size); + Children.Sort((a, b) => (int) (b.Size - a.Size)); + foreach (var child in Children) + { + if (child.Size >= CHILD_MIN_SIZE) + { + child.Write(writer, childIndent); + } else + { + break; + } + } + } + } + finally + { + System.Threading.Interlocked.Increment(ref ThreadJobsDone); + } + }); + } + + /// + /// Parse monobehaviour and scriptableobject instances + /// + public StructOrClass (UObject uObject) + { + InstanceID = uObject.GetInstanceID(); + ParsedType = uObject.GetType(); + Identifier = uObject.name + uObject.GetInstanceID(); + HashSet seenObjects = new HashSet(); + + foreach (var field in ParsedType.GetFields(BindingFlags.Instance | BindingFlags.Public | BindingFlags.NonPublic)) + { + ParseField(field, uObject, seenObjects); + } + + if (Size < ROOT_MIN_SIZE) + { + return; + } + + //if(!(uObject is ScriptableObject)) { + // return; + //} + string fileName = string.Format(dumpDir + "/{0}/{1}-{2}", + uObject is ScriptableObject ? "sobjects" : "uobjects", + Size, + Identifier.Replace("<", "(").Replace(">", ")").Replace(".", "_") + ); + + using (var writer = new StreamWriter(fileName)) + { + writer.WriteLine("{0} ({1}): {2} bytes", Identifier, ParsedType, Size); + Children.Sort((a, b) => (int) (b.Size - a.Size)); + foreach (var child in Children) + { + if (child.Size >= CHILD_MIN_SIZE) + { + child.Write(writer, childIndent); + } + } + } + } + + /// + /// Parse field objects; only called for arrays, references and structs with references in them + /// + StructOrClass(string name, object root, TypeData rootTypeData, HashSet seenObjects) + { + Identifier = name; + ParsedType = root.GetType(); + Size = rootTypeData.Size; + if (ParsedType.IsArray) + { + int i = 0; + ArraySize = GetTotalLength((Array)root); + Type elementType = ParsedType.GetElementType(); + TypeData elementTypeData = TypeData.Get(elementType); + if (elementType.IsValueType || elementType.IsPrimitive || elementType.IsEnum) + { + if (elementTypeData.DynamicSizedFields == null) + { + Size += elementTypeData.Size * ArraySize; + return; + } + + foreach (var item in (Array)root) + { + StructOrClass child = new StructOrClass((i++).ToString(), item, elementTypeData, seenObjects); + Size += child.Size; + Children.Add(child); + } + } + else + { + Size += IntPtr.Size * ArraySize; + foreach (var item in (Array)root) + { + ParseItem(item, (i++).ToString(), seenObjects); + } + } + } else + { + if (rootTypeData.DynamicSizedFields != null) + { + foreach (var fieldInfo in rootTypeData.DynamicSizedFields) + { + ParseField(fieldInfo, root, seenObjects); + } + } + } + } + + /// + /// Parse the field of the object, ignoring any seenObjects. If root is null, it is considered a static field. + /// + void ParseField(FieldInfo fieldInfo, object root, HashSet seenObjects) + { + if (!fieldInfo.FieldType.IsPointer) + { + ParseItem(fieldInfo.GetValue(root), fieldInfo.Name, seenObjects); + } + } + + void ParseItem (object obj, string objName, HashSet seenObjects) + { + if (obj == null) + { + return; + } + Type type = obj.GetType(); + if (type.IsPointer) + { + return; // again, a pointer cast to whatever the fieldtype is, shoo. + } + if (type == typeof(string)) + { + // string needs special handling + int strSize = 3 * IntPtr.Size + 2; + strSize += ((string)(obj)).Length * sizeof(char); + int pad = strSize % IntPtr.Size; + if (pad != 0) + { + strSize += IntPtr.Size - pad; + } + Size += strSize; + return; + } + // obj is not null, and a primitive/enum/array/struct/class + TypeData fieldTypeData = TypeData.Get(type); + if (type.IsClass || type.IsArray || fieldTypeData.DynamicSizedFields != null) + { + // class, array, or struct with pointers + if (!(type.IsPrimitive || type.IsValueType || type.IsEnum)) + { + if (!seenObjects.Add(obj)) + { + return; + } + } + + StructOrClass child = new StructOrClass(objName, obj, fieldTypeData, seenObjects); + Size += child.Size; + Children.Add(child); + return; + } + else + { + // primitive, enum, or a struct without pointers, embed it in parent + Size += fieldTypeData.Size; + } + } + + void Write(StreamWriter writer, string indent) + { + if (ParsedType.IsArray) + { + writer.WriteLine("{0}{1} ({2}:{3}) : {4}", indent, Identifier, ParsedType, ArraySize, Size); + } else + { + writer.WriteLine("{0}{1} ({2}) : {3}", indent, Identifier, ParsedType, Size); + } + Children.Sort((a, b) => (int) (b.Size - a.Size)); + foreach (var child in Children) + { + if (child.Size >= CHILD_MIN_SIZE) + { + child.Write(writer, indent + childIndent); + } else + { + return; + } + } + } + + static int GetTotalLength(Array val) + { + int sum = val.GetLength(0); + for (int i = 1; i < val.Rank; i++) + { + sum *= val.GetLength(i); + } + return sum; + } + } + + public class TypeData + { + public long Size { get; private set; } + public List DynamicSizedFields { get; private set; } + + static Dictionary seenTypeData; + static Dictionary seenTypeDataNested; + + public static void Clear() + { + seenTypeData = null; + } + + public static void Start() + { + seenTypeData = new Dictionary(); + seenTypeDataNested = new Dictionary(); + } + + public static TypeData Get(Type type) + { + TypeData data; + if (!seenTypeData.TryGetValue(type, out data)) + { + data = new TypeData(type); + seenTypeData[type] = data; + } + return data; + } + + public static TypeData GetNested(Type type) + { + TypeData data; + if (!seenTypeDataNested.TryGetValue(type, out data)) + { + data = new TypeData(type, true); + seenTypeDataNested[type] = data; + } + return data; + } + + public TypeData(Type type, bool nested = false) + { + if (type.IsGenericType) + { + genericTypes.Add(type); + } + Type baseType = type.BaseType; + if (baseType != null + && baseType != typeof(object) + && baseType != typeof(ValueType) + && baseType != typeof(Array) + && baseType != typeof(Enum)) + { + TypeData baseTypeData = GetNested(baseType); + Size += baseTypeData.Size; + + if (baseTypeData.DynamicSizedFields != null) + { + DynamicSizedFields = new List(baseTypeData.DynamicSizedFields); + } + } + if (type.IsPointer) + { + Size = IntPtr.Size; + } + else if (type.IsArray) + { + Type elementType = type.GetElementType(); + Size = ((elementType.IsValueType || elementType.IsPrimitive || elementType.IsEnum) ? 3 : 4) * IntPtr.Size; + } + else if (type.IsPrimitive) + { + Size = Marshal.SizeOf(type); + } + else if (type.IsEnum) + { + Size = Marshal.SizeOf(Enum.GetUnderlyingType(type)); + } + else // struct, class + { + if (!nested && type.IsClass) + { + Size = 2 * IntPtr.Size; + } + foreach (var field in type.GetFields(BindingFlags.Instance | BindingFlags.Public | BindingFlags.NonPublic)) + { + ProcessField(field, field.FieldType); + } + if (!nested && type.IsClass) + { + Size = Math.Max(3 * IntPtr.Size, Size); + int pad = (int) (Size % IntPtr.Size); + if (pad != 0) + { + Size += IntPtr.Size - pad; + } + } + } + } + + void ProcessField(FieldInfo field, Type fieldType) + { + if (IsStaticallySized(fieldType)) + { + Size += GetStaticSize(fieldType); + } + else + { + if (!(fieldType.IsValueType || fieldType.IsPrimitive || fieldType.IsEnum)) + { + Size += IntPtr.Size; + } + if (fieldType.IsPointer) + { + return; + } + if (DynamicSizedFields == null) + { + DynamicSizedFields = new List(); + } + DynamicSizedFields.Add(field); + } + } + + static bool IsStaticallySized(Type type) + { + + if (type.IsPointer || type.IsArray || type.IsClass || type.IsInterface) + { + return false; + } + if (type.IsPrimitive || type.IsEnum) + { + return true; + } + foreach (var nestedField in type.GetFields(BindingFlags.Instance | BindingFlags.Public | BindingFlags.NonPublic)) + { + if (!IsStaticallySized(nestedField.FieldType)) + { + return false; + } + } + return true; + } + + /// + /// Gets size of type. Assumes IsStaticallySized (type) is true. (primitive, enum, {struct or class with no references in it}) + /// + static int GetStaticSize(Type type) + { + if (type.IsPrimitive) + { + return Marshal.SizeOf(type); + } + if (type.IsEnum) + { + return Marshal.SizeOf(Enum.GetUnderlyingType(type)); + } + int size = 0; + foreach (var nestedField in type.GetFields(BindingFlags.Instance | BindingFlags.Public | BindingFlags.NonPublic)) + { + size += GetStaticSize(nestedField.FieldType); + } + return size; + } + } +} From e9b5631c0ccaf65bc4348a6371d9cea579db09dc Mon Sep 17 00:00:00 2001 From: Cole Thompson Date: Sat, 6 Feb 2021 09:29:56 -0600 Subject: [PATCH 15/23] modify UnityHeapDump to sort by children names (easier to diff) --- source/UnityHeapDump.cs | 35 ++++++++++------------------------- 1 file changed, 10 insertions(+), 25 deletions(-) diff --git a/source/UnityHeapDump.cs b/source/UnityHeapDump.cs index a5aa252..8bacf9d 100644 --- a/source/UnityHeapDump.cs +++ b/source/UnityHeapDump.cs @@ -12,9 +12,9 @@ public class UnityHeapDump { - const int TYPE_MIN_SIZE_TO_PRINT = 40*1024*1024; + const int TYPE_MIN_SIZE_TO_PRINT = 50*1024*1024; const int ROOT_MIN_SIZE = TYPE_MIN_SIZE_TO_PRINT; - const int CHILD_MIN_SIZE = 128*1024; + const int CHILD_MIN_SIZE = 1024*1024; static string childIndent = " "; @@ -39,7 +39,7 @@ public static void Create () Directory.CreateDirectory(dumpDir + "/uobjects"); Directory.CreateDirectory(dumpDir + "/statics"); - Regex assembliesToDump = new Regex("Assembly-CSharp,.*|Unity(Engine)?.*"); + Regex assembliesToDump = new Regex("(Assembly-CSharp,|Unity(Engine)?).*"); using (var logger = new StreamWriter(dumpDir + "/log.txt")) { @@ -49,7 +49,7 @@ public static void Create () foreach (var assembly in AppDomain.CurrentDomain.GetAssemblies()) { - if (!assembliesToDump.IsMatch(assembly.FullName)) { continue; } + if (!assembliesToDump.IsMatch(assembly.FullName)) { continue; } string assemblyFolder; if (assembly.FullName.Contains("Assembly-CSharp")) @@ -68,9 +68,6 @@ public static void Create () continue; } - // FIXME remove - if (!type.FullName.Contains("ActiveOrDefaultSettings")) { continue; } - try { types.Add(new StructOrClass(type, assemblyFolder)); @@ -85,7 +82,6 @@ public static void Create () List unityComponents = new List(); - /* foreach (var go in Resources.FindObjectsOfTypeAll()) { foreach (var component in go.GetComponents()) @@ -104,11 +100,9 @@ public static void Create () } } } - */ List unityScriptableObjects = new List(); - /* foreach (ScriptableObject scriptableObject in Resources.FindObjectsOfTypeAll()) { if (scriptableObject == null) @@ -124,9 +118,7 @@ public static void Create () parseErrors.Add(new KeyValuePair(scriptableObject.GetType(), e)); } } - */ - /* foreach (var genericType in genericTypes.ToList()) { try @@ -138,7 +130,6 @@ public static void Create () parseErrors.Add(new KeyValuePair(genericType, e)); } } - */ foreach (var pair in assemblyResults) { @@ -249,15 +240,12 @@ public StructOrClass (Type type, string assemblyFolder) using (var writer = new StreamWriter(string.Format("{0}{1}-{2}", assemblyFolder, Size, fileName))) { writer.WriteLine("Static ({0}): {1} bytes", ParsedType, Size); - Children.Sort((a, b) => (int) (b.Size - a.Size)); + Children.Sort((a, b) => String.Compare(a.Identifier, b.Identifier)); foreach (var child in Children) { if (child.Size >= CHILD_MIN_SIZE) { child.Write(writer, childIndent); - } else - { - break; } } } @@ -289,9 +277,9 @@ public StructOrClass (UObject uObject) return; } - //if(!(uObject is ScriptableObject)) { - // return; - //} + if(!(uObject is ScriptableObject)) { + return; + } string fileName = string.Format(dumpDir + "/{0}/{1}-{2}", uObject is ScriptableObject ? "sobjects" : "uobjects", Size, @@ -301,7 +289,7 @@ public StructOrClass (UObject uObject) using (var writer = new StreamWriter(fileName)) { writer.WriteLine("{0} ({1}): {2} bytes", Identifier, ParsedType, Size); - Children.Sort((a, b) => (int) (b.Size - a.Size)); + Children.Sort((a, b) => String.Compare(a.Identifier, b.Identifier)); foreach (var child in Children) { if (child.Size >= CHILD_MIN_SIZE) @@ -430,15 +418,12 @@ void Write(StreamWriter writer, string indent) { writer.WriteLine("{0}{1} ({2}) : {3}", indent, Identifier, ParsedType, Size); } - Children.Sort((a, b) => (int) (b.Size - a.Size)); + Children.Sort((a, b) => String.Compare(a.Identifier, b.Identifier)); foreach (var child in Children) { if (child.Size >= CHILD_MIN_SIZE) { child.Write(writer, indent + childIndent); - } else - { - return; } } } From 22c4bffab998b387067e9f95f8c4675aa5f4afc2 Mon Sep 17 00:00:00 2001 From: Cole Thompson Date: Sat, 6 Feb 2021 09:31:53 -0600 Subject: [PATCH 16/23] add Stack_Trace, AuditMessageCenter; replace UnityHeapDump with HeapSnapshotCollector --- source/SimpleMetrics.cs | 114 +++++++++++++++++++++++++++++++++++----- 1 file changed, 101 insertions(+), 13 deletions(-) diff --git a/source/SimpleMetrics.cs b/source/SimpleMetrics.cs index 158db1d..43f1abb 100644 --- a/source/SimpleMetrics.cs +++ b/source/SimpleMetrics.cs @@ -7,7 +7,10 @@ using System.Text; using Harmony; using UnityEngine; +using BattleTech; using BattleTech.Data; +using BattleTech.Framework; +using UnityHeapCrawler; using static BattletechPerformanceFix.Extensions; namespace BattletechPerformanceFix @@ -24,9 +27,10 @@ public static void TrackSequence(params string[] qualifiedMethods) { class SimpleMetrics : Feature { + private const Type self = typeof(SimpleMetrics); + public static bool Active = false; public void Activate() { - var self = typeof(SimpleMetrics); Main.harmony.Patch( AccessTools.Method(typeof(BattleTech.UI.SimGameOptionsMenu), "OnAddedToHierarchy") , new HarmonyMethod(AccessTools.Method(self, "Summary"))); Main.harmony.Patch( AccessTools.Method(typeof(BattleTech.UI.SGLoadSavedGameScreen), "LoadSelectedSlot") @@ -46,33 +50,84 @@ public void Activate() { "AddFiniteSubscriber".Pre("_LogMessageCenterSubscribe"); "RemoveSubscriber".Pre("_LogMessageCenterSubscribe"); "RemoveFiniteSubscriber".Pre("_LogMessageCenterSubscribe"); + + Stack_Trace(AccessTools.Constructor(typeof(ContractObjectiveOverride), new Type[]{})); + Stack_Trace(AccessTools.Constructor(typeof(ContractObjectiveOverride), new Type[]{typeof(ContractObjectiveGameLogic)})); + + // Harmony 2.x in BT when + // would've been Stack_Trace(AccessTools.Constructor(typeof(ContractOverride), new Type[]{}, false)); + var ctor = AccessTools.FindIncludingBaseTypes(typeof(ContractOverride), + t => t.GetConstructor(AccessTools.all & ~BindingFlags.Static, null, new Type[]{}, new ParameterModifier[] { })); + Stack_Trace(ctor); + + var types = new Type[]{ typeof(string), typeof(string), typeof(string), typeof(ContractTypeValue), + typeof(GameInstance), typeof(ContractOverride), typeof(GameContext), + typeof(bool), typeof(int), typeof(int), typeof(int?)}; + Stack_Trace(AccessTools.Constructor(typeof(Contract), types)); + Stack_Trace(AccessTools.Method(typeof(Contract), "PostDeserialize")); } - public static void Update_Post() { + private static void Update_Post(UnityGameInstance __instance) { if (Input.GetKey(KeyCode.LeftShift) && Input.GetKeyDown(KeyCode.M)) { Summary(); } else if (Input.GetKey(KeyCode.LeftShift) && Input.GetKeyDown(KeyCode.D)) { - DumpHeap(); + Trap (() => DumpHeap()); + } else if (Input.GetKey(KeyCode.LeftShift) && Input.GetKeyDown(KeyCode.S)) { + Trap (() => AuditMessageCenter(__instance.Game.MessageCenter)); } } private static void DumpHeap() { + /* GC.Collect(); GC.WaitForPendingFinalizers(); GC.Collect(); - Trap (() => UnityHeapDump.Create()); - } + UnityHeapDump.Create(); + */ + + var hsc = new HeapSnapshotCollector(); + hsc.AddForbiddenTypes(new Type[]{typeof(HeapSnapshotCollector)}); + + //hsc.DifferentialMode = false; + hsc.UserRootsSettings.MinItemSize = 1024*1024; + hsc.HierarchySettings.MinItemSize = 1024*1024; + hsc.HierarchySettings.PrintOnlyGameObjects = false; + hsc.ScriptableObjectsSettings.MinItemSize = 1024*1024; + hsc.PrefabsSettings.MinItemSize = 1024*1024; + hsc.UnityObjectsSettings.MinItemSize = 1024*1024; + + hsc.AddTrackedTypes(new Type[] { + typeof(ContractObjectiveOverride), + typeof(ContractOverride), + typeof(Contract) + }); + + hsc.AddRoot(HBS.SceneSingletonBehavior.Instance, "UnityGameInstance"); - private static void _Subscriber_Pre(MessageCenter __instance, MessageCenterMessageType GUID, ReceiveMessageCenterMessage subscriber) { - _LogMessageCenterSubscribe(__instance, GUID, subscriber); + GC.Collect(); + GC.WaitForPendingFinalizers(); + GC.Collect(); + hsc.Start(); } - private static void _FiniteSubscriber_Pre(MessageCenter __instance, MessageCenterMessageType GUID, ReceiveMessageCenterMessageAutoDelete subscriber) { - _LogMessageCenterSubscribe(__instance, GUID, subscriber); + public static void AuditMessageCenter(MessageCenter mc) { + LogDebug($"MessageCenter[{mc.GetHashCode()}] messageIndex {mc.messageIndex}"); + foreach (var key in mc.messageTable.Keys) { + var subscriptions = mc.messageTable[key]; + LogDebug($" message type: {key} subscribers: {subscriptions.Count}"); + foreach (var sub in subscriptions) { + var which = (sub.Callback != null) ? "Callback" : "DeleteCallback"; + var @delegate = (sub.Callback != null) ? (Delegate) sub.Callback : (Delegate) sub.DeleteCallback; + var targetInfo = (@delegate.Target != null) ? + $"{@delegate.Target.GetType().ToString()}[{@delegate.Target.GetHashCode()}]" : + "null (static method) "; + LogDebug($" {which}[{@delegate.GetHashCode()}] Method: {sub.Name()}[{@delegate.Method.GetHashCode()}] Target: {targetInfo}"); + } + } } private static void _LogMessageCenterSubscribe(MessageCenter __instance, MessageCenterMessageType GUID, Delegate subscriber) { - var meth = new StackFrame(2).GetMethod(); + var meth = new StackFrame(1).GetMethod(); var hash = __instance.GetHashCode(); LogSpam($"Tracked[{hash}] {DateTime.Now.ToString("MM/dd/yy HH:mm:ss.ffffff")} " + $"{meth.ToString().Split("(".ToCharArray())[0]} GUID: {GUID} " + @@ -196,7 +251,6 @@ public static void Instrument(MethodBase meth) { if(PreHook == null || PostHook == null) { LogInfo("Initializing simple metrics hooks"); - var self = typeof(SimpleMetrics); PreHook = new HarmonyMethod(AccessTools.Method(self, nameof(__Pre))); PreHook.prioritiy = Priority.First; PostHook = new HarmonyMethod(AccessTools.Method(self, nameof(__Post))); @@ -221,7 +275,6 @@ public static void Track(MethodBase meth) { if(TrackHook == null) { LogInfo("Initializing tracking hooks"); - var self = typeof(SimpleMetrics); TrackHook = new HarmonyMethod(AccessTools.Method(self, nameof(__Track))); TrackHook.prioritiy = Priority.First; } @@ -235,6 +288,28 @@ public static void Track(MethodBase meth) { } } + public static void Stack_Trace(MethodBase meth) { + if (meth == null) + LogError($"Cannot instrument null meth from {new StackTrace().ToString()}"); + + LogDebug($"Tracing stack of {meth.DeclaringType.FullName}::{meth.ToString()}"); + + if(StackTraceHook == null) { + LogInfo("Initializing tracing hooks"); + + StackTraceHook = new HarmonyMethod(AccessTools.Method(self, nameof(__StackTrace))); + StackTraceHook.prioritiy = Priority.First; + } + + if (meth.IsGenericMethod || meth.IsGenericMethodDefinition) { + LogError($"Cannot instrument a generic method {meth.DeclaringType.FullName}::{meth.ToString()}"); + } else if (meth.GetMethodBody() == null) { + LogError($"Cannot instrument a method with no body {meth.DeclaringType.FullName}::{meth.ToString()}"); + } else { + Trap(() => Main.harmony.Patch(meth, StackTraceHook)); + } + } + static HarmonyMethod PreHook; static HarmonyMethod PostHook; static Dictionary Metrics = new Dictionary(); @@ -260,7 +335,19 @@ public static void __Track(object __instance) { var meth = new StackFrame(1).GetMethod(); var hash = __instance.GetHashCode(); var timestamp = DateTime.Now.ToString("MM/dd/yy HH:mm:ss.ffffff"); - LogDebug($"Tracked[{hash}] {timestamp} {meth.DeclaringType.FullName}::{meth.ToString()}"); + LogDebug($"Tracked[{hash}] {timestamp} {meth.DeclaringType.FullName}::{meth.ToString()}[{meth.GetHashCode()}]"); + } + + static HarmonyMethod StackTraceHook; + public static void __StackTrace(object __instance) { + var meth = new StackFrame(1).GetMethod(); + var hash = __instance.GetHashCode(); + var timestamp = DateTime.Now.ToString("MM/dd/yy HH:mm:ss.ffffff"); + LogDebug($"StackTrace[{hash}] {timestamp} {meth.DeclaringType.FullName}::{meth.ToString()}[{meth.GetHashCode()}]"); + var st = new StackTrace(); + foreach (var line in st.ToString().Split(new [] { '\r', '\n' })) { + LogDebug($"StackTrace[{hash}] {line}"); + } } public static void Summary() { @@ -276,3 +363,4 @@ public static void Summary() { class Metric { public long times = 0; public Stopwatch timer = new Stopwatch(); } } +// vim: ts=4:sw=4 From 6f01d6f5bfd6d89712da998ce689c7bc193679ef Mon Sep 17 00:00:00 2001 From: Cole Thompson Date: Sat, 6 Feb 2021 09:35:37 -0600 Subject: [PATCH 17/23] wee fix --- source/SimpleMetrics.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/source/SimpleMetrics.cs b/source/SimpleMetrics.cs index 43f1abb..d3492c2 100644 --- a/source/SimpleMetrics.cs +++ b/source/SimpleMetrics.cs @@ -27,7 +27,7 @@ public static void TrackSequence(params string[] qualifiedMethods) { class SimpleMetrics : Feature { - private const Type self = typeof(SimpleMetrics); + private static Type self = typeof(SimpleMetrics); public static bool Active = false; public void Activate() { From 181b0fabbbaed0d770c019194615ad6d6fd7d402 Mon Sep 17 00:00:00 2001 From: Cole Thompson Date: Fri, 12 Feb 2021 13:37:46 -0600 Subject: [PATCH 18/23] various tweaks to SimpleMetrics --- source/SimpleMetrics.cs | 25 ++++++++++++------------- 1 file changed, 12 insertions(+), 13 deletions(-) diff --git a/source/SimpleMetrics.cs b/source/SimpleMetrics.cs index d3492c2..cb0eae7 100644 --- a/source/SimpleMetrics.cs +++ b/source/SimpleMetrics.cs @@ -60,6 +60,9 @@ public void Activate() { t => t.GetConstructor(AccessTools.all & ~BindingFlags.Static, null, new Type[]{}, new ParameterModifier[] { })); Stack_Trace(ctor); + ctor = AccessTools.FindIncludingBaseTypes(typeof(Contract), + t => t.GetConstructor(AccessTools.all & ~BindingFlags.Static, null, new Type[]{}, new ParameterModifier[] { })); + Stack_Trace(ctor); var types = new Type[]{ typeof(string), typeof(string), typeof(string), typeof(ContractTypeValue), typeof(GameInstance), typeof(ContractOverride), typeof(GameContext), typeof(bool), typeof(int), typeof(int), typeof(int?)}; @@ -78,23 +81,16 @@ private static void Update_Post(UnityGameInstance __instance) { } private static void DumpHeap() { - /* - GC.Collect(); - GC.WaitForPendingFinalizers(); - GC.Collect(); - UnityHeapDump.Create(); - */ - var hsc = new HeapSnapshotCollector(); hsc.AddForbiddenTypes(new Type[]{typeof(HeapSnapshotCollector)}); - //hsc.DifferentialMode = false; - hsc.UserRootsSettings.MinItemSize = 1024*1024; - hsc.HierarchySettings.MinItemSize = 1024*1024; + hsc.DifferentialMode = false; + hsc.UserRootsSettings.MinItemSize = 1024; + hsc.HierarchySettings.MinItemSize = 1024; hsc.HierarchySettings.PrintOnlyGameObjects = false; - hsc.ScriptableObjectsSettings.MinItemSize = 1024*1024; - hsc.PrefabsSettings.MinItemSize = 1024*1024; - hsc.UnityObjectsSettings.MinItemSize = 1024*1024; + hsc.ScriptableObjectsSettings.MinItemSize = 1024; + hsc.PrefabsSettings.MinItemSize = 1024; + hsc.UnityObjectsSettings.MinItemSize = 1024; hsc.AddTrackedTypes(new Type[] { typeof(ContractObjectiveOverride), @@ -103,7 +99,10 @@ private static void DumpHeap() { }); hsc.AddRoot(HBS.SceneSingletonBehavior.Instance, "UnityGameInstance"); + hsc.AddRoot(HBS.SceneSingletonBehavior.Instance, "DataManagerUnityInstance"); + hsc.AddRootTypes(typeof(DataManager)); + Resources.UnloadUnusedAssets(); GC.Collect(); GC.WaitForPendingFinalizers(); GC.Collect(); From a5d053b64598745ca67197bedf6112574605b66c Mon Sep 17 00:00:00 2001 From: Cole Thompson Date: Fri, 12 Feb 2021 13:38:29 -0600 Subject: [PATCH 19/23] remove UnityHeapDump --- source/BattletechPerformanceFix.csproj | 1 - source/UnityHeapDump.cs | 609 ------------------------- 2 files changed, 610 deletions(-) delete mode 100644 source/UnityHeapDump.cs diff --git a/source/BattletechPerformanceFix.csproj b/source/BattletechPerformanceFix.csproj index c9efcaf..781bb91 100644 --- a/source/BattletechPerformanceFix.csproj +++ b/source/BattletechPerformanceFix.csproj @@ -76,7 +76,6 @@ - diff --git a/source/UnityHeapDump.cs b/source/UnityHeapDump.cs deleted file mode 100644 index 8bacf9d..0000000 --- a/source/UnityHeapDump.cs +++ /dev/null @@ -1,609 +0,0 @@ -using System; -using System.Collections.Generic; -using System.IO; -using System.Reflection; -using System.Runtime.InteropServices; -using System.Linq; -using UnityEngine; -using UObject = UnityEngine.Object; -using System.Text.RegularExpressions; - -// adapted from https://github.com/Zuntatos/UnityHeapDump - -public class UnityHeapDump -{ - const int TYPE_MIN_SIZE_TO_PRINT = 50*1024*1024; - const int ROOT_MIN_SIZE = TYPE_MIN_SIZE_TO_PRINT; - const int CHILD_MIN_SIZE = 1024*1024; - - static string childIndent = " "; - - static int ThreadJobsPrinting = 0; - static int ThreadJobsDone = 0; - - static HashSet genericTypes = new HashSet(); - - static String dumpDir; - - public static void Create () - { - TypeData.Start(); - ThreadJobsPrinting = 0; - ThreadJobsDone = 0; - - genericTypes = new HashSet(); - - dumpDir = $"dump_{DateTime.Now.ToString("yyyyMMdd_HHmmssfff")}"; - Directory.CreateDirectory(dumpDir); - Directory.CreateDirectory(dumpDir + "/sobjects"); - Directory.CreateDirectory(dumpDir + "/uobjects"); - Directory.CreateDirectory(dumpDir + "/statics"); - - Regex assembliesToDump = new Regex("(Assembly-CSharp,|Unity(Engine)?).*"); - - using (var logger = new StreamWriter(dumpDir + "/log.txt")) - { - Dictionary> assemblyResults = new Dictionary>(); - Dictionary assemblySizes = new Dictionary(); - List> parseErrors = new List>(); - - foreach (var assembly in AppDomain.CurrentDomain.GetAssemblies()) - { - if (!assembliesToDump.IsMatch(assembly.FullName)) { continue; } - - string assemblyFolder; - if (assembly.FullName.Contains("Assembly-CSharp")) - { - assemblyFolder = string.Format(dumpDir + "/statics/{0}/", assembly.FullName.Replace("<", "(").Replace(">", ")").Replace(".", "_")); - } else - { - assemblyFolder = dumpDir + "/statics/misc/"; - } - Directory.CreateDirectory(assemblyFolder); - List types = new List(); - foreach (var type in assembly.GetTypes()) - { - if (type.IsEnum || type.IsGenericType) - { - continue; - } - - try - { - types.Add(new StructOrClass(type, assemblyFolder)); - } - catch (Exception e) - { - parseErrors.Add (new KeyValuePair(type, e)); - } - } - assemblyResults[assembly] = types; - } - - List unityComponents = new List(); - - foreach (var go in Resources.FindObjectsOfTypeAll()) - { - foreach (var component in go.GetComponents()) - { - if (component == null) - { - continue; - } - try - { - unityComponents.Add(new StructOrClass(component)); - } - catch (Exception e) - { - parseErrors.Add(new KeyValuePair(component.GetType(), e)); - } - } - } - - List unityScriptableObjects = new List(); - - foreach (ScriptableObject scriptableObject in Resources.FindObjectsOfTypeAll()) - { - if (scriptableObject == null) - { - continue; - } - try - { - unityScriptableObjects.Add(new StructOrClass(scriptableObject)); - } - catch (Exception e) - { - parseErrors.Add(new KeyValuePair(scriptableObject.GetType(), e)); - } - } - - foreach (var genericType in genericTypes.ToList()) - { - try - { - assemblyResults[genericType.Assembly].Add(new StructOrClass(genericType, dumpDir + "/statics/misc/")); - } - catch (Exception e) - { - parseErrors.Add(new KeyValuePair(genericType, e)); - } - } - - foreach (var pair in assemblyResults) - { - assemblySizes[pair.Key] = pair.Value.Sum(a => a.Size); - pair.Value.Sort((a, b) => (int) (b.Size - a.Size)); - } - - while (ThreadJobsDone < ThreadJobsPrinting) { - System.Threading.Thread.Sleep(100); - } - - TypeData.Clear(); - - var assemblySizesList = assemblySizes.ToList(); - assemblySizesList.Sort((a, b) => (int) (b.Value - a.Value)); - long assemblySizesListSize = assemblySizesList.Sum(a => a.Value); - - unityComponents.Sort((a, b) => (int) (b.Size - a.Size)); - long unityComponentsSize = unityComponents.Sum(a => a.Size); - bool printedUnityComponents = false; - - unityScriptableObjects.Sort((a, b) => (int) (b.Size - a.Size)); - long unityScriptableObjectsSize = unityScriptableObjects.Sum(a => a.Size); - bool printedUnityScriptableObjects = false; - - logger.WriteLine("Total tracked memory (including duplicates, so too high) = {0}", assemblySizesListSize + unityComponentsSize + unityScriptableObjectsSize); - - - foreach (var pair in assemblySizesList) - { - var assembly = pair.Key; - var size = pair.Value; - - if (!printedUnityComponents && size < unityComponentsSize) - { - printedUnityComponents = true; - logger.WriteLine("Unity components of total size: {0}", unityComponentsSize); - foreach (var instance in unityComponents) - { - if (instance.Size >= TYPE_MIN_SIZE_TO_PRINT) - { - logger.WriteLine(" Type {0} (ID: {1}) of size {2}", instance.ParsedType.FullName, instance.InstanceID, instance.Size); - } - } - } - - if (!printedUnityScriptableObjects && size < unityScriptableObjectsSize) - { - printedUnityScriptableObjects = true; - logger.WriteLine("Unity scriptableobjects of total size: {0}", unityScriptableObjectsSize); - foreach (var instance in unityScriptableObjects) - { - if (instance.Size >= TYPE_MIN_SIZE_TO_PRINT) - { - logger.WriteLine(" Type {0} (ID: {1}) of size {2}", instance.ParsedType.FullName, instance.InstanceID, instance.Size); - } - } - } - - logger.WriteLine("Assembly: {0} of total size: {1}", assembly, size); - foreach (var type in assemblyResults[assembly]) - { - if (type.Size >= TYPE_MIN_SIZE_TO_PRINT) - { - logger.WriteLine(" Type: {0} of size {1}", type.ParsedType.FullName, type.Size); - } - } - } - foreach (var error in parseErrors) - { - logger.WriteLine(error); - } - } - } - - class StructOrClass - { - public long Size { get; private set; } - public Type ParsedType { get; private set; } - public int InstanceID { get; private set; } - int ArraySize { get; set; } - string Identifier { get; set; } - - List Children = new List(); - - /// - /// Parse static types - /// - public StructOrClass (Type type, string assemblyFolder) - { - ParsedType = type; - HashSet seenObjects = new HashSet(); - Identifier = type.FullName; - foreach (var fieldInfo in type.GetFields(BindingFlags.Static | BindingFlags.Public | BindingFlags.NonPublic)) - { - ParseField(fieldInfo, null, seenObjects); - } - if (Size < ROOT_MIN_SIZE) - { - return; - } - - System.Threading.Interlocked.Increment(ref ThreadJobsPrinting); - System.Threading.ThreadPool.QueueUserWorkItem (obj => { - try - { - string fileName = Identifier.Replace("<", "(").Replace(">", ")").Replace(".", "_"); - using (var writer = new StreamWriter(string.Format("{0}{1}-{2}", assemblyFolder, Size, fileName))) - { - writer.WriteLine("Static ({0}): {1} bytes", ParsedType, Size); - Children.Sort((a, b) => String.Compare(a.Identifier, b.Identifier)); - foreach (var child in Children) - { - if (child.Size >= CHILD_MIN_SIZE) - { - child.Write(writer, childIndent); - } - } - } - } - finally - { - System.Threading.Interlocked.Increment(ref ThreadJobsDone); - } - }); - } - - /// - /// Parse monobehaviour and scriptableobject instances - /// - public StructOrClass (UObject uObject) - { - InstanceID = uObject.GetInstanceID(); - ParsedType = uObject.GetType(); - Identifier = uObject.name + uObject.GetInstanceID(); - HashSet seenObjects = new HashSet(); - - foreach (var field in ParsedType.GetFields(BindingFlags.Instance | BindingFlags.Public | BindingFlags.NonPublic)) - { - ParseField(field, uObject, seenObjects); - } - - if (Size < ROOT_MIN_SIZE) - { - return; - } - - if(!(uObject is ScriptableObject)) { - return; - } - string fileName = string.Format(dumpDir + "/{0}/{1}-{2}", - uObject is ScriptableObject ? "sobjects" : "uobjects", - Size, - Identifier.Replace("<", "(").Replace(">", ")").Replace(".", "_") - ); - - using (var writer = new StreamWriter(fileName)) - { - writer.WriteLine("{0} ({1}): {2} bytes", Identifier, ParsedType, Size); - Children.Sort((a, b) => String.Compare(a.Identifier, b.Identifier)); - foreach (var child in Children) - { - if (child.Size >= CHILD_MIN_SIZE) - { - child.Write(writer, childIndent); - } - } - } - } - - /// - /// Parse field objects; only called for arrays, references and structs with references in them - /// - StructOrClass(string name, object root, TypeData rootTypeData, HashSet seenObjects) - { - Identifier = name; - ParsedType = root.GetType(); - Size = rootTypeData.Size; - if (ParsedType.IsArray) - { - int i = 0; - ArraySize = GetTotalLength((Array)root); - Type elementType = ParsedType.GetElementType(); - TypeData elementTypeData = TypeData.Get(elementType); - if (elementType.IsValueType || elementType.IsPrimitive || elementType.IsEnum) - { - if (elementTypeData.DynamicSizedFields == null) - { - Size += elementTypeData.Size * ArraySize; - return; - } - - foreach (var item in (Array)root) - { - StructOrClass child = new StructOrClass((i++).ToString(), item, elementTypeData, seenObjects); - Size += child.Size; - Children.Add(child); - } - } - else - { - Size += IntPtr.Size * ArraySize; - foreach (var item in (Array)root) - { - ParseItem(item, (i++).ToString(), seenObjects); - } - } - } else - { - if (rootTypeData.DynamicSizedFields != null) - { - foreach (var fieldInfo in rootTypeData.DynamicSizedFields) - { - ParseField(fieldInfo, root, seenObjects); - } - } - } - } - - /// - /// Parse the field of the object, ignoring any seenObjects. If root is null, it is considered a static field. - /// - void ParseField(FieldInfo fieldInfo, object root, HashSet seenObjects) - { - if (!fieldInfo.FieldType.IsPointer) - { - ParseItem(fieldInfo.GetValue(root), fieldInfo.Name, seenObjects); - } - } - - void ParseItem (object obj, string objName, HashSet seenObjects) - { - if (obj == null) - { - return; - } - Type type = obj.GetType(); - if (type.IsPointer) - { - return; // again, a pointer cast to whatever the fieldtype is, shoo. - } - if (type == typeof(string)) - { - // string needs special handling - int strSize = 3 * IntPtr.Size + 2; - strSize += ((string)(obj)).Length * sizeof(char); - int pad = strSize % IntPtr.Size; - if (pad != 0) - { - strSize += IntPtr.Size - pad; - } - Size += strSize; - return; - } - // obj is not null, and a primitive/enum/array/struct/class - TypeData fieldTypeData = TypeData.Get(type); - if (type.IsClass || type.IsArray || fieldTypeData.DynamicSizedFields != null) - { - // class, array, or struct with pointers - if (!(type.IsPrimitive || type.IsValueType || type.IsEnum)) - { - if (!seenObjects.Add(obj)) - { - return; - } - } - - StructOrClass child = new StructOrClass(objName, obj, fieldTypeData, seenObjects); - Size += child.Size; - Children.Add(child); - return; - } - else - { - // primitive, enum, or a struct without pointers, embed it in parent - Size += fieldTypeData.Size; - } - } - - void Write(StreamWriter writer, string indent) - { - if (ParsedType.IsArray) - { - writer.WriteLine("{0}{1} ({2}:{3}) : {4}", indent, Identifier, ParsedType, ArraySize, Size); - } else - { - writer.WriteLine("{0}{1} ({2}) : {3}", indent, Identifier, ParsedType, Size); - } - Children.Sort((a, b) => String.Compare(a.Identifier, b.Identifier)); - foreach (var child in Children) - { - if (child.Size >= CHILD_MIN_SIZE) - { - child.Write(writer, indent + childIndent); - } - } - } - - static int GetTotalLength(Array val) - { - int sum = val.GetLength(0); - for (int i = 1; i < val.Rank; i++) - { - sum *= val.GetLength(i); - } - return sum; - } - } - - public class TypeData - { - public long Size { get; private set; } - public List DynamicSizedFields { get; private set; } - - static Dictionary seenTypeData; - static Dictionary seenTypeDataNested; - - public static void Clear() - { - seenTypeData = null; - } - - public static void Start() - { - seenTypeData = new Dictionary(); - seenTypeDataNested = new Dictionary(); - } - - public static TypeData Get(Type type) - { - TypeData data; - if (!seenTypeData.TryGetValue(type, out data)) - { - data = new TypeData(type); - seenTypeData[type] = data; - } - return data; - } - - public static TypeData GetNested(Type type) - { - TypeData data; - if (!seenTypeDataNested.TryGetValue(type, out data)) - { - data = new TypeData(type, true); - seenTypeDataNested[type] = data; - } - return data; - } - - public TypeData(Type type, bool nested = false) - { - if (type.IsGenericType) - { - genericTypes.Add(type); - } - Type baseType = type.BaseType; - if (baseType != null - && baseType != typeof(object) - && baseType != typeof(ValueType) - && baseType != typeof(Array) - && baseType != typeof(Enum)) - { - TypeData baseTypeData = GetNested(baseType); - Size += baseTypeData.Size; - - if (baseTypeData.DynamicSizedFields != null) - { - DynamicSizedFields = new List(baseTypeData.DynamicSizedFields); - } - } - if (type.IsPointer) - { - Size = IntPtr.Size; - } - else if (type.IsArray) - { - Type elementType = type.GetElementType(); - Size = ((elementType.IsValueType || elementType.IsPrimitive || elementType.IsEnum) ? 3 : 4) * IntPtr.Size; - } - else if (type.IsPrimitive) - { - Size = Marshal.SizeOf(type); - } - else if (type.IsEnum) - { - Size = Marshal.SizeOf(Enum.GetUnderlyingType(type)); - } - else // struct, class - { - if (!nested && type.IsClass) - { - Size = 2 * IntPtr.Size; - } - foreach (var field in type.GetFields(BindingFlags.Instance | BindingFlags.Public | BindingFlags.NonPublic)) - { - ProcessField(field, field.FieldType); - } - if (!nested && type.IsClass) - { - Size = Math.Max(3 * IntPtr.Size, Size); - int pad = (int) (Size % IntPtr.Size); - if (pad != 0) - { - Size += IntPtr.Size - pad; - } - } - } - } - - void ProcessField(FieldInfo field, Type fieldType) - { - if (IsStaticallySized(fieldType)) - { - Size += GetStaticSize(fieldType); - } - else - { - if (!(fieldType.IsValueType || fieldType.IsPrimitive || fieldType.IsEnum)) - { - Size += IntPtr.Size; - } - if (fieldType.IsPointer) - { - return; - } - if (DynamicSizedFields == null) - { - DynamicSizedFields = new List(); - } - DynamicSizedFields.Add(field); - } - } - - static bool IsStaticallySized(Type type) - { - - if (type.IsPointer || type.IsArray || type.IsClass || type.IsInterface) - { - return false; - } - if (type.IsPrimitive || type.IsEnum) - { - return true; - } - foreach (var nestedField in type.GetFields(BindingFlags.Instance | BindingFlags.Public | BindingFlags.NonPublic)) - { - if (!IsStaticallySized(nestedField.FieldType)) - { - return false; - } - } - return true; - } - - /// - /// Gets size of type. Assumes IsStaticallySized (type) is true. (primitive, enum, {struct or class with no references in it}) - /// - static int GetStaticSize(Type type) - { - if (type.IsPrimitive) - { - return Marshal.SizeOf(type); - } - if (type.IsEnum) - { - return Marshal.SizeOf(Enum.GetUnderlyingType(type)); - } - int size = 0; - foreach (var nestedField in type.GetFields(BindingFlags.Instance | BindingFlags.Public | BindingFlags.NonPublic)) - { - size += GetStaticSize(nestedField.FieldType); - } - return size; - } - } -} From 3dc060ce9f7c7cb3b5dc494519005520f295ce06 Mon Sep 17 00:00:00 2001 From: Cole Thompson Date: Sun, 21 Feb 2021 12:05:04 -0600 Subject: [PATCH 20/23] use Ctrl+Shift+$foo for triggering metrics, other small changes --- source/SimpleMetrics.cs | 44 ++++++++++++++++++++++++++++------------- 1 file changed, 30 insertions(+), 14 deletions(-) diff --git a/source/SimpleMetrics.cs b/source/SimpleMetrics.cs index cb0eae7..2aff674 100644 --- a/source/SimpleMetrics.cs +++ b/source/SimpleMetrics.cs @@ -71,12 +71,14 @@ public void Activate() { } private static void Update_Post(UnityGameInstance __instance) { - if (Input.GetKey(KeyCode.LeftShift) && Input.GetKeyDown(KeyCode.M)) { - Summary(); - } else if (Input.GetKey(KeyCode.LeftShift) && Input.GetKeyDown(KeyCode.D)) { - Trap (() => DumpHeap()); - } else if (Input.GetKey(KeyCode.LeftShift) && Input.GetKeyDown(KeyCode.S)) { - Trap (() => AuditMessageCenter(__instance.Game.MessageCenter)); + if (Input.GetKey(KeyCode.LeftShift) && Input.GetKey(KeyCode.LeftControl)) { + if (Input.GetKeyDown(KeyCode.M)) { + Summary(); + } else if (Input.GetKeyDown(KeyCode.D)) { + Trap (() => DumpHeap()); + } else if (Input.GetKeyDown(KeyCode.S)) { + Trap (() => AuditMessageCenter(__instance.Game.MessageCenter)); + } } } @@ -84,18 +86,32 @@ private static void DumpHeap() { var hsc = new HeapSnapshotCollector(); hsc.AddForbiddenTypes(new Type[]{typeof(HeapSnapshotCollector)}); + //hsc.UserRootsSettings.Enabled = false; + //hsc.HierarchySettings.Enabled = false; + //hsc.ScriptableObjectsSettings.Enabled = false; + //hsc.PrefabsSettings.Enabled = false; + //hsc.UnityObjectsSettings.Enabled = false; + hsc.DifferentialMode = false; - hsc.UserRootsSettings.MinItemSize = 1024; - hsc.HierarchySettings.MinItemSize = 1024; + hsc.UserRootsSettings.MinItemSize = 128*1024; + hsc.HierarchySettings.MinItemSize = 128*1024; hsc.HierarchySettings.PrintOnlyGameObjects = false; - hsc.ScriptableObjectsSettings.MinItemSize = 1024; - hsc.PrefabsSettings.MinItemSize = 1024; - hsc.UnityObjectsSettings.MinItemSize = 1024; + hsc.ScriptableObjectsSettings.MinItemSize = 128*1024; + hsc.PrefabsSettings.MinItemSize = 128*1024; + hsc.UnityObjectsSettings.MinItemSize = 128*1024; hsc.AddTrackedTypes(new Type[] { - typeof(ContractObjectiveOverride), - typeof(ContractOverride), - typeof(Contract) + typeof(Contract), + typeof(CombatGameState), + typeof(UnitSpawnPointGameLogic), + typeof(MapEncounterLayerDataCell[,]), + typeof(MapEncounterLayerDataCell[]), + typeof(MapTerrainDataCell[,]), + typeof(MapTerrainDataCell[]), + typeof(BuildingRaycastHit[]), + typeof(ObstructionGameLogic), + typeof(RegionGameLogic), + typeof(Pilot), }); hsc.AddRoot(HBS.SceneSingletonBehavior.Instance, "UnityGameInstance"); From aa887e0a09ad18807eb0fa0b8e315cc142f915dd Mon Sep 17 00:00:00 2001 From: Cole Thompson Date: Sun, 21 Feb 2021 12:14:32 -0600 Subject: [PATCH 21/23] make SimpleMetrics off by default --- source/Main.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/source/Main.cs b/source/Main.cs index 6e7b297..bca5816 100644 --- a/source/Main.cs +++ b/source/Main.cs @@ -102,7 +102,7 @@ public static void Start(string modDirectory, string json) { typeof(VersionManifestPatches), true }, { typeof(MemoryLeakFix), true }, { typeof(EnableConsole), false }, - { typeof(SimpleMetrics), true } + { typeof(SimpleMetrics), false } }; From d248019037cb05af401260e23c38dcadb9625690 Mon Sep 17 00:00:00 2001 From: Cole Thompson Date: Sun, 21 Feb 2021 12:17:44 -0600 Subject: [PATCH 22/23] remove unrelated changes (really oughtn't have used rebase...) --- README.md | 3 - source/BattletechPerformanceFix.csproj | 2 - source/Main.cs | 2 - source/MemoryLeakFix.cs | 265 ------------------------- source/NavigationMapFilterLagFix.cs | 60 ------ 5 files changed, 332 deletions(-) delete mode 100644 source/MemoryLeakFix.cs delete mode 100644 source/NavigationMapFilterLagFix.cs diff --git a/README.md b/README.md index cb6c27c..5ce6626 100644 --- a/README.md +++ b/README.md @@ -35,9 +35,6 @@ - Make all simgame room transitions instant. - RemovedContractsFix - This fix removes invalid contracts allowing saves to load if a user created contract was removed from the mods in use. -- NavigationMapFilterLagFix - - This fix removes some unnecessary code in the navigation map's difficulty filter selection. - - The performance gain is negligible in vanilla, but in modpacks using the entire IS map such as RT or BTA, this saves a few seconds. # Experimental patches - MDDB_TagsetQueryInChunks diff --git a/source/BattletechPerformanceFix.csproj b/source/BattletechPerformanceFix.csproj index 781bb91..f359a40 100644 --- a/source/BattletechPerformanceFix.csproj +++ b/source/BattletechPerformanceFix.csproj @@ -53,7 +53,6 @@ - @@ -74,7 +73,6 @@ - diff --git a/source/Main.cs b/source/Main.cs index bca5816..2df0921 100644 --- a/source/Main.cs +++ b/source/Main.cs @@ -91,7 +91,6 @@ public static void Start(string modDirectory, string json) { typeof(DataLoaderGetEntryCheck), true }, { typeof(ShopTabLagFix), true }, { typeof(ContractLagFix), true }, - { typeof(NavigationMapFilterLagFix), true }, { typeof(EnableLoggingDuringLoads), true }, { typeof(ExtraLogging), true }, { typeof(ShaderDependencyOverride), true }, @@ -100,7 +99,6 @@ public static void Start(string modDirectory, string json) { typeof(DisableSimAnimations), false }, { typeof(RemovedContractsFix), true }, { typeof(VersionManifestPatches), true }, - { typeof(MemoryLeakFix), true }, { typeof(EnableConsole), false }, { typeof(SimpleMetrics), false } }; diff --git a/source/MemoryLeakFix.cs b/source/MemoryLeakFix.cs deleted file mode 100644 index c132066..0000000 --- a/source/MemoryLeakFix.cs +++ /dev/null @@ -1,265 +0,0 @@ -using System; -using System.Diagnostics; -using System.Reflection; -using System.Reflection.Emit; -using System.Collections; -using System.Collections.Generic; -using System.Linq; -using Harmony; -using static BattletechPerformanceFix.Extensions; -using BattleTech; -using BattleTech.Analytics.Sim; -using BattleTech.Framework; -using BattleTech.Save; -using BattleTech.Save.Test; -using BattleTech.UI; -using Localize; - -namespace BattletechPerformanceFix -{ - class MemoryLeakFix: Feature - { - private static Type self = typeof(MemoryLeakFix); - - public void Activate() { - // fixes group 1: occurs on save file load - // fix 1.1: allow the BattleTechSimAnalytics class to properly remove its message subscriptions - "BeginSession".Transpile("Session_Transpile"); - "EndSession".Transpile("Session_Transpile"); - // fix 1.2: add a RemoveSubscriber() for a message type that never had one to begin with - "OnSimGameInitializeComplete".Post(); - // fix 1.3: remove OnLanguageChanged subscriptions for these objects, which never unsub and therefore leak. - // b/c the user must drop back to main menu to change the language, there's no reason - // to use these in the first place (objects are created in-game and never on the main menu) - // Contract - var contractCtorTypes = new Type[]{typeof(string), typeof(string), typeof(string), typeof(ContractTypeValue), - typeof(GameInstance), typeof(ContractOverride), typeof(GameContext), - typeof(bool), typeof(int), typeof(int), typeof(int)}; - Main.harmony.Patch(AccessTools.Constructor(typeof(Contract), contractCtorTypes), - null, null, new HarmonyMethod(self, "Contract_ctor_Transpile")); - "PostDeserialize".Transpile(); - // ContractObjectiveOverride - Main.harmony.Patch(AccessTools.Constructor(typeof(ContractObjectiveOverride), new Type[]{}), - null, null, new HarmonyMethod(self, "ContractObjectiveOverride_ctor_Transpile")); - var cooCtorTypes = new Type[]{typeof(ContractObjectiveGameLogic)}; - Main.harmony.Patch(AccessTools.Constructor(typeof(ContractObjectiveOverride), cooCtorTypes), - null, null, new HarmonyMethod(self, "ContractObjectiveOverride_ctor_cogl_Transpile")); - // ObjectiveOverride - Main.harmony.Patch(AccessTools.Constructor(typeof(ObjectiveOverride), new Type[]{}), - null, null, new HarmonyMethod(self, "ObjectiveOverride_ctor_Transpile")); - var ooCtorTypes = new Type[]{typeof(ObjectiveGameLogic)}; - Main.harmony.Patch(AccessTools.Constructor(typeof(ObjectiveOverride), ooCtorTypes), - null, null, new HarmonyMethod(self, "ObjectiveOverride_ctor_ogl_Transpile")); - // DialogueContentOverride - Main.harmony.Patch(AccessTools.Constructor(typeof(DialogueContentOverride), new Type[]{}), - null, null, new HarmonyMethod(self, "DialogueContentOverride_ctor_Transpile")); - var dcoCtorTypes = new Type[]{typeof(DialogueContent)}; - Main.harmony.Patch(AccessTools.Constructor(typeof(DialogueContentOverride), dcoCtorTypes), - null, null, new HarmonyMethod(self, "DialogueContentOverride_ctor_dc_Transpile")); - // InterpolatedText - "Init".Transpile(); - // these finalizers could never run to begin with, and they only did RemoveSubscriber; nop them - // FIXME? may need to nop out specifically the call to RemoveSubscriber (test this) - "Finalize".Transpile("TranspileNopAll"); - "Finalize".Transpile("TranspileNopAll"); - "Finalize".Transpile("TranspileNopAll"); - "Finalize".Transpile("TranspileNopAll"); - "Finalize".Transpile("TranspileNopAll"); - - // fixes group 2: occurs on entering/exiting a contract - // fix 2.1: none of these classes need to store a CombatGameState - "ContractInitialize".Post("DialogueContent_ContractInitialize_Post"); - "ContractInitialize".Post("ConversationContent_ContractInitialize_Post"); - "ContractInitialize".Post("DialogBucketDef_ContractInitialize_Post"); - - // fixes group 3: occurs on creating a new savefile - // fix 3.1: clean up the GameInstanceSave.references after serialization is complete - "PostSerialization".Post(); - } - - private static IEnumerable Session_Transpile(IEnumerable ins) - { - var meth = AccessTools.Method(self, "_UpdateMessageSubscriptions"); - return TranspileReplaceCall(ins, "UpdateMessageSubscriptions", meth); - } - - private static void _UpdateMessageSubscriptions(BattleTechSimAnalytics __instance, bool subscribe) - { - LogSpam($"{new StackTrace().GetFrame(0).GetMethod()} called"); - var mc = __instance.messageCenter; - if (mc != null) { - mc.Subscribe(MessageCenterMessageType.OnReportMechwarriorSkillUp, - new ReceiveMessageCenterMessage(__instance.ReportMechWarriorSkilledUp), subscribe); - mc.Subscribe(MessageCenterMessageType.OnReportMechwarriorHired, - new ReceiveMessageCenterMessage(__instance.ReportMechWarriorHired), subscribe); - mc.Subscribe(MessageCenterMessageType.OnReportMechWarriorKilled, - new ReceiveMessageCenterMessage(__instance.ReportMechWarriorKilled), subscribe); - mc.Subscribe(MessageCenterMessageType.OnReportShipUpgradePurchased, - new ReceiveMessageCenterMessage(__instance.ReportShipUpgradePurchased), subscribe); - mc.Subscribe(MessageCenterMessageType.OnSimGameContractComplete, - new ReceiveMessageCenterMessage(__instance.ReportContractComplete), subscribe); - mc.Subscribe(MessageCenterMessageType.OnSimRoomStateChanged, - new ReceiveMessageCenterMessage(__instance.ReportSimGameRoomChange), subscribe); - } - } - - private static void OnSimGameInitializeComplete_Post(SimGameUXCreator __instance) - { - LogSpam($"{new StackTrace().GetFrame(0).GetMethod()} called"); - __instance.sim.MessageCenter.RemoveSubscriber( - MessageCenterMessageType.OnSimGameInitialized, - new ReceiveMessageCenterMessage(__instance.OnSimGameInitializeComplete)); - } - - private static IEnumerable Contract_ctor_Transpile(IEnumerable ins) - { - LogSpam($"{new StackTrace().GetFrame(0).GetMethod()} called"); - return TranspileNopIndicesRange(ins, 125, 134); - } - - private static IEnumerable PostDeserialize_Transpile(IEnumerable ins) - { - LogSpam($"{new StackTrace().GetFrame(0).GetMethod()} called"); - return TranspileNopIndicesRange(ins, 21, 27); - } - - private static IEnumerable - ContractObjectiveOverride_ctor_Transpile(IEnumerable ins) - { - LogSpam($"{new StackTrace().GetFrame(0).GetMethod()} called"); - return TranspileNopIndicesRange(ins, 5, 14); - } - - private static IEnumerable - ContractObjectiveOverride_ctor_cogl_Transpile(IEnumerable ins) - { - LogSpam($"{new StackTrace().GetFrame(0).GetMethod()} called"); - return TranspileNopIndicesRange(ins, 9, 18); - } - - private static IEnumerable - ObjectiveOverride_ctor_Transpile(IEnumerable ins) - { - LogSpam($"{new StackTrace().GetFrame(0).GetMethod()} called"); - return TranspileNopIndicesRange(ins, 8, 17); - } - - private static IEnumerable - ObjectiveOverride_ctor_ogl_Transpile(IEnumerable ins) - { - LogSpam($"{new StackTrace().GetFrame(0).GetMethod()} called"); - return TranspileNopIndicesRange(ins, 12, 21); - } - - private static IEnumerable - DialogueContentOverride_ctor_Transpile(IEnumerable ins) - { - LogSpam($"{new StackTrace().GetFrame(0).GetMethod()} called"); - return TranspileNopIndicesRange(ins, 23, 32); - } - - private static IEnumerable - DialogueContentOverride_ctor_dc_Transpile(IEnumerable ins) - { - LogSpam($"{new StackTrace().GetFrame(0).GetMethod()} called"); - return TranspileNopIndicesRange(ins, 60, 69); - } - - private static IEnumerable Init_Transpile(IEnumerable ins) - { - LogSpam($"{new StackTrace().GetFrame(0).GetMethod()} called"); - return TranspileNopIndicesRange(ins, 3, 10); - } - - private static IEnumerable - TranspileNopIndicesRange(IEnumerable ins, int startIndex, int endIndex) - { - LogDebug($"TranspileNopIndicesRange: nopping indices {startIndex}-{endIndex}"); - if (endIndex < startIndex || startIndex < 0) { - LogError($"TranspileNopIndicesRange: invalid use with startIndex = {startIndex}," + - $" endIndex = {endIndex} (transpiled method remains unmodified)"); - return ins; - } - - var code = ins.ToList(); - try { - for (int i = startIndex; i <= endIndex; i++) { - code[i].opcode = OpCodes.Nop; - code[i].operand = null; - } - return code.AsEnumerable(); - } catch (ArgumentOutOfRangeException ex) { - LogError($"TranspileNopIndicesRange: {ex.Message} (transpiled method remains unmodified)"); - return ins; - } - } - - private static IEnumerable - TranspileReplaceCall(IEnumerable ins, string originalMethodName, - MethodInfo replacementMethod) - { - LogInfo($"TranspileReplaceCall: {originalMethodName} -> {replacementMethod.ToString()}"); - return ins.SelectMany(i => { - if (i.opcode == OpCodes.Call && - (i.operand as MethodInfo).Name.StartsWith(originalMethodName)) { - i.operand = replacementMethod; - } - return Sequence(i); - }); - } - - private static IEnumerable - TranspileReplaceOverloadedCall(IEnumerable ins, Type originalMethodClass, - string originalMethodName, Type[] originalParamTypes, - MethodInfo replacementMethod) - { - LogInfo($"TranspileReplaceOverloadedCall: {originalMethodClass.ToString()}.{originalMethodName}" + - $"({String.Concat(originalParamTypes.Select(x => x.ToString()))}) -> {replacementMethod.ToString()}"); - return ins.SelectMany(i => { - var methInfo = i.operand as MethodInfo; - if (i.opcode == OpCodes.Callvirt && - methInfo.DeclaringType == originalMethodClass && - methInfo.Name.StartsWith(originalMethodName) && - Enumerable.SequenceEqual(methInfo.GetParameters().Select(x => x.ParameterType), originalParamTypes)) - { - i.operand = replacementMethod; - } - return Sequence(i); - }); - } - - private static IEnumerable TranspileNopAll(IEnumerable ins) - { - return ins.SelectMany(i => { - i.opcode = OpCodes.Nop; - i.operand = null; - return Sequence(i); - }); - } - - private static void DialogueContent_ContractInitialize_Post(DialogueContent __instance) - { - LogSpam($"{new StackTrace().GetFrame(0).GetMethod()} called"); - __instance.combat = null; - } - - private static void ConversationContent_ContractInitialize_Post(ConversationContent __instance) - { - LogSpam($"{new StackTrace().GetFrame(0).GetMethod()} called"); - __instance.combat = null; - } - - private static void DialogBucketDef_ContractInitialize_Post(DialogBucketDef __instance) - { - LogSpam($"{new StackTrace().GetFrame(0).GetMethod()} called"); - __instance.combat = null; - } - - private static void PostSerialization_Post(GameInstanceSave __instance) - { - LogSpam($"{new StackTrace().GetFrame(0).GetMethod()} called"); - __instance.references = new SerializableReferenceContainer("the one and only"); - } - } -} -// vim: ts=4:sw=4 diff --git a/source/NavigationMapFilterLagFix.cs b/source/NavigationMapFilterLagFix.cs deleted file mode 100644 index b3d43ab..0000000 --- a/source/NavigationMapFilterLagFix.cs +++ /dev/null @@ -1,60 +0,0 @@ -using System.Collections.Generic; -using System.Linq; -using BattleTech.UI; -using Harmony; -using System.Reflection; -using System.Reflection.Emit; -using static BattletechPerformanceFix.Extensions; - -namespace BattletechPerformanceFix -{ - /* Removes a redundant call for creating difficulty callouts, saves a few seconds per filter selection */ - class NavigationMapFilterLagFix : Feature - { - public void Activate() - { - var method = AccessTools.Method(typeof(SGNavigationScreen), "OnDifficultySelectionChanged"); - var transpiler = new HarmonyMethod(typeof(NavigationMapFilterLagFix), nameof(Transpiler)); - Main.harmony.Patch(method, null, null, transpiler); - } - - // cuts out the if/else block in OnDifficultySelectionChanged() because CreateDifficultyCallouts() - // is called later by RefreshSystemIndicators() anyways just after the block - private static IEnumerable Transpiler(IEnumerable instructions) - { - int startIndex = -1; - int endIndex = -1; - - // searches for the if block start & end points, in case some other mod is modifying this function - // (which is why we don't hardcode the indices) - var code = instructions.ToList(); - for (int i = 0; i < code.Count-1; i++) { - if (startIndex == -1) { - if (code[i].opcode == OpCodes.Ldarg_1 && code[i+1].opcode == OpCodes.Brtrue) { - startIndex = i; - } - } else { - if (code[i].opcode == OpCodes.Ldarg_1 && code[i+1].opcode == OpCodes.Call && - (code[i+1].operand as MethodInfo).Name == "CreateDifficultyCallouts") { - endIndex = i+1; - break; - } - } - } - - if (startIndex != -1 && endIndex != -1) { - LogDebug("Overwriting instructions in SGNavigationScreen.OnDifficultySelectionChanged()" + - " at indices " + startIndex + "-" + endIndex + " with nops"); - for (int i = startIndex; i <= endIndex; i++) { - code[i].opcode = OpCodes.Nop; - } - } else { - LogError("Failed to find the code to overwrite in " + - "SGNavigationScreen.OnDifficultySelectionChanged(); no changes were made."); - LogError("NavigationMapFilterLagFix has not been applied, report this as a bug"); - } - - return code.AsEnumerable(); - } - } -} From c11a54d2ebe74a2c71bcd4b94277b8d7963141f0 Mon Sep 17 00:00:00 2001 From: Cole Thompson Date: Sun, 21 Feb 2021 12:21:38 -0600 Subject: [PATCH 23/23] update readme --- README.md | 2 ++ 1 file changed, 2 insertions(+) diff --git a/README.md b/README.md index 5ce6626..1baf2fc 100644 --- a/README.md +++ b/README.md @@ -39,6 +39,8 @@ # Experimental patches - MDDB_TagsetQueryInChunks - Prevent the game from softlocking/infinite loading when MDDB builds queries too large for sqlite3 to handle. +- SimpleMetrics + - Performance measurement tools used by the mod developers. Unless you know what you're doing, leave this set to false. # Upcoming patches - ParallelizeLoad (Disabled by default: Experimental, Dangerous)