Files

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