aboutsummaryrefslogtreecommitdiff
path: root/src/zenserver/frontend/html/pages/storage.js
diff options
context:
space:
mode:
Diffstat (limited to 'src/zenserver/frontend/html/pages/storage.js')
-rw-r--r--src/zenserver/frontend/html/pages/storage.js580
1 files changed, 580 insertions, 0 deletions
diff --git a/src/zenserver/frontend/html/pages/storage.js b/src/zenserver/frontend/html/pages/storage.js
new file mode 100644
index 000000000..55f510ccd
--- /dev/null
+++ b/src/zenserver/frontend/html/pages/storage.js
@@ -0,0 +1,580 @@
+// 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"
+
+////////////////////////////////////////////////////////////////////////////////
+export class Page extends ZenPage
+{
+ generate_crumbs() {}
+
+ async main()
+ {
+ this.set_title("storage");
+
+ const storage = await new Fetcher().resource("/admin/storage").json();
+ const volumes = storage.volumes || [];
+
+ for (let i = 0; i < volumes.length; i++)
+ {
+ const vol = volumes[i];
+ const label = volumes.length === 1 ? "Volume" : `Volume ${i + 1}`;
+ const section = this.add_section(label);
+ const grid = section.tag().classify("grid").classify("info-tiles");
+
+ // Volume overview tile
+ {
+ const tile = grid.tag().classify("card").classify("info-tile");
+ tile.tag().classify("card-title").text("Disk");
+ const list = tile.tag().classify("info-props");
+
+ this._prop(list, "total", Friendly.bytes(vol.total));
+ this._prop(list, "used", Friendly.bytes(vol.used));
+ this._prop(list, "free", Friendly.bytes(vol.free));
+
+ if (vol.total > 0)
+ {
+ const pct = ((vol.used / vol.total) * 100).toFixed(1) + "%";
+ this._prop(list, "utilization", pct);
+ }
+ }
+
+ // Per-directory tiles
+ const dirs = vol.directories || [];
+ for (const dir of dirs)
+ {
+ const tile = grid.tag().classify("card").classify("info-tile");
+ tile.tag().classify("card-title").text(dir.name);
+ const list = tile.tag().classify("info-props");
+
+ this._prop(list, "size", Friendly.bytes(dir.bytes));
+ this._prop(list, "files", Friendly.sep(dir.files));
+ this._prop(list, "path", dir.path);
+ }
+ }
+
+ // Cache namespaces breakdown
+ try
+ {
+ const zcache = await new Fetcher().resource("/z$/").json();
+ const namespaces = zcache["Namespaces"] || [];
+
+ if (namespaces.length > 0)
+ {
+ const section = this.add_section("Cache Namespaces");
+ const grid = section.tag().classify("grid").classify("info-tiles");
+
+ await Promise.all(namespaces.map(async (ns) => {
+ try
+ {
+ const data = await new Fetcher().resource(`/z$/${ns}/`).json();
+ const tile = grid.tag().classify("card").classify("info-tile");
+ tile.tag().classify("card-title").text(ns);
+ const list = tile.tag().classify("info-props");
+
+ this._prop(list, "disk", Friendly.bytes(data["StorageSize"].DiskSize));
+ this._prop(list, "memory", Friendly.bytes(data["StorageSize"].MemorySize));
+ this._prop(list, "entries", Friendly.sep(data["EntryCount"]));
+ this._prop(list, "buckets", data["Buckets"].length);
+ }
+ catch (e) { /* skip failed namespace */ }
+ }));
+ }
+ }
+ catch (e) { /* cache service unavailable */ }
+
+ // GC status
+ await this._render_gc_status();
+
+ // GC activity log
+ await this._render_gc_log();
+ }
+
+ async _render_gc_status()
+ {
+ try
+ {
+ const gc = await new Fetcher().resource("/admin/gc").json();
+ if (!gc)
+ {
+ return;
+ }
+
+ const section = this.add_section("Garbage Collection");
+ const grid = section.tag().classify("grid").classify("info-tiles");
+
+ // Status tile
+ {
+ const tile = grid.tag().classify("card").classify("info-tile");
+ tile.tag().classify("card-title").text("Status");
+ const list = tile.tag().classify("info-props");
+
+ this._prop(list, "status", gc.Status || "unknown");
+ if (gc.AreDiskWritesBlocked !== undefined)
+ {
+ this._prop(list, "disk writes blocked", gc.AreDiskWritesBlocked ? "yes" : "no");
+ }
+ if (gc.Config)
+ {
+ this._prop(list, "gc enabled", gc.Config.Enabled ? "yes" : "no");
+ }
+ }
+
+ // Full GC tile
+ if (gc.FullGC)
+ {
+ const tile = grid.tag().classify("card").classify("info-tile");
+ tile.tag().classify("card-title").text("Full GC");
+ const list = tile.tag().classify("info-props");
+
+ if (gc.FullGC.TimeToNext)
+ {
+ this._prop(list, "next run", Friendly.timespan(gc.FullGC.TimeToNext));
+ }
+ if (gc.FullGC.LastTime)
+ {
+ this._prop(list, "last run", Friendly.datetime(gc.FullGC.LastTime));
+ }
+ if (gc.FullGC.Elapsed)
+ {
+ this._prop(list, "last duration", Friendly.timespan(gc.FullGC.Elapsed));
+ }
+ if (gc.FullGC.RemovedDisk)
+ {
+ this._prop(list, "last disk freed", gc.FullGC.RemovedDisk);
+ }
+ if (gc.FullGC.FreedMemory)
+ {
+ this._prop(list, "last memory freed", gc.FullGC.FreedMemory);
+ }
+ if (gc.Config && gc.Config.Interval)
+ {
+ this._prop(list, "interval", Friendly.timespan(gc.Config.Interval));
+ }
+ }
+
+ // Lightweight GC tile
+ if (gc.LightweightGC)
+ {
+ const tile = grid.tag().classify("card").classify("info-tile");
+ tile.tag().classify("card-title").text("Lightweight GC");
+ const list = tile.tag().classify("info-props");
+
+ if (gc.LightweightGC.TimeToNext)
+ {
+ this._prop(list, "next run", Friendly.timespan(gc.LightweightGC.TimeToNext));
+ }
+ if (gc.LightweightGC.LastTime)
+ {
+ this._prop(list, "last run", Friendly.datetime(gc.LightweightGC.LastTime));
+ }
+ if (gc.LightweightGC.Elapsed)
+ {
+ this._prop(list, "last duration", Friendly.timespan(gc.LightweightGC.Elapsed));
+ }
+ if (gc.LightweightGC.RemovedDisk)
+ {
+ this._prop(list, "last disk freed", gc.LightweightGC.RemovedDisk);
+ }
+ if (gc.LightweightGC.FreedMemory)
+ {
+ this._prop(list, "last memory freed", gc.LightweightGC.FreedMemory);
+ }
+ if (gc.Config && gc.Config.LightweightInterval)
+ {
+ this._prop(list, "interval", Friendly.timespan(gc.Config.LightweightInterval));
+ }
+ }
+
+ // Expiration config tile
+ if (gc.Config)
+ {
+ const cfg = gc.Config;
+ const has_expiration = cfg.MaxCacheDuration || cfg.MaxProjectStoreDuration || cfg.MaxBuildStoreDuration;
+ if (has_expiration)
+ {
+ const tile = grid.tag().classify("card").classify("info-tile");
+ tile.tag().classify("card-title").text("Expiration");
+ const list = tile.tag().classify("info-props");
+
+ if (cfg.MaxCacheDuration)
+ {
+ this._prop(list, "cache", Friendly.timespan(cfg.MaxCacheDuration));
+ }
+ if (cfg.MaxProjectStoreDuration)
+ {
+ this._prop(list, "project store", Friendly.timespan(cfg.MaxProjectStoreDuration));
+ }
+ if (cfg.MaxBuildStoreDuration)
+ {
+ this._prop(list, "build store", Friendly.timespan(cfg.MaxBuildStoreDuration));
+ }
+ }
+ }
+ }
+ catch (e) { /* gc endpoint unavailable */ }
+ }
+
+ async _render_gc_log()
+ {
+ try
+ {
+ const data = await new Fetcher().resource("/admin/gclog").json();
+ const entries = data.entries || [];
+ if (entries.length === 0)
+ {
+ return;
+ }
+
+ // Parse entries (oldest first in file), reverse to show newest first
+ const parsed = [];
+ for (const entry of entries)
+ {
+ const keys = Object.keys(entry);
+ if (keys.length === 0)
+ {
+ continue;
+ }
+ const id = keys[0];
+ const run = entry[id];
+ if (run)
+ {
+ parsed.push({ id, run });
+ }
+ }
+ parsed.reverse();
+
+ this._gc_entries = parsed;
+ this._gc_page = 0;
+ this._gc_page_size = 20;
+ this._gc_sort_col = -1;
+ this._gc_sort_asc = true;
+
+ const section = this.add_section("Garbage Collection History");
+ this._gc_table_host = section.tag();
+ this._gc_pager = section.tag().classify("sessions-pager");
+ this._gc_detail = section.tag().classify("card").style("marginTop", "16px");
+ this._gc_detail.inner().style.display = "none";
+ this._gc_selected_row = null;
+
+ this._gc_render_page();
+ }
+ catch (e) { /* gc log unavailable */ }
+ }
+
+ _gc_render_page()
+ {
+ const parsed = this._gc_entries;
+ const page = this._gc_page;
+ const pageSize = this._gc_page_size;
+ const totalPages = Math.ceil(parsed.length / pageSize);
+ const start = page * pageSize;
+ const end = Math.min(start + pageSize, parsed.length);
+
+ // Clear previous table and detail
+ this._gc_table_host.inner().innerHTML = "";
+ this._gc_detail.inner().style.display = "none";
+ this._gc_selected_row = null;
+
+ const columns = ["id", "started", "elapsed", "disk freed", "memory freed"];
+
+ // Dynamically import Table (cached by the module loader after first import)
+ import("../util/widgets.js").then(({ Table }) => {
+ const table = new Table(this._gc_table_host, columns, Table.Flag_FitLeft | Table.Flag_PackRight, -1);
+
+ for (let i = start; i < end; i++)
+ {
+ const { id, run } = parsed[i];
+ const result = run.Result || {};
+ const compact = result.Compact || {};
+
+ const diskFreed = compact.RemovedDiskBytes;
+ const memFreed = result.Referencer
+ ? (result.Referencer.RemoveExpiredData || {}).FreedMemoryBytes || 0
+ : 0;
+
+ const row = table.add_row(
+ id,
+ run.StartTime ? Friendly.datetime(run.StartTime) : "-",
+ result.Elapsed ? Friendly.timespan(result.Elapsed) : "-",
+ diskFreed !== undefined ? Friendly.bytes(diskFreed) : "-",
+ memFreed ? Friendly.bytes(memFreed) : "-",
+ );
+ row.get_cell(0).style("fontFamily", "'SF Mono', 'Cascadia Mono', Consolas, 'DejaVu Sans Mono', monospace");
+ row.get_cell(0).style("fontSize", "12px");
+ for (let c = 0; c < columns.length; c++)
+ {
+ row.get_cell(c).style("textAlign", "right");
+ }
+
+ const rowElem = row.inner();
+ rowElem.style.cursor = "pointer";
+ rowElem.addEventListener("click", () => this._show_gc_detail(row, id, run));
+ }
+
+ // Sortable column headers
+ const headerCells = table.inner().firstElementChild.children;
+ for (let c = 0; c < headerCells.length; c++)
+ {
+ headerCells[c].style.textAlign = "right";
+ headerCells[c].style.cursor = "pointer";
+ headerCells[c].style.userSelect = "none";
+ headerCells[c].addEventListener("click", () => this._gc_sort_by(c));
+ }
+ this._gc_update_sort_indicator(headerCells);
+
+ // Pager
+ this._gc_pager.inner().innerHTML = "";
+ if (totalPages > 1)
+ {
+ const prev = this._gc_pager.tag("button").text("\u2039 Prev");
+ prev.style("opacity", page === 0 ? "0.4" : "1");
+ if (page > 0)
+ {
+ prev.on_click(() => { this._gc_page--; this._gc_render_page(); });
+ }
+
+ this._gc_pager.tag().classify("sessions-pager-label")
+ .text(`${start + 1}\u2013${end} of ${parsed.length}`);
+
+ const next = this._gc_pager.tag("button").text("Next \u203A");
+ next.style("opacity", page >= totalPages - 1 ? "0.4" : "1");
+ if (page < totalPages - 1)
+ {
+ next.on_click(() => { this._gc_page++; this._gc_render_page(); });
+ }
+ }
+ });
+ }
+
+ _gc_sort_by(col)
+ {
+ if (this._gc_sort_col === col)
+ {
+ this._gc_sort_asc = !this._gc_sort_asc;
+ }
+ else
+ {
+ this._gc_sort_col = col;
+ this._gc_sort_asc = true;
+ }
+
+ const key_fns = [
+ (e) => e.id,
+ (e) => e.run.StartTime || "",
+ (e) => e.run.Result?.Elapsed || "",
+ (e) => e.run.Result?.Compact?.RemovedDiskBytes ?? 0,
+ (e) => {
+ const ref = e.run.Result?.Referencer;
+ return ref ? (ref.RemoveExpiredData?.FreedMemoryBytes || 0) : 0;
+ },
+ ];
+ const key_fn = key_fns[col];
+ const dir = this._gc_sort_asc ? 1 : -1;
+
+ this._gc_entries.sort((a, b) => {
+ const ak = key_fn(a);
+ const bk = key_fn(b);
+ if (typeof ak === "number" && typeof bk === "number")
+ {
+ return (ak - bk) * dir;
+ }
+ return String(ak).localeCompare(String(bk)) * dir;
+ });
+
+ this._gc_page = 0;
+ this._gc_render_page();
+ }
+
+ _gc_update_sort_indicator(headerCells)
+ {
+ for (let c = 0; c < headerCells.length; c++)
+ {
+ const text = headerCells[c].textContent.replace(/ [▲▼]$/, "");
+ headerCells[c].textContent = text;
+ if (c === this._gc_sort_col)
+ {
+ headerCells[c].textContent += this._gc_sort_asc ? " ▲" : " ▼";
+ }
+ }
+ }
+
+ _show_gc_detail(row, id, run)
+ {
+ // Toggle selection
+ if (this._gc_selected_row)
+ {
+ this._gc_selected_row.inner().classList.remove("sessions-selected");
+ }
+
+ if (this._gc_selected_row === row)
+ {
+ this._gc_selected_row = null;
+ this._gc_detail.inner().style.display = "none";
+ return;
+ }
+
+ this._gc_selected_row = row;
+ row.inner().classList.add("sessions-selected");
+ this._gc_detail.inner().style.display = "";
+ this._gc_detail.inner().innerHTML = "";
+
+ this._gc_detail.tag().classify("card-title").text(`GC Run: ${id}`);
+
+ const result = run.Result || {};
+ const settings = run.Settings || {};
+
+ // Timing section
+ {
+ const grid = this._gc_detail.tag().classify("grid").classify("info-tiles");
+
+ // Timing tile
+ {
+ const tile = grid.tag().classify("info-tile");
+ tile.tag().style("fontWeight", "600").style("fontSize", "12px").style("color", "var(--theme_g1)")
+ .style("textTransform", "uppercase").style("letterSpacing", "0.5px").style("marginBottom", "8px")
+ .text("Timing");
+ const list = tile.tag().classify("info-props");
+
+ this._prop(list, "started", run.StartTime ? Friendly.datetime(run.StartTime) : "-");
+ this._prop(list, "total elapsed", result.Elapsed ? Friendly.timespan(result.Elapsed) : "-");
+ this._prop(list, "write block", result.WriteBlock ? Friendly.timespan(result.WriteBlock) : "-");
+
+ const timings = [
+ ["remove expired data", result.RemoveExpiredData],
+ ["create reference checkers", result.CreateReferenceCheckers],
+ ["pre-cache state", result.PreCacheState],
+ ["lock state", result.LockState],
+ ["update locked state", result.UpdateLockedState],
+ ["create reference pruners", result.CreateReferencePruners],
+ ["remove unreferenced data", result.RemoveUnreferencedData],
+ ["compact stores", result.CompactStores],
+ ["validate", result.Validate],
+ ["create reference validators", result.CreateReferenceValidators],
+ ];
+ for (const [label, value] of timings)
+ {
+ if (value)
+ {
+ this._prop(list, label, Friendly.timespan(value));
+ }
+ }
+ }
+
+ // Settings tile
+ {
+ const tile = grid.tag().classify("info-tile");
+ tile.tag().style("fontWeight", "600").style("fontSize", "12px").style("color", "var(--theme_g1)")
+ .style("textTransform", "uppercase").style("letterSpacing", "0.5px").style("marginBottom", "8px")
+ .text("Settings");
+ const list = tile.tag().classify("info-props");
+
+ const fields = [
+ ["cache expire", settings.CacheExpireTime],
+ ["project store expire", settings.ProjectStoreExpireTime],
+ ["build store expire", settings.BuildStoreExpireTime],
+ ["collect small objects", settings.CollectSmallObjects],
+ ["delete mode", settings.IsDeleteMode],
+ ["verbose", settings.Verbose],
+ ["single thread", settings.SingleThread],
+ ["compact threshold", settings.CompactBlockUsageThresholdPercent !== undefined
+ ? settings.CompactBlockUsageThresholdPercent + "%" : undefined],
+ ["validation", settings.EnableValidation],
+ ];
+ for (const [label, value] of fields)
+ {
+ if (value !== undefined)
+ {
+ this._prop(list, label, String(value));
+ }
+ }
+ }
+ }
+
+ // Per-referencer breakdown
+ const referencers = result.Referencers || [];
+ if (referencers.length > 0)
+ {
+ import("../util/widgets.js").then(({ Table }) => {
+ const sub = this._gc_detail.tag().style("marginTop", "16px");
+ sub.tag().style("fontWeight", "600").style("fontSize", "12px").style("color", "var(--theme_g1)")
+ .style("textTransform", "uppercase").style("letterSpacing", "0.5px").style("marginBottom", "8px")
+ .text("Per-Referencer Stats");
+
+ const table = new Table(sub,
+ ["name", "checked", "deleted", "memory freed", "disk freed", "elapsed"],
+ Table.Flag_FitLeft | Table.Flag_PackRight | Table.Flag_AlignNumeric, -1);
+
+ for (const ref of referencers)
+ {
+ const expired = ref.RemoveExpired || {};
+ const compact = ref.Compact || {};
+ table.add_row(
+ ref.Name || "unknown",
+ expired.Checked !== undefined ? Friendly.sep(expired.Checked) : "-",
+ expired.Deleted !== undefined ? Friendly.sep(expired.Deleted) : "-",
+ expired.FreedMemoryBytes !== undefined ? Friendly.bytes(expired.FreedMemoryBytes) : "-",
+ compact.RemovedDiskBytes !== undefined ? Friendly.bytes(compact.RemovedDiskBytes) : "-",
+ ref.Elapsed ? Friendly.timespan(ref.Elapsed) : "-",
+ );
+ }
+ });
+ }
+
+ // Per-reference-store breakdown
+ const refStores = result.ReferenceStores || [];
+ if (refStores.length > 0)
+ {
+ import("../util/widgets.js").then(({ Table }) => {
+ const sub = this._gc_detail.tag().style("marginTop", "16px");
+ sub.tag().style("fontWeight", "600").style("fontSize", "12px").style("color", "var(--theme_g1)")
+ .style("textTransform", "uppercase").style("letterSpacing", "0.5px").style("marginBottom", "8px")
+ .text("Per-Reference Store Stats");
+
+ const table = new Table(sub,
+ ["name", "checked", "deleted", "disk freed", "elapsed"],
+ Table.Flag_FitLeft | Table.Flag_PackRight | Table.Flag_AlignNumeric, -1);
+
+ for (const store of refStores)
+ {
+ const unref = store.RemoveUnreferenced || {};
+ const compact = store.Compact || {};
+ table.add_row(
+ store.Name || "unknown",
+ unref.Checked !== undefined ? Friendly.sep(unref.Checked) : "-",
+ unref.Deleted !== undefined ? Friendly.sep(unref.Deleted) : "-",
+ compact.RemovedDiskBytes !== undefined ? Friendly.bytes(compact.RemovedDiskBytes) : "-",
+ store.Elapsed ? Friendly.timespan(store.Elapsed) : "-",
+ );
+ }
+ });
+ }
+
+ // Validation
+ const validator = result.ReferenceValidator || {};
+ if (validator.Checked !== undefined)
+ {
+ const sub = this._gc_detail.tag().style("marginTop", "16px");
+ sub.tag().style("fontWeight", "600").style("fontSize", "12px").style("color", "var(--theme_g1)")
+ .style("textTransform", "uppercase").style("letterSpacing", "0.5px").style("marginBottom", "8px")
+ .text("Validation");
+ const list = sub.tag().classify("info-props");
+
+ this._prop(list, "checked", Friendly.sep(validator.Checked));
+ if (validator.Missing !== undefined)
+ {
+ this._prop(list, "missing", Friendly.sep(validator.Missing));
+ }
+ }
+ }
+
+ _prop(parent, label, value)
+ {
+ const row = parent.tag().classify("info-prop");
+ row.tag().classify("info-prop-label").text(label);
+ row.tag().classify("info-prop-value").text(String(value));
+ }
+}