From 2b016735525ba0b25cc1ed9d4219e5c1854aa582 Mon Sep 17 00:00:00 2001 From: forkless Date: Mon, 8 Jun 2026 23:13:28 +0200 Subject: [PATCH] fix: add frontend web dir and improve AMD GPU detection --- .codewhale/instructions.md | 21 + .deepseek/state/subagents.v1.json | 30 + providers/__init__.py | 18 +- web/xpu_monitor.js | 1293 +++++++++++++++++++++++++++++ 4 files changed, 1358 insertions(+), 4 deletions(-) create mode 100644 .codewhale/instructions.md create mode 100644 .deepseek/state/subagents.v1.json create mode 100644 web/xpu_monitor.js diff --git a/.codewhale/instructions.md b/.codewhale/instructions.md new file mode 100644 index 0000000..7be8297 --- /dev/null +++ b/.codewhale/instructions.md @@ -0,0 +1,21 @@ +# Project Context (Auto-generated) + +> This file was automatically generated by CodeWhale. +> You can edit or delete it at any time. + +## Bounded Project Overview + +```json +{ + "project_name": "xpusys", + "directory_structure": [], + "readme": null, + "config_files": [], + "key_source_files": [], + "counts": { + "config_files": 0, + "directory_entries": 0, + "key_source_files": 0 + } +} +``` \ No newline at end of file diff --git a/.deepseek/state/subagents.v1.json b/.deepseek/state/subagents.v1.json new file mode 100644 index 0000000..70061f2 --- /dev/null +++ b/.deepseek/state/subagents.v1.json @@ -0,0 +1,30 @@ +{ + "schema_version": 1, + "agents": [ + { + "id": "agent_81924c2d", + "session_name": "clone-fork", + "fork_context": false, + "workspace": "/home/pe1085/development/xpusys", + "agent_type": "implementer", + "prompt": "Clone the repository https://github.com/forkless/ComfyUI-XPUSYS-Monitor-AMD into the current directory /home/pe1085/development/xpusys. Use `git clone https://github.com/forkless/ComfyUI-XPUSYS-Monitor-AMD .` to clone into the existing directory.", + "assignment": { + "objective": "Clone the repository https://github.com/forkless/ComfyUI-XPUSYS-Monitor-AMD into the current directory /home/pe1085/development/xpusys. Use `git clone https://github.com/forkless/ComfyUI-XPUSYS-Monitor-AMD .` to clone into the existing directory.", + "role": "implementer" + }, + "model": "deepseek-v4-flash", + "nickname": "Beaked", + "status": { + "Failed": "Sub-agent requested unavailable tools: exec_shell\n(child model `deepseek-v4-flash` may be unavailable under the current access profile — retry agent_open with a different `model`, or remove `model` to inherit the parent's)" + }, + "result": null, + "steps_taken": 0, + "duration_ms": 17, + "allowed_tools": [ + "exec_shell" + ], + "updated_at_ms": 1780951716988, + "session_boot_id": "boot_3aa40f1d-b3c" + } + ] +} \ No newline at end of file diff --git a/providers/__init__.py b/providers/__init__.py index 11268a6..6351cff 100644 --- a/providers/__init__.py +++ b/providers/__init__.py @@ -137,13 +137,23 @@ def _is_amd_rocme() -> bool: """ Check if torch.cuda is backed by AMD ROCm. - Returns True if torch.version.roc is not None (AMD ROCm PyTorch). - Returns False if torch.version.roc is None (NVIDIA or standard CUDA). + Primary signal: torch.version.roc is not None (AMD ROCm PyTorch). + Fallback signal: GPU device name contains AMD/Radeon markers + (catches Windows ROCm builds that don't set version.roc). + + Returns False for NVIDIA or standard CUDA. """ try: import torch - # torch.version.roc: True/str = AMD ROCm, None = NVIDIA/other - return torch.version.roc is not None + # Primary: ROCm version string + if torch.version.roc is not None: + return True + # Fallback: check GPU name for AMD identifiers + name = torch.cuda.get_device_name(0).lower() + for marker in ["amd", "radeon", "advanced micro devices"]: + if marker in name: + return True + return False except Exception: return False diff --git a/web/xpu_monitor.js b/web/xpu_monitor.js new file mode 100644 index 0000000..22363a6 --- /dev/null +++ b/web/xpu_monitor.js @@ -0,0 +1,1293 @@ +/** + * xpu_monitor.js — ComfyUI-XPUSYS-Monitor + * + * Bar layout: + * [ PRED x.xx/x.xGB xx% ] [ CPU x% @ x.xGHz ] [ RAM x% ] | GPU x% @ xMHz | x°C VRAM x/xGB RSV xGB PWR xW x% | + * └── .vram-predictor-section ──┘ └──────────────── .gpu-composite-group ────────────────┘ + */ + +import { app } from "../../scripts/app.js"; +import { api } from "../../scripts/api.js"; + +// --------------------------------------------------------------------------- +// Constants +// --------------------------------------------------------------------------- + +const NS = "XPUSYS_Mon"; +const VERSION = "1.0.3"; +const GITHUB = "https://github.com/allanmeng/ComfyUI-XPUSYS-Monitor"; +const S = { + lang: `${NS}.Language`, + fontSize: `${NS}.FontSize`, + refreshMs: `${NS}.RefreshInterval`, + showPredictor: `${NS}.ShowPredictor`, + showCPU: `${NS}.ShowCPU`, + showRAM: `${NS}.ShowRAM`, + showEngine: `${NS}.ShowEngine`, + showVRAM: `${NS}.ShowVRAM`, + showRSV: `${NS}.ShowRSV`, + showPower: `${NS}.ShowPower`, +}; + +// Intel Arc PCI device ID → spec TBP (W) — Intel ARK / product pages / NotebookCheck +// Only includes cards with practical AI inference capability (≥8 GB VRAM or workstation Pro series) +const ARC_PCI_TGP = { + // ── Battlemage (Xe2) — B series consumer ───────────────────────────────── + "0xe20b": 190, // Arc B580 (desktop, 12 GB) + "0xe20a": 190, // Arc B770 (desktop, 16 GB, announced) + "0xe20c": 150, // Arc B570 (desktop, 10 GB) + "0xe208": 120, // Arc B580M (mobile, 12 GB) + "0xe209": 100, // Arc B570M (mobile, 10 GB) + + // ── Battlemage (Xe2) — B series Pro (workstation) ───────────────────────── + "0xe211": 200, // Arc Pro B60 (desktop, 24 GB, TBP 120–200 W, use max) + "0xe212": 70, // Arc Pro B50 (desktop, 16 GB) + + // ── Alchemist (Xe-HPG) — A series consumer desktop ─────────────────────── + "0x56a0": 225, // Arc A770 (desktop, 16 GB) + "0x56a1": 150, // Arc A750 (desktop, 8 GB) + "0x56a2": 75, // Arc A580 (desktop, 8 GB) + "0x56a5": 75, // Arc A380 (desktop, 6 GB — borderline, kept) + + // ── Alchemist (Xe-HPG) — A series consumer mobile ──────────────────────── + "0x5690": 150, // Arc A770M (120–150 W configurable, use max) + "0x5691": 120, // Arc A730M (80–120 W configurable, use max) + "0x5696": 80, // Arc A570M (50–80 W configurable, use max) + "0x5692": 80, // Arc A550M (60–80 W configurable, use max) + "0x5697": 50, // Arc A530M (35–50 W configurable, use max) + + // ── Alchemist (Xe-HPG) — A series Pro (workstation) ────────────────────── + "0x56b3": 130, // Arc Pro A60 (desktop, 12 GB) + "0x56b2": 75, // Arc Pro A60M (mobile, 8 GB) — device ID: 56B2 + "0x56b1": 50, // Arc Pro A40/A50 (desktop, 6 GB) — shared device ID + "0x56b0": 35, // Arc Pro A30M (mobile, 4 GB) +}; + +// Intel Arc PCI device ID → marketing name +const ARC_PCI_NAMES = { + // ── Battlemage (Xe2) — B series consumer ───────────────────────────────── + "0xe20b": "Intel Arc B580", + "0xe20a": "Intel Arc B770", + "0xe20c": "Intel Arc B570", + "0xe208": "Intel Arc B580M", + "0xe209": "Intel Arc B570M", + + // ── Battlemage (Xe2) — B series Pro (workstation) ───────────────────────── + "0xe211": "Intel Arc Pro B60", + "0xe212": "Intel Arc Pro B50", + + // ── Alchemist (Xe-HPG) — A series consumer desktop ─────────────────────── + "0x56a0": "Intel Arc A770", + "0x56a1": "Intel Arc A750", + "0x56a2": "Intel Arc A580", + "0x56a5": "Intel Arc A380", + + // ── Alchemist (Xe-HPG) — A series consumer mobile ──────────────────────── + "0x5690": "Intel Arc A770M", + "0x5691": "Intel Arc A730M", + "0x5696": "Intel Arc A570M", + "0x5692": "Intel Arc A550M", + "0x5697": "Intel Arc A530M", + + // ── Alchemist (Xe-HPG) — A series Pro (workstation) ────────────────────── + "0x56b3": "Intel Arc Pro A60", + "0x56b2": "Intel Arc Pro A60M", + "0x56b1": "Intel Arc Pro A40/A50", // A40 and A50 share the same device ID + "0x56b0": "Intel Arc Pro A30M", +}; + +// --------------------------------------------------------------------------- +// Helpers +// --------------------------------------------------------------------------- + +function resolveTGP(snap) { + // Table (Intel ARK specs) takes priority over backend reading + if (snap.device_name) { + const m = snap.device_name.match(/\[0x([0-9a-fA-F]+)\]/); + if (m) { + const key = "0x" + m[1].toLowerCase(); + if (ARC_PCI_TGP[key]) return ARC_PCI_TGP[key]; + } + } + if (snap.tgp_w > 0) return snap.tgp_w; + return 0; +} + +function resolveDeviceName(raw) { + if (!raw) return "Intel Arc GPU"; + const m = raw.match(/\[0x([0-9a-fA-F]+)\]/); + if (m) { + const key = "0x" + m[1].toLowerCase(); + if (ARC_PCI_NAMES[key]) return ARC_PCI_NAMES[key]; + } + return raw; +} + +function shortDeviceName(raw) { + const full = resolveDeviceName(raw); + const parts = full.trim().split(/\s+/); + return parts[parts.length - 1] || full; +} + +function getSetting(id, def) { + try { const v = app.extensionManager?.setting?.get(id); if (v !== undefined) return v; } catch (_) {} + try { return app.ui.settings.getSettingValue(id, def); } catch (_) {} + return def; +} + +function en() { + const manual = getSetting(S.lang, "system"); + if (manual === "en") return true; + if (manual === "zh") return false; + // "system" 或未知值 → 跟随 Comfy.Locale + const comfyLocale = getSetting("Comfy.Locale", ""); + if (comfyLocale) return !comfyLocale.toLowerCase().startsWith("zh"); + return true; // 最终 fallback:英文 +} + + +// 双语辅助:en() 为 true 时返回英文,否则返回中文 +function t(zh, en_str) { return en() ? en_str : zh; } + +function makeSliderType(min, max, step, liveUpdate = false) { + return (_name, setter, value) => { + const wrap = document.createElement("div"); + wrap.style.cssText = "display:flex;align-items:center;gap:8px;width:100%;"; + + const slider = document.createElement("input"); + slider.type = "range"; slider.min = min; slider.max = max; slider.step = step; + slider.value = value ?? min; + slider.style.cssText = "flex:1;cursor:pointer;"; + + const box = document.createElement("input"); + box.type = "number"; box.min = min; box.max = max; box.step = step; + box.value = value ?? min; + box.style.cssText = "width:62px;padding:2px 4px;background:transparent;" + + "border:1px solid #555;border-radius:3px;color:inherit;" + + "text-align:center;font-size:inherit;"; + + slider.addEventListener("input", () => { + const c = Math.max(min, Math.min(max, Number(slider.value))); + box.value = c; + }); + slider.addEventListener("mouseover", () => { + const c = Math.max(min, Math.min(max, Number(slider.value))); + box.value = c; + if (liveUpdate) setter(c); + }); + slider.addEventListener("change", () => { + const c = Math.max(min, Math.min(max, Number(slider.value))); + slider.value = c; box.value = c; setter(c); + }); + box.addEventListener("change", () => { + const c = Math.max(min, Math.min(max, Number(box.value))); + slider.value = c; box.value = c; setter(c); + }); + wrap.appendChild(slider); wrap.appendChild(box); + return wrap; + }; +} + +function makeLangSelectType() { + return (_name, setter, value) => { + const sel = document.createElement("select"); + sel.style.cssText = "background:#2a2a2a;border:1px solid #555;border-radius:4px;" + + "color:inherit;padding:3px 8px;font-size:inherit;cursor:pointer;"; + const opts = [ + { label: "系统", value: "system" }, + { label: "中文", value: "zh" }, + { label: "English", value: "en" }, + ]; + // 兼容旧版存的 boolean 值 + const normalized = value === true ? "en" : value === false ? "zh" : (value || "system"); + opts.forEach(o => { + const opt = document.createElement("option"); + opt.value = o.value; + opt.textContent = o.label; + if (normalized === o.value) opt.selected = true; + sel.appendChild(opt); + }); + sel.addEventListener("change", () => setter(sel.value)); + return sel; + }; +} + +// --------------------------------------------------------------------------- +// CSS +// --------------------------------------------------------------------------- + +function injectStyles() { + const style = document.createElement("style"); + style.textContent = ` + .xpu-monitor-bar { + font-family: 'Consolas', 'JetBrains Mono', 'Cascadia Code', 'PingFang SC', 'Microsoft YaHei', monospace; + font-size: var(--xpusys-fs, 18px); + font-variant-numeric: tabular-nums; + line-height: 1; + display: flex; + align-items: center; + gap: 0; + padding: 1px 0; + user-select: none; + white-space: nowrap; + } + .xpu-monitor-bar > *:first-child { margin-left: 0; } + + /* ── 固定宽度数字槽位 ── + 每个 min-width = 该字段最长可能输出的字符数,单位 ch(等宽字体下 1ch = 1字符宽) + n-pct : "100.0%" = 6ch + n-ghz : "5.2GHz" = 6ch + n-mhz : "2450MHz"= 7ch + n-gb : "11.9" = 4ch + n-temp : "99°C" = 4ch + n-w : "190W" = 4ch + n-ratio: "100%" = 4ch ── */ + .n-pct { display: inline-block; min-width: 6ch; text-align: right; } + .n-ghz { display: inline-block; min-width: 6ch; text-align: right; } + .n-mhz { display: inline-block; min-width: 7ch; text-align: right; } + .n-gb { display: inline-block; min-width: 4ch; text-align: right; } + .n-temp { display: inline-block; min-width: 4ch; text-align: right; } + .n-w { display: inline-block; min-width: 4ch; text-align: right; } + .n-ratio { display: inline-block; min-width: 4ch; text-align: right; } + + /* ── 显存预测胶囊 ── */ + .vram-predictor-section { + display: flex; + align-items: center; + justify-content: center; + font-family: 'Consolas', 'JetBrains Mono', 'Cascadia Code', 'PingFang SC', 'Microsoft YaHei', monospace; + font-size: var(--xpusys-fs, 18px); /* 直接读变量,与 bar 联动 */ + line-height: 1; /* 防止中文字体默认行高撑高胶囊 */ + font-variant-numeric: tabular-nums; + background: rgba(54, 207, 201, 0.10); + border: 1px solid rgba(54, 207, 201, 0.22); + border-radius: 8px; + padding: 10px 0.6em; + margin: 0 2px; + cursor: default; + position: relative; + transition: background 0.15s, box-shadow 0.15s; + box-shadow: + -2px -2px 5px rgba(255, 255, 255, 0.03), + 2px 2px 6px rgba(0, 0, 0, 0.5); + } + .vram-predictor-section:hover { + background: rgba(54, 207, 201, 0.18); + box-shadow: + -2px -2px 5px rgba(255, 255, 255, 0.05), + 2px 2px 6px rgba(0, 0, 0, 0.6); + } + /* 中文标签视觉微调:汉字渲染比等宽英文大,缩至 0.85em */ + .pred-zh { font-size: 0.85em; } + /* 让胶囊内所有 inline 元素(中文/数字/符号)flex 垂直居中,消除基线错位 */ + .vram-predictor-section .xpusys-value { + display: inline-flex; + align-items: center; + } + + /* ── 通用胶囊段落 ── */ + .cpu-section, .ram-section { + display: flex; + align-items: center; + justify-content: center; + background: rgba(255, 255, 255, 0.05); + border: 1px solid rgba(255, 255, 255, 0.08); + border-radius: 8px; + padding: 10px 0.6em; + margin: 0 1.5px; + cursor: default; + position: relative; + transition: background 0.15s, box-shadow 0.15s; + box-shadow: + -2px -2px 5px rgba(255, 255, 255, 0.03), + 2px 2px 6px rgba(0, 0, 0, 0.5); + } + /* cpu: "CPU"(3) + n-pct(6) + "@"(1) + n-ghz(6) = 16ch */ + .cpu-section { min-width: 16ch; } + /* ram: "RAM"(3) + n-pct(6) = 9ch */ + .ram-section { min-width: 9ch; } + .cpu-section:hover, .ram-section:hover { + background: rgba(255, 255, 255, 0.08); + box-shadow: + -2px -2px 5px rgba(255, 255, 255, 0.05), + 2px 2px 6px rgba(0, 0, 0, 0.6); + } + + /* ── GPU 综合体强化 ── */ + .gpu-composite-group { + display: flex; + align-items: center; + background: rgba(255, 255, 255, 0.05); + border: 1px solid rgba(255, 255, 255, 0.08); + border-radius: 8px; + padding: 0 4px; + margin: 0 1.5px; + gap: 0; + box-shadow: + -2px -2px 5px rgba(255, 255, 255, 0.03), + 2px 2px 6px rgba(0, 0, 0, 0.5); + } + + /* ── GPU 子项 ── */ + .gpu-engine, .gpu-vram, .gpu-rsv, .gpu-pwr { + display: flex; + align-items: center; + justify-content: center; + padding: 10px 0.6em; + border-right: 1px solid rgba(255, 255, 255, 0.10); + cursor: default; + position: relative; + transition: background 0.15s; + } + /* engine: "GPU"(3)+n-pct(6)+"@"(1)+n-mhz(7)+"|"(1)+n-temp(4) = 22ch */ + .gpu-engine { min-width: 22ch; } + /* vram: "VRAM"(4)+n-gb(4)+"/"(1)+n-gb(4)+" GB"(3) = 16ch */ + .gpu-vram { min-width: 16ch; } + /* rsv: "RSV"(3)+n-gb(4)+" GB"(3) = 10ch */ + .gpu-rsv { min-width: 10ch; } + /* pwr: "PWR"(3)+n-w(4)+n-ratio(4) = 11ch */ + .gpu-pwr { min-width: 11ch; } + .gpu-pwr { border-right: none; } + .gpu-engine:hover, .gpu-vram:hover, .gpu-rsv:hover, .gpu-pwr:hover { + background: rgba(255, 255, 255, 0.06); + border-radius: 3px; + } + + .xpusys-value { font-weight: 600; letter-spacing: 0.02em; } + .xpusys-ok { color: #52c41a; } + .xpusys-warn { color: #faad14; } + .xpusys-critical { color: #ff4d4f; } + .xpusys-vram-ok { color: #e8e8e8; } + .xpusys-vram-warn{ color: #ff7a00; } + .xpusys-vram-crit{ color: #ff4d4f; } + .xpusys-pwr-ok { color: #36cfc9; } + .xpusys-pwr-warn { color: #b37feb; } + .xpusys-pwr-crit { color: #ff4d4f; } + .xpusys-na { color: #555; } + + .xpusys-lock { + font-size: 10px; color: #666; cursor: pointer; + border: 1px solid #444; border-radius: 3px; + padding: 0 3px; line-height: 14px; margin-left: 4px; + transition: color 0.15s, border-color 0.15s; + } + .xpusys-lock:hover { color: #aaa; border-color: #aaa; } + + .xpusys-tooltip { + position: fixed; + background: rgba(18,18,24,0.97); + border: 1px solid rgba(255,255,255,0.15); + border-radius: 6px; + padding: 8px 12px; + font-size: 16px; + font-family: 'JetBrains Mono', monospace; + color: #ccc; + line-height: 1.7; + pointer-events: none; + z-index: 99999; + min-width: 220px; + box-shadow: 0 4px 16px rgba(0,0,0,0.6); + white-space: pre; + } + .xpusys-tooltip-title { + color: #fff; font-weight: 700; font-size: 17px; margin-bottom: 4px; + border-bottom: 1px solid rgba(255,255,255,0.1); padding-bottom: 3px; + } + .xpusys-tooltip-row { display: flex; justify-content: space-between; gap: 16px; } + .xpusys-tooltip-key { color: #888; } + .xpusys-tooltip-val { color: #e8e8e8; font-weight: 600; } + .xpusys-tooltip-note{ color: #666; font-size: 15px; margin-top: 4px; } + `; + document.head.appendChild(style); +} + +// --------------------------------------------------------------------------- +// Tooltip engine +// --------------------------------------------------------------------------- + +let _tipEl = null; +let _tipTarget = null; + +function createTooltip() { + _tipEl = document.createElement("div"); + _tipEl.className = "xpusys-tooltip"; + _tipEl.style.display = "none"; + document.body.appendChild(_tipEl); +} + +function showTooltip(el, html) { + _tipTarget = el; + _tipEl.innerHTML = html; + _tipEl.style.display = "block"; + positionTooltip(el); +} + +function positionTooltip(el) { + const r = el.getBoundingClientRect(); + const tw = _tipEl.offsetWidth, th = _tipEl.offsetHeight; + let x = r.left + r.width / 2 - tw / 2; + let y = r.bottom + 6; + x = Math.max(6, Math.min(x, window.innerWidth - tw - 6)); + if (y + th > window.innerHeight - 6) y = r.top - th - 6; + _tipEl.style.left = x + "px"; + _tipEl.style.top = y + "px"; +} + +function hideTooltip() { + if (_tipEl) _tipEl.style.display = "none"; + _tipTarget = null; +} + +function tipRow(key, val, color) { + const vc = color ? ` style="color:${color}"` : ""; + return `
` + + `${key}` + + `${val}` + + `
`; +} +function tipTitle(t) { return `
${t}
`; } +function tipNote(t) { return `
${t}
`; } + +// --------------------------------------------------------------------------- +// DOM — bar builder +// --------------------------------------------------------------------------- + +let _sec = {}; // { predictor, cpu, ram, engine, vram, rsv, pwr } each { el, valEl } + +// --------------------------------------------------------------------------- +// Predictor state (model-file scan results from /xpusys/model_sizes) +// --------------------------------------------------------------------------- +let _predModels = []; // [{ name: string, size: number }] +let _predTimer = null; // debounce handle + +function makeSection(cls, initText) { + const el = document.createElement("div"); + el.className = cls; + const valEl = document.createElement("span"); + valEl.className = "xpusys-value"; + valEl.textContent = initText; + el.appendChild(valEl); + return { el, valEl }; +} + +function buildBar() { + const bar = document.createElement("div"); + bar.id = "xpusys-bar"; + bar.className = "xpu-monitor-bar"; + + _sec.predictor = makeSection("vram-predictor-section", "PRED ---"); + bar.appendChild(_sec.predictor.el); + + _sec.cpu = makeSection("cpu-section", "CPU --.--%"); + bar.appendChild(_sec.cpu.el); + + _sec.ram = makeSection("ram-section", "RAM --.--%"); + bar.appendChild(_sec.ram.el); + + const gpuGroup = document.createElement("div"); + gpuGroup.className = "gpu-composite-group"; + + _sec.engine = makeSection("gpu-engine", "GPU ---"); + _sec.vram = makeSection("gpu-vram", "VRAM ---"); + _sec.rsv = makeSection("gpu-rsv", "RSV ---"); + _sec.pwr = makeSection("gpu-pwr", "PWR ---"); + + gpuGroup.appendChild(_sec.engine.el); + gpuGroup.appendChild(_sec.vram.el); + gpuGroup.appendChild(_sec.rsv.el); + gpuGroup.appendChild(_sec.pwr.el); + bar.appendChild(gpuGroup); + + for (const [key, sec] of Object.entries(_sec)) { + sec.el.addEventListener("mouseenter", () => { if (_snap) showTip(key, _snap); }); + sec.el.addEventListener("mousemove", () => { if (_tipTarget === sec.el && _snap) positionTooltip(sec.el); }); + sec.el.addEventListener("mouseleave", hideTooltip); + } + + return bar; +} + +function mountBar(bar) { + if (app.menu?.settingsGroup?.element) { + app.menu.settingsGroup.element.before(bar); + } else { + Object.assign(bar.style, { position: "fixed", top: "6px", right: "8px", zIndex: "9999" }); + document.body.appendChild(bar); + } +} + +// --------------------------------------------------------------------------- +// Render +// --------------------------------------------------------------------------- + +let _snap = null; + +function setVal(key, text, cls) { + const sec = _sec[key]; + if (!sec) return; + sec.valEl.textContent = text; + sec.valEl.className = "xpusys-value " + (cls || ""); +} + +function setHTML(key, html, cls) { + const sec = _sec[key]; + if (!sec) return; + sec.valEl.innerHTML = html; + sec.valEl.className = "xpusys-value " + (cls || ""); +} + +function renderSnap(snap) { + if (!snap) return; + _snap = snap; + renderCPU(snap); + renderRAM(snap); + renderEngine(snap); + renderVRAM(snap); + renderRSV(snap); + renderPWR(snap); + renderPredictor(); // re-render with latest vram_total_gb from snap + applyVisibility(); +} + +function renderCPU(snap) { + const pct = snap.cpu_pct ?? 0; + const freq = snap.cpu_freq_ghz ?? 0; + const cls = pct > 80 ? "xpusys-critical" : pct > 50 ? "xpusys-warn" : "xpusys-ok"; + let html = `CPU${pct.toFixed(1)}%`; + if (freq > 0) html += `@${freq.toFixed(2)}GHz`; + setHTML("cpu", html, cls); +} + +function renderRAM(snap) { + const pct = snap.ram_pct ?? 0; + const cls = pct > 90 ? "xpusys-critical" : pct > 70 ? "xpusys-warn" : "xpusys-ok"; + setHTML("ram", `RAM${pct.toFixed(1)}%`, cls); +} + +function renderEngine(snap) { + const load = snap.gpu_load_pct ?? 0; + const freq = snap.gpu_freq_mhz ?? 0; + const temp = snap.gpu_temp_c ?? -1; + const cls = load > 95 ? "xpusys-critical" : load > 80 ? "xpusys-warn" : "xpusys-ok"; + let html = `GPU${load.toFixed(1)}%`; + if (freq > 0) html += `@${Math.round(freq)}MHz`; + if (temp >= 0) html += `|${Math.round(temp)}°C`; + setHTML("engine", html, cls); +} + +function renderVRAM(snap) { + const used = snap.vram_driver_used_gb ?? 0; + const total = snap.vram_total_gb ?? 0; + const pct = total > 0 ? used / total : 0; + const cls = pct > 0.95 ? "xpusys-vram-crit" : pct > 0.85 ? "xpusys-vram-warn" : "xpusys-vram-ok"; + setHTML("vram", + `VRAM${used.toFixed(1)}/${total.toFixed(1)} GB`, + cls); +} + +function renderRSV(snap) { + const rsv = snap.vram_reserved_gb ?? 0; + setHTML("rsv", + `RSV${rsv.toFixed(1)} GB`, + rsv > 0.01 ? "xpusys-pwr-warn" : ""); +} + +function renderPWR(snap) { + const sec = _sec.pwr; + if (!sec) return; + + const existing = sec.el.querySelector(".xpusys-lock"); + if (existing) existing.remove(); + + if (!snap.power_available) { + setVal("pwr", "PWR N/A", "xpusys-na"); + const lock = document.createElement("span"); + lock.className = "xpusys-lock"; + lock.textContent = "🔒"; + lock.title = "点击了解详情"; + lock.addEventListener("click", e => { + e.stopPropagation(); + const dev = shortDeviceName(snap.device_name); + const msg = snap.is_admin + ? "未找到功率域 — 请检查驱动版本。" + : `${dev} 功率数据需要管理员权限。\n\n以管理员身份运行 ComfyUI 即可启用实时功率监控。`; + alert("⚡ XPUSYSMonitor — 功率说明\n\n" + msg); + }); + sec.el.appendChild(lock); + return; + } + + if (snap.power_w < 0) { setVal("pwr", "PWR N/A", "xpusys-na"); return; } + + const tgp = resolveTGP(snap); + let pCls, html; + if (tgp > 0) { + const ratio = snap.power_w / tgp; + pCls = ratio > 0.95 ? "xpusys-pwr-crit" : ratio > 0.80 ? "xpusys-pwr-warn" : "xpusys-pwr-ok"; + html = `PWR${snap.power_w.toFixed(0)}W${Math.round(ratio * 100)}%`; + } else { + pCls = snap.power_w > 170 ? "xpusys-pwr-crit" : snap.power_w > 120 ? "xpusys-pwr-warn" : "xpusys-pwr-ok"; + html = `PWR${snap.power_w.toFixed(0)}W`; + } + setHTML("pwr", html, pCls); +} + +function renderPredictor() { + if (!_sec.predictor) return; + const total = _predModels.reduce((s, m) => s + (m.size || 0), 0); + const peak = _predModels.length > 0 ? Math.max(..._predModels.map(m => m.size || 0)) : 0; + const isEn = en(); + const pred = calcPrediction(total, peak, _snap); + const { rate, color: c, label } = pred; + const vEff = pred.vEff.toFixed(1); + const risk = isEn ? label.en : label.zh; + + const html = isEn + ? `Model:${total.toFixed(2)}G/${vEff}G` + + ` | ${risk}` + + ` | Success Rate:${rate}%` + : `模型体量:${total.toFixed(2)}G/${vEff}G` + + ` | 状态:${risk}` + + ` | 预测工作流执行成功率:${rate}%`; + setHTML("predictor", html); +} + +// --------------------------------------------------------------------------- +// Predictor — 成功率计算(三级内存模型) +// --------------------------------------------------------------------------- +// 常量 +const PRED_ALPHA = 0.9; // 显存碎片化折扣 + +/** + * 双约束成功率预测(串行回收模型) + * + * 硬约束 P_peak:最大单模型能否装入显存(决定能否运行) + * 软约束 P_load:总模型量能否在显存+内存中循环(决定稳定性) + * P_success = P_peak × P_load + * + * @param {number} mTotal 所有模型总大小 (GB) + * @param {number} mPeak 最大单个模型大小 (GB) + * @param {object} snap 最新系统快照 + */ +function calcPrediction(mTotal, mPeak, snap) { + const vFree = snap?.vram_free_gb ?? 0; + const vAlloc = snap?.vram_allocated_gb ?? 0; + const vRsv = snap?.vram_reserved_gb ?? 0; + const rFree = snap?.ram_free_gb ?? 0; + const cUsed = snap?.commit_used_gb ?? 0; + const cLimit = snap?.commit_limit_gb ?? 0; + + // 可回收显存 = 空闲 + PyTorch 占用(工作流启动前会释放) + const vReclaim = vFree + vAlloc + vRsv; + const vEff = Math.max(0.1, vReclaim * PRED_ALPHA); + const cRam = rFree; // ram_free_gb 已是 OS 报告的真实空闲量,直接使用 + const sVirt = Math.max(0, cLimit - cUsed); // 与内存胶囊"虚拟内存"定义一致 + + // ── 平台系数:NVIDIA UVM 允许更大显存溢出 ───────────────────────────── + const PLATFORM_GAMMA = { + "intel": 1.0, // Intel Arc:硬约束严格 + "nvidia": 4.0, // NVIDIA:UVM 支持约 4x 溢出 + }; + const gpuVendor = snap?.gpu_vendor ?? "intel"; + const gamma = PLATFORM_GAMMA[gpuVendor] ?? 1.0; + + // ── 硬约束:峰值模型 vs 显存 ────────────────────────────────────────── + const dPeak = Math.max(0, mPeak - vEff); + // 平台差异化:NVIDIA 的 effective 显存按 gamma 倍计算 + const vEffPlatform = vEff * gamma; + const pPeak = dPeak === 0 ? 1 : Math.max(0.02, Math.exp(-3 * dPeak / vEffPlatform)); + + // ── 软约束:总量 vs 显存+内存(串行回收) ──────────────────────────── + const dLoad = Math.max(0, mTotal - vEff); + let pLoad; + if (dLoad === 0) { + pLoad = 1; + } else if (cRam > 0 && dLoad <= cRam) { + pLoad = 1 - 0.3 * Math.pow(dLoad / cRam, 0.6); + } else if (sVirt > 0 && dLoad <= cRam + sVirt) { + pLoad = 0.05 + 0.65 * Math.pow(1 - (dLoad - cRam) / sVirt, 2); + } else { + pLoad = Math.max(0, 0.05 - 0.1 * (dLoad - cRam - sVirt)); + } + + const rate = Math.max(0, Math.min(100, Math.round(pPeak * pLoad * 100))); + + let color, label; + if (rate >= 95) { + color = "#52c41a"; label = { zh: "轻松", en: "Smooth" }; + } else if (rate >= 80) { + color = "#afff00"; label = { zh: "安全", en: "Safe" }; + } else if (rate >= 40) { + color = "#faad14"; label = { zh: "预警", en: "Warning" }; + } else { + color = "#ff4d4f"; label = { zh: "危险", en: "Critical" }; + } + + return { rate, color, label, dPeak, dLoad, vEff, cRam, sVirt, pPeak, pLoad, gamma, gpuVendor }; +} + +// --------------------------------------------------------------------------- +// Visibility & font +// --------------------------------------------------------------------------- + +function applyVisibility() { + if (!_sec.cpu) return; + if (_sec.predictor) + _sec.predictor.el.style.display = getSetting(S.showPredictor, true) ? "" : "none"; + _sec.cpu.el.style.display = getSetting(S.showCPU, true) ? "" : "none"; + _sec.ram.el.style.display = getSetting(S.showRAM, true) ? "" : "none"; + _sec.engine.el.style.display = getSetting(S.showEngine, true) ? "" : "none"; + _sec.vram.el.style.display = getSetting(S.showVRAM, true) ? "" : "none"; + _sec.rsv.el.style.display = getSetting(S.showRSV, true) ? "" : "none"; + _sec.pwr.el.style.display = getSetting(S.showPower, true) ? "" : "none"; +} + +function applyFontSize(val) { + const px = (val != null && !isNaN(Number(val))) ? Number(val) : Number(getSetting(S.fontSize, 18)); + document.documentElement.style.setProperty("--xpusys-fs", px + "px"); +} + +// --------------------------------------------------------------------------- +// Tooltip content +// --------------------------------------------------------------------------- + +function showTip(key, snap) { + const el = _sec[key]?.el; + if (!el) return; + const builders = { predictor: buildPredictorTip, + cpu: buildCPUTip, ram: buildRAMTip, engine: buildEngineTip, + vram: buildVRAMTip, rsv: buildRSVTip, pwr: buildPWRTip }; + const html = builders[key]?.(snap, en()); + if (html) showTooltip(el, html); +} + +function buildCPUTip(snap, eng) { + const pct = snap.cpu_pct ?? 0; + const freq = snap.cpu_freq_ghz ?? 0; + const c = pct > 80 ? "#ff4d4f" : pct > 50 ? "#faad14" : "#52c41a"; + if (eng) { + return tipTitle("🖥️ CPU") + + tipRow("Utilisation", pct.toFixed(1) + " %", c) + + (snap.cpu_model ? tipRow("Model", snap.cpu_model) : "") + + (freq > 0 ? tipRow("Freq", freq.toFixed(2) + " GHz") : "") + + (snap.cpu_threads ? tipRow("Threads", String(snap.cpu_threads)) : "") + + tipNote("Source: psutil · cpu_percent / wmic CurrentClockSpeed\nRegistry: ProcessorNameString"); + } + return tipTitle("🖥️ 处理器") + + tipRow("占用率", pct.toFixed(1) + " %", c) + + (snap.cpu_model ? tipRow("型号", snap.cpu_model) : "") + + (freq > 0 ? tipRow("频率", freq.toFixed(2) + " GHz") : "") + + (snap.cpu_threads ? tipRow("线程数", String(snap.cpu_threads)) : "") + + tipNote("来源: psutil · cpu_percent / wmic CurrentClockSpeed\n注册表: ProcessorNameString"); +} + +function buildRAMTip(snap, eng) { + const pct = snap.ram_pct ?? 0; + const total = snap.ram_total_gb ?? 0; + const used = snap.ram_used_gb ?? 0; + const free = snap.ram_free_gb ?? 0; + const commitUsed = snap.commit_used_gb ?? 0; + const commitLimit = snap.commit_limit_gb ?? 0; + const c = pct > 90 ? "#ff4d4f" : pct > 70 ? "#faad14" : "#52c41a"; + const commitStr = commitLimit > 0 + ? `${commitUsed.toFixed(1)} / ${commitLimit.toFixed(1)} GB` + : `${commitUsed.toFixed(1)} GB`; + if (eng) { + return tipTitle("💾 System RAM") + + tipRow("Total", total.toFixed(1) + " GB") + + tipRow("Used", used.toFixed(1) + " GB", c) + + tipRow("Free", free.toFixed(1) + " GB", "#52c41a") + + (commitUsed > 0 ? tipRow("Commit", commitStr, "#b37feb") : "") + + tipNote("Source: psutil · virtual_memory\nCommit: GlobalMemoryStatusEx"); + } + return tipTitle("💾 系统内存") + + tipRow("总量", total.toFixed(1) + " GB") + + tipRow("已用", used.toFixed(1) + " GB", c) + + tipRow("空闲", free.toFixed(1) + " GB", "#52c41a") + + (commitUsed > 0 ? tipRow("虚拟内存", commitStr, "#b37feb") : "") + + tipNote("来源: psutil · virtual_memory\n虚拟内存: GlobalMemoryStatusEx"); +} + +function buildEngineTip(snap, eng) { + const load = snap.gpu_load_pct ?? 0; + const freq = snap.gpu_freq_mhz ?? 0; + const temp = snap.gpu_temp_c ?? -1; + const c = load > 95 ? "#ff4d4f" : load > 80 ? "#faad14" : "#52c41a"; + const tc = temp > 85 ? "#ff4d4f" : temp > 70 ? "#faad14" : "#36cfc9"; + if (eng) { + return tipTitle("📊 GPU Engine") + + tipRow("Load", load.toFixed(1) + " %", c) + + (freq > 0 ? tipRow("Clock", Math.round(freq) + " MHz") : "") + + (temp >= 0 ? tipRow("Temp", Math.round(temp) + " °C", tc) : "") + + tipNote("Source: Intel Level Zero · zesEngineGetActivity\n" + + " zesFrequencyGetState · zesTemperatureGetState"); + } + return tipTitle("📊 GPU 引擎") + + tipRow("负载", load.toFixed(1) + " %", c) + + (freq > 0 ? tipRow("频率", Math.round(freq) + " MHz") : "") + + (temp >= 0 ? tipRow("温度", Math.round(temp) + " °C", tc) : "") + + tipNote("来源: Intel Level Zero · zesEngineGetActivity\n" + + " zesFrequencyGetState · zesTemperatureGetState"); +} + +function buildVRAMTip(snap, eng) { + const total = snap.vram_total_gb ?? 0; + const used = snap.vram_driver_used_gb ?? 0; + const alloc = snap.vram_allocated_gb ?? 0; + const rsv = snap.vram_reserved_gb ?? 0; + const free = snap.vram_free_gb ?? 0; + // Breakdown: C = driver_used - pytorch_reserved, E = reserved - allocated + const sysEnv = Math.max(0, used - rsv); + const buf = Math.max(0, rsv - alloc); + const pct = total > 0 ? used / total : 0; + const c = pct > 0.95 ? "#ff4d4f" : pct > 0.85 ? "#ff7a00" : "#e8e8e8"; + // 统一用 GB(2位小数)显示,与 bar 上的 toFixed(1) 对齐,避免 MB÷1000 的心算误差 + const g2 = gb => gb.toFixed(2) + " GB"; + if (eng) { + return tipTitle("🧠 VRAM Breakdown") + + tipRow("Total", g2(total)) + + tipRow("Current Used", g2(used), c) + + tipRow(" System & Env (display & driver)", g2(sysEnv), "#888") + + tipRow(" Model & Compute (loaded model)", g2(alloc), "#36cfc9") + + tipRow(" Reserved Buffer (PyTorch pre-alloc)", g2(buf), "#b37feb") + + tipRow("Free", g2(free), "#52c41a") + + tipNote("Source: zesMemoryGetState / torch.xpu.memory_allocated / memory_reserved"); + } + return tipTitle("🧠 显存详情") + + tipRow("总量", g2(total)) + + tipRow("当前占用", g2(used), c) + + tipRow(" 系统与环境 (系统显示及驱动占用)", g2(sysEnv), "#888") + + tipRow(" 模型与计算 (当前加载模型与实时运算)", g2(alloc), "#36cfc9") + + tipRow(" 预留缓冲区 (PyTorch 预先霸占的待分配空间)", g2(buf), "#b37feb") + + tipRow("空闲", g2(free), "#52c41a") + + tipNote("来源: zesMemoryGetState / torch.xpu.memory_allocated / memory_reserved"); +} + +function buildRSVTip(snap, eng) { + const rsv = snap.vram_reserved_gb ?? 0; + const alloc = snap.vram_allocated_gb ?? 0; + const buf = Math.max(0, rsv - alloc); + if (eng) { + return tipTitle("💾 Reserved (PyTorch Cache)") + + tipRow("Cache Pool", (rsv * 1024).toFixed(0) + " MB", "#b37feb") + + tipRow(" In Use", (alloc * 1024).toFixed(0) + " MB", "#36cfc9") + + tipRow(" Free Buf", (buf * 1024).toFixed(0) + " MB", "#888") + + tipNote("Source: torch.xpu.memory_reserved()"); + } + return tipTitle("💾 PyTorch 缓存池") + + tipRow("缓存总量", (rsv * 1024).toFixed(0) + " MB", "#b37feb") + + tipRow(" 实际占用", (alloc * 1024).toFixed(0) + " MB", "#36cfc9") + + tipRow(" 空闲缓存", (buf * 1024).toFixed(0) + " MB", "#888") + + tipNote("来源: torch.xpu.memory_reserved()"); +} + +function buildPWRTip(snap, eng) { + if (!snap.power_available) { + const dev = shortDeviceName(snap.device_name); + if (eng) { + return tipTitle("⚡ Power — 🔒 Admin Only") + + `
${dev} power data requires admin privileges.
` + + `Run ComfyUI as Administrator to enable live power monitoring.
`; + } + return tipTitle("⚡ 功率 — 🔒 需要管理员") + + `
${dev} 功率数据需要管理员权限。
` + + `以管理员身份运行 ComfyUI 即可启用实时功率监控。
`; + } + const tgp = resolveTGP(snap); + const dev = shortDeviceName(snap.device_name); + const pct = tgp > 0 ? snap.power_w / tgp : 0; + const c = (tgp > 0 && pct > 0.95) ? "#ff4d4f" + : (tgp > 0 && pct > 0.80) ? "#b37feb" + : snap.power_w > 170 ? "#ff4d4f" + : snap.power_w > 120 ? "#b37feb" + : "#36cfc9"; + if (eng) { + let html = tipTitle(`⚡ ${dev} Power`) + + tipRow("Instant Power", snap.power_w.toFixed(1) + " W", c); + if (tgp > 0) html += tipRow("TGP Limit", tgp.toFixed(0) + " W", "#666") + + tipRow("Load Ratio", (pct * 100).toFixed(0) + " %", c); + return html + tipNote("Source: Intel Level Zero · zesPowerGetEnergyCounter\nAdmin · Dual-sample energy delta"); + } + let html = tipTitle(`⚡ ${dev} 实时功率`) + + tipRow("瞬时功率", snap.power_w.toFixed(1) + " W", c); + if (tgp > 0) html += tipRow("TGP 上限", tgp.toFixed(0) + " W", "#666") + + tipRow("负载比例", (pct * 100).toFixed(0) + " %", c); + return html + tipNote("来源: Intel Level Zero · zesPowerGetEnergyCounter\n需要管理员权限 · 双采样能量差值"); +} + +function buildPredictorTip(snap, eng) { + const total = _predModels.reduce((s, m) => s + (m.size || 0), 0); + const mPeak = _predModels.length > 0 ? Math.max(..._predModels.map(m => m.size || 0)) : 0; + const pred = calcPrediction(total, mPeak, snap); + const divider = `
`; + + // ── 标题 ── + let html = tipTitle(eng ? "🎯 Current Load List" : "🎯 当前加载清单"); + + // ── 成功率输入参数 ── + const vendorName = pred.gpuVendor === "nvidia" ? "NVIDIA UVM" : "Intel Arc"; + const overflowTol = pred.gamma.toFixed(1) + "x"; + if (eng) { + html += tipRow("Overflow Tolerance", overflowTol + " (" + vendorName + ")", "#36cfc9"); + html += tipRow("Eff. VRAM Cap", pred.vEff.toFixed(2) + " GB", "#36cfc9"); + html += tipRow("Peak Model", mPeak.toFixed(2) + " GB", pred.dPeak > 0 ? "#ff4d4f" : "#52c41a"); + html += tipRow(" VRAM Gap", pred.dPeak.toFixed(2) + " GB", pred.dPeak > 0 ? "#faad14" : "#555"); + html += tipRow(" P_peak", (pred.pPeak * 100).toFixed(0) + " %", pred.dPeak > 0 ? "#faad14" : "#52c41a"); + html += tipRow("Total Models", total.toFixed(2) + " GB", pred.color); + html += tipRow(" Load Gap", pred.dLoad.toFixed(2) + " GB", pred.dLoad > 0 ? "#faad14" : "#555"); + html += tipRow(" Avail. RAM", pred.cRam.toFixed(2) + " GB", "#b37feb"); + html += tipRow(" Avail. Commit", pred.sVirt.toFixed(2) + " GB", "#888"); + html += tipRow(" P_load", (pred.pLoad * 100).toFixed(0) + " %", pred.dLoad > 0 ? "#faad14" : "#52c41a"); + html += tipRow("Success Rate", pred.rate + " %", pred.color); + } else { + html += tipRow("溢出容忍", overflowTol + " (" + vendorName + ")", "#36cfc9"); + html += tipRow("显存上限", pred.vEff.toFixed(2) + " GB", "#36cfc9"); + html += tipRow("峰值模型", mPeak.toFixed(2) + " GB", pred.dPeak > 0 ? "#ff4d4f" : "#52c41a"); + html += tipRow(" 显存缺口", pred.dPeak.toFixed(2) + " GB", pred.dPeak > 0 ? "#faad14" : "#555"); + html += tipRow(" 显存压力", (pred.pPeak * 100).toFixed(0) + " %", pred.dPeak > 0 ? "#faad14" : "#52c41a"); + html += tipRow("模型总量", total.toFixed(2) + " GB", pred.color); + html += tipRow(" 负载缺口", pred.dLoad.toFixed(2) + " GB", pred.dLoad > 0 ? "#faad14" : "#555"); + html += tipRow(" 可用内存", pred.cRam.toFixed(2) + " GB", "#b37feb"); + html += tipRow(" 可用虚拟内存", pred.sVirt.toFixed(2) + " GB", "#888"); + html += tipRow(" 负载压力", (pred.pLoad * 100).toFixed(0) + " %", pred.dLoad > 0 ? "#faad14" : "#52c41a"); + html += tipRow("预测成功率", pred.rate + " %", pred.color); + } + + // ── 分隔线 + 模型列表 ── + html += divider; + const sorted = [..._predModels].sort((a, b) => b.size - a.size); + if (sorted.length === 0) { + html += `
${eng ? "No active models detected." : "无活跃模型"}
`; + } else { + for (const m of sorted) { + const short = m.name.split(/[\\/]/).pop(); + html += tipRow(short, m.size.toFixed(2) + " GB", "#52c41a"); + } + } + + html += tipNote(eng ? "Source: disk file size · /xpusys/model_sizes" + : "来源: 磁盘文件大小 · /xpusys/model_sizes"); + return html; +} + +// 给节点的模型 widget 挂 callback 钩子,切换模型时立即触发更新 +const MODEL_EXTS = [".safetensors", ".ckpt", ".pt", ".pth", ".bin", ".gguf", ".sft", ".pkl"]; + +function applyModelHook(node) { + let hasModel = false; + node.widgets?.forEach(w => { + if (w.type !== "combo" || w._xpusysPredHooked) return; + const wn = w.name?.toLowerCase() || ""; + const isModel = + ["model","ckpt","vae","lora","control","clip","unet"].some(k => wn.includes(k)) || + w.options?.values?.some(v => { + const s = String(v).toLowerCase(); + return MODEL_EXTS.some(ext => s.endsWith(ext)); + }); + if (!isModel) return; + hasModel = true; + const origCb = w.callback; + w.callback = function () { + const r = origCb ? origCb.apply(this, arguments) : undefined; + updatePredictor(); + return r; + }; + w._xpusysPredHooked = true; + }); + + // 只对含模型 widget 的节点挂 bypass / 删除钩子 + if (hasModel && !node._xpusysNodeHooked) { + // 监听节点被移除 + const origRemoved = node.onRemoved; + node.onRemoved = function () { + if (origRemoved) origRemoved.apply(this, arguments); + updatePredictor(); + }; + // 使用 Object.defineProperty 监听 mode 属性变化 + let _mode = node.mode; + Object.defineProperty(node, "mode", { + get: function() { return _mode; }, + set: function(v) { + if (_mode !== v) { + _mode = v; + updatePredictor(); + } + }, + configurable: true + }); + node._xpusysNodeHooked = true; + } +} + +// Debounced entry point — called by graph event hooks +function updatePredictor() { + if (_predTimer) clearTimeout(_predTimer); + _predTimer = setTimeout(_doPredictorFetch, 150); +} + +async function _doPredictorFetch() { + const nodes = app.graph?._nodes; + if (!nodes) return; + + // 后端支持的模型文件后缀 + const ALLOWED_EXTS = [".safetensors", ".gguf", ".ckpt", ".pt", ".pth", ".bin", ".onnx", ".pkl"]; + + // 按模型文件名去重的 Map + const uniqueModels = new Map(); + + nodes.forEach(node => { + if (node.mode !== 0) return; // skip bypassed / muted + const nodeType = node.type?.toLowerCase() || ""; + + // 遍历所有 widget,查找符合后缀的模型文件 + node.widgets?.forEach(w => { + if (typeof w.value !== "string") return; + const value = w.value.trim(); + if (!value) return; + + // 检查是否为已登记后缀的模型文件 + const ext = value.substring(value.lastIndexOf(".")).toLowerCase(); + if (!ALLOWED_EXTS.includes(ext)) return; + + // 按文件名去重,只保留第一次出现的记录 + if (!uniqueModels.has(value)) { + // value 可能包含子文件夹路径,如 "子文件夹/model.safetensors" + // 分离路径和文件名:path 用于查找,name 用于显示 + const lastSlash = value.lastIndexOf("/"); + const lastBackslash = value.lastIndexOf("\\"); + const sepIndex = Math.max(lastSlash, lastBackslash); + const modelPath = value; // 完整相对路径,用于后端查找 + const modelName = sepIndex >= 0 ? value.substring(sepIndex + 1) : value; // 纯文件名,用于显示 + + uniqueModels.set(value, { path: modelPath, name: modelName }); + } + }); + }); + + const activeModels = Array.from(uniqueModels.values()); + + try { + const r = await api.fetchApi("/xpusys/model_sizes", { + method: "POST", + headers: { "Content-Type": "application/json" }, + body: JSON.stringify({ models: activeModels }), + }); + if (r.ok) { + const data = await r.json(); + _predModels = data.models || []; + renderPredictor(); + } + } catch (_) {} +} + +// --------------------------------------------------------------------------- +// Data +// --------------------------------------------------------------------------- + +let _pollTimer = null; + +function onWsMessage(e) { + if (e?.detail?.type !== "xpusys_stats") return; + renderSnap(e.detail.data); +} + +async function pollOnce() { + try { + const r = await api.fetchApi("/xpusys/stats"); + if (r.ok) renderSnap(await r.json()); + } catch (_) {} +} + +function startPolling() { + if (_pollTimer) clearInterval(_pollTimer); + const ms = Math.max(200, getSetting(S.refreshMs, 1000)); + _pollTimer = setInterval(pollOnce, ms); + pollOnce(); +} + +// --------------------------------------------------------------------------- +// Extension +// --------------------------------------------------------------------------- + +app.registerExtension({ + name: `${NS}.Monitor`, + + async setup() { + injectStyles(); + createTooltip(); + + // ── 0 关于 ──────────────────────────────────────────────────────────── + app.ui.settings.addSetting({ + id: `${NS}.About`, + name: t("关于插件", "About"), + type: (_name, _setter, _value) => { + const wrap = document.createElement("div"); + wrap.style.cssText = + "line-height:1.7;color:#ccc;font-size:15px;padding:4px 0 2px;max-width:520px;"; + const zhText = + `本插件源于对 Intel Arc (XPU) 生态的支持。虽由「少数派」发起,但遵循底层标准,现已实现对 Intel (XPU) 与 NVIDIA (CUDA) 的完美兼容(AMD 支持已在计划中)。

` + + `核心亮点:` + + `独家支持模型运行显存预测,在生成前预判硬件压力;提供精准的跨平台硬件监测。填补工具空白,追求极致稳定。希望你喜欢。`; + const enText = + `Born from the Intel Arc (XPU) ecosystem. While built by the "minority," this plugin follows standard specs for seamless Intel (XPU) and NVIDIA (CUDA) support (AMD in progress).

` + + `Highlight: ` + + `Exclusive Model VRAM Prediction to anticipate hardware strain before generation. We aim to fill the gap in XPU monitoring with stable, cross-platform insights. Enjoy!`; + wrap.innerHTML = en() ? enText : zhText; + + // ── 版本号 + GitHub 按钮 ────────────────────────────────────────── + const bar = document.createElement("div"); + bar.style.cssText = + "display:flex;align-items:center;justify-content:flex-end;gap:8px;" + + "margin-top:16px;flex-wrap:wrap;border-top:1px solid #333;padding-top:10px;"; + + // 版本徽章 + const verBadge = document.createElement("span"); + verBadge.style.cssText = + "display:inline-flex;align-items:center;gap:0;border-radius:4px;overflow:hidden;" + + "font-size:12px;font-weight:600;line-height:1;"; + verBadge.innerHTML = + `${t("版本", "Version")}` + + `${VERSION}`; + + // GitHub 按钮 + const ghBtn = document.createElement("a"); + ghBtn.href = GITHUB; + ghBtn.target = "_blank"; + ghBtn.rel = "noopener noreferrer"; + ghBtn.style.cssText = + "display:inline-flex;align-items:center;gap:5px;padding:4px 10px;" + + "background:#24292e;color:#fff;border-radius:4px;font-size:12px;font-weight:600;" + + "text-decoration:none;line-height:1;transition:background .15s;"; + ghBtn.onmouseenter = () => { ghBtn.style.background = "#444d56"; }; + ghBtn.onmouseleave = () => { ghBtn.style.background = "#24292e"; }; + ghBtn.innerHTML = + `` + + `` + + `GitHub`; + + bar.appendChild(verBadge); + bar.appendChild(ghBtn); + wrap.appendChild(bar); + + return wrap; + }, + defaultValue: "", + category: [NS, t("\uE000关于", "\uE000About"), t("\uE000简介", "\uE000Introduction")], + }); + + // ── 1 通用设置 ──────────────────────────────────────────────────────── + app.ui.settings.addSetting({ + id: S.lang, name: t("界面语言", "Interface Language"), + tooltip: t("切换悬浮窗与状态栏的显示语言", "Switch display language for overlay and status bar"), + type: makeLangSelectType(), defaultValue: "system", + category: [NS, t("\uE001通用设置", "\uE001General"), t("\uE003语言", "\uE003Language")], + }); + + app.ui.settings.addSetting({ + id: S.fontSize, name: t("字体大小 (px)", "Font Size (px)"), + tooltip: t("状态栏字体大小,范围 12–22 px", "Status bar font size, range 12–22 px"), + type: makeSliderType(12, 22, 1, false), defaultValue: 16, + category: [NS, t("\uE001通用设置", "\uE001General"), t("\uE002字体大小", "\uE002Font Size")], + onChange: applyFontSize, + }); + app.ui.settings.addSetting({ + id: S.refreshMs, name: t("刷新间隔 (ms)", "Refresh Interval (ms)"), + tooltip: t("状态栏数据更新频率,范围 200–5000 ms", "Status bar update frequency, range 200–5000 ms"), + type: makeSliderType(200, 5000, 100), defaultValue: 1000, + category: [NS, t("\uE001通用设置", "\uE001General"), t("\uE001刷新间隔", "\uE001Refresh Interval")], + onChange: startPolling, + }); + + // ── 2 工作流预测 ────────────────────────────────────────────────────── + app.ui.settings.addSetting({ + id: S.showPredictor, name: t("显示显存预测(PRED)", "Show VRAM Predictor (PRED)"), + tooltip: t("在状态栏最左侧显示工作流模型显存预估值与成功率", "Show estimated VRAM usage and workflow success rate in the status bar"), + type: "boolean", defaultValue: true, + category: [NS, t("\uE002工作流预测", "\uE002Workflow Predictor"), t("\uE001显示显存预测", "\uE001Show VRAM Predictor")], + onChange: applyVisibility, + }); + + // ── 3 CPU监控 ───────────────────────────────────────────────────────── + app.ui.settings.addSetting({ + id: S.showCPU, name: t("显示 CPU 负载", "Show CPU Load"), + tooltip: t("在状态栏显示 CPU 占用率与频率", "Show CPU usage and frequency in the status bar"), + type: "boolean", defaultValue: false, + category: [NS, t("\uE003CPU监控", "\uE003CPU Monitor"), t("\uE001显示CPU负载", "\uE001Show CPU Load")], + onChange: applyVisibility, + }); + + // ── 4 内存监控 ──────────────────────────────────────────────────────── + app.ui.settings.addSetting({ + id: S.showRAM, name: t("显示内存占用", "Show RAM Usage"), + tooltip: t("在状态栏显示系统内存使用率", "Show system RAM usage in the status bar"), + type: "boolean", defaultValue: true, + category: [NS, t("\uE004内存监控", "\uE004RAM Monitor"), t("\uE001显示内存占用", "\uE001Show RAM Usage")], + onChange: applyVisibility, + }); + + // ── 5 显卡监控 ──────────────────────────────────────────────────────── + app.ui.settings.addSetting({ + id: S.showEngine, name: t("显示 GPU 引擎(负载 / 频率 / 温度)", "Show GPU Engine (Load / Freq / Temp)"), + tooltip: t("在状态栏显示 GPU 负载百分比、时钟频率和核心温度", "Show GPU load, clock frequency and core temperature in the status bar"), + type: "boolean", defaultValue: true, + category: [NS, t("\uE005显卡监控", "\uE005GPU Monitor"), t("\uE001GPU引擎", "\uE001GPU Engine")], + onChange: applyVisibility, + }); + app.ui.settings.addSetting({ + id: S.showVRAM, name: t("显示显存用量", "Show VRAM Usage"), + tooltip: t("在状态栏显示驱动层显存占用", "Show driver-level VRAM usage in the status bar"), + type: "boolean", defaultValue: true, + category: [NS, t("\uE005显卡监控", "\uE005GPU Monitor"), t("\uE002显存用量", "\uE002VRAM Usage")], + onChange: applyVisibility, + }); + app.ui.settings.addSetting({ + id: S.showRSV, name: t("显示 PyTorch 缓存池(RSV)", "Show PyTorch Cache Pool (RSV)"), + tooltip: t("在状态栏显示 torch.xpu.memory_reserved() 缓存大小", "Show torch.xpu.memory_reserved() cache size in the status bar"), + type: "boolean", defaultValue: false, + category: [NS, t("\uE005显卡监控", "\uE005GPU Monitor"), t("\uE003缓存池", "\uE003Cache Pool")], + onChange: applyVisibility, + }); + app.ui.settings.addSetting({ + id: S.showPower, name: t("显示功率(需要管理员权限)", "Show Power (Admin Required)"), + tooltip: t("在状态栏显示瞬时功耗与 TGP 负载比例", "Show instantaneous power consumption and TGP load ratio in the status bar"), + type: "boolean", defaultValue: true, + category: [NS, t("\uE005显卡监控", "\uE005GPU Monitor"), t("\uE004功率", "\uE004Power")], + onChange: applyVisibility, + }); + + const bar = buildBar(); + mountBar(bar); + applyVisibility(); + applyFontSize(); + setTimeout(applyFontSize, 0); + + api.addEventListener("message", onWsMessage); + startPolling(); + + // 对已存在节点补挂 widget 钩子,然后初始扫描 + app.graph._nodes?.forEach(n => applyModelHook(n)); + updatePredictor(); + }, + + // ── 显存预测 — 官方扩展钩子,安全无冲突 ────────────────────────────── + nodeCreated(node) { + setTimeout(() => { applyModelHook(node); updatePredictor(); }, 200); + }, + + loadedGraphNode(node) { + applyModelHook(node); + }, + + async afterConfigureGraph() { + // 对所有节点应用 hook,然后更新预测 + app.graph._nodes?.forEach(n => applyModelHook(n)); + updatePredictor(); + }, +});