aboutsummaryrefslogtreecommitdiff
path: root/src/zen/frontend/html/memory.js
diff options
context:
space:
mode:
Diffstat (limited to 'src/zen/frontend/html/memory.js')
-rw-r--r--src/zen/frontend/html/memory.js73
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) => ({
- "&": "&amp;",
- "<": "&lt;",
- ">": "&gt;",
- '"': "&quot;",
- "'": "&#39;",
- }[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);