Skip to content

Commit 8bfec76

Browse files
authored
New performance patch : FasterEditorPartList (#326)
Address issue #242 Improve the responsiveness of the part list when switching between categories, sorting and searching by tag. The part list implements a caching mechanism to avoid re-instantiating already generated icons, which avoid the bulk of the cost of swapping the shown parts in the part list. Once all icons were generated at least once, it just re-parent them between a disabled `partIconStorage` GameObject and the scrollview GameObject. Profiling show that roughly 70-80% of the time is spent doing this reparenting, which we avoid by : - Preventing the icon objects from being parented to the `partIconStorage` in the EditorPartList.ClearAllItems() method (by removing the SetParent call) - Preventing the icon objects from being parented back to the scrollview in the EditorPartList.UpdatePartIcon() method (by removing the SetParent call) Doing this cause two issues : - It prevent the icon object from ever being parented to the scrollist, so they never appear - The icons are not ordered anymore according to the sorting filter, as this was done by unparenting / reparenting them We fix that by altering `EditorPartList.UpdatePartIcons()` in two ways : - We call SetParent for newly instantiated icons so they are parented to the scrollview - We set the already instantiated icons that are about to be re-activated to be the last sibling to respect the requested sorting. Note that we do both *before* the call to `EditorPartList.UpdatePartIcon()`, as VABOrganizer is postfixing it and is relying on the icons being in their right place within the scrollview at this time. Additionally, this implement a caching mechanism storing pre-parsed tags for each `AvailablePart`, preventing the overhead of having to parse to the tokenized `AvailablePart.tags` string every time filtering is needed. There are typically 15+ tags per part, so this has a very significant impact. These changes significantly improve overall responsiveness of the part list updates when switching between categories, changing the sorting type/order or using tag search.
1 parent f3d292e commit 8bfec76

File tree

5 files changed

+235
-0
lines changed

5 files changed

+235
-0
lines changed

CHANGELOG.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@
22

33
##### 1.39.0
44
**New/improved patches**
5+
- New performance patch : [**FasterEditorPartList**](https://github.com/KSPModdingLibs/KSPCommunityFixes/issues/242) Improve the responsiveness of the part list when switching between categories, sorting and searching by tag.
56
- New KSP bugfix : **DebugConsoleDontStealInput**, fix the Alt+F12 console input field stealing input when a console entry is added.
67

78
##### 1.38.1

GameData/KSPCommunityFixes/Settings.cfg

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -485,6 +485,9 @@ KSP_COMMUNITY_FIXES
485485
// Reduce the constant overhead from ModuleColorChanger
486486
ModuleColorChangerOptimization = true
487487

488+
// Improve the responsiveness of the part list when switching between categories, sorting and searching by tag.
489+
FasterEditorPartList = true
490+
488491
// ##########################
489492
// Modding
490493
// ##########################

KSPCommunityFixes/KSPCommunityFixes.csproj

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -162,6 +162,7 @@
162162
<Compile Include="Modding\ModUpgradePipeline.cs" />
163163
<Compile Include="Performance\CraftBrowserOptimisations.cs" />
164164
<Compile Include="Modding\BaseFieldListUseFieldHost.cs" />
165+
<Compile Include="Performance\FasterEditorPartList.cs" />
165166
<Compile Include="Performance\ForceSyncSceneSwitch.cs" />
166167
<Compile Include="Performance\AsteroidAndCometDrillCache.cs" />
167168
<Compile Include="BugFixes\DoubleCurvePreserveTangents.cs" />
Lines changed: 229 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,229 @@
1+
/*
2+
The part list implements a caching mechanism to avoid re-instantiating already generated icons,
3+
which avoid the bulk of the cost of swapping the shown parts in the part list.
4+
Once all icons were generated at least once, it just re-parent them between a disabled `partIconStorage`
5+
GameObject and the scrollview GameObject.
6+
7+
Profiling show that roughly 70-80% of the time is spent doing this reparenting, which we avoid by :
8+
- Preventing the icon objects from being parented to the `partIconStorage` in the
9+
EditorPartList.ClearAllItems() method (by removing the SetParent call)
10+
- Preventing the icon objects from being parented back to the scrollview in the
11+
EditorPartList.UpdatePartIcon() method (by removing the SetParent call)
12+
13+
Doing this cause two issues :
14+
- It prevent the icon object from ever being parented to the scrollist, so they never appear
15+
- The icons are not ordered anymore according to the sorting filter, as this was done by
16+
unparenting / reparenting them
17+
18+
We fix that by altering `EditorPartList.UpdatePartIcons()` in two ways :
19+
- We call SetParent for newly instantiated icons so they are parented to the scrollview
20+
- We set the already instantiated icons that are about to be re-activated to be the last sibling to
21+
respect the requested sorting.
22+
23+
Note that we do both *before* the call to `EditorPartList.UpdatePartIcon()`, as VABOrganizer is
24+
postfixing it and is relying on the icons being in their right place within the scrollview at this time.
25+
26+
Additionally, this implement a caching mechanism storing pre-parsed tags for each `AvailablePart`,
27+
preventing the overhead of having to parse to the tokenized `AvailablePart.tags` string every time
28+
filtering is needed. There are typically 15+ tags per part, so this has a very significant impact.
29+
30+
These changes significantly improve overall responsiveness of the part list updates when switching
31+
between categories, changing the sorting type/order or using tag search.
32+
*/
33+
34+
using HarmonyLib;
35+
using KSP.UI.Screens;
36+
using System;
37+
using System.Collections.Generic;
38+
using System.Reflection;
39+
using System.Reflection.Emit;
40+
using UnityEngine;
41+
using static KSP.UI.Screens.BasePartCategorizer;
42+
43+
namespace KSPCommunityFixes.Performance
44+
{
45+
internal class FasterEditorPartList : BasePatch
46+
{
47+
protected override void ApplyPatches()
48+
{
49+
AddPatch(PatchType.Transpiler, typeof(EditorPartList), nameof(EditorPartList.ClearAllItems), nameof(NoOpSetParentTranspiler));
50+
AddPatch(PatchType.Transpiler, typeof(EditorPartList), nameof(EditorPartList.UpdatePartIcon), nameof(NoOpSetParentTranspiler));
51+
AddPatch(PatchType.Transpiler, typeof(EditorPartList), nameof(EditorPartList.UpdatePartIcons));
52+
AddPatch(PatchType.Override, typeof(BasePartCategorizer), nameof(BasePartCategorizer.PartMatchesSearch));
53+
}
54+
55+
private static void TransformSetParentNoOp(Transform instance, Transform parent, bool worldPositionStays) { }
56+
57+
private static IEnumerable<CodeInstruction> NoOpSetParentTranspiler(IEnumerable<CodeInstruction> instructions)
58+
{
59+
MethodInfo m_Transform_SetParent = AccessTools.Method(typeof(Transform), nameof(Transform.SetParent), new Type[] { typeof(Transform), typeof(bool) });
60+
MethodInfo m_Transform_SetParentNoOp = AccessTools.Method(typeof(FasterEditorPartList), nameof(TransformSetParentNoOp));
61+
62+
foreach (CodeInstruction il in instructions)
63+
{
64+
if ((il.opcode == OpCodes.Callvirt || il.opcode == OpCodes.Call) && ReferenceEquals(il.operand, m_Transform_SetParent))
65+
{
66+
il.opcode = OpCodes.Call;
67+
il.operand = m_Transform_SetParentNoOp;
68+
}
69+
yield return il;
70+
}
71+
}
72+
73+
private static void SetIconParentHelper(EditorPartList editorPartList, EditorPartIcon editorPartIcon)
74+
{
75+
editorPartIcon.transform.SetParent(editorPartList.partGrid.transform, worldPositionStays: false);
76+
}
77+
78+
private static void SetIconAsLastSiblingHelper(EditorPartIcon editorPartIcon)
79+
{
80+
editorPartIcon.transform.SetAsLastSibling();
81+
}
82+
83+
private static IEnumerable<CodeInstruction> EditorPartList_UpdatePartIcons_Transpiler(IEnumerable<CodeInstruction> instructions)
84+
{
85+
MethodInfo m_EditorPartIcon_Create = AccessTools.Method(typeof(EditorPartIcon), nameof(EditorPartIcon.Create),
86+
new Type[] { typeof(EditorPartList), typeof(AvailablePart), typeof(float), typeof(float), typeof(float) });
87+
MethodInfo m_SetIconParentHelper = AccessTools.Method(typeof(FasterEditorPartList), nameof(SetIconParentHelper));
88+
MethodInfo m_SetIconAsLastSiblingHelper = AccessTools.Method(typeof(FasterEditorPartList), nameof(SetIconAsLastSiblingHelper));
89+
90+
foreach (CodeInstruction il in instructions)
91+
{
92+
if ((il.opcode == OpCodes.Callvirt || il.opcode == OpCodes.Call) && ReferenceEquals(il.operand, m_EditorPartIcon_Create))
93+
{
94+
yield return new CodeInstruction(OpCodes.Ldarg_0);
95+
yield return new CodeInstruction(OpCodes.Ldloc_2); // first EditorPartIcon local var
96+
yield return new CodeInstruction(OpCodes.Call, m_SetIconParentHelper);
97+
yield return il;
98+
continue;
99+
}
100+
101+
if (il.opcode == OpCodes.Stloc_3)
102+
{
103+
yield return il;
104+
yield return new CodeInstruction(OpCodes.Ldloc_3); // second EditorPartIcon local var
105+
yield return new CodeInstruction(OpCodes.Call, m_SetIconAsLastSiblingHelper);
106+
continue;
107+
}
108+
109+
yield return il;
110+
}
111+
}
112+
113+
private class PartTags
114+
{
115+
public static Dictionary<AvailablePart, PartTags> partsTags = new Dictionary<AvailablePart, PartTags>();
116+
117+
public string joinedTags;
118+
public PartTag[] parsedTags;
119+
120+
public static PartTag[] GetTagsForPart(BasePartCategorizer bpcInstance, AvailablePart ap)
121+
{
122+
if (!partsTags.TryGetValue(ap, out PartTags tags) || tags.joinedTags != ap.tags)
123+
{
124+
tags = new PartTags();
125+
tags.joinedTags = ap.tags;
126+
tags.parsedTags = ParseTags(bpcInstance, ap);
127+
128+
partsTags[ap] = tags;
129+
}
130+
131+
return tags.parsedTags;
132+
}
133+
134+
private static PartTag[] ParseTags(BasePartCategorizer bpcInstance, AvailablePart ap)
135+
{
136+
string[] rawTags = SearchTagSplit(ap.tags);
137+
PartTag[] tags = new PartTag[rawTags.Length];
138+
for (int i = 0; i < rawTags.Length; i++)
139+
{
140+
string rawTag = rawTags[i];
141+
MatchType matchType = bpcInstance.TagMatchType(ref rawTag);
142+
tags[i] = new PartTag(rawTag, matchType);
143+
}
144+
return tags;
145+
}
146+
}
147+
148+
private class PartTag
149+
{
150+
public readonly string tag;
151+
public readonly MatchType matchType;
152+
153+
public PartTag(string tag, MatchType matchType)
154+
{
155+
this.tag = tag;
156+
this.matchType = matchType;
157+
}
158+
}
159+
160+
/// <summary>
161+
/// Improve BasePartCategorizer.PartMatchesSearch() performance by building a static dictionary of pre-parsed tags
162+
/// for every AvailablePart instead of re-parsing the whole AvailablePart.tags string every time.
163+
/// </summary>
164+
private static bool BasePartCategorizer_PartMatchesSearch_Override(BasePartCategorizer instance, AvailablePart part, string[] terms)
165+
{
166+
if (part.category == PartCategories.none)
167+
return false;
168+
169+
if (terms.Length == 0)
170+
return true;
171+
172+
PartTag[] tags = PartTags.GetTagsForPart(instance, part);
173+
174+
for (int i = terms.Length; i-- > 0;)
175+
{
176+
string term = terms[i];
177+
int termLength = Math.Min(term.Length, 3);
178+
179+
for (int j = tags.Length; j-- > 0;)
180+
{
181+
string tag = tags[j].tag;
182+
if (tag.Length < termLength)
183+
continue;
184+
185+
bool match;
186+
switch (tags[j].matchType)
187+
{
188+
case MatchType.TERM_CONTAINS_TAG:
189+
match = term.Contains(tag);
190+
break;
191+
case MatchType.TAG_CONTAINS_TERM:
192+
match = tag.Contains(term);
193+
break;
194+
case MatchType.EQUALS_ONLY:
195+
match = term.Equals(tag);
196+
break;
197+
case MatchType.EITHER_ENDS_WITH_EITHER:
198+
match = term.EndsWith(tag) || tag.EndsWith(term);
199+
break;
200+
case MatchType.EITHER_STARTS_WITH_EITHER:
201+
match = term.StartsWith(tag) || tag.StartsWith(term);
202+
break;
203+
case MatchType.TERM_ENDS_WITH_TAG:
204+
match = term.EndsWith(tag);
205+
break;
206+
case MatchType.TERM_STARTS_WITH_TAG:
207+
match = term.StartsWith(tag);
208+
break;
209+
case MatchType.TAG_ENDS_WITH_TERM:
210+
match = tag.EndsWith(term);
211+
break;
212+
case MatchType.TAG_STARTS_WITH_TERM:
213+
match = tag.StartsWith(term);
214+
break;
215+
case MatchType.EITHER_CONTAINS_EITHER:
216+
default:
217+
match = term.Contains(tag) || tag.Contains(term);
218+
break;
219+
}
220+
221+
if (match)
222+
return true;
223+
}
224+
}
225+
226+
return false;
227+
}
228+
}
229+
}

README.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -151,6 +151,7 @@ User options are available from the "ESC" in-game settings menu :<br/><img src="
151151
- [**GameDatabasePerf**](https://github.com/KSPModdingLibs/KSPCommunityFixes/pull/269) [KSP 1.12.3 - 1.12.5]<br/>Faster dictionary backed version of the stock `GameDatabase.GetModel*` / `GameDatabase.GetTexture*` methods. This patch is always enabled and has no entry in `Settings.cfg`.
152152
- [**PartParsingPerf**](https://github.com/KSPModdingLibs/KSPCommunityFixes/pull/269) [KSP 1.8.0 - 1.12.5]<br/>Faster part icon generation and `Part` fields parsing.
153153
- [**ModuleColorChangerOptimization**](https://github.com/KSPModdingLibs/KSPCommunityFixes/pull/304) [KSP 1.12.3 - 1.12.5]<br/>Reduce the constant overhead from ModuleColorChanger
154+
- [**FasterEditorPartList**](https://github.com/KSPModdingLibs/KSPCommunityFixes/issues/242) [KSP 1.12.3 - 1.12.5]<br/>Improve the responsiveness of the part list when switching between categories, sorting and searching by tag.
154155

155156
#### API and modding tools
156157
- **MultipleModuleInPartAPI** [KSP 1.8.0 - 1.12.5]<br/>This API allow other plugins to implement PartModules that can exist in multiple occurrence in a single part and won't suffer "module indexing mismatch" persistent data losses following part configuration changes. [See documentation on the wiki](https://github.com/KSPModdingLibs/KSPCommunityFixes/wiki/MultipleModuleInPartAPI).

0 commit comments

Comments
 (0)