102 lines
5.6 KiB
JavaScript
102 lines
5.6 KiB
JavaScript
// Performance: the run in progress against its ETA, and the last finished runs,
|
|
// server-wide. The server sends stats.performance every couple of seconds:
|
|
// {running: [{workflow, name, elapsed_ms, eta_ms, runs}], recent: [...],
|
|
// etas: {workflow: {ms, runs}}, now}. Between updates the clock keeps ticking here.
|
|
|
|
import { h } from "./ui.js";
|
|
import { modal } from "./files.js";
|
|
|
|
const MARKS = { done: ["✓", "ok"], stopped: ["■", "bad"], error: ["✕", "bad"] };
|
|
|
|
export function duration(ms, decimals = 1) {
|
|
const total = Math.max(0, ms) / 1000;
|
|
const hrs = Math.floor(total / 3600);
|
|
const min = Math.floor((total % 3600) / 60);
|
|
const sec = total % 60;
|
|
const s = decimals > 0 ? sec.toFixed(decimals).padStart(3 + decimals, "0") : String(Math.floor(sec)).padStart(2, "0");
|
|
return hrs ? `${hrs}:${String(min).padStart(2, "0")}:${s}` : `${String(min).padStart(2, "0")}:${s}`;
|
|
}
|
|
|
|
function ago(ms) {
|
|
const s = Math.max(0, ms / 1000);
|
|
if (s < 60) return "just now";
|
|
if (s < 3600) return `${Math.floor(s / 60)} min ago`;
|
|
if (s < 86400) return `${Math.floor(s / 3600)} h ago`;
|
|
return `${Math.floor(s / 86400)} d ago`;
|
|
}
|
|
|
|
/** The run in progress: clock, ETA, progress bar. `since`: ms since the stats arrived. */
|
|
function runningBlock(run, since, { big = false, workflow = null } = {}) {
|
|
const elapsed = run.elapsed_ms + since;
|
|
const eta = run.eta_ms;
|
|
const over = eta && elapsed > eta;
|
|
const left = eta ? Math.round((eta - elapsed) / 1000) : null;
|
|
const mine = !!workflow && run.workflow === workflow; // the workflow on screen: yellow clock
|
|
return h("div", { class: `rs-perf-run ${big ? "big" : ""} ${mine ? "mine" : ""}` },
|
|
h("div", { class: "rs-perf-top" },
|
|
h("span", { class: "rs-perf-clock" }, duration(elapsed), eta ? h("span", { class: "rs-perf-eta" }, ` / ${duration(eta, 0)}`) : null),
|
|
h("span", { class: "rs-grow" }),
|
|
h("span", { class: `rs-small ${over ? "rs-perf-over" : "rs-muted"}` },
|
|
eta ? (left >= 0 ? `~${left} s left` : `${-left} s over`) : "no ETA yet")),
|
|
eta ? h("div", { class: `rs-perf-bar ${over ? "over" : ""}` }, h("div", { style: `width:${Math.min(100, (elapsed / eta) * 100)}%` })) : null,
|
|
h("div", { class: "rs-muted rs-small rs-ellipsis" },
|
|
run.workflow || "Workflow", run.name ? ` · ${run.name}` : "",
|
|
eta ? ` · ETA from ${run.runs} run${run.runs > 1 ? "s" : ""}` : ""));
|
|
}
|
|
|
|
function runRow(run, now, { date = false } = {}) {
|
|
const [mark, cls] = MARKS[run.result] || ["?", "bad"];
|
|
const when = date ? new Date(run.end).toLocaleString([], { dateStyle: "short", timeStyle: "short" }) : ago(now - run.end);
|
|
return h("div", { class: "rs-perf-row", title: [run.workflow, run.name, run.result].filter(Boolean).join(" · ") },
|
|
h("span", { class: `rs-perf-mark ${cls}` }, mark),
|
|
h("span", { class: "rs-perf-name rs-ellipsis" }, run.workflow || "Workflow",
|
|
run.name ? h("span", { class: "rs-muted" }, ` · ${run.name}`) : null),
|
|
h("b", {}, duration(run.ms)),
|
|
h("span", { class: "rs-muted rs-perf-ago" }, when));
|
|
}
|
|
|
|
/** Server tab: the run in progress and the last 5 runs. */
|
|
export function perfView(perf, receivedAt, workflow = null) {
|
|
if (!perf) return [h("p", { class: "rs-muted rs-small" }, "Waiting for the server…")];
|
|
const since = Date.now() - receivedAt;
|
|
const out = perf.running.length
|
|
? perf.running.map((r) => runningBlock(r, since, { workflow }))
|
|
: [h("div", { class: "rs-perf-idle rs-muted" }, "Idle: nothing running")];
|
|
if (perf.recent.length) {
|
|
out.push(h("div", { class: "rs-perf-head rs-muted rs-small" }, "Last runs"));
|
|
out.push(...perf.recent.map((r) => runRow(r, perf.now + since)));
|
|
}
|
|
return out;
|
|
}
|
|
|
|
/** Floating card: only the timer. Idle, it shows how long the workflow on screen usually takes. */
|
|
export function timerView(perf, receivedAt, workflow) {
|
|
if (!perf) return [h("p", { class: "rs-muted rs-small" }, "Waiting for the server…")];
|
|
const since = Date.now() - receivedAt;
|
|
if (perf.running.length) return perf.running.map((r) => runningBlock(r, since, { big: true, workflow }));
|
|
const eta = workflow ? perf.etas?.[workflow] : null;
|
|
return [h("div", { class: "rs-perf-run big idle" },
|
|
h("div", { class: "rs-perf-top" },
|
|
h("span", { class: "rs-perf-clock rs-muted" }, eta ? duration(eta.ms) : "--:--"),
|
|
h("span", { class: "rs-grow" }),
|
|
h("span", { class: "rs-small rs-muted" }, "Idle")),
|
|
h("div", { class: "rs-muted rs-small rs-ellipsis" },
|
|
workflow ? (eta ? `${workflow} usually takes this · ${eta.runs} run${eta.runs > 1 ? "s" : ""}` : `${workflow}: no timings yet`)
|
|
: "Open a workflow to see its usual time"))];
|
|
}
|
|
|
|
/** The full run history, in a window. */
|
|
export async function openHistory(client) {
|
|
const list = h("div", { class: "rs-perf rs-perf-history" }, h("p", { class: "rs-muted" }, "Loading…"));
|
|
modal("Run history", "pi-history", list, null);
|
|
try {
|
|
const data = await client.request("GET", "/rigshare/api/performance/history?limit=200");
|
|
list.replaceChildren(...(data.runs.length
|
|
? [h("div", { class: "rs-muted rs-small" }, `${data.total} run${data.total === 1 ? "" : "s"}${data.total > data.runs.length ? `, the last ${data.runs.length} shown` : ""}`),
|
|
...data.runs.map((r) => runRow(r, Date.now(), { date: true }))]
|
|
: [h("p", { class: "rs-muted" }, "No runs yet.")]));
|
|
} catch (e) {
|
|
list.replaceChildren(h("p", { class: "rs-muted" }, e.message || String(e)));
|
|
}
|
|
}
|