diff --git a/invokeai/app/api/routers/app_info.py b/invokeai/app/api/routers/app_info.py index a98c8edc6a..39d570ec99 100644 --- a/invokeai/app/api/routers/app_info.py +++ b/invokeai/app/api/routers/app_info.py @@ -7,6 +7,7 @@ from fastapi.routing import APIRouter from pydantic import BaseModel, Field from invokeai.app.invocations.upscale import ESRGAN_MODELS +from invokeai.app.services.invocation_cache.invocation_cache_common import InvocationCacheStatus from invokeai.backend.image_util.invisible_watermark import InvisibleWatermark from invokeai.backend.image_util.patchmatch import PatchMatch from invokeai.backend.image_util.safety_checker import SafetyChecker @@ -113,3 +114,33 @@ async def set_log_level( async def clear_invocation_cache() -> None: """Clears the invocation cache""" ApiDependencies.invoker.services.invocation_cache.clear() + + +@app_router.put( + "/invocation_cache/enable", + operation_id="enable_invocation_cache", + responses={200: {"description": "The operation was successful"}}, +) +async def enable_invocation_cache() -> None: + """Clears the invocation cache""" + ApiDependencies.invoker.services.invocation_cache.enable() + + +@app_router.put( + "/invocation_cache/disable", + operation_id="disable_invocation_cache", + responses={200: {"description": "The operation was successful"}}, +) +async def disable_invocation_cache() -> None: + """Clears the invocation cache""" + ApiDependencies.invoker.services.invocation_cache.disable() + + +@app_router.get( + "/invocation_cache/status", + operation_id="get_invocation_cache_status", + responses={200: {"model": InvocationCacheStatus}}, +) +async def get_invocation_cache_status() -> InvocationCacheStatus: + """Clears the invocation cache""" + return ApiDependencies.invoker.services.invocation_cache.get_status() diff --git a/invokeai/app/services/invocation_cache/invocation_cache_base.py b/invokeai/app/services/invocation_cache/invocation_cache_base.py index c35a31f851..d913e050ab 100644 --- a/invokeai/app/services/invocation_cache/invocation_cache_base.py +++ b/invokeai/app/services/invocation_cache/invocation_cache_base.py @@ -2,6 +2,7 @@ from abc import ABC, abstractmethod from typing import Optional, Union from invokeai.app.invocations.baseinvocation import BaseInvocation, BaseInvocationOutput +from invokeai.app.services.invocation_cache.invocation_cache_common import InvocationCacheStatus class InvocationCacheBase(ABC): @@ -32,7 +33,7 @@ class InvocationCacheBase(ABC): @abstractmethod def delete(self, key: Union[int, str]) -> None: - """Deleteds an invocation output from the cache""" + """Deletes an invocation output from the cache""" pass @abstractmethod @@ -44,3 +45,18 @@ class InvocationCacheBase(ABC): def create_key(self, invocation: BaseInvocation) -> int: """Gets the key for the invocation's cache item""" pass + + @abstractmethod + def disable(self) -> None: + """Disables the cache, overriding the max cache size""" + pass + + @abstractmethod + def enable(self) -> None: + """Enables the cache, letting the the max cache size take effect""" + pass + + @abstractmethod + def get_status(self) -> InvocationCacheStatus: + """Returns the status of the cache""" + pass diff --git a/invokeai/app/services/invocation_cache/invocation_cache_common.py b/invokeai/app/services/invocation_cache/invocation_cache_common.py new file mode 100644 index 0000000000..6ce2d02f3b --- /dev/null +++ b/invokeai/app/services/invocation_cache/invocation_cache_common.py @@ -0,0 +1,9 @@ +from pydantic import BaseModel, Field + + +class InvocationCacheStatus(BaseModel): + size: int = Field(description="The current size of the invocation cache") + hits: int = Field(description="The number of cache hits") + misses: int = Field(description="The number of cache misses") + enabled: bool = Field(description="Whether the invocation cache is enabled") + max_size: int = Field(description="The maximum size of the invocation cache") diff --git a/invokeai/app/services/invocation_cache/invocation_cache_memory.py b/invokeai/app/services/invocation_cache/invocation_cache_memory.py index 4c0eb2106f..a1910a0bd8 100644 --- a/invokeai/app/services/invocation_cache/invocation_cache_memory.py +++ b/invokeai/app/services/invocation_cache/invocation_cache_memory.py @@ -3,18 +3,25 @@ from typing import Optional, Union from invokeai.app.invocations.baseinvocation import BaseInvocation, BaseInvocationOutput from invokeai.app.services.invocation_cache.invocation_cache_base import InvocationCacheBase +from invokeai.app.services.invocation_cache.invocation_cache_common import InvocationCacheStatus from invokeai.app.services.invoker import Invoker class MemoryInvocationCache(InvocationCacheBase): __cache: dict[Union[int, str], tuple[BaseInvocationOutput, str]] __max_cache_size: int + __disabled: bool + __hits: int + __misses: int __cache_ids: Queue __invoker: Invoker def __init__(self, max_cache_size: int = 0) -> None: self.__cache = dict() self.__max_cache_size = max_cache_size + self.__disabled = False + self.__hits = 0 + self.__misses = 0 self.__cache_ids = Queue() def start(self, invoker: Invoker) -> None: @@ -25,15 +32,17 @@ class MemoryInvocationCache(InvocationCacheBase): self.__invoker.services.latents.on_deleted(self._delete_by_match) def get(self, key: Union[int, str]) -> Optional[BaseInvocationOutput]: - if self.__max_cache_size == 0: + if self.__max_cache_size == 0 or self.__disabled: return item = self.__cache.get(key, None) if item is not None: + self.__hits += 1 return item[0] + self.__misses += 1 def save(self, key: Union[int, str], invocation_output: BaseInvocationOutput) -> None: - if self.__max_cache_size == 0: + if self.__max_cache_size == 0 or self.__disabled: return if key not in self.__cache: @@ -47,24 +56,41 @@ class MemoryInvocationCache(InvocationCacheBase): pass def delete(self, key: Union[int, str]) -> None: - if self.__max_cache_size == 0: + if self.__max_cache_size == 0 or self.__disabled: return if key in self.__cache: del self.__cache[key] def clear(self, *args, **kwargs) -> None: - if self.__max_cache_size == 0: + if self.__max_cache_size == 0 or self.__disabled: return self.__cache.clear() self.__cache_ids = Queue() + self.__misses = 0 + self.__hits = 0 def create_key(self, invocation: BaseInvocation) -> int: return hash(invocation.json(exclude={"id"})) + def disable(self) -> None: + self.__disabled = True + + def enable(self) -> None: + self.__disabled = False + + def get_status(self) -> InvocationCacheStatus: + return InvocationCacheStatus( + hits=self.__hits, + misses=self.__misses, + enabled=not self.__disabled, + size=len(self.__cache), + max_size=self.__max_cache_size, + ) + def _delete_by_match(self, to_match: str) -> None: - if self.__max_cache_size == 0: + if self.__max_cache_size == 0 or self.__disabled: return keys_to_delete = set() diff --git a/invokeai/app/services/session_queue/session_queue_common.py b/invokeai/app/services/session_queue/session_queue_common.py index a7032c7f1f..905e568fa1 100644 --- a/invokeai/app/services/session_queue/session_queue_common.py +++ b/invokeai/app/services/session_queue/session_queue_common.py @@ -162,15 +162,15 @@ class SessionQueueItemWithoutGraph(BaseModel): session_id: str = Field( description="The ID of the session associated with this queue item. The session doesn't exist in graph_executions until the queue item is executed." ) - field_values: Optional[list[NodeFieldValue]] = Field( - default=None, description="The field values that were used for this queue item" - ) - queue_id: str = Field(description="The id of the queue with which this item is associated") error: Optional[str] = Field(default=None, description="The error message if this queue item errored") created_at: Union[datetime.datetime, str] = Field(description="When this queue item was created") updated_at: Union[datetime.datetime, str] = Field(description="When this queue item was updated") started_at: Optional[Union[datetime.datetime, str]] = Field(description="When this queue item was started") completed_at: Optional[Union[datetime.datetime, str]] = Field(description="When this queue item was completed") + queue_id: str = Field(description="The id of the queue with which this item is associated") + field_values: Optional[list[NodeFieldValue]] = Field( + default=None, description="The field values that were used for this queue item" + ) @classmethod def from_dict(cls, queue_item_dict: dict) -> "SessionQueueItemDTO": diff --git a/invokeai/frontend/web/public/locales/en.json b/invokeai/frontend/web/public/locales/en.json index f48df3368f..875b611026 100644 --- a/invokeai/frontend/web/public/locales/en.json +++ b/invokeai/frontend/web/public/locales/en.json @@ -264,6 +264,22 @@ "graphQueued": "Graph queued", "graphFailedToQueue": "Failed to queue graph" }, + "invocationCache": { + "invocationCache": "Invocation Cache", + "cacheSize": "Cache Size", + "maxCacheSize": "Max Cache Size", + "hits": "Cache Hits", + "misses": "Cache Misses", + "clear": "Clear", + "clearSucceeded": "Invocation Cache Cleared", + "clearFailed": "Problem Clearing Invocation Cache", + "enable": "Enable", + "enableSucceeded": "Invocation Cache Enabled", + "enableFailed": "Problem Enabling Invocation Cache", + "disable": "Disable", + "disableSucceeded": "Invocation Cache Disabled", + "disableFailed": "Problem Disabling Invocation Cache" + }, "gallery": { "allImagesLoaded": "All Images Loaded", "assets": "Assets", diff --git a/invokeai/frontend/web/src/app/types/invokeai.ts b/invokeai/frontend/web/src/app/types/invokeai.ts index ca2e80535c..2cb2173b19 100644 --- a/invokeai/frontend/web/src/app/types/invokeai.ts +++ b/invokeai/frontend/web/src/app/types/invokeai.ts @@ -21,7 +21,8 @@ export type AppFeature = | 'multiselect' | 'pauseQueue' | 'resumeQueue' - | 'prependQueue'; + | 'prependQueue' + | 'invocationCache'; /** * A disable-able Stable Diffusion feature diff --git a/invokeai/frontend/web/src/features/queue/components/ClearInvocationCacheButton.tsx b/invokeai/frontend/web/src/features/queue/components/ClearInvocationCacheButton.tsx new file mode 100644 index 0000000000..941bc88125 --- /dev/null +++ b/invokeai/frontend/web/src/features/queue/components/ClearInvocationCacheButton.tsx @@ -0,0 +1,22 @@ +import IAIButton from 'common/components/IAIButton'; +import { memo } from 'react'; +import { useTranslation } from 'react-i18next'; +import { useClearInvocationCache } from '../hooks/useClearInvocationCache'; + +const ClearInvocationCacheButton = () => { + const { t } = useTranslation(); + const { clearInvocationCache, isDisabled, isLoading } = + useClearInvocationCache(); + + return ( + + {t('invocationCache.clear')} + + ); +}; + +export default memo(ClearInvocationCacheButton); diff --git a/invokeai/frontend/web/src/features/queue/components/InvocationCacheStatus.tsx b/invokeai/frontend/web/src/features/queue/components/InvocationCacheStatus.tsx new file mode 100644 index 0000000000..7af2c99bcf --- /dev/null +++ b/invokeai/frontend/web/src/features/queue/components/InvocationCacheStatus.tsx @@ -0,0 +1,45 @@ +import { ButtonGroup } from '@chakra-ui/react'; +import { memo } from 'react'; +import { useTranslation } from 'react-i18next'; +import { useGetInvocationCacheStatusQuery } from 'services/api/endpoints/appInfo'; +import ClearInvocationCacheButton from './ClearInvocationCacheButton'; +import ToggleInvocationCacheButton from './ToggleInvocationCacheButton'; +import StatusStatGroup from './common/StatusStatGroup'; +import StatusStatItem from './common/StatusStatItem'; + +const InvocationCacheStatus = () => { + const { data: cacheStatus } = useGetInvocationCacheStatusQuery(undefined, { + pollingInterval: 5000, + }); + const { t } = useTranslation(); + return ( + + + + + + + + + + + ); +}; + +export default memo(InvocationCacheStatus); diff --git a/invokeai/frontend/web/src/features/queue/components/QueueStatus.tsx b/invokeai/frontend/web/src/features/queue/components/QueueStatus.tsx index 72dfdd77ad..a91d3c4679 100644 --- a/invokeai/frontend/web/src/features/queue/components/QueueStatus.tsx +++ b/invokeai/frontend/web/src/features/queue/components/QueueStatus.tsx @@ -1,38 +1,39 @@ -import { Stat, StatGroup, StatLabel, StatNumber } from '@chakra-ui/react'; import { memo } from 'react'; import { useTranslation } from 'react-i18next'; import { useGetQueueStatusQuery } from 'services/api/endpoints/queue'; +import StatusStatGroup from './common/StatusStatGroup'; +import StatusStatItem from './common/StatusStatItem'; const QueueStatus = () => { const { data: queueStatus } = useGetQueueStatusQuery(); const { t } = useTranslation(); return ( - - - {t('queue.in_progress')} - {queueStatus?.queue.in_progress ?? 0} - - - {t('queue.pending')} - {queueStatus?.queue.pending ?? 0} - - - {t('queue.completed')} - {queueStatus?.queue.completed ?? 0} - - - {t('queue.failed')} - {queueStatus?.queue.failed ?? 0} - - - {t('queue.canceled')} - {queueStatus?.queue.canceled ?? 0} - - - {t('queue.total')} - {queueStatus?.queue.total} - - + + + + + + + + ); }; diff --git a/invokeai/frontend/web/src/features/queue/components/QueueTabContent.tsx b/invokeai/frontend/web/src/features/queue/components/QueueTabContent.tsx index 254e34c453..23e2fe399f 100644 --- a/invokeai/frontend/web/src/features/queue/components/QueueTabContent.tsx +++ b/invokeai/frontend/web/src/features/queue/components/QueueTabContent.tsx @@ -1,16 +1,14 @@ -import { Box, ButtonGroup, Flex } from '@chakra-ui/react'; +import { Box, Flex } from '@chakra-ui/react'; import { memo } from 'react'; -import ClearQueueButton from './ClearQueueButton'; -import PauseProcessorButton from './PauseProcessorButton'; -import PruneQueueButton from './PruneQueueButton'; +import { useFeatureStatus } from '../../system/hooks/useFeatureStatus'; +import InvocationCacheStatus from './InvocationCacheStatus'; import QueueList from './QueueList/QueueList'; import QueueStatus from './QueueStatus'; -import ResumeProcessorButton from './ResumeProcessorButton'; -import { useFeatureStatus } from '../../system/hooks/useFeatureStatus'; +import QueueTabQueueControls from './QueueTabQueueControls'; const QueueTabContent = () => { - const isPauseEnabled = useFeatureStatus('pauseQueue').isFeatureEnabled; - const isResumeEnabled = useFeatureStatus('resumeQueue').isFeatureEnabled; + const isInvocationCacheEnabled = + useFeatureStatus('invocationCache').isFeatureEnabled; return ( { gap={2} > - - {isPauseEnabled || isResumeEnabled ? ( - - {isResumeEnabled ? : <>} - {isPauseEnabled ? : <>} - - ) : ( - <> - )} - - - - - - - - - {/* - - */} + + + {isInvocationCacheEnabled && } diff --git a/invokeai/frontend/web/src/features/queue/components/QueueTabQueueControls.tsx b/invokeai/frontend/web/src/features/queue/components/QueueTabQueueControls.tsx new file mode 100644 index 0000000000..f8bf5d64d5 --- /dev/null +++ b/invokeai/frontend/web/src/features/queue/components/QueueTabQueueControls.tsx @@ -0,0 +1,30 @@ +import { ButtonGroup, Flex } from '@chakra-ui/react'; +import { useFeatureStatus } from 'features/system/hooks/useFeatureStatus'; +import { memo } from 'react'; +import ClearQueueButton from './ClearQueueButton'; +import PauseProcessorButton from './PauseProcessorButton'; +import PruneQueueButton from './PruneQueueButton'; +import ResumeProcessorButton from './ResumeProcessorButton'; + +const QueueTabQueueControls = () => { + const isPauseEnabled = useFeatureStatus('pauseQueue').isFeatureEnabled; + const isResumeEnabled = useFeatureStatus('resumeQueue').isFeatureEnabled; + return ( + + {isPauseEnabled || isResumeEnabled ? ( + + {isResumeEnabled ? : <>} + {isPauseEnabled ? : <>} + + ) : ( + <> + )} + + + + + + ); +}; + +export default memo(QueueTabQueueControls); diff --git a/invokeai/frontend/web/src/features/queue/components/ToggleInvocationCacheButton.tsx b/invokeai/frontend/web/src/features/queue/components/ToggleInvocationCacheButton.tsx new file mode 100644 index 0000000000..94b0fc82ae --- /dev/null +++ b/invokeai/frontend/web/src/features/queue/components/ToggleInvocationCacheButton.tsx @@ -0,0 +1,47 @@ +import IAIButton from 'common/components/IAIButton'; +import { memo } from 'react'; +import { useTranslation } from 'react-i18next'; +import { useGetInvocationCacheStatusQuery } from 'services/api/endpoints/appInfo'; +import { useDisableInvocationCache } from '../hooks/useDisableInvocationCache'; +import { useEnableInvocationCache } from '../hooks/useEnableInvocationCache'; + +const ToggleInvocationCacheButton = () => { + const { t } = useTranslation(); + const { data: cacheStatus } = useGetInvocationCacheStatusQuery(); + + const { + enableInvocationCache, + isDisabled: isEnableDisabled, + isLoading: isEnableLoading, + } = useEnableInvocationCache(); + + const { + disableInvocationCache, + isDisabled: isDisableDisabled, + isLoading: isDisableLoading, + } = useDisableInvocationCache(); + + if (cacheStatus?.enabled) { + return ( + + {t('invocationCache.disable')} + + ); + } + + return ( + + {t('invocationCache.enable')} + + ); +}; + +export default memo(ToggleInvocationCacheButton); diff --git a/invokeai/frontend/web/src/features/queue/components/VerticalQueueControls.tsx b/invokeai/frontend/web/src/features/queue/components/VerticalQueueControls.tsx deleted file mode 100644 index 665eaf190a..0000000000 --- a/invokeai/frontend/web/src/features/queue/components/VerticalQueueControls.tsx +++ /dev/null @@ -1,27 +0,0 @@ -import { ButtonGroup, ButtonGroupProps, Flex } from '@chakra-ui/react'; -import { memo } from 'react'; -import ClearQueueButton from './ClearQueueButton'; -import PauseProcessorButton from './PauseProcessorButton'; -import PruneQueueButton from './PruneQueueButton'; -import ResumeProcessorButton from './ResumeProcessorButton'; - -type Props = ButtonGroupProps & { - asIconButtons?: boolean; -}; - -const VerticalQueueControls = ({ asIconButtons, ...rest }: Props) => { - return ( - - - - - - - - - - - ); -}; - -export default memo(VerticalQueueControls); diff --git a/invokeai/frontend/web/src/features/queue/components/common/StatusStatGroup.tsx b/invokeai/frontend/web/src/features/queue/components/common/StatusStatGroup.tsx new file mode 100644 index 0000000000..b083715472 --- /dev/null +++ b/invokeai/frontend/web/src/features/queue/components/common/StatusStatGroup.tsx @@ -0,0 +1,22 @@ +import { StatGroup, StatGroupProps } from '@chakra-ui/react'; +import { memo } from 'react'; + +const StatusStatGroup = ({ children, ...rest }: StatGroupProps) => ( + + {children} + +); + +export default memo(StatusStatGroup); diff --git a/invokeai/frontend/web/src/features/queue/components/common/StatusStatItem.tsx b/invokeai/frontend/web/src/features/queue/components/common/StatusStatItem.tsx new file mode 100644 index 0000000000..92100d4040 --- /dev/null +++ b/invokeai/frontend/web/src/features/queue/components/common/StatusStatItem.tsx @@ -0,0 +1,47 @@ +import { + ChakraProps, + Stat, + StatLabel, + StatNumber, + StatProps, +} from '@chakra-ui/react'; +import { memo } from 'react'; + +const sx: ChakraProps['sx'] = { + '&[aria-disabled="true"]': { + color: 'base.400', + _dark: { + color: 'base.500', + }, + }, +}; + +type Props = Omit & { + label: string; + value: string | number; + isDisabled?: boolean; +}; + +const StatusStatItem = ({ + label, + value, + isDisabled = false, + ...rest +}: Props) => ( + + + {label} + + {value} + +); + +export default memo(StatusStatItem); diff --git a/invokeai/frontend/web/src/features/queue/hooks/useClearInvocationCache.ts b/invokeai/frontend/web/src/features/queue/hooks/useClearInvocationCache.ts new file mode 100644 index 0000000000..ce2fc0e46b --- /dev/null +++ b/invokeai/frontend/web/src/features/queue/hooks/useClearInvocationCache.ts @@ -0,0 +1,48 @@ +import { useAppDispatch, useAppSelector } from 'app/store/storeHooks'; +import { addToast } from 'features/system/store/systemSlice'; +import { useCallback, useMemo } from 'react'; +import { useTranslation } from 'react-i18next'; +import { + useClearInvocationCacheMutation, + useGetInvocationCacheStatusQuery, +} from 'services/api/endpoints/appInfo'; + +export const useClearInvocationCache = () => { + const { t } = useTranslation(); + const dispatch = useAppDispatch(); + const { data: cacheStatus } = useGetInvocationCacheStatusQuery(); + const isConnected = useAppSelector((state) => state.system.isConnected); + const [trigger, { isLoading }] = useClearInvocationCacheMutation({ + fixedCacheKey: 'clearInvocationCache', + }); + + const isDisabled = useMemo( + () => !cacheStatus?.size || !isConnected, + [cacheStatus?.size, isConnected] + ); + + const clearInvocationCache = useCallback(async () => { + if (isDisabled) { + return; + } + + try { + await trigger().unwrap(); + dispatch( + addToast({ + title: t('invocationCache.clearSucceeded'), + status: 'success', + }) + ); + } catch { + dispatch( + addToast({ + title: t('invocationCache.clearFailed'), + status: 'error', + }) + ); + } + }, [isDisabled, trigger, dispatch, t]); + + return { clearInvocationCache, isLoading, cacheStatus, isDisabled }; +}; diff --git a/invokeai/frontend/web/src/features/queue/hooks/useDisableInvocationCache.ts b/invokeai/frontend/web/src/features/queue/hooks/useDisableInvocationCache.ts new file mode 100644 index 0000000000..6f0f41498a --- /dev/null +++ b/invokeai/frontend/web/src/features/queue/hooks/useDisableInvocationCache.ts @@ -0,0 +1,48 @@ +import { useAppDispatch, useAppSelector } from 'app/store/storeHooks'; +import { addToast } from 'features/system/store/systemSlice'; +import { useCallback, useMemo } from 'react'; +import { useTranslation } from 'react-i18next'; +import { + useDisableInvocationCacheMutation, + useGetInvocationCacheStatusQuery, +} from 'services/api/endpoints/appInfo'; + +export const useDisableInvocationCache = () => { + const { t } = useTranslation(); + const dispatch = useAppDispatch(); + const { data: cacheStatus } = useGetInvocationCacheStatusQuery(); + const isConnected = useAppSelector((state) => state.system.isConnected); + const [trigger, { isLoading }] = useDisableInvocationCacheMutation({ + fixedCacheKey: 'disableInvocationCache', + }); + + const isDisabled = useMemo( + () => !cacheStatus?.enabled || !isConnected, + [cacheStatus?.enabled, isConnected] + ); + + const disableInvocationCache = useCallback(async () => { + if (isDisabled) { + return; + } + + try { + await trigger().unwrap(); + dispatch( + addToast({ + title: t('invocationCache.disableSucceeded'), + status: 'success', + }) + ); + } catch { + dispatch( + addToast({ + title: t('invocationCache.disableFailed'), + status: 'error', + }) + ); + } + }, [isDisabled, trigger, dispatch, t]); + + return { disableInvocationCache, isLoading, cacheStatus, isDisabled }; +}; diff --git a/invokeai/frontend/web/src/features/queue/hooks/useEnableInvocationCache.ts b/invokeai/frontend/web/src/features/queue/hooks/useEnableInvocationCache.ts new file mode 100644 index 0000000000..f061dc97ec --- /dev/null +++ b/invokeai/frontend/web/src/features/queue/hooks/useEnableInvocationCache.ts @@ -0,0 +1,48 @@ +import { useAppDispatch, useAppSelector } from 'app/store/storeHooks'; +import { addToast } from 'features/system/store/systemSlice'; +import { useCallback, useMemo } from 'react'; +import { useTranslation } from 'react-i18next'; +import { + useEnableInvocationCacheMutation, + useGetInvocationCacheStatusQuery, +} from 'services/api/endpoints/appInfo'; + +export const useEnableInvocationCache = () => { + const { t } = useTranslation(); + const dispatch = useAppDispatch(); + const { data: cacheStatus } = useGetInvocationCacheStatusQuery(); + const isConnected = useAppSelector((state) => state.system.isConnected); + const [trigger, { isLoading }] = useEnableInvocationCacheMutation({ + fixedCacheKey: 'enableInvocationCache', + }); + + const isDisabled = useMemo( + () => cacheStatus?.enabled || !isConnected, + [cacheStatus?.enabled, isConnected] + ); + + const enableInvocationCache = useCallback(async () => { + if (isDisabled) { + return; + } + + try { + await trigger().unwrap(); + dispatch( + addToast({ + title: t('invocationCache.enableSucceeded'), + status: 'success', + }) + ); + } catch { + dispatch( + addToast({ + title: t('invocationCache.enableFailed'), + status: 'error', + }) + ); + } + }, [isDisabled, trigger, dispatch, t]); + + return { enableInvocationCache, isLoading, cacheStatus, isDisabled }; +}; diff --git a/invokeai/frontend/web/src/services/api/endpoints/appInfo.ts b/invokeai/frontend/web/src/services/api/endpoints/appInfo.ts index 2d3537998d..ffe2afd2f6 100644 --- a/invokeai/frontend/web/src/services/api/endpoints/appInfo.ts +++ b/invokeai/frontend/web/src/services/api/endpoints/appInfo.ts @@ -1,4 +1,5 @@ import { api } from '..'; +import { paths } from '../schema'; import { AppConfig, AppVersion } from '../types'; export const appInfoApi = api.injectEndpoints({ @@ -19,7 +20,45 @@ export const appInfoApi = api.injectEndpoints({ providesTags: ['AppConfig'], keepUnusedDataFor: 86400000, // 1 day }), + getInvocationCacheStatus: build.query< + paths['/api/v1/app/invocation_cache/status']['get']['responses']['200']['content']['application/json'], + void + >({ + query: () => ({ + url: `app/invocation_cache/status`, + method: 'GET', + }), + providesTags: ['InvocationCacheStatus'], + }), + clearInvocationCache: build.mutation({ + query: () => ({ + url: `app/invocation_cache`, + method: 'DELETE', + }), + invalidatesTags: ['InvocationCacheStatus'], + }), + enableInvocationCache: build.mutation({ + query: () => ({ + url: `app/invocation_cache/enable`, + method: 'PUT', + }), + invalidatesTags: ['InvocationCacheStatus'], + }), + disableInvocationCache: build.mutation({ + query: () => ({ + url: `app/invocation_cache/disable`, + method: 'PUT', + }), + invalidatesTags: ['InvocationCacheStatus'], + }), }), }); -export const { useGetAppVersionQuery, useGetAppConfigQuery } = appInfoApi; +export const { + useGetAppVersionQuery, + useGetAppConfigQuery, + useClearInvocationCacheMutation, + useDisableInvocationCacheMutation, + useEnableInvocationCacheMutation, + useGetInvocationCacheStatusQuery, +} = appInfoApi; diff --git a/invokeai/frontend/web/src/services/api/index.ts b/invokeai/frontend/web/src/services/api/index.ts index 8b4f886bb0..b39b11af29 100644 --- a/invokeai/frontend/web/src/services/api/index.ts +++ b/invokeai/frontend/web/src/services/api/index.ts @@ -24,6 +24,7 @@ export const tagTypes = [ 'SessionQueueStatus', 'SessionProcessorStatus', 'BatchStatus', + 'InvocationCacheStatus', ]; export type ApiTagDescription = TagDescription<(typeof tagTypes)[number]>; export const LIST_TAG = 'LIST'; diff --git a/invokeai/frontend/web/src/services/api/schema.d.ts b/invokeai/frontend/web/src/services/api/schema.d.ts index 26cb798594..87c0c6828d 100644 --- a/invokeai/frontend/web/src/services/api/schema.d.ts +++ b/invokeai/frontend/web/src/services/api/schema.d.ts @@ -317,6 +317,34 @@ export type paths = { */ post: operations["set_log_level"]; }; + "/api/v1/app/invocation_cache": { + /** + * Clear Invocation Cache + * @description Clears the invocation cache + */ + delete: operations["clear_invocation_cache"]; + }; + "/api/v1/app/invocation_cache/enable": { + /** + * Enable Invocation Cache + * @description Clears the invocation cache + */ + put: operations["enable_invocation_cache"]; + }; + "/api/v1/app/invocation_cache/disable": { + /** + * Disable Invocation Cache + * @description Clears the invocation cache + */ + put: operations["disable_invocation_cache"]; + }; + "/api/v1/app/invocation_cache/status": { + /** + * Get Invocation Cache Status + * @description Clears the invocation cache + */ + get: operations["get_invocation_cache_status"]; + }; "/api/v1/queue/{queue_id}/enqueue_graph": { /** * Enqueue Graph @@ -1259,11 +1287,6 @@ export type components = { * @default true */ use_cache?: boolean; - /** - * CLIP - * @description CLIP (tokenizer, text encoder, LoRAs) and skipped layer count - */ - clip?: components["schemas"]["ClipField"]; /** * Skipped Layers * @description Number of layers to skip in text encoder @@ -1276,6 +1299,11 @@ export type components = { * @enum {string} */ type: "clip_skip"; + /** + * CLIP + * @description CLIP (tokenizer, text encoder, LoRAs) and skipped layer count + */ + clip?: components["schemas"]["ClipField"]; }; /** * ClipSkipInvocationOutput @@ -2020,7 +2048,7 @@ export type components = { * Clip Skip * @description The number of skipped CLIP layers */ - clip_skip: number; + clip_skip?: number; /** * Model * @description The main model used for inference @@ -2309,10 +2337,7 @@ export type components = { * @enum {string} */ scheduler?: "ddim" | "ddpm" | "deis" | "lms" | "lms_k" | "pndm" | "heun" | "heun_k" | "euler" | "euler_k" | "euler_a" | "kdpm_2" | "kdpm_2_a" | "dpmpp_2s" | "dpmpp_2s_k" | "dpmpp_2m" | "dpmpp_2m_k" | "dpmpp_2m_sde" | "dpmpp_2m_sde_k" | "dpmpp_sde" | "dpmpp_sde_k" | "unipc"; - /** - * Control - * @description ControlNet(s) to apply - */ + /** Control */ control?: components["schemas"]["ControlField"] | components["schemas"]["ControlField"][]; /** * IP-Adapter @@ -4718,6 +4743,34 @@ export type components = { */ type: "integer_output"; }; + /** InvocationCacheStatus */ + InvocationCacheStatus: { + /** + * Size + * @description The current size of the invocation cache + */ + size: number; + /** + * Hits + * @description The number of cache hits + */ + hits: number; + /** + * Misses + * @description The number of cache misses + */ + misses: number; + /** + * Enabled + * @description Whether the invocation cache is enabled + */ + enabled: boolean; + /** + * Max Size + * @description The maximum size of the invocation cache + */ + max_size: number; + }; /** * IterateInvocation * @description Iterates over a list of items @@ -7497,11 +7550,6 @@ export type components = { * @default false */ use_cache?: boolean; - /** - * Image - * @description The image to load - */ - image?: components["schemas"]["ImageField"]; /** * Metadata * @description Optional core metadata to be written to image @@ -7513,6 +7561,11 @@ export type components = { * @enum {string} */ type: "save_image"; + /** + * Image + * @description The image to load + */ + image?: components["schemas"]["ImageField"]; }; /** * Scale Latents @@ -9042,18 +9095,6 @@ export type components = { /** Ui Order */ ui_order?: number; }; - /** - * IPAdapterModelFormat - * @description An enumeration. - * @enum {string} - */ - IPAdapterModelFormat: "invokeai"; - /** - * ControlNetModelFormat - * @description An enumeration. - * @enum {string} - */ - ControlNetModelFormat: "checkpoint" | "diffusers"; /** * StableDiffusionOnnxModelFormat * @description An enumeration. @@ -9066,24 +9107,36 @@ export type components = { * @enum {string} */ StableDiffusion2ModelFormat: "checkpoint" | "diffusers"; - /** - * StableDiffusion1ModelFormat - * @description An enumeration. - * @enum {string} - */ - StableDiffusion1ModelFormat: "checkpoint" | "diffusers"; /** * CLIPVisionModelFormat * @description An enumeration. * @enum {string} */ CLIPVisionModelFormat: "diffusers"; + /** + * StableDiffusion1ModelFormat + * @description An enumeration. + * @enum {string} + */ + StableDiffusion1ModelFormat: "checkpoint" | "diffusers"; /** * StableDiffusionXLModelFormat * @description An enumeration. * @enum {string} */ StableDiffusionXLModelFormat: "checkpoint" | "diffusers"; + /** + * IPAdapterModelFormat + * @description An enumeration. + * @enum {string} + */ + IPAdapterModelFormat: "invokeai"; + /** + * ControlNetModelFormat + * @description An enumeration. + * @enum {string} + */ + ControlNetModelFormat: "checkpoint" | "diffusers"; }; responses: never; parameters: never; @@ -10505,6 +10558,62 @@ export type operations = { }; }; }; + /** + * Clear Invocation Cache + * @description Clears the invocation cache + */ + clear_invocation_cache: { + responses: { + /** @description The operation was successful */ + 200: { + content: { + "application/json": unknown; + }; + }; + }; + }; + /** + * Enable Invocation Cache + * @description Clears the invocation cache + */ + enable_invocation_cache: { + responses: { + /** @description The operation was successful */ + 200: { + content: { + "application/json": unknown; + }; + }; + }; + }; + /** + * Disable Invocation Cache + * @description Clears the invocation cache + */ + disable_invocation_cache: { + responses: { + /** @description The operation was successful */ + 200: { + content: { + "application/json": unknown; + }; + }; + }; + }; + /** + * Get Invocation Cache Status + * @description Clears the invocation cache + */ + get_invocation_cache_status: { + responses: { + /** @description Successful Response */ + 200: { + content: { + "application/json": components["schemas"]["InvocationCacheStatus"]; + }; + }; + }; + }; /** * Enqueue Graph * @description Enqueues a graph for single execution.