diff options
Diffstat (limited to 'src/zen/frontend/html/memory.js')
| -rw-r--r-- | src/zen/frontend/html/memory.js | 73 |
1 files changed, 57 insertions, 16 deletions
diff --git a/src/zen/frontend/html/memory.js b/src/zen/frontend/html/memory.js index 6b9760439..6e4d51061 100644 --- a/src/zen/frontend/html/memory.js +++ b/src/zen/frontend/html/memory.js @@ -1,17 +1,9 @@ // Copyright Epic Games, Inc. All Rights Reserved. -// Interactive memory analysis view: summary cards, memory timeline, leak/churn/hot callsite tables. +// Interactive memory analysis view: summary cards, memory timeline, and a +// tabbed callsite panel (Leaky / Churn / Hot) sharing one slot below the chart. import { getAllocSummary, getMemoryTimeline, getCallstackStats, getChurnStats, getCallstack, getAllocSizeHistogram } from "./api.js"; - -function escapeHtml(s) { - return String(s).replace(/[&<>"']/g, (c) => ({ - "&": "&", - "<": "<", - ">": ">", - '"': """, - "'": "'", - }[c])); -} +import { escapeHtml } from "./util.js"; function formatNum(n) { return Number(n || 0).toLocaleString(); @@ -187,10 +179,17 @@ export class MemoryView { churn: { sortKey: "churn_allocs", desc: true, groupMode: "none", filterText: "" }, hot: { sortKey: "total_allocs", desc: true, groupMode: "none", filterText: "" }, }; + this.activeTable = "leaks"; this.loadStateFromUrl(); this.buildLayout(); } + static TAB_DEFS = [ + { name: "leaks", label: "Leaky callsites" }, + { name: "churn", label: "Churn" }, + { name: "hot", label: "Hot callsites" }, + ]; + buildLayout() { this.container.innerHTML = `<div class="memory-view">` + @@ -222,6 +221,12 @@ export class MemoryView { `</div>` + `</div>` + `<div class="memory-grid">` + + `<div class="memory-tabbed">` + + `<div class="memory-tab-bar" role="tablist">` + + MemoryView.TAB_DEFS.map(({ name, label }) => + `<button type="button" class="memory-tab" role="tab" data-mem-tab="${name}" id="memory-tab-${name}" aria-controls="memory-tabpanel-${name}">${escapeHtml(label)}</button>` + ).join("") + + `</div>` + this.buildPanelMarkup("leaks", "Leaky callsites", "Top live allocation stacks", [ ["live_bytes", "Live bytes"], ["live_count", "Live allocs"], @@ -239,6 +244,7 @@ export class MemoryView { ["churn_allocs", "Churn allocs"], ["summary", "Summary"], ]) + + `</div>` + `<div class="memory-panel memory-callstack-panel">` + `<div class="memory-panel-header"><div class="memory-panel-title">Callstack details</div><div class="memory-panel-subtitle" id="memory-callstack-meta">Select a row to inspect its frames</div></div>` + `<div class="memory-callstack-body" id="memory-callstack-body"><div class="memory-empty">No callstack selected.</div></div>` + @@ -339,6 +345,17 @@ export class MemoryView { this.updateFilterButton(name); } + this.tabButtons = {}; + this.tabPanels = {}; + for (const { name } of MemoryView.TAB_DEFS) { + const button = this.container.querySelector(`[data-mem-tab="${name}"]`); + const panel = this.container.querySelector(`[data-mem-tabpanel="${name}"]`); + this.tabButtons[name] = button; + this.tabPanels[name] = panel; + button.addEventListener("click", () => this.setActiveTable(name)); + } + this.setActiveTable(this.activeTable, /*save=*/ false); + this.container.addEventListener("keydown", (e) => { if (e.key !== "/" || e.defaultPrevented) { return; @@ -352,20 +369,39 @@ export class MemoryView { if (activeView && activeView.hidden) { return; } - const firstFilter = this.panelRefs.leaks.filter; - if (firstFilter) { - firstFilter.focus(); - firstFilter.select(); + const activeFilter = this.panelRefs[this.activeTable]?.filter; + if (activeFilter) { + activeFilter.focus(); + activeFilter.select(); } }); this.container.tabIndex = -1; this.container.dataset.memoryView = "true"; } + setActiveTable(name, save = true) { + if (!this.tabButtons || !this.tabButtons[name]) { + return; + } + this.activeTable = name; + for (const { name: tabName } of MemoryView.TAB_DEFS) { + const isActive = tabName === name; + const button = this.tabButtons[tabName]; + const panel = this.tabPanels[tabName]; + button.classList.toggle("active", isActive); + button.setAttribute("aria-selected", isActive ? "true" : "false"); + button.tabIndex = isActive ? 0 : -1; + panel.hidden = !isActive; + } + if (save) { + this.saveStateToUrl(); + } + } + buildPanelMarkup(name, title, subtitle, sortOptions) { const sortHtml = sortOptions.map(([value, label]) => `<option value="${value}">${escapeHtml(label)}</option>`).join(""); return ` - <div class="memory-panel"> + <div class="memory-panel memory-tabpanel" data-mem-tabpanel="${name}" id="memory-tabpanel-${name}" role="tabpanel" aria-labelledby="memory-tab-${name}" hidden> <div class="memory-panel-header memory-panel-header-wrap"> <div> <div class="memory-panel-title">${escapeHtml(title)}</div> @@ -393,6 +429,10 @@ export class MemoryView { if (histogramMetric === "count" || histogramMetric === "bytes") { this.histogramMetric = histogramMetric; } + const activeTable = params.get("mem_table"); + if (activeTable && this.tableState[activeTable]) { + this.activeTable = activeTable; + } for (const [name, state] of Object.entries(this.tableState)) { const sortKey = params.get(`mem_${name}_sort`); const groupMode = params.get(`mem_${name}_group`); @@ -419,6 +459,7 @@ export class MemoryView { saveStateToUrl() { const url = new URL(window.location.href); url.searchParams.set("mem_hist_metric", this.histogramMetric); + url.searchParams.set("mem_table", this.activeTable); for (const [name, state] of Object.entries(this.tableState)) { url.searchParams.set(`mem_${name}_sort`, state.sortKey); url.searchParams.set(`mem_${name}_group`, state.groupMode); |