diff --git a/README.md b/README.md index c15cdba..9035911 100644 --- a/README.md +++ b/README.md @@ -1,6 +1,6 @@ # ComfyUI-Crystools [![Hits](https://hits.seeyoufarm.com/api/count/incr/badge.svg?url=https%3A%2F%2Fgithub.com%2Fcrystian%2FComfyUI-Crystools&count_bg=%2379C83D&title_bg=%23555555&icon=&icon_color=%23E7E7E7&title=hits&edge_flat=false)](https://hits.seeyoufarm.com) [![Donate](https://img.shields.io/badge/Donate-PayPal-blue.svg)](https://paypal.me/crystian77) -**_A powerful set of tools for your belt when you work with ComfyUI_** +**_πŸͺ› A powerful set of tools for your belt when you work with ComfyUI πŸͺ›_** You can see the progress bar & time elapsed also you can see metadata and compare between two images, compare between two JSONs, show any value to console/display, pipes, and more! This provides better nodes to load images, previews, etc, and see "hidden" data, but without loading a new workflow. @@ -23,6 +23,29 @@ This provides better nodes to load images, previews, etc, and see "hidden" data, ## General +### Resources monitor + +**πŸŽ‰Finally, you can see the resources used by ComfyUI (CPU, GPU, RAM, VRAM, and space) on the menu in real-time!** + +![Monitors](./docs/monitor.png) + +Now you can identify the bottlenecks in your workflow and know when it's time to restart the server, unload models or even close some tabs! + +You can configure the refresh rate which resources to show: + +![Monitors](./docs/monitor-settings.png) + +> **Notes:** +> - The GPU data is only available when you use CUDA (only NVIDIA cards, sorry AMD users). +> - It is not perfect, but it is a good approximation. +> - Data comes from these libraries: +> - [psutil](https://pypi.org/project/psutil/) +> - [torch](https://pytorch.org/) +> - [pynvml](https://pypi.org/project/pynvml/) (official NVIDIA library) +> - This is a beta version, so please report any issue you find. + + + ### Progress bar You can see the progress of your workflow with a progress bar on the menu! @@ -33,8 +56,6 @@ https://github.com/crystian/ComfyUI-Crystools/assets/3886806/35cc1257-2199-4b85- Additionally, it shows the time elapsed at the end of the workflow, and you can `click` on it to see the **current working node.** -Additionally, it shows the time elapsed at the end of the workflow, and you can `click` on it to see the **current working node.** - > **Notes:** > - If you don't want to see it, you can turn it off from settings (`Show progress bar in menu`) @@ -483,14 +504,19 @@ You have predefined switches (string, latent, image, conditioning) but you can u --- ## To do -- [βœ…] Review the priority of the list of nodes -- [❌] Add support for other image formats (jpg, gif, etc.) - nope, I don't need it; it's complicated - [ ] Several unit tests - [ ] Add permanent cache for preview/metadata image (to survive to F5! or restart the server) --- ## Changelog +### 1.3.0 (08/01/2024) +- Added in general Resources monitor (CPU, GPU, RAM, VRAM, and space) +- Added this icon to identify this set of tools: πŸͺ› + +### Crystools-save: 1.1.0 (07/01/2024) +- Labeling updated according to the new version of Crystools (this project) + ### 1.2.0 (05/01/2024) - progress bar added diff --git a/__init__.py b/__init__.py index fe369b8..deabf5c 100644 --- a/__init__.py +++ b/__init__.py @@ -2,7 +2,7 @@ @author: Crystian @title: Crystools @nickname: Crystools -@version: 1.2.0 +@version: 1.3.0 @project: "https://github.com/crystian/ComfyUI-Crystools", @description: Plugins for multiples uses, mainly for debugging, you need them! IG: https://www.instagram.com/crystian.ia """ @@ -18,6 +18,7 @@ from .nodes.list import CListAny, CListString from .nodes.pipe import CPipeToAny, CPipeFromAny from .nodes.utils import CUtilsCompareJsons, CUtilsStatSystem from .nodes.metadata import CMetadataExtractor, CMetadataCompare +from .general import * NODE_CLASS_MAPPINGS = { CLASSES.CBOOLEAN_NAME.value: CBoolean, diff --git a/core/keys.py b/core/keys.py index 2669a18..f56814d 100644 --- a/core/keys.py +++ b/core/keys.py @@ -12,7 +12,7 @@ class TEXTS(Enum): class CATEGORY(Enum): TESTING = "_for_testing" - MAIN = "crystools" + MAIN = "crystools πŸͺ›" PRIMITIVE = "/Primitive" DEBUGGER = "/Debugger" LIST = "/List" diff --git a/docs/monitor-settings.png b/docs/monitor-settings.png new file mode 100644 index 0000000..1c50782 Binary files /dev/null and b/docs/monitor-settings.png differ diff --git a/docs/monitor.png b/docs/monitor.png new file mode 100644 index 0000000..1c1ca80 Binary files /dev/null and b/docs/monitor.png differ diff --git a/general/__init__.py b/general/__init__.py new file mode 100644 index 0000000..b320e35 --- /dev/null +++ b/general/__init__.py @@ -0,0 +1 @@ +from .monitor import * \ No newline at end of file diff --git a/general/monitor.py b/general/monitor.py new file mode 100644 index 0000000..f470c7e --- /dev/null +++ b/general/monitor.py @@ -0,0 +1,264 @@ +import asyncio +import server +import time +import threading +import psutil +import torch +import pynvml +from comfy.model_management import get_torch_device_name, get_torch_device +from server import PromptServer +from aiohttp import web +from ..core import logger + +lock = threading.Lock() + +class CMonitor: + monitorThread = None + threadController = threading.Event() + rate = 0 + nvidia = False + pynvmlLoaded = False + cudaAvailable = False + torchDevice = 'cpu' + cudaDevice = 'cpu' + cudaDevicesFound = 0 + switchCPU = False + switchGPU = False + switchHDD = False + switchRAM = False + switchVRAM = False + + def __init__(self, rate=5, switchCPU=False, switchGPU=False, switchHDD=False, switchRAM=False, switchVRAM=False): + self.rate = rate + self.switchCPU = switchCPU + self.switchGPU = switchGPU + self.switchHDD = switchHDD + self.switchRAM = switchRAM + self.switchVRAM = switchVRAM + + try: + pynvml.nvmlInit() + self.pynvmlLoaded = True + except Exception as e: + self.pynvmlLoaded = False + logger.error('Could not init pynvml.' + str(e)) + + self.startMonitor() + + def buildMonitorData(self): + cpu = -1 + ramTotal = -1 + ramUsed = -1 + ramUsedPercent = -1 + hddTotal = -1 + hddUsed = -1 + hddUsedPercent = -1 + gpuUtilization = -1 + vramUsed = -1 + vramTotal = -1 + vramPercent = -1 + + if self.switchCPU: + cpu = psutil.cpu_percent() + + if self.switchRAM: + ram = psutil.virtual_memory() + ramTotal = ram.total + ramUsed = ram.used + ramUsedPercent = ram.percent + + if self.switchHDD: + hdd = psutil.disk_usage('/') + hddTotal = hdd.total + hddUsed = hdd.used + hddUsedPercent = hdd.percent + + deviceType = 'cpu' + gpus = [] + + if self.cudaDevice == 'cpu': + gpus.append({ + 'gpu_utilization': -1, + 'vram_total': -1, + 'vram_used': -1, + 'vram_used_percent': -1, + }) + else: + deviceType = self.cudaDevice + + if self.pynvmlLoaded and self.nvidia and self.cudaAvailable: + for deviceIndex in range(self.cudaDevicesFound): + deviceHandle = pynvml.nvmlDeviceGetHandleByIndex(deviceIndex) + + # GPU Utilization + if self.switchGPU: + utilization = pynvml.nvmlDeviceGetUtilizationRates(deviceHandle) + gpuUtilization = utilization.gpu + + # VRAM + if self.switchVRAM: + # Torch or pynvml?, pynvml is more accurate with the system, torch is more accurate with comfyUI + memory = pynvml.nvmlDeviceGetMemoryInfo(deviceHandle) + vramUsed = memory.used + vramTotal = memory.total + + # device = torch.device(deviceType) + # vramUsed = torch.cuda.memory_allocated(device) + # vramTotal = torch.cuda.get_device_properties(device).total_memory + + vramPercent = vramUsed / vramTotal * 100 + + gpus.append({ + 'gpu_utilization': gpuUtilization, + 'vram_total': vramTotal, + 'vram_used': vramUsed, + 'vram_used_percent': vramPercent, + }) + + return { + 'cpu_utilization': cpu, + 'ram_total': ramTotal, + 'ram_used': ramUsed, + 'ram_used_percent': ramUsedPercent, + 'hdd_total': hddTotal, + 'hdd_used': hddUsed, + 'hdd_used_percent': hddUsedPercent, + 'device_type': deviceType, + 'gpus': gpus, + } + + + async def send_message(self, data) -> None: + s = server.PromptServer.instance + # logger.debug(f'Sending message... {data}') + await s.send_json('crystools.monitor', data) + + def monitorLoop(self): + while self.rate > 0 and not self.threadController.is_set(): + lock.acquire() + asyncio.run(self.send_message(self.buildMonitorData())) + lock.release() + time.sleep(self.rate) + + def startMonitor(self): + if self.monitorThread is not None: + self.stopMonitor() + logger.info('Restarting monitor...') + else: + if self.rate == 0: + logger.debug('Monitor rate is 0, not starting monitor.') + return None + + logger.info('Starting monitor...') + + if self.pynvmlLoaded and pynvml.nvmlDeviceGetCount() > 0: + self.cudaDevicesFound = pynvml.nvmlDeviceGetCount() + self.nvidia = True + logger.info(f'NVIDIA Driver detected - {pynvml.nvmlSystemGetDriverVersion()}') + else: + logger.warn('No NVIDIA GPU detected.') + + try: + self.torchDevice = get_torch_device_name(get_torch_device()) + except Exception as e: + logger.error('Could not pick default device.' + str(e)) + + self.cudaDevice = 'cpu' if self.torchDevice == 'cpu' else 'cuda' + self.cudaAvailable = torch.cuda.is_available() + + if self.nvidia and self.cudaAvailable and self.torchDevice == 'cpu': + logger.warn('CUDA is available, but torch is using CPU.') + + self.threadController.clear() + + if self.monitorThread is None or not self.monitorThread.is_alive(): + self.monitorThread = threading.Thread(target=self.monitorLoop) + self.monitorThread.daemon = True + self.monitorThread.start() + + + def stopMonitor(self): + logger.debug('Stopping monitor...') + self.threadController.set() + + +cmonitor = CMonitor(1, True, True, True, True, True) + + +@PromptServer.instance.routes.patch("/crystools/monitor") +async def newSettings(request): + try: + settings = await request.json() + + if 'rate' in settings is not None: + rate = settings['rate'] + if type(rate) is not int and type(rate) is not float: + raise Exception('Rate must be an number.') + + if cmonitor.rate == 0 and rate > 0: + cmonitor.rate = rate + cmonitor.startMonitor() + else: + cmonitor.rate = rate + + + if 'switchCPU' in settings is not None: + switchCPU = settings['switchCPU'] + if type(switchCPU) is not bool: + raise Exception('switchCPU must be an boolean.') + + cmonitor.switchCPU = switchCPU + + if 'switchGPU' in settings is not None: + switchGPU = settings['switchGPU'] + if type(switchGPU) is not bool: + raise Exception('switchGPU must be an boolean.') + + cmonitor.switchGPU = switchGPU + + if 'switchHDD' in settings is not None: + switchHDD = settings['switchHDD'] + if type(switchHDD) is not bool: + raise Exception('switchHDD must be an boolean.') + + cmonitor.switchHDD = switchHDD + + if 'switchRAM' in settings is not None: + switchRAM = settings['switchRAM'] + if type(switchRAM) is not bool: + raise Exception('switchRAM must be an boolean.') + + cmonitor.switchRAM = switchRAM + + if 'switchVRAM' in settings is not None: + switchVRAM = settings['switchVRAM'] + if type(switchVRAM) is not bool: + raise Exception('switchVRAM must be an boolean.') + + cmonitor.switchVRAM = switchVRAM + + return web.Response(status=200) + except Exception as e: + logger.error(e) + return web.Response(status=400, text=str(e)) + + +@PromptServer.instance.routes.post("/crystools/monitor/switch") +async def monitorSwitch(request): + try: + switch = await request.json() + + if 'monitor' in switch is not None: + monitor = switch['monitor'] + if type(monitor) is not bool: + raise Exception('monitor must be an boolean.') + + if monitor: + cmonitor.startMonitor() + else: + cmonitor.stopMonitor() + + return web.Response(status=200) + except Exception as e: + logger.error(e) + return web.Response(status=400, text=str(e)) diff --git a/nodes/_names.py b/nodes/_names.py index dbb1e5f..7ad61bb 100644 --- a/nodes/_names.py +++ b/nodes/_names.py @@ -1,66 +1,67 @@ from enum import Enum +postfix = ' πŸͺ›' -# IMPORTANT DON'T CHANGE THE "NAME" AND "TYPE" OF THE ENUMS, IT WILL BREAK THE COMPATIBILITY! +# IMPORTANT DON'T CHANGE THE 'NAME' AND 'TYPE' OF THE ENUMS, IT WILL BREAK THE COMPATIBILITY! # remember: NAME is for search, DESC is for contextual class CLASSES(Enum): - CBOOLEAN_NAME = "Primitive boolean [Crystools]" - CBOOLEAN_DESC = "Primitive boolean [Crystools]" - CTEXT_NAME = "Primitive string [Crystools]" - CTEXT_DESC = "Primitive string [Crystools]" - CTEXTML_NAME = "Primitive string multiline [Crystools]" - CTEXTML_DESC = "Primitive string multiline [Crystools]" - CINTEGER_NAME = "Primitive integer [Crystools]" - CINTEGER_DESC = "Primitive integer [Crystools]" - CFLOAT_NAME = "Primitive float [Crystools]" - CFLOAT_DESC = "Primitive float [Crystools]" + CBOOLEAN_NAME = 'Primitive boolean [Crystools]' + CBOOLEAN_DESC = 'Primitive boolean' + postfix + CTEXT_NAME = 'Primitive string [Crystools]' + CTEXT_DESC = 'Primitive string' + postfix + CTEXTML_NAME = 'Primitive string multiline [Crystools]' + CTEXTML_DESC = 'Primitive string multiline' + postfix + CINTEGER_NAME = 'Primitive integer [Crystools]' + CINTEGER_DESC = 'Primitive integer' + postfix + CFLOAT_NAME = 'Primitive float [Crystools]' + CFLOAT_DESC = 'Primitive float' + postfix - CDEBUGGER_CONSOLE_ANY_NAME = "Show any [Crystools]" - CDEBUGGER_ANY_DESC = "Show any value to console/display [Crystools]" - CDEBUGGER_CONSOLE_ANY_TO_JSON_NAME = "Show any to JSON [Crystools]" - CDEBUGGER_CONSOLE_ANY_TO_JSON_DESC = "Show any to JSON [Crystools]" + CDEBUGGER_CONSOLE_ANY_NAME = 'Show any [Crystools]' + CDEBUGGER_ANY_DESC = 'Show any value to console/display' + postfix + CDEBUGGER_CONSOLE_ANY_TO_JSON_NAME = 'Show any to JSON [Crystools]' + CDEBUGGER_CONSOLE_ANY_TO_JSON_DESC = 'Show any to JSON' + postfix - CLIST_ANY_TYPE = "ListAny" - CLIST_ANY_NAME = "List of any [Crystools]" - CLIST_ANY_DESC = "List of any [Crystools]" - CLIST_STRING_TYPE = "ListString" - CLIST_STRING_NAME = "List of strings [Crystools]" - CLIST_STRING_DESC = "List of strings [Crystools]" + CLIST_ANY_TYPE = 'ListAny' + CLIST_ANY_NAME = 'List of any [Crystools]' + CLIST_ANY_DESC = 'List of any' + postfix + CLIST_STRING_TYPE = 'ListString' + CLIST_STRING_NAME = 'List of strings [Crystools]' + CLIST_STRING_DESC = 'List of strings' + postfix - CSWITCH_ANY_NAME = "Switch any [Crystools]" - CSWITCH_ANY_DESC = "Switch any [Crystools]" - CSWITCH_STRING_NAME = "Switch string [Crystools]" - CSWITCH_STRING_DESC = "Switch string [Crystools]" - CSWITCH_CONDITIONING_NAME = "Switch conditioning [Crystools]" - CSWITCH_CONDITIONING_DESC = "Switch conditioning [Crystools]" - CSWITCH_IMAGE_NAME = "Switch image [Crystools]" - CSWITCH_IMAGE_DESC = "Switch image [Crystools]" - CSWITCH_LATENT_NAME = "Switch latent [Crystools]" - CSWITCH_LATENT_DESC = "Switch latent [Crystools]" + CSWITCH_ANY_NAME = 'Switch any [Crystools]' + CSWITCH_ANY_DESC = 'Switch any' + postfix + CSWITCH_STRING_NAME = 'Switch string [Crystools]' + CSWITCH_STRING_DESC = 'Switch string' + postfix + CSWITCH_CONDITIONING_NAME = 'Switch conditioning [Crystools]' + CSWITCH_CONDITIONING_DESC = 'Switch conditioning' + postfix + CSWITCH_IMAGE_NAME = 'Switch image [Crystools]' + CSWITCH_IMAGE_DESC = 'Switch image' + postfix + CSWITCH_LATENT_NAME = 'Switch latent [Crystools]' + CSWITCH_LATENT_DESC = 'Switch latent' + postfix - CPIPE_ANY_TYPE = "CPipeAny" - CPIPE_TO_ANY_NAME = "Pipe to/edit any [Crystools]" - CPIPE_TO_ANY_DESC = "Pipe to/edit any [Crystools]" - CPIPE_FROM_ANY_NAME = "Pipe from any [Crystools]" - CPIPE_FROM_ANY_DESC = "Pipe from any [Crystools]" + CPIPE_ANY_TYPE = 'CPipeAny' + CPIPE_TO_ANY_NAME = 'Pipe to/edit any [Crystools]' + CPIPE_TO_ANY_DESC = 'Pipe to/edit any' + postfix + CPIPE_FROM_ANY_NAME = 'Pipe from any [Crystools]' + CPIPE_FROM_ANY_DESC = 'Pipe from any' + postfix - CIMAGE_LOAD_METADATA_NAME = "Load image with metadata [Crystools]" - CIMAGE_LOAD_METADATA_DESC = "Load image with metadata [Crystools]" - CIMAGE_GET_RESOLUTION_NAME = "Get resolution [Crystools]" - CIMAGE_GET_RESOLUTION_DESC = "Get resolution [Crystools]" - CIMAGE_PREVIEW_IMAGE_NAME = "Preview from image [Crystools]" - CIMAGE_PREVIEW_IMAGE_DESC = "Preview from image [Crystools]" - CIMAGE_PREVIEW_METADATA_NAME = "Preview from metadata [Crystools]" - CIMAGE_PREVIEW_METADATA_DESC = "Preview from metadata [Crystools]" - CIMAGE_SAVE_METADATA_NAME = "Save image with extra metadata [Crystools]" - CIMAGE_SAVE_METADATA_DESC = "Save image with extra metadata [Crystools]" + CIMAGE_LOAD_METADATA_NAME = 'Load image with metadata [Crystools]' + CIMAGE_LOAD_METADATA_DESC = 'Load image with metadata' + postfix + CIMAGE_GET_RESOLUTION_NAME = 'Get resolution [Crystools]' + CIMAGE_GET_RESOLUTION_DESC = 'Get resolution' + postfix + CIMAGE_PREVIEW_IMAGE_NAME = 'Preview from image [Crystools]' + CIMAGE_PREVIEW_IMAGE_DESC = 'Preview from image' + postfix + CIMAGE_PREVIEW_METADATA_NAME = 'Preview from metadata [Crystools]' + CIMAGE_PREVIEW_METADATA_DESC = 'Preview from metadata' + postfix + CIMAGE_SAVE_METADATA_NAME = 'Save image with extra metadata [Crystools]' + CIMAGE_SAVE_METADATA_DESC = 'Save image with extra metadata' + postfix - CMETADATA_EXTRACTOR_NAME = "Metadata extractor [Crystools]" - CMETADATA_EXTRACTOR_DESC = "Metadata extractor [Crystools]" - CMETADATA_COMPARATOR_NAME = "Metadata comparator [Crystools]" - CMETADATA_COMPARATOR_DESC = "Metadata comparator [Crystools]" + CMETADATA_EXTRACTOR_NAME = 'Metadata extractor [Crystools]' + CMETADATA_EXTRACTOR_DESC = 'Metadata extractor' + postfix + CMETADATA_COMPARATOR_NAME = 'Metadata comparator [Crystools]' + CMETADATA_COMPARATOR_DESC = 'Metadata comparator' + postfix - CUTILS_JSON_COMPARATOR_NAME = "Json comparator [Crystools]" - CUTILS_JSON_COMPARATOR_DESC = "Json comparator [Crystools]" - CUTILS_STAT_SYSTEM_NAME = "Stats system [Crystools]" - CUTILS_STAT_SYSTEM_DESC = "Stats system (powered by WAS) [Crystools]" + CUTILS_JSON_COMPARATOR_NAME = 'Json comparator [Crystools]' + CUTILS_JSON_COMPARATOR_DESC = 'Json comparator' + postfix + CUTILS_STAT_SYSTEM_NAME = 'Stats system [Crystools]' + CUTILS_STAT_SYSTEM_DESC = 'Stats system (powered by WAS)' + postfix diff --git a/nodes/debugger.py b/nodes/debugger.py index e120916..6b0cd3b 100644 --- a/nodes/debugger.py +++ b/nodes/debugger.py @@ -1,6 +1,5 @@ import json -from ..core import CONFIG, CATEGORY, BOOLEAN, BOOLEAN_FALSE, KEYS, TEXTS, STRING, logger, setWidgetValues, any - +from ..core import CONFIG, CATEGORY, BOOLEAN, BOOLEAN_FALSE, KEYS, TEXTS, STRING, logger, any class CConsoleAny: def __init__(self): diff --git a/requirements.txt b/requirements.txt index e055c5d..ded4aa5 100644 --- a/requirements.txt +++ b/requirements.txt @@ -1,3 +1,5 @@ deepdiff +torch numpy Pillow>=9.5.0 +pynvml diff --git a/web/js/common.js b/web/js/common.js index 1198528..d2c32f1 100644 --- a/web/js/common.js +++ b/web/js/common.js @@ -1,5 +1,7 @@ import { ComfyWidgets } from '../../../scripts/widgets.js'; +export const commonPrefix = 'πŸͺ›'; + export function displayContext(nodeType, app, index = 0, serialize_widgets = false, isVirtualNode = false) { function populate(text) { if (this.widgets) { @@ -54,7 +56,6 @@ export function displayContext(nodeType, app, index = 0, serialize_widgets = fal }; } - // propagate the output value to the dependents nodes, it does not work with some nodes Β―\_(ツ)_/Β― const propagateOutputToDependentsNodes = function(output, value) { if (output.links && output.links.length) { diff --git a/web/js/debugger.js b/web/js/debugger.js index 14d5127..2cd93d1 100644 --- a/web/js/debugger.js +++ b/web/js/debugger.js @@ -1,7 +1,7 @@ import { app } from '../../../scripts/app.js'; import { api } from '../../../scripts/api.js'; import { ComfyWidgets } from '../../../scripts/widgets.js'; -import { displayContext } from './common.js'; +import { commonPrefix, displayContext } from './common.js'; // "Show any" Node app.registerExtension({ @@ -22,8 +22,8 @@ app.registerExtension({ name: 'Crystools.Debugger.Metadata', registerCustomNodes() { - const category = 'crystools/Debugger'; - const friendlyName = 'Show Metadata [Crystools]'; + const category = `crystools ${commonPrefix}/Debugger`; + const friendlyName = 'Show Metadata πŸͺ›'; class MetadataNode { // color = "#FF2222"; diff --git a/web/js/monitor.js b/web/js/monitor.js new file mode 100644 index 0000000..25aeccf --- /dev/null +++ b/web/js/monitor.js @@ -0,0 +1,355 @@ +import { app } from '../../../scripts/app.js'; +import { api } from '../../../scripts/api.js'; +import { commonPrefix } from './common.js'; + +class CrystoolsMonitor { + idExtensionName = 'Crystools.monitor'; + menuPrefix = commonPrefix; + htmlIdCrystoolsRoot = 'crystools-root'; + htmlIdCrystoolsMonitorContainer = 'crystools-monitor-container'; + + idInputRate = 'Crystools.inputRate'; + defaultRate = .5; + + /** MONITORS */ + // CPU Variables + idSwitchCPU = 'Crystools.switchCPU'; + defaultSwitchCPU = true; + htmlMonitorCPURef = null; + htmlMonitorCPUSliderRef = null; + htmlMonitorCPULabelRef = null; + cssColorCPU = '#0AA015'; + + // RAM Variables + idSwitchRAM = 'Crystools.switchRAM'; + defaultSwitchRAM = true; + htmlMonitorRAMRef = null; + htmlMonitorRAMSliderRef = null; + htmlMonitorRAMLabelRef = null; + cssColorRAM = '#07630D'; + + // GPU Variables + idSwitchGPU = 'Crystools.switchGPU'; + defaultSwitchGPU = true; + htmlMonitorGPURef = null; + htmlMonitorGPUSliderRef = null; + htmlMonitorGPULabelRef = null; + cssColorGPU = '#0C86F4'; + + // VRAM Variables + idSwitchVRAM = 'Crystools.switchVRAM'; + defaultSwitchVRAM = true; + htmlMonitorVRAMRef = null; + htmlMonitorVRAMSliderRef = null; + htmlMonitorVRAMLabelRef = null; + cssColorVRAM = '#176EC7'; + + // HDD Variables + idSwitchHDD = 'Crystools.switchHDD'; + defaultSwitchHDD = false; + htmlMonitorHDDRef = null; + htmlMonitorHDDSliderRef = null; + htmlMonitorHDDLabelRef = null; + cssColorHDD = '#730F92'; + + constructor() { + this.createSettings(); + } + + createSettings = () => { + app.ui.settings.addSetting({ + id: this.idSwitchCPU, + name: this.menuPrefix + 'Display CPU monitor [menu]', + type: 'boolean', + defaultValue: this.defaultSwitchCPU, + onChange: async(value) => { + this.updateWidget(this.htmlMonitorCPURef, value); + await this.updateServer({switchCPU: value}); + }, + }); + app.ui.settings.addSetting({ + id: this.idSwitchRAM, + name: this.menuPrefix + 'Display RAM monitor [menu]', + type: 'boolean', + defaultValue: this.defaultSwitchRAM, + onChange: async(value) => { + this.updateWidget(this.htmlMonitorRAMRef, value); + await this.updateServer({switchRAM: value}); + }, + }); + app.ui.settings.addSetting({ + id: this.idSwitchGPU, + name: this.menuPrefix + 'Display GPU monitor [menu]', + type: 'boolean', + defaultValue: this.defaultSwitchGPU, + onChange: async(value) => { + this.updateWidget(this.htmlMonitorGPURef, value); + await this.updateServer({switchGPU: value}); + }, + }); + app.ui.settings.addSetting({ + id: this.idSwitchVRAM, + name: this.menuPrefix + 'Display Video RAM monitor [menu]', + type: 'boolean', + defaultValue: this.defaultSwitchVRAM, + onChange: async(value) => { + this.updateWidget(this.htmlMonitorVRAMRef, value); + await this.updateServer({switchVRAM: value}); + }, + }); + app.ui.settings.addSetting({ + id: this.idSwitchHDD, + name: this.menuPrefix + 'Display Hard disk monitor [menu]', + tooltip: 'Only the drive where the comfyUI is installed', + type: 'boolean', + defaultValue: this.defaultSwitchHDD, + onChange: async(value) => { + this.updateWidget(this.htmlMonitorHDDRef, value); + await this.updateServer({switchHDD: value}); + }, + }); + + app.ui.settings.addSetting({ + id: this.idInputRate, + name: this.menuPrefix + 'Monitors refresh rate (in seconds) [menu]', + tooltip: 'This is the time between each update of the monitors, 0 means no refresh', + type: 'slider', + attrs: { + min: 0, + max: 5, + step: 0.25, + }, + defaultValue: this.defaultRate, + onChange: async(value) => { + try { + value = parseFloat(value); + } catch (error) { + console.error(error); + } + await this.updateServer({rate: value}); + + if (value === 0) { + this.updateDisplay({ + cpu_utilization: -1, + device: 'cpu', + + gpus: [{ + gpu_utilization: -1, + vram_total: -1, + vram_used: -1, + vram_used_percent: -1, + }], + hdd_total: -1, + hdd_used: -1, + hdd_used_percent: -1, + ram_total: -1, + ram_used: -1, + ram_used_percent: -1, + }); + } + + if (this.htmlMonitorCPUSliderRef) { // validation because this run before setup + value = value.toFixed(1); + const animationConfig = `width ${value}s`; + this.htmlMonitorCPUSliderRef.style.transition = animationConfig; + this.htmlMonitorGPUSliderRef.style.transition = animationConfig; + this.htmlMonitorHDDSliderRef.style.transition = animationConfig; + this.htmlMonitorRAMSliderRef.style.transition = animationConfig; + this.htmlMonitorVRAMSliderRef.style.transition = animationConfig; + } + }, + }); + }; + + updateServer = async(data) => { + const resp = await api.fetchApi('/crystools/monitor', { + method: 'PATCH', + body: JSON.stringify(data), + cache: 'no-store', + }); + if (resp.status === 200) { + return await resp.text(); + } else { + console.error(resp); + } + return undefined; + }; + + updateAllWidget = () => { + this.updateWidget(this.htmlMonitorCPURef, app.ui.settings.getSettingValue(this.idSwitchCPU, this.defaultSwitchCPU)); + this.updateWidget(this.htmlMonitorGPURef, app.ui.settings.getSettingValue(this.idSwitchGPU, this.defaultSwitchGPU)); + this.updateWidget(this.htmlMonitorHDDRef, app.ui.settings.getSettingValue(this.idSwitchHDD, this.defaultSwitchHDD)); + this.updateWidget(this.htmlMonitorRAMRef, app.ui.settings.getSettingValue(this.idSwitchRAM, this.defaultSwitchRAM)); + this.updateWidget(this.htmlMonitorVRAMRef, app.ui.settings.getSettingValue(this.idSwitchVRAM, this.defaultSwitchVRAM)); + }; + + updateWidget = (container, value) => { + if (container) { + container.style.display = value ? 'flex' : 'none'; + } + }; + + updateDisplay = (data) => { + // console.debug('updateDisplay', data); + this.htmlMonitorCPULabelRef.innerHTML = `${Math.floor(data.cpu_utilization)}%`; + this.htmlMonitorCPUSliderRef.style.width = this.htmlMonitorCPULabelRef.innerHTML; + + const gpu = data.gpus[0]; + if (gpu) { + this.htmlMonitorGPULabelRef.innerHTML = `${Math.floor(gpu.gpu_utilization)}%`; + this.htmlMonitorGPUSliderRef.style.width = this.htmlMonitorGPULabelRef.innerHTML; + this.htmlMonitorVRAMLabelRef.innerHTML = `${Math.floor(gpu.vram_used_percent)}%`; + this.htmlMonitorVRAMSliderRef.style.width = this.htmlMonitorVRAMLabelRef.innerHTML; + } else { + console.error('no gpu found'); + } + + this.htmlMonitorHDDLabelRef.innerHTML = `${Math.floor(data.hdd_used_percent)}%`; + this.htmlMonitorHDDSliderRef.style.width = this.htmlMonitorHDDLabelRef.innerHTML; + + this.htmlMonitorRAMLabelRef.innerHTML = `${Math.floor(data.ram_used_percent)}%`; + this.htmlMonitorRAMSliderRef.style.width = this.htmlMonitorRAMLabelRef.innerHTML; + }; + + setup() { + const parentElement = document.getElementById('queue-button'); + + let ctoolsRoot = document.getElementById(this.htmlIdCrystoolsRoot); + if (!ctoolsRoot) { + ctoolsRoot = document.createElement('div'); + ctoolsRoot.setAttribute('id', this.htmlIdCrystoolsRoot); + ctoolsRoot.style.display = 'flex'; + ctoolsRoot.style.width = '100%'; + ctoolsRoot.style.flexDirection = 'column'; + parentElement.insertAdjacentElement('afterend', ctoolsRoot); + } + + const htmlContainer = document.createElement('div'); + htmlContainer.setAttribute('id', this.htmlIdCrystoolsMonitorContainer); + htmlContainer.style.width = '100%'; + htmlContainer.style.cursor = 'crosshair'; + htmlContainer.style.order = '3'; + htmlContainer.style.margin = '4px 0'; + ctoolsRoot.append(htmlContainer); + + htmlContainer.append(this.createMonitorCPU()); + htmlContainer.append(this.createMonitorRAM()); + htmlContainer.append(this.createMonitorGPU()); + htmlContainer.append(this.createMonitorVRAM()); + htmlContainer.append(this.createMonitorHDD()); + + this.updateAllWidget(); + this.registerListeners(); + } + + createMonitor = (id, label, color) => { + const option = { + rootRef: null, + sliderRef: null, + labelRef: null, + }; + + const htmlMain = document.createElement('div'); + htmlMain.setAttribute('id', id); + htmlMain.style.margin = '2px 10px'; + htmlMain.style.height = '12px'; + htmlMain.style.position = 'relative'; + htmlMain.style.display = 'flex'; + htmlMain.style.alignItems = 'center'; + htmlMain.style.flexDirection = 'row'; + option.rootRef = htmlMain; + + const htmlMonitorText = document.createElement('div'); + htmlMonitorText.style.width = '35px'; + htmlMonitorText.style.fontSize = '10px'; + htmlMonitorText.innerHTML = label; + htmlMain.append(htmlMonitorText); + + const htmlMonitorContent = document.createElement('div'); + htmlMonitorContent.style.height = '100%'; + htmlMonitorContent.style.flexGrow = '1'; + htmlMonitorContent.style.position = 'relative'; + htmlMonitorContent.style.backgroundColor = 'var(--bg-color)'; + htmlMain.append(htmlMonitorContent); + + const htmlMonitorSlider = document.createElement('div'); + htmlMonitorSlider.style.position = 'absolute'; + htmlMonitorSlider.style.height = '100%'; + htmlMonitorSlider.style.width = '0'; + htmlMonitorSlider.style.backgroundColor = color; + htmlMonitorSlider.style.transition = 'width 0.5s'; + option.sliderRef = htmlMonitorSlider; + htmlMonitorContent.append(htmlMonitorSlider); + + const htmlMonitorLabel = document.createElement('div'); + htmlMonitorLabel.style.position = 'relative'; + htmlMonitorLabel.style.width = '100%'; + htmlMonitorLabel.style.color = 'var(--drag-text)'; + htmlMonitorLabel.style.fontSize = '10px'; + htmlMonitorLabel.innerHTML = '0%'; + option.labelRef = htmlMonitorLabel; + htmlMonitorContent.append(htmlMonitorLabel); + + return option; + }; + + createMonitorCPU = () => { + const options = this.createMonitor('crystools-monitor-cpu', 'CPU', this.cssColorCPU); + + this.htmlMonitorCPURef = options.rootRef; + this.htmlMonitorCPUSliderRef = options.sliderRef; + this.htmlMonitorCPULabelRef = options.labelRef; + return this.htmlMonitorCPURef; + }; + + createMonitorRAM = () => { + const options = this.createMonitor('crystools-monitor-ram', 'RAM', this.cssColorRAM); + + this.htmlMonitorRAMRef = options.rootRef; + this.htmlMonitorRAMSliderRef = options.sliderRef; + this.htmlMonitorRAMLabelRef = options.labelRef; + return this.htmlMonitorRAMRef; + }; + + createMonitorGPU = () => { + const options = this.createMonitor('crystools-monitor-gpu', 'GPU', this.cssColorGPU); + + this.htmlMonitorGPURef = options.rootRef; + this.htmlMonitorGPUSliderRef = options.sliderRef; + this.htmlMonitorGPULabelRef = options.labelRef; + return this.htmlMonitorGPURef; + }; + + createMonitorVRAM = () => { + const options = this.createMonitor('crystools-monitor-vram', 'VRAM', this.cssColorVRAM); + + this.htmlMonitorVRAMRef = options.rootRef; + this.htmlMonitorVRAMSliderRef = options.sliderRef; + this.htmlMonitorVRAMLabelRef = options.labelRef; + return this.htmlMonitorVRAMRef; + }; + + createMonitorHDD = () => { + const options = this.createMonitor('crystools-monitor-hdd', 'HDD', this.cssColorHDD); + + this.htmlMonitorHDDRef = options.rootRef; + this.htmlMonitorHDDSliderRef = options.sliderRef; + this.htmlMonitorHDDLabelRef = options.labelRef; + return this.htmlMonitorHDDRef; + }; + + registerListeners = () => { + api.addEventListener('crystools.monitor', (event) => { + if (event?.detail === undefined) { + return; + } + this.updateDisplay(event.detail); + }); + }; +} + +const crystoolsMonitor = new CrystoolsMonitor(); +app.registerExtension({ + name: crystoolsMonitor.idExtensionName, + setup: crystoolsMonitor.setup.bind(crystoolsMonitor), +}); diff --git a/web/js/progressBar.js b/web/js/progressBar.js index 8e55caf..b05c586 100644 --- a/web/js/progressBar.js +++ b/web/js/progressBar.js @@ -1,5 +1,6 @@ import { api } from '../../../scripts/api.js'; import { app } from '../../../scripts/app.js'; +import { commonPrefix } from './common.js'; const status = { executing: 'Executing', @@ -11,7 +12,7 @@ class CrystoolsProgressBar { idExtensionName = 'Crystools.progressBar'; idShowProgressBar = 'Crystools.showStatus'; defaultShowStatus = true; - menuPrefix = ''; + menuPrefix = commonPrefix; htmlIdCrystoolsRoot = 'crystools-root'; htmlIdCrystoolsProgressBarContainer = 'crystools-progress-bar-container'; @@ -29,12 +30,9 @@ class CrystoolsProgressBar { // not on setup because this affect the order on settings, I prefer to options at first createSettings = () => { - /** Form on settings: (the order is important) */ - - // use new save button app.ui.settings.addSetting({ id: this.idShowProgressBar, - name: this.menuPrefix + 'Show progress bar in menu', + name: this.menuPrefix + 'Show progress bar [menu]', type: 'boolean', defaultValue: this.defaultShowStatus, onChange: this.showProgressBar, @@ -42,11 +40,11 @@ class CrystoolsProgressBar { }; showProgressBar = (value) => { - const ctools = document.getElementById(this.htmlIdCrystoolsProgressBarContainer); + const container = document.getElementById(this.htmlIdCrystoolsProgressBarContainer); // validation because this run before setup - if (ctools) { - ctools.style.display = value ? 'block' : 'none'; + if (container) { + container.style.display = value ? 'block' : 'none'; } }; @@ -59,7 +57,7 @@ class CrystoolsProgressBar { if (this.timeStart > 0 && timeElapsed > 0) { this.htmlProgressLabelRef.innerHTML = new Date(timeElapsed).toISOString().substr(11, 8); } - this.htmlProgressSliderRef.style.backgroundColor = 'var(--bg-color)'; + this.htmlProgressSliderRef.style.width = '0'; } else if (this.currentStatus === status.execution_error) { // an error occurred @@ -71,7 +69,7 @@ class CrystoolsProgressBar { // on going this.htmlProgressLabelRef.innerHTML = `${this.currentProgress}%`; this.htmlProgressSliderRef.style.width = this.htmlProgressLabelRef.innerHTML; - this.htmlProgressSliderRef.style.backgroundColor = 'green'; + this.htmlProgressSliderRef.style.backgroundColor = 'green'; // by reset the color } }; @@ -88,27 +86,29 @@ class CrystoolsProgressBar { parentElement.insertAdjacentElement('afterend', ctoolsRoot); } - const progressBarContainer = document.createElement('div'); - progressBarContainer.setAttribute('id', this.htmlIdCrystoolsProgressBarContainer); - progressBarContainer.setAttribute('title', 'click to see the current working node'); - progressBarContainer.style.margin = '4px 0'; - progressBarContainer.style.width = '100%'; - progressBarContainer.style.cursor = 'pointer'; - progressBarContainer.style.order = '1'; - progressBarContainer.addEventListener('click', this.centerNode); - ctoolsRoot.append(progressBarContainer); + const htmlContainer = document.createElement('div'); + htmlContainer.setAttribute('id', this.htmlIdCrystoolsProgressBarContainer); + htmlContainer.setAttribute('title', 'click to see the current working node'); + htmlContainer.style.margin = '4px 0'; + htmlContainer.style.width = '100%'; + htmlContainer.style.cursor = 'pointer'; + htmlContainer.style.order = '1'; + htmlContainer.addEventListener('click', this.centerNode); + ctoolsRoot.append(htmlContainer); const progressBar = document.createElement('div'); progressBar.style.margin = '0 10px'; progressBar.style.height = '18px'; progressBar.style.position = 'relative'; progressBar.style.backgroundColor = 'var(--bg-color)'; - progressBarContainer.append(progressBar); + htmlContainer.append(progressBar); const progressSlider = document.createElement('div'); progressSlider.style.position = 'absolute'; progressSlider.style.height = '100%'; progressSlider.style.width = '0'; + progressSlider.style.transition = 'width 0.2s'; + progressSlider.style.backgroundColor = 'green'; this.htmlProgressSliderRef = progressSlider; progressBar.append(this.htmlProgressSliderRef);