From a0cdcdef57943cc96c6093f919b6fcbf08f63a92 Mon Sep 17 00:00:00 2001 From: psychedelicious <4822129+psychedelicious@users.noreply.github.com> Date: Sun, 16 Feb 2025 08:53:07 +1000 Subject: [PATCH] perf(ui): debounce invoke readiness calculations --- .../frontend/web/src/app/components/App.tsx | 2 + .../InvokeButtonTooltip.tsx | 89 +------ .../web/src/features/queue/hooks/useInvoke.ts | 52 +--- .../web/src/features/queue/store/readiness.ts | 249 +++++++++--------- 4 files changed, 141 insertions(+), 251 deletions(-) diff --git a/invokeai/frontend/web/src/app/components/App.tsx b/invokeai/frontend/web/src/app/components/App.tsx index f1b334628e..137ba27ae2 100644 --- a/invokeai/frontend/web/src/app/components/App.tsx +++ b/invokeai/frontend/web/src/app/components/App.tsx @@ -27,6 +27,7 @@ import { useStarterModelsToast } from 'features/modelManagerV2/hooks/useStarterM import { ShareWorkflowModal } from 'features/nodes/components/sidePanel/WorkflowListMenu/ShareWorkflowModal'; import { CancelAllExceptCurrentQueueItemConfirmationAlertDialog } from 'features/queue/components/CancelAllExceptCurrentQueueItemConfirmationAlertDialog'; import { ClearQueueConfirmationsAlertDialog } from 'features/queue/components/ClearQueueConfirmationAlertDialog'; +import { useReadinessWatcher } from 'features/queue/store/readiness'; import { DeleteStylePresetDialog } from 'features/stylePresets/components/DeleteStylePresetDialog'; import { StylePresetModal } from 'features/stylePresets/components/StylePresetForm/StylePresetModal'; import RefreshAfterResetModal from 'features/system/components/SettingsModal/RefreshAfterResetModal'; @@ -101,6 +102,7 @@ const HookIsolator = memo( const dispatch = useAppDispatch(); // singleton! + useReadinessWatcher(); useSocketIO(); useGlobalModifiersInit(); useGlobalHotkeys(); diff --git a/invokeai/frontend/web/src/features/queue/components/InvokeButtonTooltip/InvokeButtonTooltip.tsx b/invokeai/frontend/web/src/features/queue/components/InvokeButtonTooltip/InvokeButtonTooltip.tsx index 595ecb650a..f59c1b443d 100644 --- a/invokeai/frontend/web/src/features/queue/components/InvokeButtonTooltip/InvokeButtonTooltip.tsx +++ b/invokeai/frontend/web/src/features/queue/components/InvokeButtonTooltip/InvokeButtonTooltip.tsx @@ -1,22 +1,15 @@ import type { TooltipProps } from '@invoke-ai/ui-library'; import { Divider, Flex, ListItem, Text, Tooltip, UnorderedList } from '@invoke-ai/ui-library'; import { useStore } from '@nanostores/react'; -import { $true } from 'app/store/nanostores/util'; import { useAppSelector } from 'app/store/storeHooks'; -import { useCanvasManagerSafe } from 'features/controlLayers/contexts/CanvasManagerProviderGate'; import { selectSendToCanvas } from 'features/controlLayers/store/canvasSettingsSlice'; import { selectIterations } from 'features/controlLayers/store/paramsSlice'; import { selectDynamicPromptsIsLoading } from 'features/dynamicPrompts/store/dynamicPromptsSlice'; import { selectAutoAddBoardId } from 'features/gallery/store/gallerySelectors'; -import { $templates } from 'features/nodes/store/nodesSlice'; import type { Reason } from 'features/queue/store/readiness'; import { - buildSelectIsReadyToEnqueueCanvasTab, - buildSelectIsReadyToEnqueueUpscaleTab, - buildSelectIsReadyToEnqueueWorkflowsTab, - buildSelectReasonsWhyCannotEnqueueCanvasTab, - buildSelectReasonsWhyCannotEnqueueUpscaleTab, - buildSelectReasonsWhyCannotEnqueueWorkflowsTab, + $isReadyToEnqueue, + $reasonsWhyCannotEnqueue, selectPromptsCount, selectWorkflowsBatchSize, } from 'features/queue/store/readiness'; @@ -26,7 +19,6 @@ import { memo, useMemo } from 'react'; import { useTranslation } from 'react-i18next'; import { enqueueMutationFixedCacheKeyOptions, useEnqueueBatchMutation } from 'services/api/endpoints/queue'; import { useBoardName } from 'services/api/hooks/useBoardName'; -import { $isConnected } from 'services/events/stores'; type Props = TooltipProps & { prepend?: boolean; @@ -60,56 +52,8 @@ const TooltipContent = memo(({ prepend = false }: { prepend?: boolean }) => { TooltipContent.displayName = 'TooltipContent'; const CanvasTabTooltipContent = memo(({ prepend = false }: { prepend?: boolean }) => { - const isConnected = useStore($isConnected); - const canvasManager = useCanvasManagerSafe(); - const canvasIsFiltering = useStore(canvasManager?.stateApi.$isFiltering ?? $true); - const canvasIsTransforming = useStore(canvasManager?.stateApi.$isTransforming ?? $true); - const canvasIsRasterizing = useStore(canvasManager?.stateApi.$isRasterizing ?? $true); - const canvasIsSelectingObject = useStore(canvasManager?.stateApi.$isSegmenting ?? $true); - const canvasIsCompositing = useStore(canvasManager?.compositor.$isBusy ?? $true); - - const selectIsReady = useMemo( - () => - buildSelectIsReadyToEnqueueCanvasTab({ - isConnected, - canvasIsFiltering, - canvasIsTransforming, - canvasIsRasterizing, - canvasIsSelectingObject, - canvasIsCompositing, - }), - [ - isConnected, - canvasIsCompositing, - canvasIsFiltering, - canvasIsRasterizing, - canvasIsSelectingObject, - canvasIsTransforming, - ] - ); - - const selectReasons = useMemo( - () => - buildSelectReasonsWhyCannotEnqueueCanvasTab({ - isConnected, - canvasIsFiltering, - canvasIsTransforming, - canvasIsRasterizing, - canvasIsSelectingObject, - canvasIsCompositing, - }), - [ - isConnected, - canvasIsCompositing, - canvasIsFiltering, - canvasIsRasterizing, - canvasIsSelectingObject, - canvasIsTransforming, - ] - ); - - const isReady = useAppSelector(selectIsReady); - const reasons = useAppSelector(selectReasons); + const isReady = useStore($isReadyToEnqueue); + const reasons = useStore($reasonsWhyCannotEnqueue); return ( @@ -129,13 +73,8 @@ const CanvasTabTooltipContent = memo(({ prepend = false }: { prepend?: boolean } CanvasTabTooltipContent.displayName = 'CanvasTabTooltipContent'; const UpscaleTabTooltipContent = memo(({ prepend = false }: { prepend?: boolean }) => { - const isConnected = useStore($isConnected); - - const selectIsReady = useMemo(() => buildSelectIsReadyToEnqueueUpscaleTab({ isConnected }), [isConnected]); - const selectReasons = useMemo(() => buildSelectReasonsWhyCannotEnqueueUpscaleTab({ isConnected }), [isConnected]); - - const isReady = useAppSelector(selectIsReady); - const reasons = useAppSelector(selectReasons); + const isReady = useStore($isReadyToEnqueue); + const reasons = useStore($reasonsWhyCannotEnqueue); return ( @@ -153,20 +92,8 @@ const UpscaleTabTooltipContent = memo(({ prepend = false }: { prepend?: boolean UpscaleTabTooltipContent.displayName = 'UpscaleTabTooltipContent'; const WorkflowsTabTooltipContent = memo(({ prepend = false }: { prepend?: boolean }) => { - const isConnected = useStore($isConnected); - const templates = useStore($templates); - - const selectIsReady = useMemo( - () => buildSelectIsReadyToEnqueueWorkflowsTab({ isConnected, templates }), - [isConnected, templates] - ); - const selectReasons = useMemo( - () => buildSelectReasonsWhyCannotEnqueueWorkflowsTab({ isConnected, templates }), - [isConnected, templates] - ); - - const isReady = useAppSelector(selectIsReady); - const reasons = useAppSelector(selectReasons); + const isReady = useStore($isReadyToEnqueue); + const reasons = useStore($reasonsWhyCannotEnqueue); return ( diff --git a/invokeai/frontend/web/src/features/queue/hooks/useInvoke.ts b/invokeai/frontend/web/src/features/queue/hooks/useInvoke.ts index 0b06aae380..26c369fe1e 100644 --- a/invokeai/frontend/web/src/features/queue/hooks/useInvoke.ts +++ b/invokeai/frontend/web/src/features/queue/hooks/useInvoke.ts @@ -1,61 +1,15 @@ import { useStore } from '@nanostores/react'; import { enqueueRequested } from 'app/store/actions'; -import { $true } from 'app/store/nanostores/util'; import { useAppDispatch, useAppSelector } from 'app/store/storeHooks'; -import { useCanvasManagerSafe } from 'features/controlLayers/contexts/CanvasManagerProviderGate'; -import { $templates } from 'features/nodes/store/nodesSlice'; -import { - buildSelectIsReadyToEnqueueCanvasTab, - buildSelectIsReadyToEnqueueUpscaleTab, - buildSelectIsReadyToEnqueueWorkflowsTab, -} from 'features/queue/store/readiness'; +import { $isReadyToEnqueue } from 'features/queue/store/readiness'; import { selectActiveTab } from 'features/ui/store/uiSelectors'; -import { useCallback, useMemo } from 'react'; +import { useCallback } from 'react'; import { enqueueMutationFixedCacheKeyOptions, useEnqueueBatchMutation } from 'services/api/endpoints/queue'; -import { $isConnected } from 'services/events/stores'; export const useInvoke = () => { const dispatch = useAppDispatch(); const tabName = useAppSelector(selectActiveTab); - const isConnected = useStore($isConnected); - const canvasManager = useCanvasManagerSafe(); - const canvasIsFiltering = useStore(canvasManager?.stateApi.$isFiltering ?? $true); - const canvasIsTransforming = useStore(canvasManager?.stateApi.$isTransforming ?? $true); - const canvasIsRasterizing = useStore(canvasManager?.stateApi.$isRasterizing ?? $true); - const canvasIsSelectingObject = useStore(canvasManager?.stateApi.$isSegmenting ?? $true); - const canvasIsCompositing = useStore(canvasManager?.compositor.$isBusy ?? $true); - const templates = useStore($templates); - - const selectIsReady = useMemo(() => { - if (tabName === 'canvas') { - return buildSelectIsReadyToEnqueueCanvasTab({ - isConnected, - canvasIsFiltering, - canvasIsTransforming, - canvasIsRasterizing, - canvasIsSelectingObject, - canvasIsCompositing, - }); - } - if (tabName === 'upscaling') { - return buildSelectIsReadyToEnqueueUpscaleTab({ isConnected }); - } - if (tabName === 'workflows') { - return buildSelectIsReadyToEnqueueWorkflowsTab({ isConnected, templates }); - } - return () => false; - }, [ - tabName, - isConnected, - canvasIsFiltering, - canvasIsTransforming, - canvasIsRasterizing, - canvasIsSelectingObject, - canvasIsCompositing, - templates, - ]); - - const isReady = useAppSelector(selectIsReady); + const isReady = useStore($isReadyToEnqueue); const [_, { isLoading }] = useEnqueueBatchMutation(enqueueMutationFixedCacheKeyOptions); const queueBack = useCallback(() => { diff --git a/invokeai/frontend/web/src/features/queue/store/readiness.ts b/invokeai/frontend/web/src/features/queue/store/readiness.ts index ff3026e651..0223e68936 100644 --- a/invokeai/frontend/web/src/features/queue/store/readiness.ts +++ b/invokeai/frontend/web/src/features/queue/store/readiness.ts @@ -1,7 +1,12 @@ +import { useStore } from '@nanostores/react'; import { createSelector } from '@reduxjs/toolkit'; import { getConnectedEdges } from '@xyflow/react'; import { createMemoizedSelector } from 'app/store/createMemoizedSelector'; +import { $true } from 'app/store/nanostores/util'; +import { useAppSelector } from 'app/store/storeHooks'; import type { AppConfig } from 'app/types/invokeai'; +import { useAssertSingleton } from 'common/hooks/useAssertSingleton'; +import { useCanvasManagerSafe } from 'features/controlLayers/contexts/CanvasManagerProviderGate'; import type { ParamsState } from 'features/controlLayers/store/paramsSlice'; import { selectParamsSlice } from 'features/controlLayers/store/paramsSlice'; import { selectCanvasSlice } from 'features/controlLayers/store/selectors'; @@ -16,6 +21,7 @@ import { import type { DynamicPromptsState } from 'features/dynamicPrompts/store/dynamicPromptsSlice'; import { selectDynamicPromptsSlice } from 'features/dynamicPrompts/store/dynamicPromptsSlice'; import { getShouldProcessPrompt } from 'features/dynamicPrompts/util/getShouldProcessPrompt'; +import { $templates } from 'features/nodes/store/nodesSlice'; import { selectNodesSlice } from 'features/nodes/store/selectors'; import type { NodesState, Templates } from 'features/nodes/store/types'; import type { WorkflowSettingsState } from 'features/nodes/store/workflowSettingsSlice'; @@ -46,8 +52,13 @@ import { isBatchNode, isExecutableNode, isInvocationNode } from 'features/nodes/ import type { UpscaleState } from 'features/parameters/store/upscaleSlice'; import { selectUpscaleSlice } from 'features/parameters/store/upscaleSlice'; import { selectConfigSlice } from 'features/system/store/configSlice'; +import { selectActiveTab } from 'features/ui/store/uiSelectors'; +import type { TabName } from 'features/ui/store/uiTypes'; import i18n from 'i18next'; -import { forEach, groupBy, upperFirst } from 'lodash-es'; +import { debounce, forEach, groupBy, upperFirst } from 'lodash-es'; +import { atom, computed } from 'nanostores'; +import { useEffect } from 'react'; +import { $isConnected } from 'services/events/stores'; import { assert } from 'tsafe'; /** @@ -68,6 +79,122 @@ const LAYER_TYPE_TO_TKEY = { export type Reason = { prefix?: string; content: string }; +export const $reasonsWhyCannotEnqueue = atom([]); +export const $isReadyToEnqueue = computed($reasonsWhyCannotEnqueue, (reasons) => reasons.length === 0); + +const debouncedUpdateReasons = debounce( + ( + tab: TabName, + isConnected: boolean, + canvas: CanvasState, + params: ParamsState, + dynamicPrompts: DynamicPromptsState, + canvasIsFiltering: boolean, + canvasIsTransforming: boolean, + canvasIsRasterizing: boolean, + canvasIsCompositing: boolean, + canvasIsSelectingObject: boolean, + nodes: NodesState, + workflowSettings: WorkflowSettingsState, + templates: Templates, + upscale: UpscaleState, + config: AppConfig + ) => { + if (tab === 'canvas') { + $reasonsWhyCannotEnqueue.set( + getReasonsWhyCannotEnqueueCanvasTab({ + isConnected, + canvas, + params, + dynamicPrompts, + canvasIsFiltering, + canvasIsTransforming, + canvasIsRasterizing, + canvasIsCompositing, + canvasIsSelectingObject, + }) + ); + } else if (tab === 'workflows') { + $reasonsWhyCannotEnqueue.set( + getReasonsWhyCannotEnqueueWorkflowsTab({ + isConnected, + nodes, + workflowSettings, + templates, + }) + ); + } else if (tab === 'upscaling') { + $reasonsWhyCannotEnqueue.set( + getReasonsWhyCannotEnqueueUpscaleTab({ + isConnected, + upscale, + config, + params, + }) + ); + } else { + $reasonsWhyCannotEnqueue.set([]); + } + }, + 300 +); + +export const useReadinessWatcher = () => { + useAssertSingleton('useReadinessWatcher'); + const canvasManager = useCanvasManagerSafe(); + const tab = useAppSelector(selectActiveTab); + const canvas = useAppSelector(selectCanvasSlice); + const params = useAppSelector(selectParamsSlice); + const dynamicPrompts = useAppSelector(selectDynamicPromptsSlice); + const nodes = useAppSelector(selectNodesSlice); + const workflowSettings = useAppSelector(selectWorkflowSettingsSlice); + const upscale = useAppSelector(selectUpscaleSlice); + const config = useAppSelector(selectConfigSlice); + const templates = useStore($templates); + const isConnected = useStore($isConnected); + const canvasIsFiltering = useStore(canvasManager?.stateApi.$isFiltering ?? $true); + const canvasIsTransforming = useStore(canvasManager?.stateApi.$isTransforming ?? $true); + const canvasIsRasterizing = useStore(canvasManager?.stateApi.$isRasterizing ?? $true); + const canvasIsSelectingObject = useStore(canvasManager?.stateApi.$isSegmenting ?? $true); + const canvasIsCompositing = useStore(canvasManager?.compositor.$isBusy ?? $true); + + useEffect(() => { + debouncedUpdateReasons( + tab, + isConnected, + canvas, + params, + dynamicPrompts, + canvasIsFiltering, + canvasIsTransforming, + canvasIsRasterizing, + canvasIsCompositing, + canvasIsSelectingObject, + nodes, + workflowSettings, + templates, + upscale, + config + ); + }, [ + canvas, + canvasIsCompositing, + canvasIsFiltering, + canvasIsRasterizing, + canvasIsSelectingObject, + canvasIsTransforming, + config, + dynamicPrompts, + isConnected, + nodes, + params, + tab, + templates, + upscale, + workflowSettings, + ]); +}; + const disconnectedReason = (t: typeof i18n.t) => ({ content: t('parameters.invoke.systemDisconnected') }); export const resolveBatchValue = (batchNode: InvocationNode, nodes: InvocationNode[], edges: AnyEdge[]) => { @@ -485,126 +612,6 @@ const getReasonsWhyCannotEnqueueCanvasTab = (arg: { return reasons; }; -export const buildSelectReasonsWhyCannotEnqueueCanvasTab = (arg: { - isConnected: boolean; - canvasIsFiltering: boolean; - canvasIsTransforming: boolean; - canvasIsRasterizing: boolean; - canvasIsCompositing: boolean; - canvasIsSelectingObject: boolean; -}) => { - const { - isConnected, - canvasIsFiltering, - canvasIsTransforming, - canvasIsRasterizing, - canvasIsCompositing, - canvasIsSelectingObject, - } = arg; - - return createSelector( - selectCanvasSlice, - selectParamsSlice, - selectDynamicPromptsSlice, - (canvas, params, dynamicPrompts) => - getReasonsWhyCannotEnqueueCanvasTab({ - isConnected, - canvas, - params, - dynamicPrompts, - canvasIsFiltering, - canvasIsTransforming, - canvasIsRasterizing, - canvasIsCompositing, - canvasIsSelectingObject, - }) - ); -}; - -export const buildSelectIsReadyToEnqueueCanvasTab = (arg: { - isConnected: boolean; - canvasIsFiltering: boolean; - canvasIsTransforming: boolean; - canvasIsRasterizing: boolean; - canvasIsCompositing: boolean; - canvasIsSelectingObject: boolean; -}) => { - const { - isConnected, - canvasIsFiltering, - canvasIsTransforming, - canvasIsRasterizing, - canvasIsCompositing, - canvasIsSelectingObject, - } = arg; - - return createSelector( - selectCanvasSlice, - selectParamsSlice, - selectDynamicPromptsSlice, - (canvas, params, dynamicPrompts) => - getReasonsWhyCannotEnqueueCanvasTab({ - isConnected, - canvas, - params, - dynamicPrompts, - canvasIsFiltering, - canvasIsTransforming, - canvasIsRasterizing, - canvasIsCompositing, - canvasIsSelectingObject, - }).length === 0 - ); -}; - -export const buildSelectReasonsWhyCannotEnqueueUpscaleTab = (arg: { isConnected: boolean }) => { - const { isConnected } = arg; - return createSelector(selectUpscaleSlice, selectConfigSlice, selectParamsSlice, (upscale, config, params) => - getReasonsWhyCannotEnqueueUpscaleTab({ isConnected, upscale, config, params }) - ); -}; - -export const buildSelectIsReadyToEnqueueUpscaleTab = (arg: { isConnected: boolean }) => { - const { isConnected } = arg; - - return createSelector( - selectUpscaleSlice, - selectConfigSlice, - selectParamsSlice, - (upscale, config, params) => - getReasonsWhyCannotEnqueueUpscaleTab({ isConnected, upscale, config, params }).length === 0 - ); -}; - -export const buildSelectReasonsWhyCannotEnqueueWorkflowsTab = (arg: { isConnected: boolean; templates: Templates }) => { - const { isConnected, templates } = arg; - - return createSelector(selectNodesSlice, selectWorkflowSettingsSlice, (nodes, workflowSettings) => - getReasonsWhyCannotEnqueueWorkflowsTab({ - isConnected, - nodes, - workflowSettings, - templates, - }) - ); -}; - -export const buildSelectIsReadyToEnqueueWorkflowsTab = (arg: { isConnected: boolean; templates: Templates }) => { - const { isConnected, templates } = arg; - - return createSelector( - selectNodesSlice, - selectWorkflowSettingsSlice, - (nodes, workflowSettings) => - getReasonsWhyCannotEnqueueWorkflowsTab({ - isConnected, - nodes, - workflowSettings, - templates, - }).length === 0 - ); -}; - export const selectPromptsCount = createSelector( selectParamsSlice, selectDynamicPromptsSlice,