From 46a785dea457911c88f6367b0d5066c0f17766c7 Mon Sep 17 00:00:00 2001 From: Phantomical Date: Wed, 22 Oct 2025 19:31:32 -0700 Subject: [PATCH 1/3] perf: Optimize `MonoUtilities.RefreshContextWindows` This calls `UnityEngine.Object.GetObjectsOfType` which is terribly slow. It is not used anywhere in stock which is probably why it hasn't been patched yet. However, several mods do make use of it. On my KSP instance this saves > 1s on every scene switch across a few different mods. The improvement here is to maintain our own list of active UIPartActionWindow instances and use that list instead of calling `GetObjectsOfType`. It proactively removes all dead instances when one is deleted. Any change that results in `UIPartActionWindow.OnDestroy` not being called will result in a memory leak anyways so I suspect this is a non-issue. --- GameData/KSPCommunityFixes/Settings.cfg | 3 + KSPCommunityFixes/KSPCommunityFixes.csproj | 1 + .../Performance/MonoUtilitiesCache.cs | 63 +++++++++++++++++++ 3 files changed, 67 insertions(+) create mode 100644 KSPCommunityFixes/Performance/MonoUtilitiesCache.cs diff --git a/GameData/KSPCommunityFixes/Settings.cfg b/GameData/KSPCommunityFixes/Settings.cfg index 5930b88..c0bc001 100644 --- a/GameData/KSPCommunityFixes/Settings.cfg +++ b/GameData/KSPCommunityFixes/Settings.cfg @@ -492,6 +492,9 @@ KSP_COMMUNITY_FIXES // Improve the responsiveness of the part list when switching between categories, sorting and searching by tag. FasterEditorPartList = true + // Improve performance when mods try to refresh all part info windows on a part. + MonoUtilitiesCache = true + // ########################## // Modding // ########################## diff --git a/KSPCommunityFixes/KSPCommunityFixes.csproj b/KSPCommunityFixes/KSPCommunityFixes.csproj index 5b2be6c..a2ed25c 100644 --- a/KSPCommunityFixes/KSPCommunityFixes.csproj +++ b/KSPCommunityFixes/KSPCommunityFixes.csproj @@ -219,6 +219,7 @@ + diff --git a/KSPCommunityFixes/Performance/MonoUtilitiesCache.cs b/KSPCommunityFixes/Performance/MonoUtilitiesCache.cs new file mode 100644 index 0000000..7b8647d --- /dev/null +++ b/KSPCommunityFixes/Performance/MonoUtilitiesCache.cs @@ -0,0 +1,63 @@ +using System.Collections.Generic; + +namespace KSPCommunityFixes +{ + // MonoUtilites.RefreshContextWindows calls Object.FindObjectsOfType. + // This is quite slow but doesn't show up in stock because the method isn't + // used anywhere in the KSP codebase. Several mods, however, do make use of + // it and this can account for a notable chunk of scene switch times. + // + // We patch UIPartActionWindow to indpendently track live instances and + // then patch RefreshContextWindows to just use the list we are tracking. + class MonoUtilitiesCache : BasePatch + { + static readonly List PartActionWindows = new List(); + + protected override void ApplyPatches() + { + AddPatch(PatchType.Postfix, typeof(UIPartActionWindow), nameof(UIPartActionWindow.Awake)); + AddPatch(PatchType.Postfix, typeof(UIPartActionWindow), nameof(UIPartActionWindow.OnDestroy)); + AddPatch(PatchType.Override, typeof(MonoUtilities), nameof(MonoUtilities.RefreshContextWindows)); + } + + static void UIPartActionWindow_Awake_Postfix(UIPartActionWindow __instance) + { + PartActionWindows.Add(__instance); + } + + static void UIPartActionWindow_OnDestroy_Postfix(UIPartActionWindow __instance) + { + int i = 0; + int j = 0; + var paws = PartActionWindows; + int count = paws.Count; + + for (; i < count; ++i) + { + var paw = paws[i]; + if (ReferenceEquals(paw, __instance) || paw == null) + continue; + + paws[j++] = paw; + } + + paws.RemoveRange(j, count - j); + } + + static void MonoUtilities_RefreshContextWindows_Override(Part part) + { + var paws = PartActionWindows; + int count = paws.Count; + + for (int i = 0; i < count; ++i) + { + var paw = paws[i]; + if (paw == null) + continue; + if (paw.part != part) + continue; + paw.displayDirty = true; + } + } + } +} \ No newline at end of file From 80005694b7498f1f0f8f5ce450feec878e2ea094 Mon Sep 17 00:00:00 2001 From: Phantomical Date: Thu, 23 Oct 2025 02:20:59 -0700 Subject: [PATCH 2/3] Simplify and move patch into MinorPerfTweaks --- GameData/KSPCommunityFixes/Settings.cfg | 3 - KSPCommunityFixes/KSPCommunityFixes.csproj | 1 - .../Performance/MinorPerfTweaks.cs | 12 ++++ .../Performance/MonoUtilitiesCache.cs | 63 ------------------- 4 files changed, 12 insertions(+), 67 deletions(-) delete mode 100644 KSPCommunityFixes/Performance/MonoUtilitiesCache.cs diff --git a/GameData/KSPCommunityFixes/Settings.cfg b/GameData/KSPCommunityFixes/Settings.cfg index c0bc001..5930b88 100644 --- a/GameData/KSPCommunityFixes/Settings.cfg +++ b/GameData/KSPCommunityFixes/Settings.cfg @@ -492,9 +492,6 @@ KSP_COMMUNITY_FIXES // Improve the responsiveness of the part list when switching between categories, sorting and searching by tag. FasterEditorPartList = true - // Improve performance when mods try to refresh all part info windows on a part. - MonoUtilitiesCache = true - // ########################## // Modding // ########################## diff --git a/KSPCommunityFixes/KSPCommunityFixes.csproj b/KSPCommunityFixes/KSPCommunityFixes.csproj index a2ed25c..5b2be6c 100644 --- a/KSPCommunityFixes/KSPCommunityFixes.csproj +++ b/KSPCommunityFixes/KSPCommunityFixes.csproj @@ -219,7 +219,6 @@ - diff --git a/KSPCommunityFixes/Performance/MinorPerfTweaks.cs b/KSPCommunityFixes/Performance/MinorPerfTweaks.cs index a3117ba..52398c6 100644 --- a/KSPCommunityFixes/Performance/MinorPerfTweaks.cs +++ b/KSPCommunityFixes/Performance/MinorPerfTweaks.cs @@ -16,6 +16,8 @@ protected override void ApplyPatches() AddPatch(PatchType.Override, typeof(Part), nameof(Part.isKerbalEVA)); AddPatch(PatchType.Override, typeof(VolumeNormalizer), nameof(VolumeNormalizer.Update)); + + AddPatch(PatchType.Override, typeof(MonoUtilities), nameof(MonoUtilities.RefreshContextWindows)); } // When FlightGlobals._fetch is null/destroyed, the stock "fetch" getter fallback to a FindObjectOfType() @@ -89,5 +91,15 @@ private static void VolumeNormalizer_Update_Override(VolumeNormalizer vn) vn.volume = newVolume; } + + // MonoUtilities.RefreshContextWindows calls Object.FindObjectsOfType. + // This is quite slow. The method is not used in stock but several mods + // do use it and it would otherwise take up a notable chunk of scene + // switch times. + private static void MonoUtilities_RefreshContextWindows_Override(Part part) + { + if (part.IsNotNullRef() && part.PartActionWindow.IsNotNullOrDestroyed()) + part.PartActionWindow.displayDirty = true; + } } } diff --git a/KSPCommunityFixes/Performance/MonoUtilitiesCache.cs b/KSPCommunityFixes/Performance/MonoUtilitiesCache.cs deleted file mode 100644 index 7b8647d..0000000 --- a/KSPCommunityFixes/Performance/MonoUtilitiesCache.cs +++ /dev/null @@ -1,63 +0,0 @@ -using System.Collections.Generic; - -namespace KSPCommunityFixes -{ - // MonoUtilites.RefreshContextWindows calls Object.FindObjectsOfType. - // This is quite slow but doesn't show up in stock because the method isn't - // used anywhere in the KSP codebase. Several mods, however, do make use of - // it and this can account for a notable chunk of scene switch times. - // - // We patch UIPartActionWindow to indpendently track live instances and - // then patch RefreshContextWindows to just use the list we are tracking. - class MonoUtilitiesCache : BasePatch - { - static readonly List PartActionWindows = new List(); - - protected override void ApplyPatches() - { - AddPatch(PatchType.Postfix, typeof(UIPartActionWindow), nameof(UIPartActionWindow.Awake)); - AddPatch(PatchType.Postfix, typeof(UIPartActionWindow), nameof(UIPartActionWindow.OnDestroy)); - AddPatch(PatchType.Override, typeof(MonoUtilities), nameof(MonoUtilities.RefreshContextWindows)); - } - - static void UIPartActionWindow_Awake_Postfix(UIPartActionWindow __instance) - { - PartActionWindows.Add(__instance); - } - - static void UIPartActionWindow_OnDestroy_Postfix(UIPartActionWindow __instance) - { - int i = 0; - int j = 0; - var paws = PartActionWindows; - int count = paws.Count; - - for (; i < count; ++i) - { - var paw = paws[i]; - if (ReferenceEquals(paw, __instance) || paw == null) - continue; - - paws[j++] = paw; - } - - paws.RemoveRange(j, count - j); - } - - static void MonoUtilities_RefreshContextWindows_Override(Part part) - { - var paws = PartActionWindows; - int count = paws.Count; - - for (int i = 0; i < count; ++i) - { - var paw = paws[i]; - if (paw == null) - continue; - if (paw.part != part) - continue; - paw.displayDirty = true; - } - } - } -} \ No newline at end of file From fe0d768101c76afd6df75366e58d6e3ddec32351 Mon Sep 17 00:00:00 2001 From: Phantomical Date: Thu, 23 Oct 2025 15:05:46 -0700 Subject: [PATCH 3/3] Also override RefreshPartContextWindow --- KSPCommunityFixes/Performance/MinorPerfTweaks.cs | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/KSPCommunityFixes/Performance/MinorPerfTweaks.cs b/KSPCommunityFixes/Performance/MinorPerfTweaks.cs index 52398c6..5a35270 100644 --- a/KSPCommunityFixes/Performance/MinorPerfTweaks.cs +++ b/KSPCommunityFixes/Performance/MinorPerfTweaks.cs @@ -18,6 +18,7 @@ protected override void ApplyPatches() AddPatch(PatchType.Override, typeof(VolumeNormalizer), nameof(VolumeNormalizer.Update)); AddPatch(PatchType.Override, typeof(MonoUtilities), nameof(MonoUtilities.RefreshContextWindows)); + AddPatch(PatchType.Override, typeof(MonoUtilities), nameof(MonoUtilities.RefreshPartContextWindow)); } // When FlightGlobals._fetch is null/destroyed, the stock "fetch" getter fallback to a FindObjectOfType() @@ -101,5 +102,11 @@ private static void MonoUtilities_RefreshContextWindows_Override(Part part) if (part.IsNotNullRef() && part.PartActionWindow.IsNotNullOrDestroyed()) part.PartActionWindow.displayDirty = true; } + + private static void MonoUtilities_RefreshPartContextWindow_Override(Part part) + { + if (part.IsNotNullRef() && part.PartActionWindow.IsNotNullOrDestroyed()) + part.PartActionWindow.displayDirty = true; + } } }