Runs are timed from ComfyUI's execution events and kept in runs.json (last 500), so ETAs survive restarts and a workflow's timings can be cleared. The Server tab shows the last 5 runs and a History window; the floating card is a bigger timer with a clear button, docked to the left of the Server card.
115 lines
5.6 KiB
JavaScript
115 lines
5.6 KiB
JavaScript
// Floating cards over the canvas: the server monitor and performance. Docking,
|
|
// dragging and the magnet come from FloatCard (float.js).
|
|
|
|
import { h } from "./ui.js";
|
|
import { FloatCard } from "./float.js";
|
|
import { timerView } from "./perf.js";
|
|
import { confirmModal } from "./files.js";
|
|
|
|
function mini(label, pct, text) {
|
|
const p = Math.max(0, Math.min(100, pct || 0));
|
|
const level = p > 90 ? "hot" : p > 70 ? "warm" : "";
|
|
return h("div", { class: "rs-mini", title: `${label}: ${text}` },
|
|
h("span", { class: "rs-mini-label" }, label),
|
|
h("span", { class: "rs-mini-track" }, h("span", { class: `rs-meter-fill ${level}`, style: `width:${p}%` })),
|
|
h("span", { class: "rs-mini-text" }, text));
|
|
}
|
|
|
|
/** Server monitor; admins also get ComfyUI's memory actions. */
|
|
export class ServerWidget extends FloatCard {
|
|
constructor(client, onChange, { inAppMode } = {}) {
|
|
super({ key: "rigshare.serverFloat", title: "Server", icon: "pi-server", inAppMode, onChange });
|
|
this.client = client;
|
|
client.on("stats", () => this.render());
|
|
client.on("self", () => this.render());
|
|
this.start();
|
|
}
|
|
|
|
async free(options, done) {
|
|
try {
|
|
await this.client.request("POST", "/api/free", options);
|
|
this.client.emit("toast", { severity: "success", summary: "RigShare", detail: done });
|
|
} catch (e) {
|
|
this.client.emit("toast", { severity: "error", summary: "RigShare", detail: e.message || String(e) });
|
|
}
|
|
}
|
|
|
|
render() {
|
|
if (!this.el) return;
|
|
const st = this.client.stats;
|
|
const admin = !!this.client.perms?.admin;
|
|
this.actions.style.display = admin ? "" : "none";
|
|
if (admin && !this.actions.childElementCount) {
|
|
this.actions.append(
|
|
h("button", {
|
|
class: "rs-btn rs-btn-sm", title: "Unload the models ComfyUI is holding in memory",
|
|
onclick: () => this.free({ unload_models: true }, "Models unloaded"),
|
|
}, "Unload models"),
|
|
h("button", {
|
|
class: "rs-btn rs-btn-sm", title: "Unload the models and clear the node cache (the next run starts from scratch)",
|
|
onclick: () => this.free({ unload_models: true, free_memory: true }, "Models and cache freed"),
|
|
}, "Free models & cache"));
|
|
}
|
|
if (!st) {
|
|
this.body.replaceChildren(h("div", { class: "rs-muted rs-small" }, "Waiting for data…"));
|
|
return;
|
|
}
|
|
const rows = [];
|
|
if (st.cpu != null) rows.push(mini("CPU", st.cpu, `${Math.round(st.cpu)}%`));
|
|
if (st.ram_total) rows.push(mini("RAM", (st.ram_used / st.ram_total) * 100, `${Math.round(st.ram_used)}G`));
|
|
for (const g of st.gpus || []) {
|
|
rows.push(mini(`GPU${g.index}`, g.util, `${g.util}%${g.temp != null ? ` ${g.temp}°` : ""}`));
|
|
rows.push(mini(`VRAM${g.index}`, (g.vram_used / g.vram_total) * 100, `${g.vram_used.toFixed(1)}G`));
|
|
}
|
|
const q = st.queue;
|
|
if (q) rows.push(h("div", { class: "rs-mini-queue" }, q.running || q.pending ? `${q.running} running · ${q.pending} queued` : "queue idle"));
|
|
this.body.replaceChildren(...rows);
|
|
}
|
|
}
|
|
|
|
/** Performance: only the timer (the history is in the Server tab). */
|
|
export class PerfWidget extends FloatCard {
|
|
constructor(client, onChange, { inAppMode, workflow } = {}) {
|
|
super({ key: "rigshare.perfFloat", title: "Performance", icon: "pi-stopwatch", inAppMode, onChange, className: "rs-float-perf" });
|
|
this.client = client;
|
|
this.workflow = workflow ?? (() => null); // name of the workflow on screen
|
|
this.receivedAt = Date.now();
|
|
client.on("stats", () => { this.receivedAt = Date.now(); this.render(); });
|
|
// The clock keeps ticking between server updates; the workflow on screen may change.
|
|
setInterval(() => { if (this.el) this.render(); }, 250);
|
|
this.start();
|
|
}
|
|
|
|
headButtons() {
|
|
return [h("button", {
|
|
class: "rs-float-close", title: "Clear the timings of the workflow on screen",
|
|
onclick: () => this.clear(),
|
|
}, h("i", { class: "pi pi-eraser" }))];
|
|
}
|
|
|
|
async clear() {
|
|
const wf = this.workflow();
|
|
if (!wf) return this.client.emit("toast", { severity: "info", summary: "RigShare", detail: "Open a workflow first." });
|
|
const runs = this.client.stats?.performance?.etas?.[wf]?.runs;
|
|
const ok = await confirmModal("Clear timings",
|
|
`Forget every recorded run of "${wf}"? Its ETA starts over${runs ? ` (it is based on ${runs} run${runs > 1 ? "s" : ""})` : ""}.`,
|
|
"Clear", true);
|
|
if (!ok) return;
|
|
try {
|
|
const res = await this.client.request("DELETE", `/rigshare/api/performance?workflow=${encodeURIComponent(wf)}`);
|
|
this.client.emit("toast", { severity: "success", summary: "RigShare", detail: `Cleared ${res.cleared} run${res.cleared === 1 ? "" : "s"} of "${wf}"` });
|
|
} catch (e) {
|
|
this.client.emit("toast", { severity: "error", summary: "RigShare", detail: e.message || String(e) });
|
|
}
|
|
}
|
|
|
|
render() {
|
|
if (!this.el) return;
|
|
const key = JSON.stringify([this.client.stats?.performance?.running, this.client.stats?.performance?.etas, this.workflow()]);
|
|
const running = this.client.stats?.performance?.running?.length;
|
|
if (!running && key === this.lastKey) return; // idle: nothing moves
|
|
this.lastKey = key;
|
|
this.body.replaceChildren(...timerView(this.client.stats?.performance, this.receivedAt, this.workflow()));
|
|
}
|
|
}
|