From 1bfe47a86145649dcc40706f8e1261bd9d4f0bd1 Mon Sep 17 00:00:00 2001 From: Yousef Harfoush Date: Thu, 23 Apr 2026 22:50:00 +0800 Subject: [PATCH] Add AMD GPU monitoring features with compact and detailed UI - Implemented a compact action bar for real-time GPU, CPU, RAM, VRAM, GTT, and temperature monitoring. - Created a detailed monitor panel with draggable functionality and collapse/close options. - Added event listeners to update UI elements based on GPU and system data. - Introduced a settings menu for toggling the visibility of individual metrics in the compact view. - Included styles for visual representation of utilization and temperature thresholds. - Added persistence for monitor visibility and position using localStorage. --- .gitignore | 2 + __init__.py | 219 +++++++++++++++----- web/actionbar.js | 268 +++++++++++++++++++++++++ web/index.js | 3 +- web/{monitor.js => panel.js} | 376 ++++++++++++++++++----------------- 5 files changed, 639 insertions(+), 229 deletions(-) create mode 100644 .gitignore create mode 100644 web/actionbar.js rename web/{monitor.js => panel.js} (50%) diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..109fe69 --- /dev/null +++ b/.gitignore @@ -0,0 +1,2 @@ +__pycache__/* +tools/__pycache__/* diff --git a/__init__.py b/__init__.py index 9ffd20c..459fdbb 100644 --- a/__init__.py +++ b/__init__.py @@ -1,5 +1,6 @@ import os import sys +import re import json import time import subprocess @@ -14,9 +15,15 @@ "vram_total": 0, "vram_used_percent": 0, "gpu_temperature": 0, + "gtt_used": 0, + "gtt_total": 0, + "gtt_used_percent": 0, "last_update": 0 } +# For CPU percent fallback calculation (from /proc/stat) +cpu_prev = None + # Monitor thread control monitor_thread = None thread_control = threading.Event() @@ -81,61 +88,163 @@ def get_gpu_info(rocm_smi_path): """Get current GPU information""" global gpu_stats - # Get GPU utilization - try: - info = run_rocm_smi_command(rocm_smi_path, '--showuse', '--json') - if isinstance(info, dict) and 'card0' in info: - card_info = info['card0'] # Use first GPU - if 'GPU use (%)' in card_info: - gpu_use = card_info['GPU use (%)'] - if isinstance(gpu_use, str): - gpu_use = gpu_use.replace('%', '') - gpu_stats["gpu_utilization"] = int(float(gpu_use)) - except: - pass - - # Get VRAM information + # Query rocm-smi once for multiple JSON fields to reduce subprocess overhead try: - info = run_rocm_smi_command(rocm_smi_path, '--showmeminfo', 'vram', '--json') + # Request use, vram, and gtt meminfo plus temps in one JSON call + info = run_rocm_smi_command(rocm_smi_path, '--showuse', '--showmeminfo', 'vram', 'gtt', '--showtemp', '--json') if isinstance(info, dict) and 'card0' in info: - card_info = info['card0'] # Use first GPU - - # Parse the B (bytes) format ROCm 5.x/6.x uses - if 'VRAM Total Memory (B)' in card_info and 'VRAM Total Used Memory (B)' in card_info: - vram_total_bytes = int(card_info['VRAM Total Memory (B)']) - vram_used_bytes = int(card_info['VRAM Total Used Memory (B)']) - - # Convert to MB for display - vram_total = vram_total_bytes / (1024 * 1024) - vram_used = vram_used_bytes / (1024 * 1024) - - gpu_stats["vram_total"] = int(vram_total) - gpu_stats["vram_used"] = int(vram_used) - gpu_stats["vram_used_percent"] = int((vram_used / vram_total) * 100) + card_info = info['card0'] + # GPU utilization + try: + if 'GPU use (%)' in card_info: + gpu_use = card_info['GPU use (%)'] + if isinstance(gpu_use, str): + gpu_use = gpu_use.replace('%', '') + gpu_stats["gpu_utilization"] = int(float(gpu_use)) + except: + pass + # VRAM (bytes keys used by ROCm) + try: + if 'VRAM Total Memory (B)' in card_info and 'VRAM Total Used Memory (B)' in card_info: + vram_total_bytes = int(card_info['VRAM Total Memory (B)']) + vram_used_bytes = int(card_info['VRAM Total Used Memory (B)']) + vram_total = vram_total_bytes / (1024 * 1024) + vram_used = vram_used_bytes / (1024 * 1024) + gpu_stats["vram_total"] = int(vram_total) + gpu_stats["vram_used"] = int(vram_used) + gpu_stats["vram_used_percent"] = int((vram_used / vram_total) * 100) if vram_total > 0 else 0 + except: + pass + # Temperature + try: + if 'Temperature (Sensor edge) (C)' in card_info: + temp_str = card_info['Temperature (Sensor edge) (C)'] + if isinstance(temp_str, str): + temp_str = temp_str.replace('°C', '').strip() + gpu_stats["gpu_temperature"] = int(float(temp_str)) + elif 'Temperature (Sensor junction) (C)' in card_info: + temp_str = card_info['Temperature (Sensor junction) (C)'] + if isinstance(temp_str, str): + temp_str = temp_str.replace('°C', '').strip() + gpu_stats["gpu_temperature"] = int(float(temp_str)) + except: + pass except: pass - # Get temperature + # GTT information: try to read from the same JSON response first + gtt_parsed = False try: - info = run_rocm_smi_command(rocm_smi_path, '--showtemp', '--json') if isinstance(info, dict) and 'card0' in info: - card_info = info['card0'] # Use first GPU - - # Try different temperature sensors, starting with edge - if 'Temperature (Sensor edge) (C)' in card_info: - temp_str = card_info['Temperature (Sensor edge) (C)'] - if isinstance(temp_str, str): - temp_str = temp_str.replace('°C', '').strip() - gpu_stats["gpu_temperature"] = int(float(temp_str)) - elif 'Temperature (Sensor junction) (C)' in card_info: - temp_str = card_info['Temperature (Sensor junction) (C)'] - if isinstance(temp_str, str): - temp_str = temp_str.replace('°C', '').strip() - gpu_stats["gpu_temperature"] = int(float(temp_str)) + card_info = info['card0'] + if 'GTT Total Memory (B)' in card_info and 'GTT Total Used Memory (B)' in card_info: + gtt_total_bytes = int(card_info['GTT Total Memory (B)']) + gtt_used_bytes = int(card_info['GTT Total Used Memory (B)']) + gtt_total = gtt_total_bytes / (1024 * 1024) + gtt_used = gtt_used_bytes / (1024 * 1024) + gpu_stats['gtt_total'] = int(gtt_total) + gpu_stats['gtt_used'] = int(gtt_used) + gpu_stats['gtt_used_percent'] = int((gtt_used / gtt_total) * 100) if gtt_total > 0 else 0 + gtt_parsed = True except: pass + if not gtt_parsed: + try: + info_text = run_rocm_smi_command(rocm_smi_path, '--showmeminfo', 'gtt') + if isinstance(info_text, str) and info_text: + m_total = re.search(r"GTT.*Total.*?(\d+[\.,]?\d*)\s*(MiB|GiB|MB|GB|B)?", info_text, re.I) + m_used = re.search(r"GTT.*Used.*?(\d+[\.,]?\d*)\s*(MiB|GiB|MB|GB|B)?", info_text, re.I) + if m_total: + total_val = float(m_total.group(1).replace(',', '.')) + unit = (m_total.group(2) or 'MB').lower() + if 'g' in unit: + total_mb = total_val * 1024 + elif 'b' == unit: + total_mb = total_val / (1024*1024) + else: + total_mb = total_val + gpu_stats['gtt_total'] = int(total_mb) + if m_used: + used_val = float(m_used.group(1).replace(',', '.')) + unit = (m_used.group(2) or 'MB').lower() + if 'g' in unit: + used_mb = used_val * 1024 + elif 'b' == unit: + used_mb = used_val / (1024*1024) + else: + used_mb = used_val + gpu_stats['gtt_used'] = int(used_mb) + if gpu_stats.get('gtt_total', 0) > 0: + gpu_stats['gtt_used_percent'] = int((gpu_stats['gtt_used'] / gpu_stats['gtt_total']) * 100) + except: + pass + + gpu_stats["last_update"] = time.time() + # Update CPU and system RAM info + try: + # Prefer psutil if available + import psutil + try: + cpu_pct = psutil.cpu_percent(interval=None) + gpu_stats['cpu_utilization'] = int(round(cpu_pct)) + except Exception: + pass + + try: + vm = psutil.virtual_memory() + total_mb = int(vm.total / (1024 * 1024)) + used_mb = int((vm.total - getattr(vm, 'available', vm.total - getattr(vm, 'used', 0))) / (1024 * 1024)) + gpu_stats['system_ram_total'] = total_mb + gpu_stats['system_ram_used'] = used_mb + gpu_stats['system_ram_used_percent'] = int((used_mb / total_mb) * 100) if total_mb > 0 else 0 + except Exception: + pass + except Exception: + # Fallbacks if psutil not available + # CPU percent via /proc/stat + global cpu_prev + try: + with open('/proc/stat', 'r') as f: + line = f.readline() + parts = line.split() + if parts[0] == 'cpu': + vals = list(map(int, parts[1:])) + idle = vals[3] + total = sum(vals) + if cpu_prev is not None: + prev_total, prev_idle = cpu_prev + total_diff = total - prev_total + idle_diff = idle - prev_idle + if total_diff > 0: + cpu_pct = (1.0 - (idle_diff / total_diff)) * 100.0 + gpu_stats['cpu_utilization'] = int(round(cpu_pct)) + cpu_prev = (total, idle) + except Exception: + pass + + # System RAM total/available from /proc/meminfo (kB -> MB) + try: + meminfo = {} + with open('/proc/meminfo', 'r') as f: + for l in f: + parts = l.split() + if len(parts) >= 2: + key = parts[0].rstrip(':') + meminfo[key] = int(parts[1]) + if 'MemTotal' in meminfo: + total_mb = int(meminfo['MemTotal'] / 1024) + avail_kb = meminfo.get('MemAvailable', None) + if avail_kb is None: + # Fallback: estimate available from MemFree + Buffers + Cached + avail_kb = meminfo.get('MemFree', 0) + meminfo.get('Buffers', 0) + meminfo.get('Cached', 0) + used_mb = int((meminfo['MemTotal'] - avail_kb) / 1024) + gpu_stats['system_ram_total'] = total_mb + gpu_stats['system_ram_used'] = used_mb + gpu_stats['system_ram_used_percent'] = int((used_mb / total_mb) * 100) if total_mb > 0 else 0 + except Exception: + pass return gpu_stats def send_monitor_update(): @@ -147,9 +256,29 @@ def send_monitor_update(): 'gpu_temperature': gpu_stats['gpu_temperature'], 'vram_total': gpu_stats['vram_total'], 'vram_used': gpu_stats['vram_used'], - 'vram_used_percent': gpu_stats['vram_used_percent'] + 'vram_used_percent': gpu_stats['vram_used_percent'], + 'gtt_total': gpu_stats.get('gtt_total', 0), + 'gtt_used': gpu_stats.get('gtt_used', 0), + 'gtt_used_percent': gpu_stats.get('gtt_used_percent', 0) }] } + # Include CPU and system RAM total (MB) at top level + try: + data['cpu_utilization'] = int(gpu_stats.get('cpu_utilization', 0)) + except Exception: + data['cpu_utilization'] = 0 + try: + data['system_ram_total'] = int(gpu_stats.get('system_ram_total', 0)) + except Exception: + data['system_ram_total'] = 0 + try: + data['system_ram_used'] = int(gpu_stats.get('system_ram_used', 0)) + except Exception: + data['system_ram_used'] = 0 + try: + data['system_ram_used_percent'] = int(gpu_stats.get('system_ram_used_percent', 0)) + except Exception: + data['system_ram_used_percent'] = 0 # Send the data try: diff --git a/web/actionbar.js b/web/actionbar.js new file mode 100644 index 0000000..d50b9e7 --- /dev/null +++ b/web/actionbar.js @@ -0,0 +1,268 @@ +import { app } from "../../scripts/app.js"; +import { api } from "../../scripts/api.js"; + +let compactRef = null; + +const makeBar = (color, titleText) => { + const wrapper = document.createElement('div'); + wrapper.style.display = 'flex'; + wrapper.style.flexDirection = 'column'; + wrapper.style.alignItems = 'flex-start'; + wrapper.style.gap = '1px'; + + const title = document.createElement('div'); + title.style.fontSize = '10px'; + title.style.color = '#ccc'; + title.style.fontWeight = '600'; + title.style.marginBottom = '1px'; + title.textContent = titleText; + + const barOuter = document.createElement('div'); + barOuter.style.width = '64px'; + barOuter.style.height = '16px'; + barOuter.style.background = '#222'; + barOuter.style.borderRadius = '6px'; + barOuter.style.position = 'relative'; + barOuter.style.overflow = 'hidden'; + + const barInner = document.createElement('div'); + barInner.style.height = '100%'; + barInner.style.width = '0%'; + barInner.style.background = color; + barInner.style.transition = 'width 0.3s ease'; + + const label = document.createElement('div'); + label.style.position = 'absolute'; + label.style.top = '0'; + label.style.left = '4px'; + label.style.height = '100%'; + label.style.lineHeight = '16px'; + label.style.fontSize = '11px'; + label.style.color = '#fff'; + label.style.textShadow = '1px 1px 1px #000'; + label.style.pointerEvents = 'none'; + label.style.whiteSpace = 'nowrap'; + label.textContent = '0%'; + + barOuter.appendChild(barInner); + barOuter.appendChild(label); + wrapper.appendChild(title); + wrapper.appendChild(barOuter); + + return { wrapper, barInner, label }; +}; + +const createCompactElement = () => { + const el = document.createElement('div'); + el.className = 'amd-gpu-compact'; + el.style.display = 'flex'; + el.style.flexDirection = 'column'; + el.style.gap = '2px'; + el.style.alignItems = 'flex-start'; + el.style.padding = '1px 3px'; + el.style.background = 'transparent'; + + const cpu = makeBar('#8ad1ff', 'CPU'); + const ram = makeBar('#47a0ff', 'RAM (GB)'); + const gpu = makeBar('#47a0ff', 'GPU'); + const vram = makeBar('#9be56b', 'VRAM (GB)'); + const gtt = makeBar('#ffad33', 'GTT (GB)'); + const tmp = makeBar('#ff4d4d', 'TEMP'); + + const row = document.createElement('div'); + row.style.display = 'flex'; + row.style.gap = '4px'; + row.style.alignItems = 'center'; + row.appendChild(cpu.wrapper); + row.appendChild(ram.wrapper); + row.appendChild(gpu.wrapper); + row.appendChild(vram.wrapper); + row.appendChild(gtt.wrapper); + row.appendChild(tmp.wrapper); + + el.appendChild(row); + + compactRef = { + el, + cpuWrapper: cpu.wrapper, + cpuBar: cpu.barInner, + cpuLabel: cpu.label, + ramWrapper: ram.wrapper, + ramBar: ram.barInner, + ramLabel: ram.label, + gpuWrapper: gpu.wrapper, + gpuBar: gpu.barInner, + gpuLabel: gpu.label, + vramWrapper: vram.wrapper, + vramBar: vram.barInner, + vramLabel: vram.label, + gttWrapper: gtt.wrapper, + gttBar: gtt.barInner, + gttLabel: gtt.label, + tmpWrapper: tmp.wrapper, + tmpBar: tmp.barInner, + tmpLabel: tmp.label + }; + + return compactRef; +}; + +const updateCompactUI = (compact, data) => { + if (!compact || !data) return; + const now = Date.now(); + if (compact._lastUpdate && (now - compact._lastUpdate) < 250) return; + compact._lastUpdate = now; + const gpu = (data.gpus && data.gpus[0]) || {}; + const utilization = Math.max(0, Math.min(100, gpu.gpu_utilization || 0)); + const vramPercent = Math.max(0, Math.min(100, gpu.vram_used_percent || 0)); + const rawTotal = Number(gpu.gtt_total || 0); + const rawUsed = Number(gpu.gtt_used || 0); + let gttTotalMB = rawTotal; let gttUsedMB = rawUsed; + if (rawTotal > 1024 * 1024) gttTotalMB = rawTotal / (1024 * 1024); + if (rawUsed > 1024 * 1024) gttUsedMB = rawUsed / (1024 * 1024); + gttTotalMB = Math.round(gttTotalMB); gttUsedMB = Math.round(gttUsedMB); + let gttPercent = typeof gpu.gtt_used_percent === 'number' ? gpu.gtt_used_percent : (gttTotalMB > 0 ? Math.max(0, Math.min(100, Math.round((gttUsedMB / gttTotalMB) * 100))) : 0); + const temp = gpu.gpu_temperature || 0; + + compact.gpuBar.style.width = `${utilization}%`; + compact.gpuLabel.textContent = `${utilization}%`; + compact.vramBar.style.width = `${vramPercent}%`; + + const rawVramTotal = Number(gpu.vram_total || 0); + const rawVramUsed = Number(gpu.vram_used || 0); + let vramTotalMB = rawVramTotal; let vramUsedMB = rawVramUsed; + if (rawVramTotal > 1024 * 1024) vramTotalMB = rawVramTotal / (1024 * 1024); + if (rawVramUsed > 1024 * 1024) vramUsedMB = rawVramUsed / (1024 * 1024); + const vramUsedGB = (vramUsedMB / 1024); const vramTotalGB = (vramTotalMB / 1024); + compact.vramLabel.textContent = `${vramUsedGB.toFixed(1)} / ${vramTotalGB.toFixed(1)}`; + + compact.gttBar.style.width = `${gttPercent}%`; + const gttUsedGB = (gttUsedMB / 1024); const gttTotalGB = (gttTotalMB / 1024); + compact.gttLabel.textContent = `${gttUsedGB.toFixed(1)} / ${gttTotalGB.toFixed(1)}`; + + compact.tmpBar.style.width = `${Math.min(100, temp)}%`; + compact.tmpLabel.textContent = `${temp}°C`; + + const setColor = (bar, value) => { + if (value > 85) bar.style.background = '#ff4d4d'; + else if (value > 70) bar.style.background = '#ffad33'; + else bar.style.background = '#47a0ff'; + }; + + setColor(compact.gpuBar, utilization); + setColor(compact.vramBar, vramPercent); + setColor(compact.gttBar, gttPercent); + setColor(compact.tmpBar, Math.min(100, temp)); + + const cpuUtil = Number(data.cpu_utilization || 0); + if (compact.cpuBar) { + compact.cpuBar.style.width = `${Math.max(0, Math.min(100, cpuUtil))}%`; + compact.cpuLabel.textContent = `${cpuUtil}%`; + if (cpuUtil > 90) compact.cpuBar.style.background = '#ff4d4d'; + else if (cpuUtil > 60) compact.cpuBar.style.background = '#ffad33'; + else compact.cpuBar.style.background = '#47a0ff'; + } + + const usedMB = Number(data.system_ram_used || 0); + const totalMB2 = Number(data.system_ram_total || 0); + const ramPercent = Number(data.system_ram_used_percent || (totalMB2>0?Math.round((usedMB/totalMB2)*100):0)); + if (compact.ramBar) { + compact.ramBar.style.width = `${Math.max(0, Math.min(100, ramPercent))}%`; + compact.ramLabel.textContent = `${(usedMB/1024).toFixed(1)} / ${(totalMB2/1024).toFixed(1)}`; + // Use same color thresholds as other bars + if (ramPercent > 90) compact.ramBar.style.background = '#ff4d4d'; + else if (ramPercent > 70) compact.ramBar.style.background = '#ffad33'; + else compact.ramBar.style.background = '#47a0ff'; + } +}; + +const addActionBarButton = () => { + if (!compactRef) return; + if (document.querySelector('.amd-gpu-compact')) return; + + const settingsGroupElem = app && app.menu && app.menu.settingsGroup && app.menu.settingsGroup.element ? app.menu.settingsGroup.element : null; + const queueAnchor = document.getElementById('queue-button') || document.querySelector('.comfyui-queue-button')?.parentElement; + const toolbar = queueAnchor || document.querySelector('.comfyui-toolbar') || document.querySelector('.toolbar') || null; + if (!settingsGroupElem && !toolbar) return; + + try { + if (!compactRef) compactRef = createCompactElement(); + try { compactRef.el.title = 'AMD GPU Monitor'; compactRef.el.setAttribute('aria-label', 'AMD GPU Monitor'); } catch (e) {} + try { + compactRef.el.style.cursor = 'pointer'; + compactRef.el.addEventListener('click', () => { + const monitorElem = document.querySelector('.amd-gpu-monitor'); + if (!monitorElem) return; + const isHidden = getComputedStyle(monitorElem).display === 'none' || monitorElem.style.display === 'none'; + if (isHidden) { + // Show panel and hide compact actionbar + monitorElem.style.display = 'block'; + try { localStorage.removeItem('amd-gpu-monitor-closed'); } catch (e) {} + try { compactRef.el.style.display = 'none'; } catch (e) {} + } else { + // Hide panel and show compact actionbar + monitorElem.style.display = 'none'; + try { localStorage.setItem('amd-gpu-monitor-closed', 'true'); } catch (e) {} + try { compactRef.el.style.display = ''; } catch (e) {} + } + }); + } catch (e) {} + + const node = compactRef.el; + if (settingsGroupElem && typeof settingsGroupElem.before === 'function') settingsGroupElem.before(node); else if (toolbar) toolbar.appendChild(node); + + try { + if (!document.querySelector('.amd-gpu-compact-menu-btn')) { + const menuBtn = document.createElement('button'); + menuBtn.className = 'amd-gpu-compact-menu-btn comfyui-button'; + menuBtn.title = 'AMD Monitor options'; + menuBtn.textContent = '▾'; + menuBtn.style.marginLeft = '0px'; + + const menu = document.createElement('div'); + menu.className = 'amd-gpu-compact-menu'; + menu.style.position = 'absolute'; + menu.style.background = '#222'; + menu.style.border = '1px solid #333'; + menu.style.padding = '1px'; + menu.style.borderRadius = '1px'; + menu.style.display = 'none'; + menu.style.zIndex = '10001'; + menu.style.color = '#ddd'; + menu.style.fontSize = '12px'; + + const addToggle = (key, labelText, wrapper) => { + const item = document.createElement('div'); + item.style.display = 'flex'; item.style.alignItems = 'center'; item.style.gap = '6px'; item.style.marginBottom = '4px'; + const cb = document.createElement('input'); cb.type = 'checkbox'; cb.checked = localStorage.getItem(key) !== '0'; cb.addEventListener('change', () => { const show = cb.checked; localStorage.setItem(key, show ? '1' : '0'); try { wrapper.style.display = show ? '' : 'none'; } catch (e) {} }); + const lbl = document.createElement('div'); lbl.textContent = labelText; + item.appendChild(cb); item.appendChild(lbl); menu.appendChild(item); + const show = localStorage.getItem(key) !== '0'; try { wrapper.style.display = show ? '' : 'none'; } catch (e) {} + }; + + document.body.appendChild(menu); + // Order: CPU, RAM, GPU, VRAM, GTT, TEMP + addToggle('amd-compact-show-cpu', 'CPU', compactRef.cpuWrapper); + addToggle('amd-compact-show-ram', 'RAM', compactRef.ramWrapper); + addToggle('amd-compact-show-gpu', 'GPU', compactRef.gpuWrapper); + addToggle('amd-compact-show-vram', 'VRAM', compactRef.vramWrapper); + addToggle('amd-compact-show-gtt', 'GTT', compactRef.gttWrapper); + addToggle('amd-compact-show-temp', 'TEMP', compactRef.tmpWrapper); + + menuBtn.addEventListener('click', (e) => { e.stopPropagation(); if (menu.style.display === 'none') { const rect = menuBtn.getBoundingClientRect(); menu.style.left = `${rect.left}px`; menu.style.top = `${rect.bottom + 6}px`; menu.style.display = ''; } else { menu.style.display = 'none'; } }); + document.addEventListener('click', () => { menu.style.display = 'none'; }); + if (node.parentElement) node.parentElement.insertBefore(menuBtn, node.nextSibling); + } + } catch (e) {} + } catch (e) {} +}; + +// Register listener for updates +api.addEventListener("amd_gpu_monitor", (event) => { + try { if (compactRef) updateCompactUI(compactRef, event.detail); } catch (e) {} +}); + +// Wait and add action bar button +try { if (app && app.ui && app.ui.loaded) app.ui.loaded.then(() => { if (!compactRef) createCompactElement(); setTimeout(addActionBarButton, 300); }); else { if (!compactRef) createCompactElement(); setTimeout(addActionBarButton, 300); } } catch (e) { if (!compactRef) createCompactElement(); setTimeout(addActionBarButton, 300); } + +export { createCompactElement, updateCompactUI }; diff --git a/web/index.js b/web/index.js index cb13995..1756391 100644 --- a/web/index.js +++ b/web/index.js @@ -1 +1,2 @@ -import "./monitor.js"; +import "./panel.js"; +import "./actionbar.js"; diff --git a/web/monitor.js b/web/panel.js similarity index 50% rename from web/monitor.js rename to web/panel.js index afd0c5c..8d1ad5b 100644 --- a/web/monitor.js +++ b/web/panel.js @@ -1,7 +1,7 @@ import { app } from "../../scripts/app.js"; import { api } from "../../scripts/api.js"; -// Create the monitor UI element +// Create the detailed monitor UI element and handle updates const createMonitorElement = () => { // Create main container const container = document.createElement("div"); @@ -29,9 +29,9 @@ const createMonitorElement = () => { title.style.justifyContent = "space-between"; title.innerHTML = 'AMD GPU Monitor'; - // Add collapse button + // Add collapse and close buttons const collapseButton = document.createElement("button"); - collapseButton.innerHTML = "−"; // Unicode minus sign + collapseButton.innerHTML = "−"; collapseButton.style.background = "none"; collapseButton.style.border = "none"; collapseButton.style.color = "#888"; @@ -40,9 +40,8 @@ const createMonitorElement = () => { collapseButton.style.padding = "0 5px"; collapseButton.title = "Collapse/Expand"; - // Add close button const closeButton = document.createElement("button"); - closeButton.innerHTML = "×"; // Unicode times sign + closeButton.innerHTML = "×"; closeButton.style.background = "none"; closeButton.style.border = "none"; closeButton.style.color = "#888"; @@ -50,33 +49,92 @@ const createMonitorElement = () => { closeButton.style.fontSize = "14px"; closeButton.style.padding = "0 5px"; closeButton.title = "Close"; - + const buttonContainer = document.createElement("div"); buttonContainer.appendChild(collapseButton); buttonContainer.appendChild(closeButton); - title.appendChild(buttonContainer); container.appendChild(title); - - // Content container that can be collapsed + const content = document.createElement("div"); content.className = "amd-gpu-monitor-content"; container.appendChild(content); - - // GPU Utilization section + + // CPU + const cpuSection = document.createElement("div"); + cpuSection.style.marginBottom = "8px"; + const cpuLabel = document.createElement("div"); + cpuLabel.textContent = "CPU Utilization:"; + cpuLabel.style.marginBottom = "2px"; + const cpuBarContainer = document.createElement("div"); + cpuBarContainer.style.height = "15px"; + cpuBarContainer.style.backgroundColor = "#333"; + cpuBarContainer.style.borderRadius = "3px"; + cpuBarContainer.style.position = "relative"; + const cpuBar = document.createElement("div"); + cpuBar.className = "amd-cpu-bar"; + cpuBar.style.height = "100%"; + cpuBar.style.width = "0%"; + cpuBar.style.backgroundColor = "#47a0ff"; + cpuBar.style.borderRadius = "3px"; + cpuBar.style.transition = "width 0.5s ease-out, background-color 0.3s"; + const cpuText = document.createElement("div"); + cpuText.className = "amd-cpu-text"; + cpuText.textContent = "0%"; + cpuText.style.position = "absolute"; + cpuText.style.top = "0"; + cpuText.style.left = "5px"; + cpuText.style.lineHeight = "15px"; + cpuText.style.textShadow = "1px 1px 1px #000"; + cpuBarContainer.appendChild(cpuBar); + cpuBarContainer.appendChild(cpuText); + cpuSection.appendChild(cpuLabel); + cpuSection.appendChild(cpuBarContainer); + content.appendChild(cpuSection); + + // RAM + const ramSection = document.createElement("div"); + ramSection.style.marginBottom = "8px"; + const ramLabel = document.createElement("div"); + ramLabel.textContent = "System RAM (GB):"; + ramLabel.style.marginBottom = "2px"; + const ramBarContainer = document.createElement("div"); + ramBarContainer.style.height = "15px"; + ramBarContainer.style.backgroundColor = "#333"; + ramBarContainer.style.borderRadius = "3px"; + ramBarContainer.style.position = "relative"; + const ramBar = document.createElement("div"); + ramBar.className = "amd-ram-bar"; + ramBar.style.height = "100%"; + ramBar.style.width = "0%"; + ramBar.style.backgroundColor = "#47a0ff"; + ramBar.style.borderRadius = "3px"; + ramBar.style.transition = "width 0.5s ease-out, background-color 0.3s"; + const ramText = document.createElement("div"); + ramText.className = "amd-ram-text"; + ramText.textContent = "0.0 / 0.0 (0%)"; + ramText.style.position = "absolute"; + ramText.style.top = "0"; + ramText.style.left = "5px"; + ramText.style.lineHeight = "15px"; + ramText.style.textShadow = "1px 1px 1px #000"; + ramBarContainer.appendChild(ramBar); + ramBarContainer.appendChild(ramText); + ramSection.appendChild(ramLabel); + ramSection.appendChild(ramBarContainer); + content.appendChild(ramSection); + + // GPU Utilization const gpuSection = document.createElement("div"); gpuSection.style.marginBottom = "8px"; - const gpuLabel = document.createElement("div"); gpuLabel.textContent = "GPU Utilization:"; gpuLabel.style.marginBottom = "2px"; - const gpuBarContainer = document.createElement("div"); gpuBarContainer.style.height = "15px"; gpuBarContainer.style.backgroundColor = "#333"; gpuBarContainer.style.borderRadius = "3px"; gpuBarContainer.style.position = "relative"; - const gpuBar = document.createElement("div"); gpuBar.className = "amd-gpu-utilization-bar"; gpuBar.style.height = "100%"; @@ -84,7 +142,6 @@ const createMonitorElement = () => { gpuBar.style.backgroundColor = "#47a0ff"; gpuBar.style.borderRadius = "3px"; gpuBar.style.transition = "width 0.5s ease-out, background-color 0.3s"; - const gpuText = document.createElement("div"); gpuText.className = "amd-gpu-utilization-text"; gpuText.textContent = "0%"; @@ -93,27 +150,23 @@ const createMonitorElement = () => { gpuText.style.left = "5px"; gpuText.style.lineHeight = "15px"; gpuText.style.textShadow = "1px 1px 1px #000"; - gpuBarContainer.appendChild(gpuBar); gpuBarContainer.appendChild(gpuText); gpuSection.appendChild(gpuLabel); gpuSection.appendChild(gpuBarContainer); content.appendChild(gpuSection); - - // VRAM Usage section + + // VRAM const vramSection = document.createElement("div"); vramSection.style.marginBottom = "8px"; - const vramLabel = document.createElement("div"); - vramLabel.textContent = "VRAM Usage:"; + vramLabel.textContent = "VRAM Usage (GB):"; vramLabel.style.marginBottom = "2px"; - const vramBarContainer = document.createElement("div"); vramBarContainer.style.height = "15px"; vramBarContainer.style.backgroundColor = "#333"; vramBarContainer.style.borderRadius = "3px"; vramBarContainer.style.position = "relative"; - const vramBar = document.createElement("div"); vramBar.className = "amd-vram-bar"; vramBar.style.height = "100%"; @@ -121,35 +174,62 @@ const createMonitorElement = () => { vramBar.style.backgroundColor = "#47a0ff"; vramBar.style.borderRadius = "3px"; vramBar.style.transition = "width 0.5s ease-out, background-color 0.3s"; - const vramText = document.createElement("div"); vramText.className = "amd-vram-text"; - vramText.textContent = "0MB / 0MB (0%)"; + vramText.textContent = "0.0 / 0.0 (0%)"; vramText.style.position = "absolute"; vramText.style.top = "0"; vramText.style.left = "5px"; vramText.style.lineHeight = "15px"; vramText.style.textShadow = "1px 1px 1px #000"; - vramBarContainer.appendChild(vramBar); vramBarContainer.appendChild(vramText); vramSection.appendChild(vramLabel); vramSection.appendChild(vramBarContainer); content.appendChild(vramSection); - - // Temperature section + + // GTT + const gttSection = document.createElement("div"); + gttSection.style.marginBottom = "8px"; + const gttLabel = document.createElement("div"); + gttLabel.textContent = "GTT Usage (GB):"; + gttLabel.style.marginBottom = "2px"; + const gttBarContainer = document.createElement("div"); + gttBarContainer.style.height = "15px"; + gttBarContainer.style.backgroundColor = "#333"; + gttBarContainer.style.borderRadius = "3px"; + gttBarContainer.style.position = "relative"; + const gttBar = document.createElement("div"); + gttBar.className = "amd-gtt-bar"; + gttBar.style.height = "100%"; + gttBar.style.width = "0%"; + gttBar.style.backgroundColor = "#47a0ff"; + gttBar.style.borderRadius = "3px"; + gttBar.style.transition = "width 0.5s ease-out, background-color 0.3s"; + const gttText = document.createElement("div"); + gttText.className = "amd-gtt-text"; + gttText.textContent = "0.0 / 0.0 (0%)"; + gttText.style.position = "absolute"; + gttText.style.top = "0"; + gttText.style.left = "5px"; + gttText.style.lineHeight = "15px"; + gttText.style.textShadow = "1px 1px 1px #000"; + gttBarContainer.appendChild(gttBar); + gttBarContainer.appendChild(gttText); + gttSection.appendChild(gttLabel); + gttSection.appendChild(gttBarContainer); + content.appendChild(gttSection); + + // Temp const tempSection = document.createElement("div"); - const tempLabel = document.createElement("div"); tempLabel.textContent = "GPU Temperature:"; tempLabel.style.marginBottom = "2px"; - const tempBarContainer = document.createElement("div"); tempBarContainer.style.height = "15px"; tempBarContainer.style.backgroundColor = "#333"; tempBarContainer.style.borderRadius = "3px"; tempBarContainer.style.position = "relative"; - const tempBar = document.createElement("div"); tempBar.className = "amd-temp-bar"; tempBar.style.height = "100%"; @@ -157,7 +237,6 @@ const createMonitorElement = () => { tempBar.style.backgroundColor = "#47a0ff"; tempBar.style.borderRadius = "3px"; tempBar.style.transition = "width 0.5s ease-out, background-color 0.3s"; - const tempText = document.createElement("div"); tempText.className = "amd-temp-text"; tempText.textContent = "0°C"; @@ -166,14 +245,13 @@ const createMonitorElement = () => { tempText.style.left = "5px"; tempText.style.lineHeight = "15px"; tempText.style.textShadow = "1px 1px 1px #000"; - tempBarContainer.appendChild(tempBar); tempBarContainer.appendChild(tempText); tempSection.appendChild(tempLabel); tempSection.appendChild(tempBarContainer); content.appendChild(tempSection); - - // Add event listener for collapsing + + // Collapse/close handlers and draggable logic let isCollapsed = false; collapseButton.addEventListener("click", () => { if (isCollapsed) { @@ -186,51 +264,33 @@ const createMonitorElement = () => { isCollapsed = true; } }); - - // Add event listener for closing closeButton.addEventListener("click", () => { container.style.display = "none"; - // Store the closed state in localStorage localStorage.setItem("amd-gpu-monitor-closed", "true"); + try { const compactElem = document.querySelector('.amd-gpu-compact'); if (compactElem) compactElem.style.display = ''; } catch (e) {} }); - - // Make the monitor draggable + let isDragging = false; let dragOffsetX, dragOffsetY; - title.addEventListener("mousedown", (e) => { - // Only handle main button (left button) if (e.button !== 0) return; - isDragging = true; dragOffsetX = e.clientX - container.offsetLeft; dragOffsetY = e.clientY - container.offsetTop; - - // Prevent text selection during drag e.preventDefault(); }); - document.addEventListener("mousemove", (e) => { if (!isDragging) return; - const x = e.clientX - dragOffsetX; const y = e.clientY - dragOffsetY; - - // Keep monitor within window bounds const maxX = window.innerWidth - container.offsetWidth; const maxY = window.innerHeight - container.offsetHeight; - container.style.left = Math.max(0, Math.min(x, maxX)) + "px"; container.style.top = Math.max(0, Math.min(y, maxY)) + "px"; - - // We're now positioning with left instead of right container.style.right = "auto"; }); - document.addEventListener("mouseup", () => { isDragging = false; - - // Save position to localStorage if (container.style.left && container.style.top) { localStorage.setItem("amd-gpu-monitor-position", JSON.stringify({ left: container.style.left, @@ -238,8 +298,7 @@ const createMonitorElement = () => { })); } }); - - // Load saved position if available + const savedPosition = localStorage.getItem("amd-gpu-monitor-position"); if (savedPosition) { try { @@ -247,157 +306,108 @@ const createMonitorElement = () => { container.style.left = left; container.style.top = top; container.style.right = "auto"; - } catch (e) { - // Silently fail and use default position - } + } catch (e) {} } - - // Check if monitor was closed previously - if (localStorage.getItem("amd-gpu-monitor-closed") === "true") { - container.style.display = "none"; - } - - // Add a button to show the monitor again - const showButton = document.createElement("button"); - showButton.textContent = "Show AMD GPU Monitor"; - showButton.style.position = "fixed"; - showButton.style.top = "5px"; - showButton.style.right = "5px"; - showButton.style.zIndex = "999"; - showButton.style.padding = "5px"; - showButton.style.borderRadius = "3px"; - showButton.style.backgroundColor = "#333"; - showButton.style.color = "#fff"; - showButton.style.border = "none"; - showButton.style.fontSize = "12px"; - showButton.style.cursor = "pointer"; - showButton.style.display = "none"; - - showButton.addEventListener("click", () => { - container.style.display = "block"; - showButton.style.display = "none"; - localStorage.removeItem("amd-gpu-monitor-closed"); - }); - - document.body.appendChild(showButton); - - // Toggle showButton visibility based on monitor visibility - const updateShowButtonVisibility = () => { - if (container.style.display === "none") { - showButton.style.display = "block"; - } else { - showButton.style.display = "none"; - } - }; - - // Create a MutationObserver to watch for changes to container's display style - const observer = new MutationObserver((mutations) => { - mutations.forEach((mutation) => { - if (mutation.attributeName === "style") { - updateShowButtonVisibility(); - } - }); - }); - - observer.observe(container, { attributes: true }); - - // Initial visibility check - updateShowButtonVisibility(); - - return { container, gpuBar, gpuText, vramBar, vramText, tempBar, tempText }; + if (localStorage.getItem("amd-gpu-monitor-closed") === "true") container.style.display = "none"; + + return { container, gpuBar, gpuText, vramBar, vramText, cpuBar, cpuText, tempBar, tempText, gttBar, gttText, ramBar, ramText }; }; -// Update the monitor UI with new data -const updateMonitorUI = (monitor, data) => { - // Check if we have GPU data - if (!data || !data.gpus || data.gpus.length === 0) return; - - const gpu = data.gpus[0]; // Use the first GPU - - // Update GPU utilization +// Update the detailed panel with new data +const updatePanelUI = (monitor, data) => { + if (!monitor || !data) return; + const now = Date.now(); + if (monitor._lastUpdate && (now - monitor._lastUpdate) < 250) return; + monitor._lastUpdate = now; + const gpu = (data.gpus && data.gpus[0]) || {}; + if (monitor.gpuBar && monitor.gpuText) { const utilization = gpu.gpu_utilization || 0; monitor.gpuBar.style.width = `${utilization}%`; monitor.gpuText.textContent = `${utilization}%`; - - // Change color based on utilization - if (utilization > 80) { - monitor.gpuBar.style.backgroundColor = '#ff4d4d'; // Red for high - } else if (utilization > 50) { - monitor.gpuBar.style.backgroundColor = '#ffad33'; // Orange for medium - } else { - monitor.gpuBar.style.backgroundColor = '#47a0ff'; // Blue for low - } + if (utilization > 80) monitor.gpuBar.style.backgroundColor = '#ff4d4d'; + else if (utilization > 50) monitor.gpuBar.style.backgroundColor = '#ffad33'; + else monitor.gpuBar.style.backgroundColor = '#47a0ff'; } - - // Update VRAM usage + if (monitor.vramBar && monitor.vramText) { const vramPercent = gpu.vram_used_percent || 0; const vramUsed = gpu.vram_used || 0; const vramTotal = gpu.vram_total || 1; - monitor.vramBar.style.width = `${vramPercent}%`; - - // Format the text to show MB or GB - let vramUsedText = vramUsed; - let vramTotalText = vramTotal; - let unit = 'MB'; - - if (vramTotal >= 1024) { - vramUsedText = (vramUsed / 1024).toFixed(1); - vramTotalText = (vramTotal / 1024).toFixed(1); - unit = 'GB'; - } - - monitor.vramText.textContent = `${vramUsedText}${unit} / ${vramTotalText}${unit} (${vramPercent}%)`; - - // Change color based on VRAM usage - if (vramPercent > 85) { - monitor.vramBar.style.backgroundColor = '#ff4d4d'; // Red for high - } else if (vramPercent > 70) { - monitor.vramBar.style.backgroundColor = '#ffad33'; // Orange for medium - } else { - monitor.vramBar.style.backgroundColor = '#47a0ff'; // Blue for low - } + // Always display VRAM in GB (one decimal) + const vramUsedGB = (vramUsed / 1024); + const vramTotalGB = (vramTotal / 1024); + monitor.vramText.textContent = `${vramUsedGB.toFixed(1)} / ${vramTotalGB.toFixed(1)} (${vramPercent}%)`; + if (vramPercent > 85) monitor.vramBar.style.backgroundColor = '#ff4d4d'; + else if (vramPercent > 70) monitor.vramBar.style.backgroundColor = '#ffad33'; + else monitor.vramBar.style.backgroundColor = '#47a0ff'; } - - // Update temperature + + if (monitor.cpuBar && monitor.cpuText) { + const cpuUtil = Number(data.cpu_utilization || 0); + monitor.cpuBar.style.width = `${Math.max(0, Math.min(100, cpuUtil))}%`; + monitor.cpuText.textContent = `${cpuUtil}%`; + if (cpuUtil > 90) monitor.cpuBar.style.backgroundColor = '#ff4d4d'; + else if (cpuUtil > 60) monitor.cpuBar.style.backgroundColor = '#ffad33'; + else monitor.cpuBar.style.backgroundColor = '#47a0ff'; + } + + if (monitor.gttBar && monitor.gttText) { + const rawTotal = Number((data.gpus && data.gpus[0] && data.gpus[0].gtt_total) || 0); + const rawUsed = Number((data.gpus && data.gpus[0] && data.gpus[0].gtt_used) || 0); + let gttTotalMB = rawTotal; let gttUsedMB = rawUsed; + if (rawTotal > 1024 * 1024) gttTotalMB = rawTotal / (1024 * 1024); + if (rawUsed > 1024 * 1024) gttUsedMB = rawUsed / (1024 * 1024); + let gttPercent = (data.gpus && data.gpus[0] && typeof data.gpus[0].gtt_used_percent === 'number') ? data.gpus[0].gtt_used_percent : (gttTotalMB > 0 ? Math.round((gttUsedMB / gttTotalMB) * 100) : 0); + monitor.gttBar.style.width = `${gttPercent}%`; + // Always display GTT in GB (one decimal) + const gttUsedGB = (gttUsedMB / 1024); + const gttTotalGB = (gttTotalMB / 1024); + monitor.gttText.textContent = `${gttUsedGB.toFixed(1)} / ${gttTotalGB.toFixed(1)} (${gttPercent}%)`; + if (gttPercent > 85) monitor.gttBar.style.backgroundColor = '#ff4d4d'; + else if (gttPercent > 70) monitor.gttBar.style.backgroundColor = '#ffad33'; + else monitor.gttBar.style.backgroundColor = '#47a0ff'; + } + if (monitor.tempBar && monitor.tempText) { const temp = gpu.gpu_temperature || 0; - - // Assume max reasonable temp is 100°C for the progress bar const tempPercent = Math.min(temp, 100); monitor.tempBar.style.width = `${tempPercent}%`; monitor.tempText.textContent = `${temp}°C`; - - // Change color based on temperature - if (temp > 80) { - monitor.tempBar.style.backgroundColor = '#ff4d4d'; // Red for high - } else if (temp > 60) { - monitor.tempBar.style.backgroundColor = '#ffad33'; // Orange for medium - } else { - monitor.tempBar.style.backgroundColor = '#47a0ff'; // Blue for low - } + if (temp > 80) monitor.tempBar.style.backgroundColor = '#ff4d4d'; + else if (temp > 60) monitor.tempBar.style.backgroundColor = '#ffad33'; + else monitor.tempBar.style.backgroundColor = '#47a0ff'; + } + + if (monitor.ramBar && monitor.ramText) { + const usedMB = Number(data.system_ram_used || 0); + const totalMB = Number(data.system_ram_total || 0); + const percent = Number(data.system_ram_used_percent || (totalMB>0?Math.round((usedMB/totalMB)*100):0)); + monitor.ramBar.style.width = `${Math.max(0, Math.min(100, percent))}%`; + // Always display system RAM in GB (one decimal) + const usedGB = (usedMB / 1024); + const totalGB = (totalMB / 1024); + monitor.ramText.textContent = `${usedGB.toFixed(1)} / ${totalGB.toFixed(1)} (${percent}%)`; + if (percent > 90) monitor.ramBar.style.backgroundColor = '#ff4d4d'; + else if (percent > 70) monitor.ramBar.style.backgroundColor = '#ffad33'; + else monitor.ramBar.style.backgroundColor = '#47a0ff'; } }; -// Main app function +// Initialize panel const main = () => { - // Create the monitor UI const monitor = createMonitorElement(); document.body.appendChild(monitor.container); - - // Set up WebSocket listener for GPU updates - api.addEventListener("amd_gpu_monitor", (event) => { - updateMonitorUI(monitor, event.detail); - }); + // If the panel is visible on start, hide the compact actionbar if present + const isClosed = localStorage.getItem('amd-gpu-monitor-closed') === 'true'; + if (!isClosed) { + try { const compactElem = document.querySelector('.amd-gpu-compact'); if (compactElem) compactElem.style.display = 'none'; } catch (e) {} + } + if (isClosed) monitor.container.style.display = 'none'; + api.addEventListener("amd_gpu_monitor", (event) => { updatePanelUI(monitor, event.detail); }); }; -// Wait for DOM to be loaded -app.registerExtension({ - name: "amd.gpu.monitor", - async setup() { - // Wait a bit for the UI to be fully loaded - setTimeout(main, 1000); - }, -}); +try { if (app && app.ui && app.ui.loaded) app.ui.loaded.then(main); else setTimeout(main, 200); } catch (e) { setTimeout(main, 200); } + +export { createMonitorElement, updatePanelUI };