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