// 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 { Modal } from "../util/modal.js" import { Table, Toolbar } from "../util/widgets.js" //////////////////////////////////////////////////////////////////////////////// export class Page extends ZenPage { async main() { this.set_title("projects"); // Project Service Stats const stats_section = this._collapsible_section("Project Service Stats"); stats_section.tag().classify("dropall").text("raw yaml \u2192").on_click(() => { window.open("/stats/prj.yaml", "_blank"); }); this._stats_grid = stats_section.tag().classify("grid").classify("stats-tiles"); const stats = await new Fetcher().resource("stats", "prj").json(); if (stats) { this._render_stats(stats); } this.connect_stats_ws((all_stats) => { const stats = all_stats["prj"]; if (stats) { this._render_stats(stats); } }); // Projects list var section = this._collapsible_section("Projects"); section.tag().classify("dropall").text("drop-all").on_click(() => this.drop_all()); var columns = [ "name", "project dir", "engine dir", "oplogs", "actions", ]; this._project_table = section.add_widget(Table, columns, Table.Flag_FitLeft|Table.Flag_PackRight|Table.Flag_Sortable|Table.Flag_AlignNumeric); var projects = await new Fetcher().resource("/prj/list").json(); projects.sort((a, b) => (b.LastAccessTime || 0) - (a.LastAccessTime || 0)); for (const project of projects) { var row = this._project_table.add_row( "", "", "", "", ); var cell = row.get_cell(0); cell.tag().text(project.Id).on_click(() => this.view_project(project.Id)); if (project.ProjectRootDir) { row.get_cell(1).tag("a").text(project.ProjectRootDir) .attr("href", "vscode://" + project.ProjectRootDir.replace(/\\/g, "/")); } if (project.EngineRootDir) { row.get_cell(2).tag("a").text(project.EngineRootDir) .attr("href", "vscode://" + project.EngineRootDir.replace(/\\/g, "/")); } cell = row.get_cell(-1); const action_tb = new Toolbar(cell, true).left(); action_tb.add("view").on_click(() => this.view_project(project.Id)); action_tb.add("drop").on_click(() => this.drop_project(project.Id)); row.attr("zs_name", project.Id); // Fetch project details to get oplog count new Fetcher().resource("prj", project.Id).json().then((info) => { const oplogs = info["oplogs"] || []; row.get_cell(3).text(Friendly.sep(oplogs.length)).style("textAlign", "right"); // Right-align the corresponding header cell const header = this._project_table._element.firstElementChild; if (header && header.children[4]) { header.children[4].style.textAlign = "right"; } }).catch(() => {}); } // Project detail area (inside projects section so it collapses together) this._project_host = section; this._project_container = null; this._selected_project = null; // Restore project from URL if present const prj_param = this.get_param("project"); if (prj_param) { this.view_project(prj_param); } } _collapsible_section(name) { const section = this.add_section(name); const container = section._parent.inner(); const heading = container.firstElementChild; heading.style.cursor = "pointer"; heading.style.userSelect = "none"; const indicator = document.createElement("span"); indicator.textContent = " \u25BC"; indicator.style.fontSize = "0.7em"; heading.appendChild(indicator); let collapsed = false; heading.addEventListener("click", (e) => { if (e.target !== heading && e.target !== indicator) { return; } collapsed = !collapsed; indicator.textContent = collapsed ? " \u25B6" : " \u25BC"; let sibling = heading.nextElementSibling; while (sibling) { sibling.style.display = collapsed ? "none" : ""; sibling = sibling.nextElementSibling; } }); return section; } _clear_param(name) { this._params.delete(name); const url = new URL(window.location); url.searchParams.delete(name); history.replaceState(null, "", url); } _render_stats(stats) { const safe = (obj, path) => path.split(".").reduce((a, b) => a && a[b], obj); const grid = this._stats_grid; grid.inner().innerHTML = ""; // HTTP Requests tile { const req = safe(stats, "requests"); if (req) { const tile = grid.tag().classify("card").classify("stats-tile"); tile.tag().classify("card-title").text("HTTP Requests"); const columns = tile.tag().classify("tile-columns"); const left = columns.tag().classify("tile-metrics"); const reqData = req.requests || req; this._metric(left, Friendly.sep(safe(stats, "store.requestcount") || 0), "total requests", true); if (reqData.rate_mean > 0) { this._metric(left, Friendly.sep(reqData.rate_mean, 1) + "/s", "req/sec (mean)"); } if (reqData.rate_1 > 0) { this._metric(left, Friendly.sep(reqData.rate_1, 1) + "/s", "req/sec (1m)"); } const badRequests = safe(stats, "store.badrequestcount") || 0; this._metric(left, Friendly.sep(badRequests), "bad requests"); const right = columns.tag().classify("tile-metrics"); this._metric(right, Friendly.duration(reqData.t_avg || 0), "avg latency", true); if (reqData.t_p75) { this._metric(right, Friendly.duration(reqData.t_p75), "p75"); } if (reqData.t_p95) { this._metric(right, Friendly.duration(reqData.t_p95), "p95"); } if (reqData.t_p99) { this._metric(right, Friendly.duration(reqData.t_p99), "p99"); } } } // Store Operations tile { const store = safe(stats, "store"); if (store) { const tile = grid.tag().classify("card").classify("stats-tile"); tile.tag().classify("card-title").text("Store Operations"); const columns = tile.tag().classify("tile-columns"); const left = columns.tag().classify("tile-metrics"); const proj = store.project || {}; this._metric(left, Friendly.sep(proj.readcount || 0), "project reads", true); this._metric(left, Friendly.sep(proj.writecount || 0), "project writes"); this._metric(left, Friendly.sep(proj.deletecount || 0), "project deletes"); const right = columns.tag().classify("tile-metrics"); const oplog = store.oplog || {}; this._metric(right, Friendly.sep(oplog.readcount || 0), "oplog reads", true); this._metric(right, Friendly.sep(oplog.writecount || 0), "oplog writes"); this._metric(right, Friendly.sep(oplog.deletecount || 0), "oplog deletes"); } } // Op & Chunk tile { const store = safe(stats, "store"); if (store) { const tile = grid.tag().classify("card").classify("stats-tile"); tile.tag().classify("card-title").text("Ops & Chunks"); const columns = tile.tag().classify("tile-columns"); const left = columns.tag().classify("tile-metrics"); const op = store.op || {}; const opTotal = (op.hitcount || 0) + (op.misscount || 0); const opRatio = opTotal > 0 ? (((op.hitcount || 0) / opTotal) * 100).toFixed(1) + "%" : "-"; this._metric(left, opRatio, "op hit ratio", true); this._metric(left, Friendly.sep(op.hitcount || 0), "op hits"); this._metric(left, Friendly.sep(op.misscount || 0), "op misses"); this._metric(left, Friendly.sep(op.writecount || 0), "op writes"); const right = columns.tag().classify("tile-metrics"); const chunk = store.chunk || {}; const chunkTotal = (chunk.hitcount || 0) + (chunk.misscount || 0); const chunkRatio = chunkTotal > 0 ? (((chunk.hitcount || 0) / chunkTotal) * 100).toFixed(1) + "%" : "-"; this._metric(right, chunkRatio, "chunk hit ratio", true); this._metric(right, Friendly.sep(chunk.hitcount || 0), "chunk hits"); this._metric(right, Friendly.sep(chunk.misscount || 0), "chunk misses"); this._metric(right, Friendly.sep(chunk.writecount || 0), "chunk writes"); } } // Storage tile { const tile = grid.tag().classify("card").classify("stats-tile"); tile.tag().classify("card-title").text("Storage"); const columns = tile.tag().classify("tile-columns"); const left = columns.tag().classify("tile-metrics"); this._metric(left, safe(stats, "store.size.disk") != null ? Friendly.bytes(safe(stats, "store.size.disk")) : "-", "store disk", true); this._metric(left, safe(stats, "store.size.memory") != null ? Friendly.bytes(safe(stats, "store.size.memory")) : "-", "store memory"); const right = columns.tag().classify("tile-metrics"); this._metric(right, safe(stats, "cid.size.total") != null ? Friendly.bytes(safe(stats, "cid.size.total")) : "-", "cid total", true); this._metric(right, safe(stats, "cid.size.tiny") != null ? Friendly.bytes(safe(stats, "cid.size.tiny")) : "-", "cid tiny"); this._metric(right, safe(stats, "cid.size.small") != null ? Friendly.bytes(safe(stats, "cid.size.small")) : "-", "cid small"); this._metric(right, safe(stats, "cid.size.large") != null ? Friendly.bytes(safe(stats, "cid.size.large")) : "-", "cid large"); } } _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); } async view_project(project_id) { // Toggle off if already selected if (this._selected_project === project_id) { this._selected_project = null; this._clear_project_detail(); this._clear_param("project"); return; } this._selected_project = project_id; this._clear_project_detail(); this.set_param("project", project_id); const info = await new Fetcher().resource("prj", project_id).json(); if (this._selected_project !== project_id) { return; } const section = this._project_host.add_section(project_id); this._project_container = section; // Oplogs table const oplog_section = section.add_section("Oplogs"); const oplog_table = oplog_section.add_widget( Table, ["name", "marker", "size", "ops", "expired", "actions"], Table.Flag_FitLeft|Table.Flag_PackRight|Table.Flag_Sortable|Table.Flag_AlignNumeric ); let totalSize = 0, totalOps = 0; const total_row = oplog_table.add_row("TOTAL"); total_row.get_cell(0).style("fontWeight", "bold"); total_row.get_cell(2).style("textAlign", "right").style("fontWeight", "bold"); total_row.get_cell(3).style("textAlign", "right").style("fontWeight", "bold"); // Right-align header for numeric columns (size, ops) const header = oplog_table._element.firstElementChild; for (let i = 3; i < header.children.length - 1; i++) { header.children[i].style.textAlign = "right"; } for (const oplog of info["oplogs"] || []) { const name = oplog["id"]; const row = oplog_table.add_row(""); var cell = row.get_cell(0); cell.tag().text(name).link("", { "page": "oplog", "project": project_id, "oplog": name, }); cell = row.get_cell(-1); const action_tb = new Toolbar(cell, true).left(); action_tb.add("list").link("", { "page": "oplog", "project": project_id, "oplog": name }); action_tb.add("tree").link("", { "page": "tree", "project": project_id, "oplog": name }); action_tb.add("drop").on_click(() => this.drop_oplog(project_id, name)); new Fetcher().resource("prj", project_id, "oplog", name).json().then((data) => { row.get_cell(1).text(data["markerpath"]); row.get_cell(2).text(Friendly.bytes(data["totalsize"])).style("textAlign", "right"); row.get_cell(3).text(Friendly.sep(data["opcount"])).style("textAlign", "right"); row.get_cell(4).text(data["expired"]); totalSize += data["totalsize"] || 0; totalOps += data["opcount"] || 0; total_row.get_cell(2).text(Friendly.bytes(totalSize)).style("textAlign", "right").style("fontWeight", "bold"); total_row.get_cell(3).text(Friendly.sep(totalOps)).style("textAlign", "right").style("fontWeight", "bold"); }).catch(() => {}); } } _clear_project_detail() { if (this._project_container) { this._project_container._parent.inner().remove(); this._project_container = null; } } drop_oplog(project_id, oplog_id) { const drop = async () => { await new Fetcher().resource("prj", project_id, "oplog", oplog_id).delete(); // Refresh the project view this._selected_project = null; this._clear_project_detail(); this.view_project(project_id); }; new Modal() .title("Confirmation") .message(`Drop oplog '${oplog_id}'?`) .option("Yes", () => drop()) .option("No"); } drop_project(project_id) { const drop = async () => { await new Fetcher().resource("prj", project_id).delete(); this.reload(); }; new Modal() .title("Confirmation") .message(`Drop project '${project_id}'?`) .option("Yes", () => drop()) .option("No"); } async drop_all() { const drop = async () => { for (const row of this._project_table) { const project_id = row.attr("zs_name"); await new Fetcher().resource("prj", project_id).delete(); } this.reload(); }; new Modal() .title("Confirmation") .message("Drop every project?") .option("Yes", () => drop()) .option("No"); } }