Skip to content

Commit 55b14c8

Browse files
perf(ui): optimize redux selectors for workflow editor
- Build selectors for each node in a react context so components can re-use the same selectors - Cache the selectors in the context
1 parent 79f65e5 commit 55b14c8

File tree

97 files changed

+906
-777
lines changed

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

97 files changed

+906
-777
lines changed

invokeai/frontend/web/src/app/components/GlobalHookIsolator.tsx

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,8 @@ import { useDndMonitor } from 'features/dnd/useDndMonitor';
1515
import { useDynamicPromptsWatcher } from 'features/dynamicPrompts/hooks/useDynamicPromptsWatcher';
1616
import { useStarterModelsToast } from 'features/modelManagerV2/hooks/useStarterModelsToast';
1717
import { useWorkflowBuilderWatcher } from 'features/nodes/components/sidePanel/workflow/IsolatedWorkflowBuilderWatcher';
18+
import { useSyncExecutionState } from 'features/nodes/hooks/useNodeExecutionState';
19+
import { useSyncNodeErrors } from 'features/nodes/store/util/fieldValidators';
1820
import { useReadinessWatcher } from 'features/queue/store/readiness';
1921
import { configChanged } from 'features/system/store/configSlice';
2022
import { selectLanguage } from 'features/system/store/systemSelectors';
@@ -47,10 +49,12 @@ export const GlobalHookIsolator = memo(
4749
useCloseChakraTooltipsOnDragFix();
4850
useNavigationApi();
4951
useDndMonitor();
52+
useSyncNodeErrors();
5053

5154
// Persistent subscription to the queue counts query - canvas relies on this to know if there are pending
5255
// and/or in progress canvas sessions.
5356
useGetQueueCountsByDestinationQuery(queueCountArg);
57+
useSyncExecutionState();
5458

5559
useEffect(() => {
5660
i18n.changeLanguage(language);

invokeai/frontend/web/src/app/store/createMemoizedSelector.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -18,3 +18,5 @@ export const getSelectorsOptions = {
1818
argsMemoize: lruMemoize,
1919
}),
2020
};
21+
22+
export const createLruSelector = createSelectorCreator(lruMemoize);

invokeai/frontend/web/src/features/changeBoardModal/components/ChangeBoardModal.tsx

Lines changed: 1 addition & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,6 @@
11
import type { ComboboxOnChange, ComboboxOption } from '@invoke-ai/ui-library';
22
import { Combobox, ConfirmationAlertDialog, Flex, FormControl, Text } from '@invoke-ai/ui-library';
33
import { createSelector } from '@reduxjs/toolkit';
4-
import { createMemoizedSelector } from 'app/store/createMemoizedSelector';
54
import { useAppDispatch, useAppSelector } from 'app/store/storeHooks';
65
import { useAssertSingleton } from 'common/hooks/useAssertSingleton';
76
import {
@@ -14,7 +13,7 @@ import { useTranslation } from 'react-i18next';
1413
import { useListAllBoardsQuery } from 'services/api/endpoints/boards';
1514
import { useAddImagesToBoardMutation, useRemoveImagesFromBoardMutation } from 'services/api/endpoints/images';
1615

17-
const selectImagesToChange = createMemoizedSelector(
16+
const selectImagesToChange = createSelector(
1817
selectChangeBoardModalSlice,
1918
(changeBoardModal) => changeBoardModal.image_names
2019
);

invokeai/frontend/web/src/features/nodes/components/flow/Flow.tsx

Lines changed: 91 additions & 81 deletions
Original file line numberDiff line numberDiff line change
@@ -22,7 +22,6 @@ import { useConnection } from 'features/nodes/hooks/useConnection';
2222
import { useIsValidConnection } from 'features/nodes/hooks/useIsValidConnection';
2323
import { useIsWorkflowEditorLocked } from 'features/nodes/hooks/useIsWorkflowEditorLocked';
2424
import { useNodeCopyPaste } from 'features/nodes/hooks/useNodeCopyPaste';
25-
import { useSyncExecutionState } from 'features/nodes/hooks/useNodeExecutionState';
2625
import {
2726
$addNodeCmdk,
2827
$cursorPos,
@@ -83,23 +82,16 @@ export const Flow = memo(() => {
8382
const nodes = useAppSelector(selectNodes);
8483
const edges = useAppSelector(selectEdges);
8584
const viewport = useStore($viewport);
86-
const needsFit = useStore($needsFit);
87-
const mayUndo = useAppSelector(selectMayUndo);
88-
const mayRedo = useAppSelector(selectMayRedo);
8985
const shouldSnapToGrid = useAppSelector(selectShouldSnapToGrid);
9086
const selectionMode = useAppSelector(selectSelectionMode);
9187
const { onConnectStart, onConnect, onConnectEnd } = useConnection();
9288
const flowWrapper = useRef<HTMLDivElement>(null);
9389
const isValidConnection = useIsValidConnection();
94-
const cancelConnection = useReactFlowStore(selectCancelConnection);
9590
const updateNodeInternals = useUpdateNodeInternals();
96-
const store = useAppStore();
97-
const isWorkflowsFocused = useIsRegionFocused('workflows');
9891
const isLocked = useIsWorkflowEditorLocked();
9992

10093
useFocusRegion('workflows', flowWrapper);
10194

102-
useSyncExecutionState();
10395
const [borderRadius] = useToken('radii', ['base']);
10496
const flowStyles = useMemo<CSSProperties>(() => ({ borderRadius }), [borderRadius]);
10597

@@ -110,12 +102,12 @@ export const Flow = memo(() => {
110102
if (!flow) {
111103
return;
112104
}
113-
if (needsFit) {
105+
if ($needsFit.get()) {
114106
$needsFit.set(false);
115107
flow.fitView();
116108
}
117109
},
118-
[dispatch, needsFit]
110+
[dispatch]
119111
);
120112

121113
const onEdgesChange: OnEdgesChange<AnyEdge> = useCallback(
@@ -214,6 +206,83 @@ export const Flow = memo(() => {
214206

215207
// #endregion
216208

209+
const onNodeClick = useCallback<NodeMouseHandler<AnyNode>>((e, node) => {
210+
if (!$isSelectingOutputNode.get()) {
211+
return;
212+
}
213+
if (!isInvocationNode(node)) {
214+
return;
215+
}
216+
const { id } = node.data;
217+
$outputNodeId.set(id);
218+
$isSelectingOutputNode.set(false);
219+
}, []);
220+
221+
return (
222+
<>
223+
<ReactFlow<AnyNode, AnyEdge>
224+
id="workflow-editor"
225+
ref={flowWrapper}
226+
defaultViewport={viewport}
227+
nodeTypes={nodeTypes}
228+
edgeTypes={edgeTypes}
229+
nodes={nodes}
230+
edges={edges}
231+
onInit={onInit}
232+
onNodeClick={onNodeClick}
233+
onMouseMove={onMouseMove}
234+
onNodesChange={onNodesChange}
235+
onEdgesChange={onEdgesChange}
236+
onReconnect={onReconnect}
237+
onReconnectStart={onReconnectStart}
238+
onReconnectEnd={onReconnectEnd}
239+
onConnectStart={onConnectStart}
240+
onConnect={onConnect}
241+
onConnectEnd={onConnectEnd}
242+
onMoveEnd={handleMoveEnd}
243+
connectionLineComponent={CustomConnectionLine}
244+
isValidConnection={isValidConnection}
245+
edgesFocusable={!isLocked}
246+
edgesReconnectable={!isLocked}
247+
nodesDraggable={!isLocked}
248+
nodesConnectable={!isLocked}
249+
nodesFocusable={!isLocked}
250+
elementsSelectable={!isLocked}
251+
minZoom={0.1}
252+
snapToGrid={shouldSnapToGrid}
253+
snapGrid={snapGrid}
254+
connectionRadius={30}
255+
proOptions={proOptions}
256+
style={flowStyles}
257+
onPaneClick={handlePaneClick}
258+
deleteKeyCode={null}
259+
selectionMode={selectionMode}
260+
elevateEdgesOnSelect
261+
nodeDragThreshold={1}
262+
noDragClassName={NO_DRAG_CLASS}
263+
noWheelClassName={NO_WHEEL_CLASS}
264+
noPanClassName={NO_PAN_CLASS}
265+
>
266+
<Background />
267+
</ReactFlow>
268+
<HotkeyIsolator />
269+
</>
270+
);
271+
});
272+
273+
Flow.displayName = 'Flow';
274+
275+
const HotkeyIsolator = memo(() => {
276+
const isLocked = useIsWorkflowEditorLocked();
277+
278+
const mayUndo = useAppSelector(selectMayUndo);
279+
const mayRedo = useAppSelector(selectMayRedo);
280+
281+
const cancelConnection = useReactFlowStore(selectCancelConnection);
282+
283+
const store = useAppStore();
284+
const isWorkflowsFocused = useIsRegionFocused('workflows');
285+
217286
const { copySelection, pasteSelection, pasteSelectionWithEdges } = useNodeCopyPaste();
218287

219288
useRegisteredHotkeys({
@@ -239,12 +308,12 @@ export const Flow = memo(() => {
239308
}
240309
});
241310
if (nodeChanges.length > 0) {
242-
dispatch(nodesChanged(nodeChanges));
311+
store.dispatch(nodesChanged(nodeChanges));
243312
}
244313
if (edgeChanges.length > 0) {
245-
dispatch(edgesChanged(edgeChanges));
314+
store.dispatch(edgesChanged(edgeChanges));
246315
}
247-
}, [dispatch, store]);
316+
}, [store]);
248317
useRegisteredHotkeys({
249318
id: 'selectAll',
250319
category: 'workflows',
@@ -273,20 +342,20 @@ export const Flow = memo(() => {
273342
id: 'undo',
274343
category: 'workflows',
275344
callback: () => {
276-
dispatch(undo());
345+
store.dispatch(undo());
277346
},
278347
options: { enabled: isWorkflowsFocused && !isLocked && mayUndo, preventDefault: true },
279-
dependencies: [mayUndo, isLocked, isWorkflowsFocused],
348+
dependencies: [store, mayUndo, isLocked, isWorkflowsFocused],
280349
});
281350

282351
useRegisteredHotkeys({
283352
id: 'redo',
284353
category: 'workflows',
285354
callback: () => {
286-
dispatch(redo());
355+
store.dispatch(redo());
287356
},
288357
options: { enabled: isWorkflowsFocused && !isLocked && mayRedo, preventDefault: true },
289-
dependencies: [mayRedo, isLocked, isWorkflowsFocused],
358+
dependencies: [store, mayRedo, isLocked, isWorkflowsFocused],
290359
});
291360

292361
const onEscapeHotkey = useCallback(() => {
@@ -313,12 +382,12 @@ export const Flow = memo(() => {
313382
edgeChanges.push({ type: 'remove', id });
314383
});
315384
if (nodeChanges.length > 0) {
316-
dispatch(nodesChanged(nodeChanges));
385+
store.dispatch(nodesChanged(nodeChanges));
317386
}
318387
if (edgeChanges.length > 0) {
319-
dispatch(edgesChanged(edgeChanges));
388+
store.dispatch(edgesChanged(edgeChanges));
320389
}
321-
}, [dispatch, store]);
390+
}, [store]);
322391
useRegisteredHotkeys({
323392
id: 'deleteSelection',
324393
category: 'workflows',
@@ -327,65 +396,6 @@ export const Flow = memo(() => {
327396
dependencies: [deleteSelection, isWorkflowsFocused, isLocked],
328397
});
329398

330-
const onNodeClick = useCallback<NodeMouseHandler<AnyNode>>((e, node) => {
331-
if (!$isSelectingOutputNode.get()) {
332-
return;
333-
}
334-
if (!isInvocationNode(node)) {
335-
return;
336-
}
337-
const { id } = node.data;
338-
$outputNodeId.set(id);
339-
$isSelectingOutputNode.set(false);
340-
}, []);
341-
342-
return (
343-
<ReactFlow<AnyNode, AnyEdge>
344-
id="workflow-editor"
345-
ref={flowWrapper}
346-
defaultViewport={viewport}
347-
nodeTypes={nodeTypes}
348-
edgeTypes={edgeTypes}
349-
nodes={nodes}
350-
edges={edges}
351-
onInit={onInit}
352-
onNodeClick={onNodeClick}
353-
onMouseMove={onMouseMove}
354-
onNodesChange={onNodesChange}
355-
onEdgesChange={onEdgesChange}
356-
onReconnect={onReconnect}
357-
onReconnectStart={onReconnectStart}
358-
onReconnectEnd={onReconnectEnd}
359-
onConnectStart={onConnectStart}
360-
onConnect={onConnect}
361-
onConnectEnd={onConnectEnd}
362-
onMoveEnd={handleMoveEnd}
363-
connectionLineComponent={CustomConnectionLine}
364-
isValidConnection={isValidConnection}
365-
edgesFocusable={!isLocked}
366-
edgesReconnectable={!isLocked}
367-
nodesDraggable={!isLocked}
368-
nodesConnectable={!isLocked}
369-
nodesFocusable={!isLocked}
370-
elementsSelectable={!isLocked}
371-
minZoom={0.1}
372-
snapToGrid={shouldSnapToGrid}
373-
snapGrid={snapGrid}
374-
connectionRadius={30}
375-
proOptions={proOptions}
376-
style={flowStyles}
377-
onPaneClick={handlePaneClick}
378-
deleteKeyCode={null}
379-
selectionMode={selectionMode}
380-
elevateEdgesOnSelect
381-
nodeDragThreshold={1}
382-
noDragClassName={NO_DRAG_CLASS}
383-
noWheelClassName={NO_WHEEL_CLASS}
384-
noPanClassName={NO_PAN_CLASS}
385-
>
386-
<Background />
387-
</ReactFlow>
388-
);
399+
return null;
389400
});
390-
391-
Flow.displayName = 'Flow';
401+
HotkeyIsolator.displayName = 'HotkeyIsolator';

invokeai/frontend/web/src/features/nodes/components/flow/edges/util/buildEdgeSelectors.ts

Lines changed: 14 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -1,16 +1,16 @@
11
import { createSelector } from '@reduxjs/toolkit';
22
import { colorTokenToCssVar } from 'common/util/colorTokenToCssVar';
3-
import { selectNodesSlice } from 'features/nodes/store/selectors';
3+
import { selectNodes } from 'features/nodes/store/selectors';
44
import type { Templates } from 'features/nodes/store/types';
55
import { selectWorkflowSettingsSlice } from 'features/nodes/store/workflowSettingsSlice';
66
import { isInvocationNode } from 'features/nodes/types/invocation';
77

88
import { getFieldColor } from './getEdgeColor';
99

1010
export const buildSelectAreConnectedNodesSelected = (source: string, target: string) =>
11-
createSelector(selectNodesSlice, (nodes): boolean => {
12-
const sourceNode = nodes.nodes.find((node) => node.id === source);
13-
const targetNode = nodes.nodes.find((node) => node.id === target);
11+
createSelector(selectNodes, (nodes): boolean => {
12+
const sourceNode = nodes.find((node) => node.id === source);
13+
const targetNode = nodes.find((node) => node.id === target);
1414

1515
return Boolean(sourceNode?.selected || targetNode?.selected);
1616
});
@@ -22,10 +22,13 @@ export const buildSelectEdgeColor = (
2222
target: string,
2323
targetHandleId: string | null | undefined
2424
) =>
25-
createSelector(selectNodesSlice, selectWorkflowSettingsSlice, (nodes, workflowSettings): string => {
25+
createSelector(selectNodes, selectWorkflowSettingsSlice, (nodes, workflowSettings): string => {
2626
const { shouldColorEdges } = workflowSettings;
27-
const sourceNode = nodes.nodes.find((node) => node.id === source);
28-
const targetNode = nodes.nodes.find((node) => node.id === target);
27+
if (!shouldColorEdges) {
28+
return colorTokenToCssVar('base.500');
29+
}
30+
const sourceNode = nodes.find((node) => node.id === source);
31+
const targetNode = nodes.find((node) => node.id === target);
2932

3033
if (!sourceNode || !sourceHandleId || !targetNode || !targetHandleId) {
3134
return colorTokenToCssVar('base.500');
@@ -37,7 +40,7 @@ export const buildSelectEdgeColor = (
3740
const outputFieldTemplate = sourceNodeTemplate?.outputs[sourceHandleId];
3841
const sourceType = isInvocationToInvocationEdge ? outputFieldTemplate?.type : undefined;
3942

40-
return sourceType && shouldColorEdges ? getFieldColor(sourceType) : colorTokenToCssVar('base.500');
43+
return sourceType ? getFieldColor(sourceType) : colorTokenToCssVar('base.500');
4144
});
4245

4346
export const buildSelectEdgeLabel = (
@@ -47,9 +50,9 @@ export const buildSelectEdgeLabel = (
4750
target: string,
4851
targetHandleId: string | null | undefined
4952
) =>
50-
createSelector(selectNodesSlice, (nodes): string | null => {
51-
const sourceNode = nodes.nodes.find((node) => node.id === source);
52-
const targetNode = nodes.nodes.find((node) => node.id === target);
53+
createSelector(selectNodes, (nodes): string | null => {
54+
const sourceNode = nodes.find((node) => node.id === source);
55+
const targetNode = nodes.find((node) => node.id === target);
5356

5457
if (!sourceNode || !sourceHandleId || !targetNode || !targetHandleId) {
5558
return null;

0 commit comments

Comments
 (0)