Skip to content

Commit 2d4b6e4

Browse files
Merge pull request #245 from RtlZeroMemory/feat/ts-perf-wave2
perf(core): reduce routing commit overhead
2 parents c70f26f + 54294fd commit 2d4b6e4

2 files changed

Lines changed: 69 additions & 80 deletions

File tree

packages/core/src/app/widgetRenderer.ts

Lines changed: 63 additions & 80 deletions
Original file line numberDiff line numberDiff line change
@@ -1238,16 +1238,7 @@ export class WidgetRenderer<S> {
12381238
private readonly _pooledToastContainers: { rect: Rect; props: ToastContainerProps }[] = [];
12391239
private readonly _pooledToastFocusableActionIds: string[] = [];
12401240
private readonly _pooledActiveExitKeys = new Set<string>();
1241-
// Pooled Sets for tracking previous IDs (GC cleanup detection)
12421241
private readonly _pooledPrevTreeIds = new Set<string>();
1243-
private readonly _pooledPrevDropdownIds = new Set<string>();
1244-
private readonly _pooledPrevVirtualListIds = new Set<string>();
1245-
private readonly _pooledPrevTableIds = new Set<string>();
1246-
private readonly _pooledPrevTreeStoreIds = new Set<string>();
1247-
private readonly _pooledPrevCommandPaletteIds = new Set<string>();
1248-
private readonly _pooledPrevToolApprovalDialogIds = new Set<string>();
1249-
private readonly _pooledPrevDiffViewerIds = new Set<string>();
1250-
private readonly _pooledPrevLogsConsoleIds = new Set<string>();
12511242
private _runtimeBreadcrumbs: WidgetRuntimeBreadcrumbSnapshot = EMPTY_WIDGET_RUNTIME_BREADCRUMBS;
12521243
private _constraintBreadcrumbs: RuntimeBreadcrumbConstraintsSummary | null = null;
12531244
private _constraintExprIndexByInstanceId: ReadonlyMap<
@@ -4285,37 +4276,27 @@ export class WidgetRenderer<S> {
42854276
}
42864277
}
42874278

4288-
if (doCommit && (hasRoutingWidgets || hadRoutingWidgets)) {
4279+
const canSkipFullRoutingRebuildOnCommit =
4280+
doCommit &&
4281+
hadRoutingWidgets &&
4282+
hasRoutingWidgets &&
4283+
identityDamageFromCommit !== null &&
4284+
identityDamageFromCommit.routingRelevantChanged === false;
4285+
4286+
if (
4287+
doCommit &&
4288+
(hasRoutingWidgets || hadRoutingWidgets) &&
4289+
!canSkipFullRoutingRebuildOnCommit
4290+
) {
42894291
didRoutingRebuild = true;
42904292
this.hadRoutingWidgets = hasRoutingWidgets;
42914293
const routingToken = PERF_DETAIL_ENABLED ? perfMarkStart("routing_rebuild") : 0;
42924294
const getRectForInstance = (instanceId: InstanceId) =>
42934295
this._pooledRectByInstanceId.get(instanceId) ?? ZERO_RECT;
42944296

42954297
// Rebuild complex widget metadata maps (id -> props) for routing.
4296-
// Use pooled Sets to track previous IDs for GC cleanup detection (avoids per-frame allocations).
42974298
this._pooledPrevTreeIds.clear();
4298-
for (const k of this.treeById.keys()) this._pooledPrevTreeIds.add(k);
4299-
this._pooledPrevDropdownIds.clear();
4300-
for (const k of this.dropdownById.keys()) this._pooledPrevDropdownIds.add(k);
4301-
this._pooledPrevVirtualListIds.clear();
4302-
for (const k of this.virtualListById.keys()) this._pooledPrevVirtualListIds.add(k);
4303-
this._pooledPrevTableIds.clear();
4304-
for (const k of this.tableById.keys()) this._pooledPrevTableIds.add(k);
4305-
this._pooledPrevTreeStoreIds.clear();
4306-
for (const k of this.treeById.keys()) this._pooledPrevTreeStoreIds.add(k);
4307-
for (const k of this.filePickerById.keys()) this._pooledPrevTreeStoreIds.add(k);
4308-
for (const k of this.fileTreeExplorerById.keys()) this._pooledPrevTreeStoreIds.add(k);
4309-
this._pooledPrevCommandPaletteIds.clear();
4310-
for (const k of this.commandPaletteById.keys()) this._pooledPrevCommandPaletteIds.add(k);
4311-
this._pooledPrevToolApprovalDialogIds.clear();
4312-
for (const k of this.toolApprovalDialogById.keys())
4313-
this._pooledPrevToolApprovalDialogIds.add(k);
4314-
this._pooledPrevDiffViewerIds.clear();
4315-
for (const k of this.diffViewerById.keys()) this._pooledPrevDiffViewerIds.add(k);
4316-
this._pooledPrevLogsConsoleIds.clear();
4317-
for (const k of this.logsConsoleById.keys()) this._pooledPrevLogsConsoleIds.add(k);
4318-
4299+
for (const treeId of this.treeById.keys()) this._pooledPrevTreeIds.add(treeId);
43194300
this.virtualListById.clear();
43204301
this.buttonById.clear();
43214302
this.linkById.clear();
@@ -4567,9 +4548,9 @@ export class WidgetRenderer<S> {
45674548
this.closeOnEscapeByLayerId = this._pooledCloseOnEscape;
45684549
this.closeOnBackdropByLayerId = this._pooledCloseOnBackdrop;
45694550
this.onCloseByLayerId = this._pooledOnClose;
4570-
this.dropdownStack = Object.freeze(this._pooledDropdownStack.slice());
4571-
this.overlayShortcutOwners = Object.freeze(this._pooledOverlayShortcutOwners.slice());
4572-
this.toastContainers = Object.freeze(this._pooledToastContainers.slice());
4551+
this.dropdownStack = this._pooledDropdownStack.slice();
4552+
this.overlayShortcutOwners = this._pooledOverlayShortcutOwners.slice();
4553+
this.toastContainers = this._pooledToastContainers.slice();
45734554
this.rebuildOverlayShortcutBindings();
45744555

45754556
// Build toast action maps using pooled collections.
@@ -4598,7 +4579,7 @@ export class WidgetRenderer<S> {
45984579

45994580
this.toastActionByFocusId = this._pooledToastActionByFocusId;
46004581
this.toastActionLabelByFocusId = this._pooledToastActionLabelByFocusId;
4601-
this.toastFocusableActionIds = Object.freeze(this._pooledToastFocusableActionIds.slice());
4582+
this.toastFocusableActionIds = this._pooledToastFocusableActionIds.slice();
46024583

46034584
const baseFocusList = this.baseFocusList;
46044585
const baseEnabledById = this.baseEnabledById;
@@ -4799,8 +4780,8 @@ export class WidgetRenderer<S> {
47994780
this.closeOnEscapeByLayerId = this._pooledCloseOnEscape;
48004781
this.closeOnBackdropByLayerId = this._pooledCloseOnBackdrop;
48014782
this.onCloseByLayerId = this._pooledOnClose;
4802-
this.dropdownStack = Object.freeze(this._pooledDropdownStack.slice());
4803-
this.toastContainers = Object.freeze(this._pooledToastContainers.slice());
4783+
this.dropdownStack = this._pooledDropdownStack.slice();
4784+
this.toastContainers = this._pooledToastContainers.slice();
48044785
this.rebuildOverlayShortcutBindings();
48054786

48064787
this._pooledToastActionByFocusId.clear();
@@ -4828,7 +4809,7 @@ export class WidgetRenderer<S> {
48284809

48294810
this.toastActionByFocusId = this._pooledToastActionByFocusId;
48304811
this.toastActionLabelByFocusId = this._pooledToastActionLabelByFocusId;
4831-
this.toastFocusableActionIds = Object.freeze(this._pooledToastFocusableActionIds.slice());
4812+
this.toastFocusableActionIds = this._pooledToastFocusableActionIds.slice();
48324813

48334814
const baseFocusList = this.baseFocusList;
48344815
const baseEnabledById = this.baseEnabledById;
@@ -4860,6 +4841,12 @@ export class WidgetRenderer<S> {
48604841

48614842
if (doCommit && !didRoutingRebuild) {
48624843
this.hadRoutingWidgets = hasRoutingWidgets;
4844+
if (canSkipFullRoutingRebuildOnCommit && !doLayout) {
4845+
// Full routing rebuild usually refreshes shortcut bindings. When this
4846+
// fast path skips that rebuild on commit-only turns, keep shortcut
4847+
// bindings in sync with async command palette item updates.
4848+
this.rebuildOverlayShortcutBindings();
4849+
}
48634850
}
48644851

48654852
if (doCommit && didRoutingRebuild) {
@@ -4896,27 +4883,21 @@ export class WidgetRenderer<S> {
48964883
}
48974884

48984885
// Garbage collect per-dropdown routing state for dropdowns that were removed.
4899-
for (const prevDropdownId of this._pooledPrevDropdownIds) {
4900-
if (!this.dropdownById.has(prevDropdownId)) {
4901-
this.dropdownSelectedIndexById.delete(prevDropdownId);
4902-
}
4886+
for (const dropdownId of this.dropdownSelectedIndexById.keys()) {
4887+
if (!this.dropdownById.has(dropdownId)) this.dropdownSelectedIndexById.delete(dropdownId);
49034888
}
49044889

49054890
// Garbage collect local state for virtual lists that were removed.
4906-
for (const prevId of this._pooledPrevVirtualListIds) {
4907-
if (!this.virtualListById.has(prevId)) {
4908-
this.virtualListStore.delete(prevId);
4909-
if (this.pressedVirtualList?.id === prevId) {
4910-
this.pressedVirtualList = null;
4911-
}
4912-
}
4891+
for (const virtualListId of this.virtualListStore.keys()) {
4892+
if (!this.virtualListById.has(virtualListId)) this.virtualListStore.delete(virtualListId);
4893+
}
4894+
if (this.pressedVirtualList && !this.virtualListById.has(this.pressedVirtualList.id)) {
4895+
this.pressedVirtualList = null;
49134896
}
49144897

49154898
// Garbage collect local state for tables that were removed.
4916-
for (const prevId of this._pooledPrevTableIds) {
4917-
if (!this.tableById.has(prevId)) {
4918-
this.tableStore.delete(prevId);
4919-
}
4899+
for (const tableId of this.tableStore.keys()) {
4900+
if (!this.tableById.has(tableId)) this.tableStore.delete(tableId);
49204901
}
49214902

49224903
// Garbage collect per-tree lazy-loading caches for trees that were removed.
@@ -4931,13 +4912,13 @@ export class WidgetRenderer<S> {
49314912
}
49324913

49334914
// Garbage collect treeStore entries for tree-like widgets that were removed.
4934-
for (const prevId of this._pooledPrevTreeStoreIds) {
4915+
for (const treeLikeId of this.treeStore.keys()) {
49354916
if (
4936-
!this.treeById.has(prevId) &&
4937-
!this.filePickerById.has(prevId) &&
4938-
!this.fileTreeExplorerById.has(prevId)
4917+
!this.treeById.has(treeLikeId) &&
4918+
!this.filePickerById.has(treeLikeId) &&
4919+
!this.fileTreeExplorerById.has(treeLikeId)
49394920
) {
4940-
this.treeStore.delete(prevId);
4921+
this.treeStore.delete(treeLikeId);
49414922
}
49424923
}
49434924

@@ -4962,33 +4943,35 @@ export class WidgetRenderer<S> {
49624943
}
49634944

49644945
// Garbage collect command palette async state for palettes that were removed.
4965-
for (const prevId of this._pooledPrevCommandPaletteIds) {
4966-
if (!this.commandPaletteById.has(prevId)) {
4967-
this.commandPaletteItemsById.delete(prevId);
4968-
this.commandPaletteLoadingById.delete(prevId);
4969-
this.commandPaletteFetchTokenById.delete(prevId);
4970-
this.commandPaletteLastQueryById.delete(prevId);
4971-
this.commandPaletteLastSourcesRefById.delete(prevId);
4972-
}
4946+
for (const id of this.commandPaletteItemsById.keys()) {
4947+
if (!this.commandPaletteById.has(id)) this.commandPaletteItemsById.delete(id);
4948+
}
4949+
for (const id of this.commandPaletteLoadingById.keys()) {
4950+
if (!this.commandPaletteById.has(id)) this.commandPaletteLoadingById.delete(id);
4951+
}
4952+
for (const id of this.commandPaletteFetchTokenById.keys()) {
4953+
if (!this.commandPaletteById.has(id)) this.commandPaletteFetchTokenById.delete(id);
4954+
}
4955+
for (const id of this.commandPaletteLastQueryById.keys()) {
4956+
if (!this.commandPaletteById.has(id)) this.commandPaletteLastQueryById.delete(id);
4957+
}
4958+
for (const id of this.commandPaletteLastSourcesRefById.keys()) {
4959+
if (!this.commandPaletteById.has(id)) this.commandPaletteLastSourcesRefById.delete(id);
49734960
}
49744961

4975-
for (const prevId of this._pooledPrevToolApprovalDialogIds) {
4976-
if (!this.toolApprovalDialogById.has(prevId)) {
4977-
this.toolApprovalFocusedActionById.delete(prevId);
4978-
}
4962+
for (const id of this.toolApprovalFocusedActionById.keys()) {
4963+
if (!this.toolApprovalDialogById.has(id)) this.toolApprovalFocusedActionById.delete(id);
49794964
}
49804965

4981-
for (const prevId of this._pooledPrevDiffViewerIds) {
4982-
if (!this.diffViewerById.has(prevId)) {
4983-
this.diffViewerFocusedHunkById.delete(prevId);
4984-
this.diffViewerExpandedHunksById.delete(prevId);
4985-
}
4966+
for (const id of this.diffViewerFocusedHunkById.keys()) {
4967+
if (!this.diffViewerById.has(id)) this.diffViewerFocusedHunkById.delete(id);
4968+
}
4969+
for (const id of this.diffViewerExpandedHunksById.keys()) {
4970+
if (!this.diffViewerById.has(id)) this.diffViewerExpandedHunksById.delete(id);
49864971
}
49874972

4988-
for (const prevId of this._pooledPrevLogsConsoleIds) {
4989-
if (!this.logsConsoleById.has(prevId)) {
4990-
this.logsConsoleLastGTimeById.delete(prevId);
4991-
}
4973+
for (const id of this.logsConsoleLastGTimeById.keys()) {
4974+
if (!this.logsConsoleById.has(id)) this.logsConsoleLastGTimeById.delete(id);
49924975
}
49934976
}
49944977

packages/core/src/runtime/localState.ts

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -140,6 +140,7 @@ export type VirtualListStateStore = Readonly<{
140140
get: (id: string) => VirtualListLocalState;
141141
set: (id: string, patch: VirtualListLocalStatePatch) => VirtualListLocalState;
142142
delete: (id: string) => void;
143+
keys: () => IterableIterator<string>;
143144
}>;
144145

145146
/** Create a new virtual list state store instance. */
@@ -173,6 +174,7 @@ export function createVirtualListStateStore(): VirtualListStateStore {
173174
delete: (id) => {
174175
table.delete(id);
175176
},
177+
keys: () => table.keys(),
176178
});
177179
}
178180

@@ -222,6 +224,7 @@ export type TableStateStore = Readonly<{
222224
get: (id: string) => TableLocalState;
223225
set: (id: string, patch: TableLocalStatePatch) => TableLocalState;
224226
delete: (id: string) => void;
227+
keys: () => IterableIterator<string>;
225228
}>;
226229

227230
/** Create a new table state store instance. */
@@ -253,6 +256,7 @@ export function createTableStateStore(): TableStateStore {
253256
delete: (id) => {
254257
table.delete(id);
255258
},
259+
keys: () => table.keys(),
256260
});
257261
}
258262

@@ -331,6 +335,7 @@ export type TreeStateStore = Readonly<{
331335
get: (id: string) => TreeLocalState;
332336
set: (id: string, patch: TreeLocalStatePatch) => TreeLocalState;
333337
delete: (id: string) => void;
338+
keys: () => IterableIterator<string>;
334339
/** Add a key to the loading set. */
335340
startLoading: (id: string, nodeKey: string) => TreeLocalState;
336341
/** Remove a key from the loading set. */
@@ -363,6 +368,7 @@ export function createTreeStateStore(): TreeStateStore {
363368
delete: (id) => {
364369
table.delete(id);
365370
},
371+
keys: () => table.keys(),
366372
startLoading: (id, nodeKey) => {
367373
const prev = table.get(id) ?? DEFAULT_TREE_STATE;
368374
const newLoading = new Set(prev.loadingKeys);

0 commit comments

Comments
 (0)