Merge pull request #5 from crystian/dev

Resources monitor
This commit is contained in:
Crystian
2024-01-08 02:23:06 +01:00
committed by GitHub
14 changed files with 737 additions and 87 deletions
+31 -5
View File
@@ -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
+2 -1
View File
@@ -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,
+1 -1
View File
@@ -12,7 +12,7 @@ class TEXTS(Enum):
class CATEGORY(Enum):
TESTING = "_for_testing"
MAIN = "crystools"
MAIN = "crystools 🪛"
PRIMITIVE = "/Primitive"
DEBUGGER = "/Debugger"
LIST = "/List"
Binary file not shown.

After

Width:  |  Height:  |  Size: 33 KiB

BIN
View File
Binary file not shown.

After

Width:  |  Height:  |  Size: 53 KiB

+1
View File
@@ -0,0 +1 @@
from .monitor import *
+264
View File
@@ -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))
+55 -54
View File
@@ -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
+1 -2
View File
@@ -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):
+2
View File
@@ -1,3 +1,5 @@
deepdiff
torch
numpy
Pillow>=9.5.0
pynvml
+2 -1
View File
@@ -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) {
+3 -3
View File
@@ -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";
+355
View File
@@ -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),
});
+20 -20
View File
@@ -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);