From 7e2eeec1f31cd52307f925b5c4271bef6a5ec357 Mon Sep 17 00:00:00 2001 From: psychedelicious <4822129+psychedelicious@users.noreply.github.com> Date: Mon, 1 Jan 2024 15:54:31 +1100 Subject: [PATCH] feat(ui): optimized workflow building - Store workflow in nanostore as singleton instead of building for each consumer - Debounce the build (already was indirectly debounced) - When the workflow is needed, imperatively grab it from the nanostores, instead of letting react handle it via reactivity --- .../features/nodes/components/flow/Flow.tsx | 3 +- .../sidePanel/workflow/WorkflowJSONTab.tsx | 7 +++-- .../src/features/nodes/hooks/useWorkflow.ts | 19 ------------- .../nodes/hooks/useWorkflowWatcher.ts | 25 +++++++++++++++++ .../nodes/util/workflow/buildWorkflow.ts | 14 ++++++---- .../hooks/useDownloadWorkflow.ts | 28 ++++++++++--------- .../workflowLibrary/hooks/useSaveWorkflow.ts | 9 ++++-- .../hooks/useSaveWorkflowAs.ts | 9 ++++-- 8 files changed, 67 insertions(+), 47 deletions(-) delete mode 100644 invokeai/frontend/web/src/features/nodes/hooks/useWorkflow.ts create mode 100644 invokeai/frontend/web/src/features/nodes/hooks/useWorkflowWatcher.ts diff --git a/invokeai/frontend/web/src/features/nodes/components/flow/Flow.tsx b/invokeai/frontend/web/src/features/nodes/components/flow/Flow.tsx index 159e76c39a..b25ee9da8a 100644 --- a/invokeai/frontend/web/src/features/nodes/components/flow/Flow.tsx +++ b/invokeai/frontend/web/src/features/nodes/components/flow/Flow.tsx @@ -3,6 +3,7 @@ import { useAppDispatch, useAppSelector } from 'app/store/storeHooks'; import { useGlobalMenuCloseTrigger } from 'common/hooks/useGlobalMenuCloseTrigger'; import { useIsValidConnection } from 'features/nodes/hooks/useIsValidConnection'; import { $mouseOverNode } from 'features/nodes/hooks/useMouseOverNode'; +import { useWorkflowWatcher } from 'features/nodes/hooks/useWorkflowWatcher'; import { connectionEnded, connectionMade, @@ -80,7 +81,7 @@ export const Flow = memo(() => { const flowWrapper = useRef(null); const cursorPosition = useRef(null); const isValidConnection = useIsValidConnection(); - + useWorkflowWatcher(); const [borderRadius] = useToken('radii', ['base']); const flowStyles = useMemo( diff --git a/invokeai/frontend/web/src/features/nodes/components/sidePanel/workflow/WorkflowJSONTab.tsx b/invokeai/frontend/web/src/features/nodes/components/sidePanel/workflow/WorkflowJSONTab.tsx index b51100306f..02fa863f62 100644 --- a/invokeai/frontend/web/src/features/nodes/components/sidePanel/workflow/WorkflowJSONTab.tsx +++ b/invokeai/frontend/web/src/features/nodes/components/sidePanel/workflow/WorkflowJSONTab.tsx @@ -1,16 +1,17 @@ import { Flex } from '@chakra-ui/react'; +import { useStore } from '@nanostores/react'; import DataViewer from 'features/gallery/components/ImageMetadataViewer/DataViewer'; -import { useWorkflow } from 'features/nodes/hooks/useWorkflow'; +import { $builtWorkflow } from 'features/nodes/hooks/useWorkflowWatcher'; import { memo } from 'react'; import { useTranslation } from 'react-i18next'; const WorkflowJSONTab = () => { - const workflow = useWorkflow(); + const workflow = useStore($builtWorkflow); const { t } = useTranslation(); return ( - + ); }; diff --git a/invokeai/frontend/web/src/features/nodes/hooks/useWorkflow.ts b/invokeai/frontend/web/src/features/nodes/hooks/useWorkflow.ts deleted file mode 100644 index fecb15c2e9..0000000000 --- a/invokeai/frontend/web/src/features/nodes/hooks/useWorkflow.ts +++ /dev/null @@ -1,19 +0,0 @@ -import { useAppSelector } from 'app/store/storeHooks'; -import { buildWorkflow } from 'features/nodes/util/workflow/buildWorkflow'; -import { useMemo } from 'react'; -import { useDebounce } from 'use-debounce'; - -export const useWorkflow = () => { - const nodes_ = useAppSelector((state) => state.nodes.nodes); - const edges_ = useAppSelector((state) => state.nodes.edges); - const workflow_ = useAppSelector((state) => state.workflow); - const [nodes] = useDebounce(nodes_, 300); - const [edges] = useDebounce(edges_, 300); - const [workflow] = useDebounce(workflow_, 300); - const builtWorkflow = useMemo( - () => buildWorkflow({ nodes, edges, workflow }), - [nodes, edges, workflow] - ); - - return builtWorkflow; -}; diff --git a/invokeai/frontend/web/src/features/nodes/hooks/useWorkflowWatcher.ts b/invokeai/frontend/web/src/features/nodes/hooks/useWorkflowWatcher.ts new file mode 100644 index 0000000000..564445df0f --- /dev/null +++ b/invokeai/frontend/web/src/features/nodes/hooks/useWorkflowWatcher.ts @@ -0,0 +1,25 @@ +import { useAppSelector } from 'app/store/storeHooks'; +import type { WorkflowV2 } from 'features/nodes/types/workflow'; +import type { BuildWorkflowArg } from 'features/nodes/util/workflow/buildWorkflow'; +import { buildWorkflow } from 'features/nodes/util/workflow/buildWorkflow'; +import { debounce } from 'lodash-es'; +import { atom } from 'nanostores'; +import { useEffect } from 'react'; + +export const $builtWorkflow = atom(null); + +const debouncedBuildWorkflow = debounce((arg: BuildWorkflowArg) => { + $builtWorkflow.set(buildWorkflow(arg)); +}, 300); + +export const useWorkflowWatcher = () => { + const buildWorkflowArg = useAppSelector(({ nodes, workflow }) => ({ + nodes: nodes.nodes, + edges: nodes.edges, + workflow, + })); + + useEffect(() => { + debouncedBuildWorkflow(buildWorkflowArg); + }, [buildWorkflowArg]); +}; diff --git a/invokeai/frontend/web/src/features/nodes/util/workflow/buildWorkflow.ts b/invokeai/frontend/web/src/features/nodes/util/workflow/buildWorkflow.ts index 1389732fd0..1e1c383410 100644 --- a/invokeai/frontend/web/src/features/nodes/util/workflow/buildWorkflow.ts +++ b/invokeai/frontend/web/src/features/nodes/util/workflow/buildWorkflow.ts @@ -2,19 +2,23 @@ import { logger } from 'app/logging/logger'; import { parseify } from 'common/util/serialize'; import type { NodesState, WorkflowsState } from 'features/nodes/store/types'; import { isInvocationNode, isNotesNode } from 'features/nodes/types/invocation'; -import type { WorkflowV2 } from 'features/nodes/types/workflow'; -import { zWorkflowEdge, zWorkflowNode } from 'features/nodes/types/workflow'; -import i18n from 'i18next'; +import type { + WorkflowV2} from 'features/nodes/types/workflow'; +import { + zWorkflowEdge, + zWorkflowNode, +} from 'features/nodes/types/workflow'; +import i18n from 'i18n'; import { cloneDeep, omit } from 'lodash-es'; import { fromZodError } from 'zod-validation-error'; -type BuildWorkflowArg = { +export type BuildWorkflowArg = { nodes: NodesState['nodes']; edges: NodesState['edges']; workflow: WorkflowsState; }; -type BuildWorkflowFunction = (arg: BuildWorkflowArg) => WorkflowV2; +export type BuildWorkflowFunction = (arg: BuildWorkflowArg) => WorkflowV2; export const buildWorkflow: BuildWorkflowFunction = ({ nodes, diff --git a/invokeai/frontend/web/src/features/workflowLibrary/hooks/useDownloadWorkflow.ts b/invokeai/frontend/web/src/features/workflowLibrary/hooks/useDownloadWorkflow.ts index 8b0a640ef9..77215f7287 100644 --- a/invokeai/frontend/web/src/features/workflowLibrary/hooks/useDownloadWorkflow.ts +++ b/invokeai/frontend/web/src/features/workflowLibrary/hooks/useDownloadWorkflow.ts @@ -1,17 +1,19 @@ -import { useWorkflow } from 'features/nodes/hooks/useWorkflow'; -import { useCallback } from 'react'; +import { $builtWorkflow } from 'features/nodes/hooks/useWorkflowWatcher'; + +const downloadWorkflow = () => { + const workflow = $builtWorkflow.get(); + if (!workflow) { + return; + } + const blob = new Blob([JSON.stringify(workflow, null, 2)]); + const a = document.createElement('a'); + a.href = URL.createObjectURL(blob); + a.download = `${workflow.name || 'My Workflow'}.json`; + document.body.appendChild(a); + a.click(); + a.remove(); +}; export const useDownloadWorkflow = () => { - const workflow = useWorkflow(); - const downloadWorkflow = useCallback(() => { - const blob = new Blob([JSON.stringify(workflow, null, 2)]); - const a = document.createElement('a'); - a.href = URL.createObjectURL(blob); - a.download = `${workflow.name || 'My Workflow'}.json`; - document.body.appendChild(a); - a.click(); - a.remove(); - }, [workflow]); - return downloadWorkflow; }; diff --git a/invokeai/frontend/web/src/features/workflowLibrary/hooks/useSaveWorkflow.ts b/invokeai/frontend/web/src/features/workflowLibrary/hooks/useSaveWorkflow.ts index 91d591bd89..17293d954c 100644 --- a/invokeai/frontend/web/src/features/workflowLibrary/hooks/useSaveWorkflow.ts +++ b/invokeai/frontend/web/src/features/workflowLibrary/hooks/useSaveWorkflow.ts @@ -1,7 +1,7 @@ import type { ToastId } from '@chakra-ui/react'; import { useToast } from '@chakra-ui/react'; import { useAppDispatch } from 'app/store/storeHooks'; -import { useWorkflow } from 'features/nodes/hooks/useWorkflow'; +import { $builtWorkflow } from 'features/nodes/hooks/useWorkflowWatcher'; import { workflowIDChanged, workflowSaved, @@ -30,12 +30,15 @@ const isWorkflowWithID = ( export const useSaveLibraryWorkflow: UseSaveLibraryWorkflow = () => { const { t } = useTranslation(); const dispatch = useAppDispatch(); - const workflow = useWorkflow(); const [updateWorkflow, updateWorkflowResult] = useUpdateWorkflowMutation(); const [createWorkflow, createWorkflowResult] = useCreateWorkflowMutation(); const toast = useToast(); const toastRef = useRef(); const saveWorkflow = useCallback(async () => { + const workflow = $builtWorkflow.get(); + if (!workflow) { + return; + } toastRef.current = toast({ title: t('workflows.savingWorkflow'), status: 'loading', @@ -64,7 +67,7 @@ export const useSaveLibraryWorkflow: UseSaveLibraryWorkflow = () => { isClosable: true, }); } - }, [workflow, updateWorkflow, dispatch, toast, t, createWorkflow]); + }, [updateWorkflow, dispatch, toast, t, createWorkflow]); return { saveWorkflow, isLoading: updateWorkflowResult.isLoading || createWorkflowResult.isLoading, diff --git a/invokeai/frontend/web/src/features/workflowLibrary/hooks/useSaveWorkflowAs.ts b/invokeai/frontend/web/src/features/workflowLibrary/hooks/useSaveWorkflowAs.ts index 2162299afc..5081c2010c 100644 --- a/invokeai/frontend/web/src/features/workflowLibrary/hooks/useSaveWorkflowAs.ts +++ b/invokeai/frontend/web/src/features/workflowLibrary/hooks/useSaveWorkflowAs.ts @@ -1,7 +1,7 @@ import type { ToastId } from '@chakra-ui/react'; import { useToast } from '@chakra-ui/react'; import { useAppDispatch } from 'app/store/storeHooks'; -import { useWorkflow } from 'features/nodes/hooks/useWorkflow'; +import { $builtWorkflow } from 'features/nodes/hooks/useWorkflowWatcher'; import { workflowIDChanged, workflowNameChanged, @@ -28,12 +28,15 @@ type UseSaveWorkflowAs = () => UseSaveWorkflowAsReturn; export const useSaveWorkflowAs: UseSaveWorkflowAs = () => { const { t } = useTranslation(); const dispatch = useAppDispatch(); - const workflow = useWorkflow(); const [createWorkflow, createWorkflowResult] = useCreateWorkflowMutation(); const toast = useToast(); const toastRef = useRef(); const saveWorkflowAs = useCallback( async ({ name: newName, onSuccess, onError }: SaveWorkflowAsArg) => { + const workflow = $builtWorkflow.get(); + if (!workflow) { + return; + } toastRef.current = toast({ title: t('workflows.savingWorkflow'), status: 'loading', @@ -64,7 +67,7 @@ export const useSaveWorkflowAs: UseSaveWorkflowAs = () => { }); } }, - [toast, workflow, createWorkflow, dispatch, t] + [toast, createWorkflow, dispatch, t] ); return { saveWorkflowAs,