Skip to content

Commit 0b11d55

Browse files
authored
Reduce Craft Browser Open and Search Times (#284)
- Speed up `CheckCraftFileType` by avoiding loading the whole craft file, instead only checking the file path. The `type` field in the loadmeta could be used here if it turns out that the path does not always contain the facility name, although if the loadmeta file doesn't exist the craft file would need to be read for it to be generated. - Prevent `CraftBrowserDialog` from rebuilding the craft list twice in a frame. - Prevent the craft list from rebuilding yet again on the next frame. - Disable the call to `Debug.Log` when the thumbnail is not found. - Replace `CraftSearch.SearchRoutine` to prevent the coroutine from yielding for a frame on each entry. Instead use an 8 ms budget, and make some small optimisations such as avoiding the use of `GetComponent`. - Reduce the wait time after the last keystroke to begin filtering from 0.25 to 0.1 seconds.
1 parent 5367575 commit 0b11d55

File tree

4 files changed

+217
-0
lines changed

4 files changed

+217
-0
lines changed

GameData/KSPCommunityFixes/Settings.cfg

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -474,6 +474,9 @@ KSP_COMMUNITY_FIXES
474474

475475
PartParsingPerf = true
476476

477+
// Significantly reduces the time it takes to open the craft browser and to search by name. Most noticeable with lots of craft.
478+
CraftBrowserOptimisations = true
479+
477480
// ##########################
478481
// Modding
479482
// ##########################

KSPCommunityFixes/KSPCommunityFixes.csproj

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -159,6 +159,7 @@
159159
<Compile Include="Library\ShaderHelpers.cs" />
160160
<Compile Include="Modding\KSPFieldEnumDesc.cs" />
161161
<Compile Include="Modding\ModUpgradePipeline.cs" />
162+
<Compile Include="Performance\CraftBrowserOptimisations.cs" />
162163
<Compile Include="Modding\BaseFieldListUseFieldHost.cs" />
163164
<Compile Include="Performance\ForceSyncSceneSwitch.cs" />
164165
<Compile Include="Performance\AsteroidAndCometDrillCache.cs" />
Lines changed: 212 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,212 @@
1+
// Disable logging when the thumbnail is not found.
2+
// Useful for testing with Unity Explorer installed, where debug calls take measurably longer.
3+
#define DISABLE_THUMBNAIL_LOGGING
4+
5+
using HarmonyLib;
6+
using KSP.Localization;
7+
using KSP.UI.Screens;
8+
using System;
9+
using System.Collections;
10+
using System.Collections.Generic;
11+
using System.Diagnostics;
12+
using System.IO;
13+
using System.Linq;
14+
using System.Reflection;
15+
using System.Reflection.Emit;
16+
using UnityEngine;
17+
using Debug = UnityEngine.Debug;
18+
19+
namespace KSPCommunityFixes.Performance
20+
{
21+
class CraftBrowserOptimisations : BasePatch
22+
{
23+
protected override Version VersionMin => new Version(1, 12, 0);
24+
25+
// These toggles are here for comparison purposes.
26+
public static bool patchCheckCraftFileType = true;
27+
public static bool preventImmediateRebuilds = true;
28+
public static bool preventDelayedRebuild = true;
29+
public static bool preventSearchYield = true;
30+
public static bool useTimeBudget = true;
31+
32+
private static int lastBuiltFrame;
33+
34+
public static float searchKeystrokeDelay = 0.1f;
35+
36+
protected override void ApplyPatches()
37+
{
38+
AddPatch(PatchType.Postfix, typeof(CraftBrowserDialog), nameof(CraftBrowserDialog.setbottomButtons));
39+
40+
AddPatch(PatchType.Prefix, typeof(ShipConstruction), nameof(ShipConstruction.CheckCraftFileType));
41+
42+
AddPatch(PatchType.Prefix, typeof(CraftBrowserDialog), nameof(CraftBrowserDialog.BuildPlayerCraftList));
43+
44+
AddPatch(PatchType.Postfix, typeof(CraftBrowserDialog), nameof(CraftBrowserDialog.Start));
45+
AddPatch(PatchType.Postfix, typeof(CraftBrowserDialog), nameof(CraftBrowserDialog.ReDisplay));
46+
47+
#if DISABLE_THUMBNAIL_LOGGING
48+
AddPatch(PatchType.Transpiler, typeof(ShipConstruction), nameof(ShipConstruction.GetThumbnail), new Type[] { typeof(string), typeof(bool), typeof(bool), typeof(FileInfo) });
49+
#endif
50+
51+
AddPatch(PatchType.Prefix, typeof(CraftSearch), nameof(CraftSearch.SearchRoutine));
52+
}
53+
54+
// Fix a miscellanous bug where setbottomButtons ignores showMergeOption. This is useful for Halbann/LazySpawner and BD's Vessel Mover.
55+
static void CraftBrowserDialog_setbottomButtons_Postfix(CraftBrowserDialog __instance) =>
56+
__instance.btnMerge.gameObject.SetActive(__instance.btnMerge.gameObject.activeSelf && __instance.showMergeOption);
57+
58+
// Speed up CheckCraftFileType by avoiding loading the whole craft file, instead only checking the file path.
59+
// Loadmeta could be used here if it turns out that the path does not always contain the facility name.
60+
static bool ShipConstruction_CheckCraftFileType_Prefix(string filePath, ref EditorFacility __result)
61+
{
62+
if (!patchCheckCraftFileType)
63+
return true;
64+
65+
if (string.IsNullOrEmpty(filePath))
66+
{
67+
__result = EditorFacility.None;
68+
return false;
69+
}
70+
71+
// Search the file path for either "SPH" or "VAB".
72+
bool sph = false;
73+
string facilityString = filePath.Split(Path.DirectorySeparatorChar).FirstOrDefault(f => (sph = f == "SPH") || f == "VAB");
74+
75+
if (string.IsNullOrEmpty(facilityString))
76+
{
77+
// Fallback in case the split fails for any reason.
78+
__result = facilityString.Contains("SPH") ? EditorFacility.SPH :
79+
(facilityString.Contains("VAB") ? EditorFacility.VAB : EditorFacility.None);
80+
}
81+
else
82+
{
83+
__result = sph ? EditorFacility.SPH : EditorFacility.VAB;
84+
}
85+
86+
return false;
87+
}
88+
89+
// Prevent CraftBrowserDialog from rebuilding the craft list twice in a frame, as happens by default.
90+
// Only the UI is rebuilt, but it still causes it to take 30% longer to open the dialog.
91+
// I think a frame check is a simpler alternative to transpile patches.
92+
static bool CraftBrowserDialog_BuildPlayerCraftList_Prefix(CraftBrowserDialog __instance)
93+
{
94+
if (!preventImmediateRebuilds)
95+
return true;
96+
97+
if (Time.frameCount == lastBuiltFrame)
98+
return false;
99+
100+
lastBuiltFrame = Time.frameCount;
101+
102+
return true;
103+
}
104+
105+
// Prevent the craft list from rebuilding yet again on the next frame due to some poor logic in DirectoryController.
106+
private static void PreventDelayedRebuild(CraftBrowserDialog dialog)
107+
{
108+
if (!preventDelayedRebuild)
109+
return;
110+
111+
dialog.directoryController.isEnabledThisFrame = false;
112+
}
113+
114+
static void CraftBrowserDialog_Start_Postfix(CraftBrowserDialog __instance)
115+
{
116+
PreventDelayedRebuild(__instance);
117+
CraftSearch.Instance.searchKeystrokeDelay = searchKeystrokeDelay;
118+
}
119+
120+
static void CraftBrowserDialog_ReDisplay_Postfix(CraftBrowserDialog __instance) =>
121+
PreventDelayedRebuild(__instance);
122+
123+
// Disable the call to Debug.Log when the thumbnail is not found.
124+
// On a new save with a large number of imported craft, the game will generate a lot of useless log entries.
125+
static IEnumerable<CodeInstruction> ShipConstruction_GetThumbnail_Transpiler(IEnumerable<CodeInstruction> instructions)
126+
{
127+
MethodInfo debug = AccessTools.Method(typeof(Debug), nameof(Debug.Log), new Type[] { typeof(object) });
128+
129+
foreach (CodeInstruction instruction in instructions)
130+
{
131+
if (instruction.Calls(debug))
132+
{
133+
instruction.opcode = OpCodes.Pop;
134+
instruction.operand = null;
135+
break;
136+
}
137+
}
138+
139+
return instructions;
140+
}
141+
142+
// Replace the search routine to prevent the search routine from yielding
143+
// a frame on each craft, and to make some small optimisations.
144+
static bool CraftSearch_SearchRoutine_Prefix(CraftSearch __instance, ref IEnumerator __result)
145+
{
146+
if (!preventSearchYield)
147+
return true;
148+
149+
__result = SearchRoutine(__instance);
150+
return false;
151+
}
152+
153+
private static IEnumerator SearchRoutine(CraftSearch __instance)
154+
{
155+
// Delay the start of the search routine by the searchKeystrokeDelay.
156+
while (__instance.searchTimer + __instance.searchKeystrokeDelay > Time.realtimeSinceStartup)
157+
yield return null;
158+
159+
bool filtered = false;
160+
string searchTerm = __instance.searchField.text;
161+
List<CraftEntry> entries = CraftSearch.craftBrowserDialog.craftList;
162+
float timer = Time.realtimeSinceStartup;
163+
float budget = 1f / 60f / 2f; // 8 ms.
164+
bool searchEmpty = string.IsNullOrWhiteSpace(searchTerm);
165+
166+
#if DEBUG
167+
Stopwatch stopwatch = Stopwatch.StartNew();
168+
#endif
169+
170+
foreach (CraftEntry entry in entries)
171+
{
172+
bool result = searchEmpty || FasterCraftMatchesSearch(entry, searchTerm);
173+
174+
if (entry.gameObject.activeSelf != result)
175+
entry.gameObject.SetActive(result);
176+
177+
if (!filtered && !result)
178+
filtered = true;
179+
180+
if (useTimeBudget && Time.realtimeSinceStartup - timer > budget)
181+
{
182+
timer = Time.realtimeSinceStartup;
183+
yield return null;
184+
}
185+
}
186+
187+
#if DEBUG
188+
stopwatch.Stop();
189+
Debug.Log($"[CraftBrowserOptimisations]: Filtered {entries.Count} craft in {stopwatch.Elapsed.Milliseconds:N3} ms.");
190+
#endif
191+
192+
__instance.hasFiltered?.Invoke(filtered);
193+
194+
if (string.IsNullOrWhiteSpace(searchTerm) && __instance.IsDifferentSearch)
195+
__instance.StopSearch();
196+
197+
__instance.previousSearch = searchTerm;
198+
}
199+
200+
private static bool FasterCraftMatchesSearch(CraftEntry craft, string searchTerm)
201+
{
202+
if (craft.craftName.IndexOf(searchTerm, StringComparison.OrdinalIgnoreCase) > -1)
203+
return true;
204+
205+
if (craft.craftProfileInfo == null || string.IsNullOrEmpty(craft.craftProfileInfo.description))
206+
return false;
207+
208+
string text = Localizer.Format(craft.craftProfileInfo.description);
209+
return text.IndexOf(searchTerm, StringComparison.OrdinalIgnoreCase) > -1;
210+
}
211+
}
212+
}

README.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -141,6 +141,7 @@ User options are available from the "ESC" in-game settings menu :<br/><img src="
141141
- [**MinorPerfTweaks**](https://github.com/KSPModdingLibs/KSPCommunityFixes/pull/257) [KSP 1.12.3 - 1.12.5]<br/>Various small performance patches (volume normalizer, eva module checks)
142142
- [**FloatingOriginPerf**](https://github.com/KSPModdingLibs/KSPCommunityFixes/pull/257) [KSP 1.12.3 - 1.12.5]<br/>General micro-optimization of floating origin shifts. Main benefit is in large particle count situations (ie, launches with many engines) but this helps a bit in other cases as well.
143143
- [**FasterPartFindTransform**](https://github.com/KSPModdingLibs/KSPCommunityFixes/pull/255) [KSP 1.12.3 - 1.12.5]<br/>Faster, and minimal GC alloc relacements for the Part FindModelTransform* and FindHeirarchyTransform* methods.
144+
- [**CraftBrowserOptimisations**](https://github.com/KSPModdingLibs/KSPCommunityFixes/pull/284) [KSP 1.12.0 - 1.12.5]<br/>Significantly reduces the time it takes to open the craft browser and to search by name. Most noticeable with lots of craft.
144145
- [**OptimisedVectorLines**](https://github.com/KSPModdingLibs/KSPCommunityFixes/pull/281) [KSP 1.12.0 - 1.12.5]<br/>Improve performance in the Map View when a large number of vessels and bodies are visible via faster drawing of orbit lines and CommNet lines.
145146

146147
#### API and modding tools

0 commit comments

Comments
 (0)