// Copyright Epic Games, Inc. All Rights Reserved. "use strict"; import { ZenPage } from "./page.js" import { Fetcher } from "../util/fetcher.js" import { Friendly } from "../util/friendly.js" import { Table } from "../util/widgets.js" const MAX_HISTORY_POINTS = 60; // Windows FILETIME: 100ns ticks since 1601-01-01 const FILETIME_EPOCH_OFFSET_MS = 11644473600000n; function filetimeToDate(ticks) { if (!ticks) return null; const ms = BigInt(ticks) / 10000n - FILETIME_EPOCH_OFFSET_MS; return new Date(Number(ms)); } function formatTime(date) { if (!date) return "-"; return date.toLocaleTimeString([], { hour: "2-digit", minute: "2-digit", second: "2-digit" }); } function truncateHash(hash) { if (!hash || hash.length <= 15) return hash; return hash.slice(0, 6) + "\u2026" + hash.slice(-6); } function formatDuration(startDate, endDate) { if (!startDate || !endDate) return "-"; const ms = endDate - startDate; if (ms < 0) return "-"; if (ms < 1000) return ms + " ms"; if (ms < 60000) return (ms / 1000).toFixed(2) + " s"; const m = Math.floor(ms / 60000); const s = ((ms % 60000) / 1000).toFixed(0).padStart(2, "0"); return `${m}m ${s}s`; } //////////////////////////////////////////////////////////////////////////////// export class Page extends ZenPage { async main() { this.set_title("compute"); this._history = { timestamps: [], pending: [], running: [], completed: [], cpu: [] }; this._selected_worker = null; this._chart_js = null; this._queue_chart = null; this._cpu_chart = null; this._ws_paused = false; try { this._ws_paused = localStorage.getItem("zen-ws-paused") === "true"; } catch (e) {} document.addEventListener("zen-ws-toggle", (e) => { this._ws_paused = e.detail.paused; }); // Action Queue section const queue_section = this._collapsible_section("Action Queue"); this._queue_grid = queue_section.tag().classify("grid").classify("stats-tiles"); this._chart_host = queue_section; // Performance Metrics section const perf_section = this._collapsible_section("Performance Metrics"); this._perf_host = perf_section; this._perf_grid = null; // Workers section const workers_section = this._collapsible_section("Workers"); this._workers_host = workers_section; this._workers_table = null; this._worker_detail_container = null; // Queues section const queues_section = this._collapsible_section("Queues"); this._queues_host = queues_section; this._queues_table = null; // Action History section const history_section = this._collapsible_section("Recent Actions"); this._history_host = history_section; this._history_table = null; // System Resources section const sys_section = this._collapsible_section("System Resources"); this._sys_grid = sys_section.tag().classify("grid").classify("stats-tiles"); // Load Chart.js dynamically this._load_chartjs(); // Initial fetch await this._fetch_all(); // Poll this._poll_timer = setInterval(() => { if (!this._ws_paused) { this._fetch_all(); } }, 2000); } async _load_chartjs() { if (window.Chart) { this._chart_js = window.Chart; this._init_charts(); return; } try { const script = document.createElement("script"); script.src = "https://cdn.jsdelivr.net/npm/chart.js@4.4.0/dist/chart.umd.min.js"; script.onload = () => { this._chart_js = window.Chart; this._init_charts(); }; document.head.appendChild(script); } catch (e) { /* Chart.js not available */ } } _init_charts() { if (!this._chart_js) { return; } // Queue history chart { const card = this._chart_host.tag().classify("card"); card.tag().classify("card-title").text("Action Queue History"); const container = card.tag(); container.style("position", "relative").style("height", "300px").style("marginTop", "20px"); const canvas = document.createElement("canvas"); container.inner().appendChild(canvas); this._queue_chart = new this._chart_js(canvas.getContext("2d"), { type: "line", data: { labels: [], datasets: [ { label: "Pending", data: [], borderColor: "#f0883e", backgroundColor: "rgba(240, 136, 62, 0.1)", tension: 0.4, fill: true }, { label: "Running", data: [], borderColor: "#58a6ff", backgroundColor: "rgba(88, 166, 255, 0.1)", tension: 0.4, fill: true }, { label: "Completed", data: [], borderColor: "#3fb950", backgroundColor: "rgba(63, 185, 80, 0.1)", tension: 0.4, fill: true }, ] }, options: { responsive: true, maintainAspectRatio: false, plugins: { legend: { display: true, labels: { color: "#8b949e" } } }, scales: { x: { display: false }, y: { beginAtZero: true, ticks: { color: "#8b949e" }, grid: { color: "#21262d" } } } } }); } // CPU sparkline (will be appended to CPU card later) this._cpu_canvas = document.createElement("canvas"); this._cpu_chart = new this._chart_js(this._cpu_canvas.getContext("2d"), { type: "line", data: { labels: [], datasets: [{ data: [], borderColor: "#58a6ff", backgroundColor: "rgba(88, 166, 255, 0.15)", borderWidth: 1.5, tension: 0.4, fill: true, pointRadius: 0 }] }, options: { responsive: true, maintainAspectRatio: false, animation: false, plugins: { legend: { display: false }, tooltip: { enabled: false } }, scales: { x: { display: false }, y: { display: false, min: 0, max: 100 } } } }); } async _fetch_all() { try { const [stats, sysinfo, workers_data, queues_data, history_data] = await Promise.all([ new Fetcher().resource("/stats/compute").json().catch(() => null), new Fetcher().resource("/compute/sysinfo").json().catch(() => null), new Fetcher().resource("/compute/workers").json().catch(() => null), new Fetcher().resource("/compute/queues").json().catch(() => null), new Fetcher().resource("/compute/jobs/history").param("limit", "50").json().catch(() => null), ]); if (stats) { this._render_queue_stats(stats); this._update_queue_chart(stats); this._render_perf(stats); } if (sysinfo) { this._render_sysinfo(sysinfo); } if (workers_data) { this._render_workers(workers_data); } if (queues_data) { this._render_queues(queues_data); } if (history_data) { this._render_action_history(history_data); } } catch (e) { /* service unavailable */ } } _render_queue_stats(data) { const grid = this._queue_grid; grid.inner().innerHTML = ""; const tiles = [ { title: "Pending Actions", value: data.actions_pending || 0, label: "waiting to be scheduled" }, { title: "Running Actions", value: data.actions_submitted || 0, label: "currently executing" }, { title: "Completed Actions", value: data.actions_complete || 0, label: "results available" }, ]; for (const t of tiles) { const tile = grid.tag().classify("card").classify("stats-tile"); tile.tag().classify("card-title").text(t.title); const body = tile.tag().classify("tile-metrics"); this._metric(body, Friendly.sep(t.value), t.label, true); } } _update_queue_chart(data) { const h = this._history; h.timestamps.push(new Date().toLocaleTimeString()); h.pending.push(data.actions_pending || 0); h.running.push(data.actions_submitted || 0); h.completed.push(data.actions_complete || 0); while (h.timestamps.length > MAX_HISTORY_POINTS) { h.timestamps.shift(); h.pending.shift(); h.running.shift(); h.completed.shift(); } if (this._queue_chart) { this._queue_chart.data.labels = h.timestamps; this._queue_chart.data.datasets[0].data = h.pending; this._queue_chart.data.datasets[1].data = h.running; this._queue_chart.data.datasets[2].data = h.completed; this._queue_chart.update("none"); } } _render_perf(data) { if (!this._perf_grid) { this._perf_grid = this._perf_host.tag().classify("grid").classify("stats-tiles"); } const grid = this._perf_grid; grid.inner().innerHTML = ""; const retired = data.actions_retired || {}; // Completion rate card { const tile = grid.tag().classify("card").classify("stats-tile"); tile.tag().classify("card-title").text("Completion Rate"); const body = tile.tag().classify("tile-columns"); const left = body.tag().classify("tile-metrics"); this._metric(left, this._fmt_rate(retired.rate_1), "1 min rate", true); this._metric(left, this._fmt_rate(retired.rate_5), "5 min rate"); this._metric(left, this._fmt_rate(retired.rate_15), "15 min rate"); const right = body.tag().classify("tile-metrics"); this._metric(right, Friendly.sep(retired.count || 0), "total retired", true); this._metric(right, this._fmt_rate(retired.rate_mean), "mean rate"); } } _fmt_rate(rate) { if (rate == null) return "-"; return rate.toFixed(2) + "/s"; } _render_workers(data) { const workerIds = data.workers || []; if (!this._workers_table) { this._workers_table = this._workers_host.add_widget( Table, ["name", "platform", "cores", "timeout", "functions", "worker ID"], Table.Flag_FitLeft|Table.Flag_PackRight|Table.Flag_Sortable|Table.Flag_AlignNumeric, -1 ); } if (workerIds.length === 0) { this._workers_table.clear(); return; } // Fetch each worker's descriptor Promise.all( workerIds.map(id => new Fetcher().resource("/compute/workers", id).json() .then(desc => ({ id, desc })) .catch(() => ({ id, desc: null })) ) ).then(results => { this._workers_table.clear(); for (const { id, desc } of results) { const name = desc ? (desc.name || "-") : "-"; const host = desc ? (desc.host || "-") : "-"; const cores = desc ? (desc.cores != null ? desc.cores : "-") : "-"; const timeout = desc ? (desc.timeout != null ? desc.timeout + "s" : "-") : "-"; const functions = desc ? (desc.functions ? desc.functions.length : 0) : "-"; const row = this._workers_table.add_row( "", host, String(cores), String(timeout), String(functions), id, ); // Worker ID column: monospace for hex readability row.get_cell(5).style("fontFamily", "'SF Mono', 'Cascadia Mono', Consolas, 'DejaVu Sans Mono', monospace"); // Make name clickable to expand detail const cell = row.get_cell(0); cell.tag().text(name).on_click(() => this._toggle_worker_detail(id, desc)); // Highlight selected if (id === this._selected_worker) { row.style("background", "var(--theme_p3)"); } } this._worker_descriptors = Object.fromEntries(results.map(r => [r.id, r.desc])); // Re-render detail if still selected if (this._selected_worker && this._worker_descriptors[this._selected_worker]) { this._show_worker_detail(this._selected_worker, this._worker_descriptors[this._selected_worker]); } else if (this._selected_worker) { this._selected_worker = null; this._clear_worker_detail(); } }); } _toggle_worker_detail(id, desc) { if (this._selected_worker === id) { this._selected_worker = null; this._clear_worker_detail(); return; } this._selected_worker = id; this._show_worker_detail(id, desc); } _clear_worker_detail() { if (this._worker_detail_container) { this._worker_detail_container._parent.inner().remove(); this._worker_detail_container = null; } } _show_worker_detail(id, desc) { this._clear_worker_detail(); if (!desc) { return; } const section = this._workers_host.add_section(desc.name || id); this._worker_detail_container = section; // Basic info table const info_table = section.add_widget( Table, ["property", "value"], Table.Flag_FitLeft|Table.Flag_PackRight ); const fields = [ ["Worker ID", id], ["Path", desc.path || "-"], ["Platform", desc.host || "-"], ["Build System", desc.buildsystem_version || "-"], ["Cores", desc.cores != null ? String(desc.cores) : "-"], ["Timeout", desc.timeout != null ? desc.timeout + "s" : "-"], ]; for (const [label, value] of fields) { info_table.add_row(label, value); } // Functions const functions = desc.functions || []; if (functions.length > 0) { const fn_section = section.add_section("Functions"); const fn_table = fn_section.add_widget( Table, ["name", "version"], Table.Flag_FitLeft|Table.Flag_PackRight ); for (const f of functions) { fn_table.add_row(f.name || "-", f.version || "-"); } } // Executables const executables = desc.executables || []; if (executables.length > 0) { const exec_section = section.add_section("Executables"); const exec_table = exec_section.add_widget( Table, ["path", "hash", "size"], Table.Flag_FitLeft|Table.Flag_PackRight|Table.Flag_AlignNumeric ); let totalSize = 0; for (const e of executables) { exec_table.add_row(e.name || "-", e.hash || "-", e.size != null ? Friendly.bytes(e.size) : "-"); totalSize += e.size || 0; } const total_row = exec_table.add_row("TOTAL", "", Friendly.bytes(totalSize)); total_row.get_cell(0).style("fontWeight", "bold"); total_row.get_cell(2).style("fontWeight", "bold"); } // Files const files = desc.files || []; if (files.length > 0) { const files_section = section.add_section("Files"); const files_table = files_section.add_widget( Table, ["name", "hash"], Table.Flag_FitLeft|Table.Flag_PackRight ); for (const f of files) { files_table.add_row(typeof f === "string" ? f : (f.name || "-"), typeof f === "string" ? "" : (f.hash || "")); } } // Directories const dirs = desc.dirs || []; if (dirs.length > 0) { const dirs_section = section.add_section("Directories"); for (const d of dirs) { dirs_section.tag().classify("detail-tag").text(d); } } // Environment const env = desc.environment || []; if (env.length > 0) { const env_section = section.add_section("Environment"); for (const e of env) { env_section.tag().classify("detail-tag").text(e); } } } _render_queues(data) { const queues = data.queues || []; if (this._queues_table) { this._queues_table.clear(); } else { this._queues_table = this._queues_host.add_widget( Table, ["ID", "status", "active", "completed", "failed", "abandoned", "cancelled", "token"], Table.Flag_FitLeft|Table.Flag_PackRight|Table.Flag_Sortable|Table.Flag_AlignNumeric, -1 ); } for (const q of queues) { const id = q.queue_id != null ? String(q.queue_id) : "-"; const status = q.state === "cancelled" ? "cancelled" : q.state === "draining" ? "draining" : q.is_complete ? "complete" : "active"; this._queues_table.add_row( id, status, String(q.active_count ?? 0), String(q.completed_count ?? 0), String(q.failed_count ?? 0), String(q.abandoned_count ?? 0), String(q.cancelled_count ?? 0), q.queue_token || "-", ); } } _render_action_history(data) { const entries = data.history || []; if (this._history_table) { this._history_table.clear(); } else { this._history_table = this._history_host.add_widget( Table, ["LSN", "queue", "status", "function", "started", "finished", "duration", "worker ID", "action ID"], Table.Flag_FitLeft|Table.Flag_PackRight|Table.Flag_Sortable|Table.Flag_AlignNumeric, -1 ); // Right-align hash column headers to match data cells const hdr = this._history_table.inner().firstElementChild; hdr.children[7].style.textAlign = "right"; hdr.children[8].style.textAlign = "right"; } // Entries arrive oldest-first; reverse to show newest at top for (const entry of [...entries].reverse()) { const lsn = entry.lsn != null ? String(entry.lsn) : "-"; const queueId = entry.queueId ? String(entry.queueId) : "-"; const status = entry.succeeded == null ? "unknown" : entry.succeeded ? "ok" : "failed"; const desc = entry.actionDescriptor || {}; const fn = desc.Function || "-"; const startDate = filetimeToDate(entry.time_Running); const endDate = filetimeToDate(entry.time_Completed ?? entry.time_Failed); const workerId = entry.workerId || "-"; const actionId = entry.actionId || "-"; const row = this._history_table.add_row( lsn, queueId, status, fn, formatTime(startDate), formatTime(endDate), formatDuration(startDate, endDate), truncateHash(workerId), truncateHash(actionId), ); // Hash columns: force right-align (AlignNumeric misses hex strings starting with a-f), // use monospace for readability, and show full value on hover const mono = "'SF Mono', 'Cascadia Mono', Consolas, 'DejaVu Sans Mono', monospace"; row.get_cell(7).style("textAlign", "right").style("fontFamily", mono).attr("title", workerId); row.get_cell(8).style("textAlign", "right").style("fontFamily", mono).attr("title", actionId); } } _render_sysinfo(data) { const grid = this._sys_grid; grid.inner().innerHTML = ""; // CPU card { const cpuUsage = data.cpu_usage || 0; const tile = grid.tag().classify("card").classify("stats-tile"); tile.tag().classify("card-title").text("CPU Usage"); const body = tile.tag().classify("tile-metrics"); this._metric(body, cpuUsage.toFixed(1) + "%", "percent", true); // Progress bar const bar = body.tag().classify("progress-bar"); bar.tag().classify("progress-fill").style("width", cpuUsage + "%"); // CPU sparkline this._history.cpu.push(cpuUsage); while (this._history.cpu.length > MAX_HISTORY_POINTS) this._history.cpu.shift(); if (this._cpu_chart) { const sparkContainer = body.tag(); sparkContainer.style("position", "relative").style("height", "60px").style("marginTop", "12px"); sparkContainer.inner().appendChild(this._cpu_canvas); this._cpu_chart.data.labels = this._history.cpu.map(() => ""); this._cpu_chart.data.datasets[0].data = this._history.cpu; this._cpu_chart.update("none"); } // CPU details this._stat_row(body, "Packages", data.cpu_count != null ? String(data.cpu_count) : "-"); this._stat_row(body, "Physical Cores", data.core_count != null ? String(data.core_count) : "-"); this._stat_row(body, "Logical Processors", data.lp_count != null ? String(data.lp_count) : "-"); } // Memory card { const memUsed = data.memory_used || 0; const memTotal = data.memory_total || 1; const memPercent = (memUsed / memTotal) * 100; const tile = grid.tag().classify("card").classify("stats-tile"); tile.tag().classify("card-title").text("Memory"); const body = tile.tag().classify("tile-metrics"); this._stat_row(body, "Used", Friendly.bytes(memUsed)); this._stat_row(body, "Total", Friendly.bytes(memTotal)); const bar = body.tag().classify("progress-bar"); bar.tag().classify("progress-fill").style("width", memPercent + "%"); } // Disk card { const diskUsed = data.disk_used || 0; const diskTotal = data.disk_total || 1; const diskPercent = (diskUsed / diskTotal) * 100; const tile = grid.tag().classify("card").classify("stats-tile"); tile.tag().classify("card-title").text("Disk"); const body = tile.tag().classify("tile-metrics"); this._stat_row(body, "Used", Friendly.bytes(diskUsed)); this._stat_row(body, "Total", Friendly.bytes(diskTotal)); const bar = body.tag().classify("progress-bar"); bar.tag().classify("progress-fill").style("width", diskPercent + "%"); } } _stat_row(parent, label, value) { const row = parent.tag().classify("stats-row"); row.tag().classify("stats-label").text(label); row.tag().classify("stats-value").text(value); } _metric(parent, value, label, hero = false) { const m = parent.tag().classify("tile-metric"); if (hero) { m.classify("tile-metric-hero"); } m.tag().classify("metric-value").text(value); m.tag().classify("metric-label").text(label); } }