Files
Edouard Durand 69ad03e237 Performance: own run log, History window, timer-only floating card with clear
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.
2026-09-27 05:15:18 +00:00

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()));
}
}