(function () {
"use strict";
const P = JSON.parse(document.getElementById("cg-payload").textContent);
const V = P.views || {};
const SUM = P.summary || {};
const VIEW_META = [
{ id: "evidence", label: "Overview", icon: "M3 3h7v7H3z M14 3h7v7h-7z M3 14h7v7H3z M14 14h7v7h-7z", graph: false },
{ id: "trace", label: "Execution graph", icon: "M3 4h6v5H3z M15 3h6v5h-6z M15 16h6v5h-6z M9 6h6 M12 6v12h3", graph: true, layout: "layered", direction: "LR" },
{ id: "span_costs", label: "Timings", icon: "M4 5h16 M4 12h11 M4 19h6", graph: false },
{ id: "measurements", label: "Measurements", icon: "M3 12h4l3-8 4 16 3-8h4", graph: false },
{ id: "memory", label: "Memory", icon: "M3 20h18 M4 16h4v-6h5v-5h4v9h4", graph: false },
{ id: "gpu", label: "GPU", icon: "M6 6h12v12H6z M10 10h4v4h-4z M9 2v4 M15 2v4 M9 18v4 M15 18v4 M2 9h4 M2 15h4 M18 9h4 M18 15h4", graph: false },
];
let currentView = P.default_view || "evidence";
let heatMode = "time";
let selectedId = null;
let hoveredId = null;
let graphFocusId = null;
const renderedViews = new Set();
let graphState = null;
let graphView = { x: 0, y: 0, k: 1 };
const spanOpen = new Set();
const tableStates = new Map();
let hierarchyVisible = !matchMedia("(max-width: 900px)").matches;
let inspectorReturnFocus = null;
let inspectorOpen = false;
let treeInitialized = false;
const root = document.documentElement;
const pref = matchMedia("(prefers-color-scheme:dark)").matches ? "dark" : "light";
let savedTheme = null;
try { savedTheme = localStorage.getItem("cg-theme"); } catch (_) { /* Local files may deny storage. */ }
root.setAttribute("data-theme", ["light", "dark"].includes(savedTheme) ? savedTheme : pref);
function esc(s) {
return String(s).replace(/[&<>"']/g, (ch) =>
({ "&": "&", "<": "<", ">": ">", '"': """, "'": "'" }[ch])
);
}
function idStr(v) { return v == null ? "" : String(v); }
function fmtMs(ms) {
if (ms == null || !Number.isFinite(ms)) return "—";
return Number(ms).toFixed(2) + " ms";
}
function fmtShape(s) {
if (s == null) return "—";
return Array.isArray(s) ? s.join(" × ") : String(s);
}
function fmtBytes(n) {
if (n == null || !Number.isFinite(n) || n < 0) return "—";
var b = Number(n);
if (b >= 1073741824) return (b / 1073741824).toFixed(2) + " GiB";
if (b >= 1048576) return (b / 1048576).toFixed(2) + " MiB";
if (b >= 1024) return (b / 1024).toFixed(1) + " KiB";
return b + " B";
}
function fmtNsMs(ns) {
if (ns == null || !Number.isFinite(ns)) return "—";
if (ns === 0) return "0 ms";
if (ns < 1000) return ns + " ns";
if (ns < 1e6) return (ns / 1000).toFixed(2) + " µs";
if (ns >= 1e9) return (ns / 1e9).toFixed(2) + " s";
return (ns / 1e6).toFixed(2) + " ms";
}
function formatDeviceTimings(timings) {
if (!Array.isArray(timings) || !timings.length) return "—";
return timings.map(function (timing) {
return (timing.device || "device") + " / " + (timing.clock_id || "clock") + ": " +
fmtNsMs(timing.busy_ns);
}).join(" · ");
}
function isScalar(value) {
return value == null || ["string", "number", "boolean"].includes(typeof value);
}
function humanize(value) {
var text = String(value || "").replace(/_/g, " ");
return text.charAt(0).toUpperCase() + text.slice(1);
}
function fmtValue(value) {
if (value == null || value === "") return "—";
if (typeof value === "boolean") return value ? "Yes" : "No";
if (Array.isArray(value)) {
return value.map(function (item) { return isScalar(item) ? fmtValue(item) : JSON.stringify(item); }).join(", ") || "—";
}
if (typeof value === "object") {
return Object.keys(value).map(function (key) {
return humanize(key) + ": " + fmtValue(value[key]);
}).join(" · ") || "—";
}
return String(value);
}
function safeStatus(value) {
return String(value || "unknown").toLowerCase().replace(/[^a-z0-9_-]/g, "-");
}
function statusLabel(value) {
var status = safeStatus(value);
var marks = {
valid: "✓", available: "✓", captured: "✓", complete: "✓",
warning: "!", partial: "!", failed: "×", invalid: "×", unavailable: "—",
absent: "—", missing: "—", unknown: "?",
};
return '<span class="status-badge status-' + esc(status) + '"><span aria-hidden="true">' +
esc(marks[status] || "•") + '</span> ' + esc(humanize(status)) + "</span>";
}
function heatColor(n) {
var ratio = heatMode === "memory" ?
(n.peak_live_bytes != null && SUM.logical_peak_live_bytes != null ? (SUM.logical_peak_live_bytes > 0 ? n.peak_live_bytes / SUM.logical_peak_live_bytes : 0) : null) : n.host_self_ratio;
if (ratio == null && heatMode === "time" && n.host_total_time_ns > 0) ratio = (n.host_self_time_ns || 0) / n.host_total_time_ns;
if (ratio == null) return "var(--border-strong)";
return "color-mix(in srgb, var(--heat-high) " + Math.round(Math.max(0, Math.min(1, ratio)) * 100) + "%, var(--heat-low))";
}
function initResizers() {
document.querySelectorAll(".resize-handle").forEach(function (handle) {
var side = handle.dataset.side;
var pane = document.querySelector(side === "left" ? ".pane-sidebar" : ".pane-inspector");
var property = side === "left" ? "--sidebar-w" : "--inspector-w";
function resize(width) {
var max = Math.min(400, window.innerWidth / 3);
var value = Math.round(Math.max(220, Math.min(max, width)));
root.style.setProperty(property, value + "px");
handle.setAttribute("aria-valuenow", value);
handle.setAttribute("aria-valuemin", "220");
handle.setAttribute("aria-valuemax", Math.floor(max));
}
handle.setAttribute("aria-valuenow", side === "left" ? "288" : "320");
handle.setAttribute("aria-valuemin", "220");
handle.setAttribute("aria-valuemax", "400");
handle.addEventListener("keydown", function (e) {
if (!["ArrowLeft", "ArrowRight", "Home", "End"].includes(e.key)) return;
e.preventDefault();
var delta = (e.key === "ArrowRight" ? 16 : -16) * (side === "left" ? 1 : -1);
resize(e.key === "Home" ? 220 : e.key === "End" ? 400 : pane.offsetWidth + delta);
});
handle.addEventListener("pointerdown", function (e) {
if (e.button !== 0) return;
e.preventDefault();
handle.setPointerCapture(e.pointerId);
handle.classList.add("active");
var startX = e.clientX, startWidth = pane.offsetWidth;
function move(ev) { resize(startWidth + (ev.clientX - startX) * (side === "left" ? 1 : -1)); }
function up() {
handle.classList.remove("active");
handle.removeEventListener("pointermove", move);
handle.removeEventListener("pointerup", up);
handle.removeEventListener("pointercancel", up);
}
handle.addEventListener("pointermove", move);
handle.addEventListener("pointerup", up);
handle.addEventListener("pointercancel", up);
});
});
}
function renderCoverage() {
document.querySelector("[data-coverage]").textContent = SUM.entrypoint || "Captured run";
var provenance = (V.evidence || {}).provenance || {};
document.querySelector("[data-run-context]").textContent = [provenance.device, provenance.phase === "infer" ? "Inference" : humanize(provenance.phase), provenance.capture_step ? "Step " + provenance.capture_step : ""].filter(Boolean).join(" · ");
document.title = (SUM.entrypoint ? SUM.entrypoint + " · " : "") + "Candle graph";
}
function announce(message) {
document.getElementById("viewer-status").textContent = message;
}
function updatePanes() {
var graph = currentView === "trace";
var inspectorWasHidden = document.querySelector(".pane-inspector").hidden;
var focusInWorkspace = document.querySelector(".layout").contains(document.activeElement) && !document.querySelector(".pane-inspector").contains(document.activeElement);
var narrow = matchMedia("(max-width: 900px)").matches;
document.querySelector(".pane-sidebar").hidden = !graph || !hierarchyVisible;
document.querySelector('.resize-handle[data-side="left"]').hidden = !graph || !hierarchyVisible;
document.getElementById("hierarchy-btn").setAttribute("aria-expanded", String(hierarchyVisible));
var showInspector = inspectorOpen && selectedId != null && (graph || currentView === "span_costs");
document.querySelector(".pane-inspector").hidden = !showInspector;
document.querySelector('.resize-handle[data-side="right"]').hidden = !showInspector;
document.querySelector(".pane-canvas").inert = narrow && showInspector;
document.querySelector(".pane-sidebar").inert = narrow && showInspector;
if (narrow && showInspector && (inspectorWasHidden || focusInWorkspace)) {
document.getElementById("close-inspector").focus();
}
}
function renderPeakBreakdown() {
var panel = document.getElementById("peak-breakdown");
if (!panel) return;
var logical = P.views.memory && P.views.memory.logical;
var rows = logical && logical.peak && logical.peak.live_allocations || [];
if (!rows.length) {
panel.innerHTML = "<p class=\"section-empty\">No peak allocations recorded.</p>";
return;
}
panel.innerHTML =
'<table><thead><tr><th>Tensor</th><th>Op</th><th>Size</th><th>Shape</th></tr></thead><tbody>' +
rows.map(function (r) {
return "<tr><td>" + esc((r.tensor_ids || []).join(", ")) + "</td><td>" + esc(r.op_name || "—") +
"</td><td>" + esc(fmtBytes(r.bytes)) + "</td><td>" + esc(fmtShape(r.shape)) + "</td></tr>";
}).join("") +
"</tbody></table>";
}
function setInspector(o) {
var empty = !o;
inspectorOpen = !empty;
var insp = document.getElementById("inspector");
if (insp) insp.classList.toggle("is-empty", empty);
if (o && !document.querySelector(".pane-inspector").contains(document.activeElement)) inspectorReturnFocus = document.activeElement;
o = o || {};
var fields = {
label: empty ? "Nothing selected" : (o.label || o.name || "—"),
kind: humanize(o.kind) || "—",
device_time: formatDeviceTimings(o.device_timings),
self_time: fmtNsMs(o.host_self_time_ns),
total_time: fmtNsMs(o.host_total_time_ns),
shape: fmtShape(o.shape),
dtype: o.dtype || "—",
dense: fmtBytes(o.dense_bytes),
peak_bytes: fmtBytes(o.peak_live_bytes),
bytes: fmtBytes(o.allocated_bytes),
};
Object.keys(fields).forEach(function (k) {
var el = document.querySelector('[data-field="' + k + '"]');
if (el) {
el.textContent = fields[k];
el.parentElement.hidden = empty || (fields[k] === "—" && k !== "label");
}
});
var link = document.getElementById("selection-graph-link");
link.href = "#trace?node=" + encodeURIComponent(idStr(o.id));
link.hidden = !o.id || o.kind === "edge";
updatePanes();
}
function initTabs() {
var tabs = document.querySelector("[data-view-tabs]");
if (!tabs) return;
tabs.innerHTML = VIEW_META.map(function (m) {
return '<button type="button" role="tab" class="tab" id="view-tab-' + esc(m.id) +
'" data-view="' + esc(m.id) + '" aria-controls="view-panel-' + esc(m.id) +
'" aria-selected="false" tabindex="-1">' + '<svg viewBox="0 0 24 24" aria-hidden="true"><path d="' + m.icon + '"/></svg><span>' + esc(m.label) + "</span></button>";
}).join("");
tabs.onclick = function (e) {
var btn = e.target.closest("[data-view]");
if (!btn) return;
selectView(btn.dataset.view, true);
};
tabs.onkeydown = function (e) {
if (!["ArrowLeft", "ArrowRight", "Home", "End"].includes(e.key)) return;
var buttons = Array.from(tabs.querySelectorAll('[role="tab"]'));
var active = buttons.indexOf(document.activeElement);
if (active < 0) return;
e.preventDefault();
var next = active;
if (e.key === "Home") next = 0;
else if (e.key === "End") next = buttons.length - 1;
else if (e.key === "ArrowLeft") next = (active - 1 + buttons.length) % buttons.length;
else next = (active + 1) % buttons.length;
selectView(buttons[next].dataset.view, true);
buttons[next].focus();
};
}
function selectView(id, push) {
var meta = VIEW_META.find(function (m) { return m.id === id; }) || VIEW_META[0];
currentView = meta.id;
document.querySelectorAll("[data-view-tabs] [data-view]").forEach(function (b) {
var selected = b.dataset.view === currentView;
b.setAttribute("aria-selected", selected ? "true" : "false");
b.setAttribute("tabindex", selected ? "0" : "-1");
if (selected) b.scrollIntoView({ block: "nearest", inline: "nearest" });
});
document.querySelectorAll("[data-view-panel]").forEach(function (panel) {
panel.hidden = panel.dataset.viewPanel !== currentView;
});
document.querySelectorAll("[data-trace-only]").forEach(function (control) {
control.hidden = currentView !== "trace";
});
updatePanes();
hideTooltip();
refreshView();
if (push) {
var targetHash = "#" + currentView;
if (location.hash !== targetHash) history.pushState(null, "", targetHash);
announce(meta.label + " view");
}
}
function refreshView() {
var meta = VIEW_META.find(function (m) { return m.id === currentView; }) || VIEW_META[0];
if (meta.graph) {
if (graphState) { graphState.applyView(); graphState.updateHighlight(); }
else drawGraph(V.trace || { nodes: [], edges: [] }, meta);
return;
}
if (renderedViews.has(meta.id)) {
if (meta.id === "span_costs") panelFor(meta.id).querySelectorAll('[data-span-cost-id]').forEach(function (button) { button.closest("tr").classList.toggle("sel", button.dataset.spanCostId === selectedId); });
return;
}
renderedViews.add(meta.id);
if (meta.id === "evidence") renderEvidenceView(V.evidence || {});
else if (meta.id === "span_costs") renderSpanCosts(V.span_costs || { items: [] });
else if (meta.id === "measurements") renderMeasurementsView(V.measurements || {});
else if (meta.id === "memory") renderMemoryView(V.memory || { timeline: [], peak_breakdown: [], summary: {} });
else if (meta.id === "gpu") renderGpuView(V.gpu || {});
}
function panelFor(id) {
return document.querySelector('[data-view-panel="' + id + '"]');
}
function findSpanRow(tree, id) {
return Array.from(tree.querySelectorAll("[data-span-id]")).find(function (row) {
return row.dataset.spanId === id;
}) || null;
}
function formatCell(key, value) {
var lower = String(key || "").toLowerCase();
if (lower === "timestamp" && typeof value === "string" && Number.isFinite(Date.parse(value))) {
return new Intl.DateTimeFormat(undefined, { dateStyle: "medium", timeStyle: "long" }).format(new Date(value));
}
if (value && typeof value === "object" && value.kind && "value" in value) {
if (value.kind === "duration_ns") return fmtNsMs(value.value);
if (value.kind === "bytes") return fmtBytes(value.value);
return fmtValue(value.value);
}
if (value && typeof value === "object" && !Array.isArray(value) && lower.includes("bytes")) {
return Object.keys(value).map(function (name) { return name + ": " + fmtBytes(value[name]); }).join(" · ");
}
if (typeof value === "number" && lower.includes("bytes")) return fmtBytes(value);
if (typeof value === "number" && lower.endsWith("_ns")) return fmtNsMs(value);
if (typeof value === "number" && lower.endsWith("_ms")) return fmtMs(value);
if (typeof value === "number" && lower.includes("percent")) return value.toFixed(2) + "%";
return fmtValue(value);
}
function renderKeyValues(record, omitted) {
var skip = new Set(omitted || []);
var entries = Object.keys(record || {}).filter(function (key) {
return !skip.has(key) && !Array.isArray(record[key]);
});
if (!entries.length) return '<p class="section-empty">No details recorded.</p>';
return '<dl class="key-values">' + entries.map(function (key) {
return '<div><dt>' + esc(humanize(key)) + '</dt><dd>' + esc(formatCell(key, record[key])) + '</dd></div>';
}).join("") + "</dl>";
}
function renderNoticeList(items, kind, emptyText) {
items = Array.isArray(items) ? items : [];
if (!items.length) return '<p class="section-empty">' + esc(emptyText) + "</p>";
return '<ul class="notice-list" role="list">' + items.map(function (item) {
var record = isScalar(item) ? { message: item } : (item || {});
var severity = record.qualification || record.severity || record.status || kind;
var title = record.title || humanize(record.code) || record.name || humanize(severity);
var message = record.summary || record.message || record.detail || record.description || "";
if (Array.isArray(record.requires) && record.requires.length) {
message += (message ? " " : "") + "Requires: " + record.requires.map(humanize).join(", ") + ".";
}
return '<li class="notice notice-' + esc(safeStatus(severity)) + '">' +
'<div class="notice-title">' + statusLabel(severity) + '<strong>' + esc(title) + '</strong></div>' +
(message ? '<p>' + esc(message).replace(/`([^`]+)`/g, "<code>$1</code>") + "</p>" : "") + "</li>";
}).join("") + "</ul>";
}
function disclosure(title, hint, content, open, id) {
return '<details class="disclosure"' + (open ? ' open' : '') + (id ? ' id="' + esc(id) + '"' : '') + '><summary>' + esc(title) +
(hint ? '<span>' + esc(hint) + '</span>' : '') + '</summary><div class="disclosure-content">' + content + '</div></details>';
}
function tableKey(caption) { return caption.toLowerCase().replace(/[^a-z0-9]+/g, "-"); }
function renderDataTable(rows, caption, limit, options) {
rows = Array.isArray(rows) ? rows : [];
if (!rows.length) return '<p class="section-empty">No ' + esc(caption.toLowerCase()) + ' recorded.</p>';
var id = tableKey(caption);
var state = tableStates.get(id);
options = options || {};
if (!state || state.source !== rows) {
var keys = options.keys || Array.from(new Set(rows.flatMap(function (r) { return Object.keys(isScalar(r) ? { value: r } : r || {}); })));
state = Object.assign({ id: id, source: rows, rows: rows, caption: caption, keys: keys, query: "", kind: "all", page: 0, pageSize: limit || 25, sort: null, descending: true }, options);
state.index = rows.map(function (r) { return JSON.stringify(r).toLowerCase(); });
tableStates.set(id, state);
}
return '<div class="table-browser" data-table="' + esc(id) + '"><div class="table-controls"><label class="filter-field" for="filter-' + id + '">Search ' + esc(caption.toLowerCase()) +
'<input id="filter-' + id + '" name="' + id + '-search" type="search" data-table-filter value="' + esc(state.query) + '" placeholder="Filter by name or value…" autocomplete="off" spellcheck="false"></label>' +
(state.costs || state.filterKey ? '<label for="' + (state.costs ? 'kind-filter' : id + '-kind') + '">' + esc(state.filterLabel || 'Node type') + '<select id="' + (state.costs ? 'kind-filter' : id + '-kind') + '" data-table-kind><option value="all">All ' + esc(state.filterLabel ? state.filterLabel.toLowerCase() + 's' : 'types') + '</option>' + Array.from(new Set(rows.map(function (r) { return r[state.filterKey || 'kind']; }))).sort().map(function (k) {
return '<option value="' + esc(k) + '"' + (state.kind === k ? ' selected' : '') + '>' + esc(humanize(k)) + '</option>';
}).join("") + '</select></label>' : '') +
'<button type="button" class="btn" data-table-clear' + (!state.query && state.kind === "all" ? ' hidden' : '') + '>Clear filters</button></div><div data-table-results>' + tableResults(state) + '</div></div>';
}
function tableResults(state) {
var rows = state.rows.filter(function (r, i) { return (!state.query || state.index[i].includes(state.query.toLowerCase().trim())) && (state.kind === "all" || r[state.filterKey || 'kind'] === state.kind); });
if (state.sort) rows.sort(function (a, b) {
var av = a[state.sort], bv = b[state.sort];
if (av == null) return bv == null ? 0 : 1;
if (bv == null) return -1;
var cmp = typeof av === "number" && typeof bv === "number" ? av - bv : fmtValue(av).localeCompare(fmtValue(bv), undefined, { numeric: true });
return state.descending ? -cmp : cmp;
});
state.page = Math.max(0, Math.min(state.page, Math.ceil(rows.length / state.pageSize) - 1));
var start = state.page * state.pageSize;
var pageRows = rows.slice(start, start + state.pageSize);
var maxSelf = state.costs ? state.rows.reduce(function (max, r) { return Math.max(max, r.host_self_time_ns || 0); }, 1) : 1;
var html = '<div class="table-wrap" role="region" aria-label="' + esc(state.caption) + '" tabindex="0"><table class="data-table' + (state.costs ? ' span-cost-table' : '') + '"><caption class="sr">' + esc(state.caption) + '</caption><thead><tr>' + state.keys.map(function (key) {
var sorted = state.sort === key;
return '<th scope="col" aria-sort="' + (sorted ? (state.descending ? 'descending' : 'ascending') : 'none') + '"><button type="button" class="table-sort" data-sort="' + esc(key) + '">' +
esc((state.labels || {})[key] || humanize(key)) + ' <span aria-hidden="true">' + (sorted ? (state.descending ? '↓' : '↑') : '↕') + '</span></button></th>';
}).join("") + '</tr></thead><tbody>';
html += pageRows.map(function (row) {
var record = isScalar(row) ? { value: row } : (row || {});
return '<tr' + (state.costs && selectedId === idStr(row.id) ? ' class="sel"' : '') + '>' + state.keys.map(function (key) {
var value = record[key];
var cell = esc(formatCell(key, value));
var numeric = typeof value === "number";
if (key === "level") cell = statusLabel(value);
if (state.measurements && ["mean", "rms", "abs_max", "norm"].includes(key) && Number.isFinite(value)) cell = esc(Number(value.toPrecision(6)).toString());
if (state.measurements && key === "shape") cell = esc(value && value.length ? fmtShape(value) : "Scalar");
if (state.measurements && key === "state") cell = statusLabel(value);
if (state.measurements && ["mean", "rms", "abs_max"].includes(key) && record.non_finite > 0) cell = '<span class="measurement-invalid">Not finite</span>';
if (state.measurements && key === "span_id") {
var span = (V.trace.nodes || []).find(function (n) { return idStr(n.id) === idStr(value); });
if (span) cell = '<a href="#trace?node=' + encodeURIComponent(idStr(value)) + '">' + esc(span.label || span.name || value) + '</a>';
}
if (state.costs && key === "name") cell = '<button type="button" class="table-row-action" data-span-cost-id="' + esc(idStr(row.id)) + '">' + esc(value) + '</button>';
if (state.costs && key === "device_timings") cell = esc(formatDeviceTimings(value));
if (state.costs && key === "host_self_time_ns") cell += '<span class="cost-track" aria-hidden="true"><span style="width:' + Math.round((value || 0) / maxSelf * 100) + '%"></span></span>';
if (cell.length > 480 && typeof value === "object") cell = '<details><summary>View details</summary>' + cell + '</details>';
return '<td' + (numeric ? ' class="numeric"' : '') + (numeric && !(state.measurements && record.non_finite > 0 && ["mean", "rms", "abs_max"].includes(key)) ? ' title="' + esc(value) + '"' : '') + '>' + cell + '</td>';
}).join("") + '</tr>';
}).join("");
if (!pageRows.length) html += '<tr><td colspan="' + state.keys.length + '"><p class="section-empty">No matches. Try a shorter search or clear the filters.</p></td></tr>';
html += '</tbody></table></div><div class="pagination"><p role="status">' + (rows.length ? (start + 1) + '–' + Math.min(start + state.pageSize, rows.length) : '0') +
' of ' + rows.length.toLocaleString() + ' rows' + (rows.length !== state.rows.length ? ' · filtered from ' + state.rows.length.toLocaleString() : '') + '</p><div class="pagination-actions">' +
'<button type="button" class="btn" data-page="-1"' + (state.page === 0 ? ' disabled' : '') + '>Previous</button><button type="button" class="btn" data-page="1"' +
(start + state.pageSize >= rows.length ? ' disabled' : '') + '>Next</button></div></div>';
return html;
}
function updateTable(browser, focusSort) {
var state = tableStates.get(browser.dataset.table);
browser.querySelector('[data-table-results]').innerHTML = tableResults(state);
browser.querySelector('[data-table-clear]').hidden = !state.query && state.kind === "all";
if (focusSort) Array.from(browser.querySelectorAll('[data-sort]')).find(function (b) { return b.dataset.sort === focusSort; }).focus();
}
function capability(name) { return ((V.evidence || {}).capabilities || {})[name] || { level: "unavailable", reason: "No coverage declaration." }; }
function capabilityNote(name) { return capability(name).reason || ""; }
function metric(label, value, note, cap) {
return '<div class="metric"><div class="metric-label">' + esc(label) + (cap ? statusLabel(capability(cap).level) : '') + '</div><div class="metric-value">' + esc(value) + '</div><p>' + esc(note) + '</p></div>';
}
function graphUnavailable() {
if (!SUM.capture_complete) return "This capture did not complete. Timing rankings and the execution graph are withheld; diagnostic evidence remains available.";
if (!SUM.structurally_valid) return "The trace structure is invalid. No execution graph or timing ranking can be derived safely.";
return "No graph nodes were recorded in this capture.";
}
function renderEvidenceView(data) {
var provenance = data.provenance || {};
var health = data.health || {};
var healthStatus = !health.capture_complete ? "failed" : (!health.structurally_valid ? "invalid" : "complete");
var caps = data.capabilities || {};
var limitations = Object.values(caps).filter(function (c) { return c.level !== "complete"; }).length;
var graphAvailable = (V.trace && V.trace.nodes || []).length > 0;
var phase = provenance.phase === "infer" ? "Inference" : humanize(provenance.phase);
var hotspots = ((V.span_costs || {}).items || []).filter(function (n) { return n.kind !== "tensor" && n.host_self_time_ns > 0; }).slice().sort(function (a,b) { return b.host_self_time_ns - a.host_self_time_ns; }).slice(0,5);
var maxSelf = hotspots.length ? hotspots[0].host_self_time_ns : 1;
var coverageNames = [["nested_host_time", "Host timings"], ["nested_device_time", "Device timings"], ["logical_memory_coverage", "Logical memory"], ["physical_memory_coverage", "Physical memory"], ["operation_coverage", "Operations"], ["gpu_correlation", "GPU correlation"]];
var statusText = healthStatus === "complete" ? 'Capture complete' : healthStatus === "failed" ? 'Capture incomplete' : 'Invalid trace';
var html = '<div class="evidence-header"><div><p class="eyebrow">Run overview</p><h1>' + esc(provenance.entrypoint || "Captured run") + '</h1><div class="run-subtitle"><span>' + esc(phase) + '</span><span>' + esc(provenance.device || "Device unknown") + '</span><span>Step ' + esc(provenance.capture_step || "—") + '</span></div></div>' +
'<div class="heading-actions">' + (graphAvailable ? '<a class="btn primary" href="#span_costs">Explore timings →</a><a class="btn" href="#trace">Open graph</a>' : '') + '</div></div>' +
'<div class="run-status' + (healthStatus !== "complete" ? ' is-failed' : '') + '">' + statusLabel(healthStatus) + '<strong>' + statusText + '</strong><p>' +
esc(healthStatus === "complete" ? (limitations ? limitations + " evidence classes have limits. Check coverage before drawing conclusions." : "All declared evidence classes are complete.") : graphUnavailable()) + '</p><a href="#evidence?section=coverage-details">Review coverage</a></div>' +
'<div class="metric-strip">' + metric("Measured wall time", fmtNsMs(SUM.outer_wall_time_ns), provenance.measured_region_device_synchronized ? "Measured region bounded by device synchronization." : "Host wall time. GPU completion is not implied.", "outer_wall_time") +
metric("Logical memory peak", fmtBytes(SUM.logical_peak_live_bytes), "Recorded storage lifetimes; not physical device usage.", "logical_memory_coverage") +
metric("Recorded work", (health.coverage && health.coverage.spans != null ? health.coverage.spans.toLocaleString() + " spans" : "—"), (health.coverage && health.coverage.operations != null ? health.coverage.operations + " operations" : "Operation count unknown") + " · " + (health.coverage && health.coverage.tensors != null ? health.coverage.tensors + " tensor checkpoints" : "Tensor count unknown") + ". Counts reflect captured evidence.") + '</div>' +
'<div class="overview-columns"><section class="evidence-card"><div class="section-heading"><h2>Where host time went</h2>' + (graphAvailable ? '<a href="#span_costs">All timings →</a>' : '') + '</div><p class="section-intro">Largest recorded self times, excluding child work. Bars are relative to the largest row; this is not a partition of wall time.</p>' +
(hotspots.length ? '<ol class="hotspot-list">' + hotspots.map(function (n) { return '<li><a class="hotspot-link" href="#trace?node=' + encodeURIComponent(idStr(n.id)) + '"><span class="hotspot-name">' + esc(n.name) + '</span><span class="hotspot-time">' + esc(fmtNsMs(n.host_self_time_ns)) + ' →</span><span class="cost-track" aria-hidden="true"><span style="width:' + (n.host_self_time_ns / maxSelf * 100).toFixed(1) + '%"></span></span></a></li>'; }).join("") + '</ol>' : '<p class="section-empty">' + esc(graphAvailable ? "No nonzero host self times were recorded." : graphUnavailable()) + '</p>') +
'</section><section class="evidence-card"><div class="section-heading"><h2>What this run can tell you</h2></div><dl class="coverage-list">' + coverageNames.map(function (pair) {
return '<div><dt>' + esc(pair[1]) + '</dt><dd>' + statusLabel(capability(pair[0]).level) + '</dd></div>';
}).join("") + '</dl><p class="coverage-footnote">Missing evidence means unknown, never zero. <a href="#evidence?section=coverage-details">All coverage and reasons →</a></p></section></div>';
if ((data.findings || []).length) html += '<section class="evidence-card"><div class="section-heading"><h2>Findings supported by this run</h2></div>' + renderNoticeList(data.findings, "information", "") + '</section>';
html += disclosure("Capture issues", (health.issues || []).length + " recorded", renderNoticeList(health.issues, "warning", "No capture issues were recorded."), healthStatus !== "complete", "capture-issues");
html += disclosure("Evidence coverage and limits", Object.keys(caps).length + " evidence classes", renderDataTable(Object.keys(caps).map(function (name) { return Object.assign({ evidence: humanize(name) }, caps[name]); }), "Evidence coverage", 25, { keys: ["evidence", "level", "reason", "source"] }) + '<h3 class="details-subheading">Evidence gaps</h3>' + renderNoticeList(data.gaps, "missing", "No evidence gaps were reported."), false, "coverage-details");
html += disclosure("Run details", "Provenance and capture settings", renderKeyValues(provenance) + '<h3 class="details-subheading">Trace health</h3>' + renderKeyValues(health, ["issues"]), false, "run-details");
html += disclosure("Recorded facts", (data.facts || []).length + " facts", renderDataTable(data.facts, "Recorded facts"));
html += disclosure("Tensor checkpoints", (data.tensors || []).length + " checkpoints", renderDataTable(data.tensors, "Tensor checkpoints"));
var measurements = V.measurements || {};
html += '<section class="evidence-card"><div class="section-heading"><h2>Recorded measurements</h2><a href="#measurements">Inspect measurements →</a></div><p class="section-intro">' + (measurements.tensor_stats || []).length + ' scalar and tensor-statistic observations · ' + (measurements.gradients || []).length + ' gradient observations. Inspect losses, numerical health, and declared gradient expectations.</p></section>';
panelFor("evidence").innerHTML = html;
}
function renderMeasurementsView(data) {
var stats = (data.tensor_stats || []).map(function (row) {
return row.non_finite > 0 ? Object.assign({}, row, { mean: null, rms: null, abs_max: null }) : row;
});
var gradients = data.gradients || [];
var scalar = function (row) { return row.elements === 1 && Array.isArray(row.shape) && row.shape.length === 0; };
var scalars = stats.filter(scalar);
var tensors = stats.filter(function (row) { return !scalar(row); });
var nonFinite = stats.filter(function (row) { return row.non_finite > 0; }).length;
var states = ["present", "zero", "missing", "non_finite"].map(function (state) {
return gradients.filter(function (row) { return row.state === state; }).length + ' ' + humanize(state).toLowerCase();
}).join(' · ');
var html = '<div class="view-heading"><div><p class="eyebrow">Losses, numerical health and gradients</p><h1>Inspect measurements</h1><p>Values recorded during this invocation. Search by label, family or value; repeated labels remain separate observations.</p></div></div>';
if (!SUM.capture_complete || !SUM.structurally_valid) html += '<div class="run-status is-failed">' + statusLabel(!SUM.capture_complete ? 'failed' : 'invalid') + '<p>Diagnostic observations only. This capture cannot support a normal run conclusion. Gradient records and graph links are withheld.</p><a href="#evidence?section=capture-issues">Review capture issues</a></div>';
html += '<div class="metric-strip">' + metric('Scalar observations', String(scalars.length), 'Single-element, rank-zero records, including host-recorded values.') +
metric('Tensor summaries', String(tensors.length), nonFinite + ' scalar or tensor records contain non-finite values. Counts describe observations, not full model coverage.') +
metric('Gradient observations', String(gradients.length), states + '. ' + capabilityNote('gradient_coverage'), 'gradient_coverage') + '</div>';
if (nonFinite) html += '<div class="run-status is-failed">' + statusLabel('non_finite') + '<p>' + nonFinite + ' observations contain NaN or infinity. Their numeric summaries are withheld; serialized placeholder zeros are not measured zeros.</p></div>';
html += '<section class="evidence-card" id="scalar-values"><div class="section-heading"><h2>Scalar values</h2></div><p class="section-intro">Loss terms, optimizer settings and other scalar-shaped observations. Zero can be intentional; these records do not establish why a value changed.</p>' + renderDataTable(scalars, 'Scalar values', 25, { measurements: true, keys: ['label', 'mean', 'non_finite', 'span_id'], labels: { mean: 'Value', non_finite: 'Non-finite elements', span_id: 'Recorded in' } }) + '</section>';
html += '<section class="evidence-card" id="tensor-statistics"><div class="section-heading"><h2>Tensor statistics</h2></div><p class="section-intro">RMS describes magnitude; absolute maximum highlights extremes; mean describes the center. Only explicitly recorded tensors are represented.</p>' + renderDataTable(tensors, 'Tensor statistics', 25, { measurements: true, keys: ['label', 'shape', 'dtype', 'elements', 'rms', 'abs_max', 'mean', 'non_finite', 'span_id'], labels: { rms: 'RMS', abs_max: 'Absolute max', non_finite: 'Non-finite elements', span_id: 'Recorded in' } }) + '</section>';
html += '<section class="evidence-card" id="gradient-measurements"><div class="section-heading"><h2>Gradients</h2>' + statusLabel(capability('gradient_coverage').level) + '</div><p class="section-intro">' + esc(capabilityNote('gradient_coverage')) + ' Missing and zero gradients can be expected for inactive or data-conditional families. The root preserves the recorded pre-clip or post-clip identity; norms from different roots are not combined.</p>' + renderDataTable(gradients, 'Gradient measurements', 25, { measurements: true, filterKey: 'state', filterLabel: 'State', sort: 'norm', descending: true, keys: ['root', 'key', 'family', 'expectation', 'state', 'norm'], labels: { key: 'Parameter', expectation: 'Family expectation', norm: 'Recorded norm' } }) + '</section>';
panelFor('measurements').innerHTML = html;
}
function renderMemoryView(data) {
var logical = data.logical;
var physical = data.physical;
var timeline = logical && logical.timeline || [];
var peak = logical && logical.peak;
var html = '<div class="view-heading"><div><p class="eyebrow">Storage and device observations</p><h1>Understand memory</h1><p>Follow recorded storage lifetimes and inspect the allocations alive at the peak.</p></div></div>';
html += '<div class="metric-strip">' + metric("Logical peak", peak ? fmtBytes(peak.live_bytes) : "Unknown", peak ? "At " + fmtNsMs(peak.timestamp_ns) + " since capture start." : "No storage peak can be inferred.", "logical_memory_coverage") +
metric("Recorded allocations", logical ? String(logical.storage_allocation_count) : "Unknown", logical ? logical.matched_storage_free_count + " matched frees in this capture." : "Storage-lifetime events were not captured.") +
metric("Physical memory", physical ? "Observed" : "Unknown", "Independent device samples; not inferred from tensor shapes.", "physical_memory_coverage") + '</div>';
html += '<section class="evidence-card"><div class="section-heading"><h2>Logical storage over time</h2>' + statusLabel(capability("logical_memory_coverage").level) + '</div><p class="section-intro">' + esc(capabilityNote("logical_memory_coverage")) + ' Changes occur at recorded allocation and free events.</p>';
if (!timeline.length) html += '<div class="content-empty"><strong>No logical memory timeline</strong><p>' + esc(capabilityNote("logical_memory_coverage")) + ' Missing observations are not zero memory use.</p><a class="btn" href="#evidence?section=coverage-details">Review memory coverage</a></div>';
else {
var maxTs = timeline.reduce(function (m,p) { return Math.max(m,p.timestamp_ns); },1);
var maxLive = timeline.reduce(function (m,p) { return Math.max(m,p.live_bytes); },1);
var x = function (ts) { return 88 + ts / maxTs * 824; };
var y = function (bytes) { return 244 - bytes / maxLive * 192; };
var path = "M" + x(timeline[0].timestamp_ns) + " " + y(timeline[0].live_bytes);
timeline.slice(1).forEach(function (point) { path += "H" + x(point.timestamp_ns) + "V" + y(point.live_bytes); });
html += '<svg class="memory-chart" viewBox="0 0 960 300" role="img" aria-labelledby="memory-chart-title memory-chart-desc"><title id="memory-chart-title">Recorded logical storage</title><desc id="memory-chart-desc">Step chart of ' + timeline.length + ' storage events. ' + esc(peak ? 'Peak ' + fmtBytes(peak.live_bytes) + ' at ' + fmtNsMs(peak.timestamp_ns) + '.' : '') + ' The full values are in the timeline table below.</desc>';
for (var tick = 0; tick <= 3; tick++) {
var value = maxLive * tick / 3;
html += '<line class="chart-grid" x1="88" x2="912" y1="' + y(value) + '" y2="' + y(value) + '"/><text class="chart-label" x="76" y="' + (y(value) + 4) + '" text-anchor="end">' + esc(fmtBytes(value)) + '</text>';
html += '<text class="chart-label" x="' + x(maxTs * tick / 3) + '" y="268" text-anchor="middle">' + esc(fmtNsMs(maxTs * tick / 3)) + '</text>';
}
html += '<path d="' + path + '" fill="none" stroke="var(--chart)" stroke-width="3"/>';
if (peak) {
var px = x(peak.timestamp_ns), py = y(peak.live_bytes);
html += '<circle cx="' + px + '" cy="' + py + '" r="5" fill="var(--chart)"/><text class="chart-label" x="' + px + '" y="' + (py - 16) + '" text-anchor="' + (px > 700 ? 'end' : 'start') + '">Peak ' + esc(fmtBytes(peak.live_bytes)) + '</text>';
}
html += '<text class="chart-label" x="500" y="296" text-anchor="middle">Time since capture start · host clock</text></svg>';
var cats = peak && peak.live_bytes_by_category || {};
html += '<div class="memory-categories">' + Object.keys(cats).map(function (k) { return '<span>' + esc(humanize(k)) + ' <strong>' + esc(fmtBytes(cats[k])) + '</strong> at peak</span>'; }).join("") + '</div>';
}
html += '</section>';
if (peak) html += disclosure("Allocations at the logical peak", (peak.live_allocations || []).length + " live allocations", renderDataTable(peak.live_allocations, "Peak allocations"), true);
html += disclosure("Timeline observations", timeline.length + " events", renderDataTable(timeline, "Logical memory timeline", 25, { keys: ["timestamp_ns", "live_bytes", "live_bytes_by_device", "live_bytes_by_category"], labels: { timestamp_ns: "Time since capture start", live_bytes: "Live storage", live_bytes_by_device: "By device", live_bytes_by_category: "By category" } }));
html += '<section class="evidence-card"><div class="section-heading"><h2>Physical device memory</h2>' + statusLabel(capability("physical_memory_coverage").level) + '</div><p class="section-intro">' + esc(capabilityNote("physical_memory_coverage")) + '</p>' + (physical ? renderDataTable(physical.by_device, "Physical device memory") : '<p class="section-empty">Used, reserved, free, and capacity are independent observations. This capture has no physical device samples.</p>') + '</section>';
panelFor("memory").innerHTML = html;
}
function renderSpanCosts(data) {
var items = data.items || [];
var html = '<div class="view-heading"><div><p class="eyebrow">Host performance</p><h1>Explore timings</h1><p>Find expensive work, then select a row to inspect it or locate it in the graph.</p></div>' + statusLabel(capability("nested_host_time").level) + '</div>';
html += '<p class="view-summary"><strong>Self</strong> excludes child work. <strong>Total</strong> includes it; nested totals overlap. Device busy time stays on its own clock. Memory columns show logical storage.</p>';
html += '<p class="view-summary">' + esc(capabilityNote("nested_host_time")) + '</p>';
if (!items.length) html += '<div class="content-empty"><strong>No timing ranking</strong><p>' + esc(graphUnavailable()) + '</p><a class="btn" href="#evidence">Review capture details</a></div>';
else html += renderDataTable(items, "Span timings", 25, { costs: true, sort: "host_self_time_ns", keys: ["name", "kind", "host_self_time_ns", "host_total_time_ns", "device_timings", "peak_live_bytes", "allocated_bytes"], labels: { name: "Span or operation", kind: "Type", host_self_time_ns: "Host self", host_total_time_ns: "Host total", device_timings: "Device busy", peak_live_bytes: "Logical peak", allocated_bytes: "Allocated" } });
panelFor("span_costs").innerHTML = html;
}
function renderGpuView(data) {
var available = data.status === "available";
var levels = [capability("gpu_correlation").level, capability("provenance_binding").level];
var status = levels.includes("invalid") ? "invalid" : levels.includes("unavailable") ? "unavailable" : levels.includes("partial") ? "partial" : (available ? "available" : "unavailable");
var html = '<div class="view-heading"><div><p class="eyebrow">Nsight Systems evidence</p><h1>Inspect GPU activity</h1><p>Kernel activity and projected phases from the captured Nsight reports.</p></div>' + statusLabel(status) + '</div>';
if (!available) {
html += '<div class="gpu-empty"><span class="gpu-empty-mark">GPU / NO REPORT</span><h2>No GPU report in this profile</h2><p>' + esc(data.reason || "This capture does not include Nsight Systems evidence.") + '</p><p>GPU activity is unknown. Check the other views for captured host and memory evidence.</p><p>To include GPU activity, generate this viewer with the matching normalized Nsight directory.</p><code>candle-graph view application.jsonl --nsight-dir nsight --output viewer.html</code><a class="btn" href="#evidence">Back to run overview</a></div>';
} else {
html += '<div class="run-status">' + statusLabel(status) + '<p>' + esc(capabilityNote("gpu_correlation")) + ' ' + esc(capabilityNote("provenance_binding")) + '</p></div>';
html += '<p class="view-summary">Host, device-event, and Nsight times use separate clocks. Global kernel summaries are not exact phase attribution.</p>';
[["Phase GPU attribution", data.phase_attribution], ["CUDA kernels", data.kernels], ["CUDA runtime calls", data.runtime_calls], ["GPU memory operations", data.memory_operations], ["NVTX projected ranges", data.nvtx_ranges], ["GPU timeline", data.gpu_timeline]].forEach(function (section, index) {
html += disclosure(section[0], (section[1] || []).length + " rows", renderDataTable(section[1], section[0]), index < 2);
});
}
html += disclosure("GPU coverage and diagnostics", "Provenance, clock and join limits", renderKeyValues({ correlation: data.correlation_capability, provenance_binding: data.provenance_capability, provenance: data.provenance, coverage: data.coverage, semantic_correlation: data.correlation, limits: data.limits }) + renderNoticeList((data.diagnostics || []).concat(data.provenance && data.provenance.diagnostics || []), "warning", "No normalization diagnostics."), status === "invalid" || status === "partial");
html += disclosure("Capture sources", "Hashed Nsight artifacts", renderDataTable((data.raw_report ? [data.raw_report] : []).concat(data.source_csv || []), "Hashed Nsight artifacts"));
panelFor("gpu").innerHTML = html;
}
function buildSpanTree() {
var tree = document.getElementById("span-tree");
if (!tree) return;
var q = (document.querySelector("[data-span-search]").value || "").trim().toLowerCase();
var spans = q ? ((V.trace || {}).nodes || []).map(function (n) { return Object.assign({}, n, { name: n.label || n.name }); }) : P.span_tree || [];
var rootKey = "__candle_graph_root__";
var byParent = Object.create(null);
var byId = new Map(spans.map(function (span) { return [idStr(span.id), span]; }));
spans.forEach(function (s) {
var p = s.parent_id == null ? rootKey : idStr(s.parent_id);
(byParent[p] = byParent[p] || []).push(s);
});
Object.keys(byParent).forEach(function (k) {
byParent[k].sort(function (a, b) { return (b.host_total_time_ns || 0) - (a.host_total_time_ns || 0); });
});
if (!treeInitialized && byParent[rootKey]) {
treeInitialized = true;
byParent[rootKey].forEach(function (s) { spanOpen.add(idStr(s.id)); });
}
function render() {
tree.innerHTML = "";
q = (document.querySelector("[data-span-search]").value || "").trim().toLowerCase();
var matches = new Set(spans.filter(function (s) { return String(s.name || "").toLowerCase().includes(q); }).map(function (s) { return idStr(s.id); }));
var visible = new Set(matches);
matches.forEach(function (id) {
var parent = byId.get(id);
var visited = new Set([id]);
while (parent && parent.parent_id != null && !visited.has(idStr(parent.parent_id))) {
var parentId = idStr(parent.parent_id);
visited.add(parentId);
visible.add(parentId);
parent = byId.get(parentId);
}
});
document.getElementById("span-search-status").textContent = q ? matches.size + " of " + spans.length + " nodes match" : spans.length + " recorded spans";
if (q && !matches.size) tree.innerHTML = '<p class="section-empty">No matching spans. Try a shorter name or clear the search.</p>';
function add(list, depth) {
(list || []).forEach(function (s) {
var id = idStr(s.id);
if (q && !visible.has(id)) return;
var kids = (byParent[id] || []).filter(function (child) { return !q || visible.has(idStr(child.id)); });
var has = kids.length > 0;
var expanded = !!q || spanOpen.has(id);
var row = document.createElement("div");
row.className = "span-row" + (q && !matches.has(id) ? " context-row" : "");
row.title = s.name;
row.setAttribute("role", "treeitem");
row.setAttribute("aria-level", String(depth + 1));
row.style.paddingLeft = depth * 14 + 8 + "px";
row.dataset.spanId = id;
row.tabIndex = selectedId === id ? 0 : -1;
row.setAttribute("aria-selected", selectedId === id ? "true" : "false");
if (has) row.setAttribute("aria-expanded", expanded ? "true" : "false");
if (has) {
var b = document.createElement("button");
b.type = "button";
b.className = "tw";
b.textContent = expanded ? "▾" : "▸";
b.tabIndex = -1;
b.disabled = !!q;
if (q) b.title = "Search expands matching branches.";
b.setAttribute("aria-label", (expanded ? "Collapse " : "Expand ") + s.name);
b.onclick = function (e) {
e.stopPropagation();
if (spanOpen.has(id)) spanOpen.delete(id); else spanOpen.add(id);
render();
var restored = findSpanRow(tree, id);
if (restored) restored.focus();
};
row.appendChild(b);
} else {
var sp = document.createElement("span");
sp.className = "tw";
sp.textContent = "·";
row.appendChild(sp);
}
var name = document.createElement("span");
name.className = "span-name";
name.textContent = s.name;
row.appendChild(name);
var ms = document.createElement("span");
ms.className = "span-ms";
ms.textContent = fmtNsMs(s.host_total_time_ns);
row.appendChild(ms);
function activate() {
tree.querySelectorAll("[aria-selected=true]").forEach(function (x) {
x.setAttribute("aria-selected", "false");
x.tabIndex = -1;
});
row.setAttribute("aria-selected", "true");
row.tabIndex = 0;
selectedId = id;
if (matchMedia("(max-width: 900px)").matches) hierarchyVisible = false;
setInspector(Object.assign({ label: s.name }, s));
highlightNode(id);
centerOnNode(id);
}
row.onclick = activate;
row.onkeydown = function (event) {
var rows = Array.from(tree.querySelectorAll("[data-span-id]"));
var index = rows.indexOf(row);
var target = null;
if (event.key === "Enter" || event.key === " ") activate();
else if (event.key === "ArrowDown") target = rows[Math.min(rows.length - 1, index + 1)];
else if (event.key === "ArrowUp") target = rows[Math.max(0, index - 1)];
else if (event.key === "Home") target = rows[0];
else if (event.key === "End") target = rows[rows.length - 1];
else if (event.key === "ArrowRight" && has) {
if (!spanOpen.has(id)) {
spanOpen.add(id);
render();
target = findSpanRow(tree, id);
} else {
target = rows[index + 1];
}
} else if (event.key === "ArrowLeft") {
if (!q && has && spanOpen.has(id)) {
spanOpen.delete(id);
render();
target = findSpanRow(tree, id);
} else if (s.parent_id != null) {
target = findSpanRow(tree, idStr(s.parent_id));
}
} else return;
event.preventDefault();
if (target) {
tree.querySelectorAll('[tabindex="0"]').forEach(function (x) { x.tabIndex = -1; });
target.tabIndex = 0;
target.focus();
}
};
tree.appendChild(row);
if (has && expanded) add(kids, depth + 1);
});
}
add(byParent[rootKey], 0);
if (!tree.querySelector('[tabindex="0"]')) {
var first = tree.querySelector("[data-span-id]");
if (first) first.tabIndex = 0;
}
}
var search = document.querySelector("[data-span-search]");
if (search) search.oninput = buildSpanTree;
render();
}
function highlightNode(id) {
selectedId = id;
graphFocusId = id;
if (graphState) graphState.updateHighlight();
document.querySelectorAll("#span-tree [data-span-id]").forEach(function (el) {
var selected = el.dataset.spanId === id;
el.setAttribute("aria-selected", selected ? "true" : "false");
el.tabIndex = selected ? 0 : -1;
});
}
function svgPoint(svg, clientX, clientY) {
var r = svg.getBoundingClientRect();
return { x: clientX - r.left, y: clientY - r.top };
}
function fitView(vis) {
if (!vis.length) return;
var wrap = document.getElementById("view-panel-trace");
var pw = wrap.clientWidth || 800;
var ph = wrap.clientHeight || 400;
var coords = vis.filter(function (n) { return Number.isFinite(n._x) && Number.isFinite(n._y); });
if (!coords.length) return;
var pad = 64;
var minX = Math.min.apply(null, coords.map(function (n) { return n._x; }));
var maxX = Math.max.apply(null, coords.map(function (n) { return n._x + (n._w || 0); }));
var minY = Math.min.apply(null, coords.map(function (n) { return n._y; }));
var maxY = Math.max.apply(null, coords.map(function (n) { return n._y + (n._h || 0); }));
var gw = Math.max(maxX - minX + pad * 2, 1);
var gh = Math.max(maxY - minY + pad * 2, 1);
graphView.k = Math.min(2.5, Math.max(0.06, Math.min(pw / gw, ph / gh)));
var cx = (minX + maxX) / 2;
var cy = (minY + maxY) / 2;
graphView.x = pw / 2 - cx * graphView.k;
graphView.y = ph / 2 - cy * graphView.k;
updateZoomLabel();
}
function zoomAt(sx, sy, factor) {
var k0 = graphView.k;
var k1 = Math.min(4, Math.max(0.06, k0 * factor));
var gx = (sx - graphView.x) / k0;
var gy = (sy - graphView.y) / k0;
graphView.k = k1;
graphView.x = sx - gx * k1;
graphView.y = sy - gy * k1;
updateZoomLabel();
}
function updateZoomLabel() {
var el = document.getElementById("zoom-label");
if (el) el.textContent = Math.round(graphView.k * 100) + "%";
}
function neighborSet(nodeId, edges) {
var s = new Set([nodeId]);
edges.forEach(function (e) {
if (e._from === nodeId) s.add(e._to);
if (e._to === nodeId) s.add(e._from);
});
return s;
}
var tooltip = document.getElementById("graph-tooltip");
function showTooltip(n, clientX, clientY) {
if (!tooltip || !n) return;
var wrap = document.getElementById("view-panel-trace");
var r = wrap.getBoundingClientRect();
tooltip.hidden = false;
tooltip.innerHTML =
'<div class="tt-title">' + esc(n.label || n.name || "") + "</div>" +
'<div class="tt-meta">host self ' + esc(fmtNsMs(n.host_self_time_ns)) + " · host total " + esc(fmtNsMs(n.host_total_time_ns)) +
(n.peak_live_bytes != null ? " · logical peak " + esc(fmtBytes(n.peak_live_bytes)) : "") +
(n.allocated_bytes != null ? " · allocated " + esc(fmtBytes(n.allocated_bytes)) : "") + "</div>";
tooltip.classList.add("visible");
var tx = Math.min(clientX - r.left + 12, r.width - tooltip.offsetWidth - 8);
var ty = Math.min(clientY - r.top + 12, r.height - tooltip.offsetHeight - 8);
tooltip.style.left = Math.max(8, tx) + "px";
tooltip.style.top = Math.max(8, ty) + "px";
}
function hideTooltip() {
if (tooltip) { tooltip.classList.remove("visible"); tooltip.hidden = true; }
}
function centerOnNode(id) {
requestAnimationFrame(function () {
if (!graphState || currentView !== "trace") return;
var node = graphState.nodes.find(function (n) { return n._id === id; });
if (!node) return;
var wrap = panelFor("trace");
graphView.k = Math.max(1, Math.min(1.2, graphView.k));
graphView.x = wrap.clientWidth / 2 - (node._x + node._w / 2) * graphView.k;
graphView.y = wrap.clientHeight / 2 - (node._y + node._h / 2) * graphView.k;
graphState.applyView();
updateZoomLabel();
});
}
function revealInTree(id) {
var spans = P.span_tree || [];
var byId = new Map(spans.map(function (span) { return [idStr(span.id), span]; }));
var node = byId.get(id);
var seen = new Set();
while (node && node.parent_id != null && !seen.has(idStr(node.parent_id))) {
var parent = idStr(node.parent_id);
spanOpen.add(parent);
seen.add(parent);
node = byId.get(parent);
}
buildSpanTree();
var row = findSpanRow(document.getElementById("span-tree"), id);
if (row) row.scrollIntoView({ block: "nearest" });
}
function drawGraph(data, meta) {
var layout = meta.layout || "layered";
var direction = meta.direction || "LR";
var svg = document.getElementById("graph-canvas");
var empty = document.getElementById("empty-graph");
var NS = svg.namespaceURI;
var nodes = (data.nodes || []).map(function (n, i) {
return Object.assign({}, n, { _id: idStr(n.id != null ? n.id : i) });
});
if (!nodes.length) {
svg.innerHTML = "";
if (empty) {
empty.hidden = false;
empty.querySelector("p").textContent = graphUnavailable();
}
wrapGraphControls(false);
graphState = null;
return;
}
if (empty) empty.hidden = true;
wrapGraphControls(true);
svg.removeAttribute("aria-hidden");
var edges = (data.edges || []).map(function (e, i) {
return Object.assign({}, e, {
_id: String(e.id != null ? e.id : "e" + i),
_from: idStr(e.from),
_to: idStr(e.to),
label: e.label || (e.kind === "call" && e.host_duration_ns ? fmtNsMs(e.host_duration_ns) : ""),
});
});
document.querySelector("[data-graph-stats]").textContent = nodes.length + " nodes · " + edges.length + " connections";
if (layout === "tree") CGLayout.layoutTree(nodes, edges);
else CGLayout.layoutLayered(nodes, edges, direction);
var byId = Object.create(null);
nodes.forEach(function (n) { byId[n._id] = n; });
if (!byId[graphFocusId]) graphFocusId = byId[selectedId] ? selectedId : nodes[0]._id;
var visEdges = edges.filter(function (e) { return byId[e._from] && byId[e._to]; });
CGLayout.assignEdgePorts(nodes, visEdges, byId, layout, direction);
var rootG = document.createElementNS(NS, "g");
var bandG = document.createElementNS(NS, "g");
var edgeG = document.createElementNS(NS, "g");
var nodeG = document.createElementNS(NS, "g");
rootG.appendChild(bandG);
rootG.appendChild(edgeG);
rootG.appendChild(nodeG);
function focusSet() {
var id = hoveredId || selectedId;
if (!id) return null;
return neighborSet(id, visEdges);
}
function updateClasses() {
var focus = focusSet();
nodeG.querySelectorAll(".node").forEach(function (el) {
var id = el.dataset.nodeId;
el.classList.toggle("dim", focus && !focus.has(id) && id !== selectedId);
el.classList.toggle("sel", id === selectedId);
el.setAttribute("aria-pressed", String(id === selectedId));
el.setAttribute("tabindex", id === graphFocusId ? "0" : "-1");
});
edgeG.querySelectorAll(".edge-group").forEach(function (el) {
var lit = focus && (focus.has(el.dataset.from) || focus.has(el.dataset.to));
el.classList.toggle("dim", focus && !lit);
el.classList.toggle("sel", el.dataset.edgeId === selectedId);
});
}
function applyTransform() {
rootG.setAttribute("transform", "translate(" + graphView.x + "," + graphView.y + ") scale(" + graphView.k + ")");
}
function applyHeat() {
nodeG.querySelectorAll(".node-card").forEach(function (card, i) { card.setAttribute("stroke", heatColor(nodes[i])); });
}
function buildSVG() {
while (svg.firstChild) svg.removeChild(svg.firstChild);
applyTransform();
svg.appendChild(rootG);
bandG.innerHTML = "";
edgeG.innerHTML = "";
nodeG.innerHTML = "";
CGLayout.layerBands(nodes, direction).forEach(function (b, i) {
var rect = document.createElementNS(NS, "rect");
rect.setAttribute("class", "layer-band");
rect.setAttribute("x", b.x);
rect.setAttribute("y", b.y);
rect.setAttribute("width", b.w);
rect.setAttribute("height", b.h);
rect.setAttribute("rx", "12");
if (i % 2) rect.setAttribute("opacity", "0.55");
bandG.appendChild(rect);
});
visEdges.forEach(function (e) {
var edgeKind = e.kind === "data" ? "edge-composition" : "edge-default";
var pathD = CGLayout.routeEdge(e);
var g = document.createElementNS(NS, "g");
g.setAttribute("class", "edge-group");
g.dataset.from = e._from;
g.dataset.to = e._to;
g.dataset.edgeId = e._id;
var p = document.createElementNS(NS, "path");
p.setAttribute("class", "edge " + edgeKind);
p.setAttribute("stroke-width", "2");
p.setAttribute("fill", "none");
p.setAttribute("marker-end", "url(#arrow)");
p.setAttribute("d", pathD);
g.appendChild(p);
var label = e.label || (e.kind === "call" && e.host_duration_ns ? fmtNsMs(e.host_duration_ns) : "");
if (label) {
var mid = CGLayout.edgeMidpoint(e);
if (mid) {
var lbl = document.createElementNS(NS, "text");
lbl.setAttribute("class", "edge-label");
lbl.setAttribute("x", mid.x);
lbl.setAttribute("y", mid.y);
lbl.setAttribute("text-anchor", "middle");
lbl.textContent = label;
g.appendChild(lbl);
}
}
edgeG.appendChild(g);
});
nodes.forEach(function (n, nodeIndex) {
var gg = document.createElementNS(NS, "g");
var kind = n.kind || "function";
gg.setAttribute("class", "node " + kind);
gg.dataset.nodeId = n._id;
gg.setAttribute("tabindex", n._id === selectedId || (!byId[selectedId] && nodeIndex === 0) ? "0" : "-1");
gg.setAttribute("role", "button");
gg.setAttribute("aria-label", (n.label || n.name || "Span") + ", host self " +
fmtNsMs(n.host_self_time_ns) + ", host total " + fmtNsMs(n.host_total_time_ns));
var card = document.createElementNS(NS, "rect");
card.setAttribute("class", "node-card");
card.setAttribute("x", n._x);
card.setAttribute("y", n._y);
card.setAttribute("width", n._w);
card.setAttribute("height", n._h);
card.setAttribute("rx", "8");
card.setAttribute("stroke", heatColor(n));
gg.appendChild(card);
var lines = [humanize(kind)].concat(n._titleLines || [CGLayout.labelOf(n)], n._subLines || []);
var textY = n._y + 24;
lines.forEach(function (line, index) {
var text = document.createElementNS(NS, "text");
text.setAttribute("x", n._x + 16);
text.setAttribute("y", textY);
text.setAttribute("class", index === 0 ? "nb-kind" : index <= n._titleLines.length ? "nb-title" : "nb-sub");
text.textContent = line;
gg.appendChild(text);
textY += index === 0 ? 22 : 18;
});
gg.onfocus = function () { centerOnNode(n._id); };
gg.onmouseenter = function (ev) {
hoveredId = n._id;
updateClasses();
showTooltip(n, ev.clientX, ev.clientY);
};
gg.onmouseleave = function () {
if (hoveredId === n._id) hoveredId = null;
updateClasses();
hideTooltip();
};
gg.onmousemove = function (ev) { showTooltip(n, ev.clientX, ev.clientY); };
gg.onclick = function (ev) {
ev.stopPropagation();
selectedId = n._id;
graphFocusId = n._id;
setInspector(n);
updateClasses();
nodeG.querySelectorAll(".node").forEach(function (node) { node.setAttribute("tabindex", node === gg ? "0" : "-1"); });
revealInTree(n._id);
centerOnNode(n._id);
announce((n.label || n.name) + " selected. Details opened.");
};
gg.onkeydown = function (ev) {
if (ev.key === "Enter" || ev.key === " ") {
ev.preventDefault();
gg.onclick(ev);
return;
}
if (!["ArrowLeft", "ArrowRight", "ArrowUp", "ArrowDown", "Home", "End"].includes(ev.key)) return;
ev.preventDefault();
ev.stopPropagation();
var candidates = nodes.filter(function (other) {
if (other === n) return false;
if (ev.key === "ArrowLeft") return other._x < n._x;
if (ev.key === "ArrowRight") return other._x > n._x;
if (ev.key === "ArrowUp") return other._y < n._y;
if (ev.key === "ArrowDown") return other._y > n._y;
return true;
});
candidates.sort(function (a,b) { return Math.hypot(a._x - n._x, a._y - n._y) - Math.hypot(b._x - n._x,b._y - n._y); });
var target = ev.key === "Home" ? nodes[0] : ev.key === "End" ? nodes[nodes.length-1] : candidates[0];
if (!target) return;
var element = Array.from(nodeG.children).find(function (el) { return el.dataset.nodeId === target._id; });
nodeG.querySelectorAll(".node").forEach(function (node) { node.setAttribute("tabindex", node === element ? "0" : "-1"); });
graphFocusId = target._id;
element.focus({ preventScroll: true });
centerOnNode(target._id);
};
nodeG.appendChild(gg);
});
updateClasses();
}
function ensureDefs() {
var defs = svg.querySelector("defs");
if (!defs) {
defs = document.createElementNS(NS, "defs");
svg.insertBefore(defs, svg.firstChild);
}
if (!svg.querySelector("#arrow")) {
var m = document.createElementNS(NS, "marker");
m.setAttribute("id", "arrow");
m.setAttribute("viewBox", "0 0 10 10");
m.setAttribute("refX", "9");
m.setAttribute("refY", "5");
m.setAttribute("markerWidth", "6");
m.setAttribute("markerHeight", "6");
m.setAttribute("orient", "auto-start-reverse");
var path = document.createElementNS(NS, "path");
path.setAttribute("d", "M 0 0 L 10 5 L 0 10 z");
path.setAttribute("fill", "var(--edge)");
m.appendChild(path);
defs.appendChild(m);
}
}
buildSVG();
ensureDefs();
if (graphView._fit) {
fitView(nodes);
graphView._fit = false;
applyTransform();
}
graphState = {
nodes: nodes,
applyView: applyTransform,
applyHeat: applyHeat,
updateHighlight: updateClasses,
};
}
function wrapGraphControls(enabled) {
["fit-btn", "reset-btn", "zoom-in", "zoom-out", "zoom-fit", "export-btn"].forEach(function (id) { document.getElementById(id).disabled = !enabled; });
document.querySelectorAll(".legend-float, .canvas-controls, .canvas-hint").forEach(function (el) { el.hidden = !enabled; });
}
function initCanvasControls() {
var wrap = panelFor("trace");
var svg = document.getElementById("graph-canvas");
function fit() {
if (!graphState) return;
fitView(graphState.nodes);
graphState.applyView();
}
function zoom(factor) {
if (!graphState) return;
zoomAt(wrap.clientWidth / 2, wrap.clientHeight / 2, factor);
graphState.applyView();
}
document.getElementById("fit-btn").onclick = fit;
document.getElementById("zoom-fit").onclick = fit;
document.getElementById("reset-btn").onclick = function () {
if (!graphState) return;
graphView = { x: 24, y: 24, k: 1 };
graphState.applyView();
updateZoomLabel();
};
document.getElementById("zoom-in").onclick = function () { zoom(1.2); };
document.getElementById("zoom-out").onclick = function () { zoom(1 / 1.2); };
svg.addEventListener("wheel", function (e) {
if (!graphState) return;
e.preventDefault();
var pt = svgPoint(svg, e.clientX, e.clientY);
zoomAt(pt.x, pt.y, Math.exp(-Math.max(-120,Math.min(120,e.deltaY)) * .002));
graphState.applyView();
}, { passive: false });
var pointers = new Map();
var dragged = false;
function gesture() {
var p = Array.from(pointers.values());
return { x: p.reduce(function (sum, point) { return sum + point.x; },0) / p.length, y: p.reduce(function (sum, point) { return sum + point.y; },0) / p.length, distance: p.length > 1 ? Math.hypot(p[0].x-p[1].x,p[0].y-p[1].y) : 0 };
}
svg.addEventListener("pointerdown", function (e) {
if (!graphState || e.button !== 0) return;
if (!pointers.size) dragged = false;
pointers.set(e.pointerId, { x: e.clientX, y: e.clientY });
// Capture only the background initially; node clicks retain their native target.
if (!e.target.closest(".node")) svg.setPointerCapture(e.pointerId);
});
svg.addEventListener("pointermove", function (e) {
if (!pointers.has(e.pointerId)) return;
var before = gesture();
var previous = pointers.get(e.pointerId);
if (!dragged && Math.hypot(previous.x-e.clientX,previous.y-e.clientY) < 3) return;
dragged = true;
svg.setPointerCapture(e.pointerId);
pointers.set(e.pointerId, { x: e.clientX, y: e.clientY });
var after = gesture();
graphView.x += after.x-before.x;
graphView.y += after.y-before.y;
if (before.distance && after.distance) {
var pt = svgPoint(svg, after.x, after.y);
zoomAt(pt.x, pt.y, after.distance / before.distance);
}
svg.classList.add("is-panning");
hideTooltip();
graphState.applyView();
});
function release(e) {
pointers.delete(e.pointerId);
if (svg.hasPointerCapture(e.pointerId)) svg.releasePointerCapture(e.pointerId);
if (!pointers.size) svg.classList.remove("is-panning");
}
svg.addEventListener("pointerup", release);
svg.addEventListener("pointercancel", release);
svg.addEventListener("lostpointercapture", function (e) { pointers.delete(e.pointerId); });
svg.addEventListener("click", function (e) {
if (dragged) { e.stopPropagation(); dragged = false; }
}, true);
svg.addEventListener("keydown", function (e) {
if (!graphState || e.target !== svg) return;
var moves = { ArrowLeft: [40,0], ArrowRight: [-40,0], ArrowUp: [0,40], ArrowDown: [0,-40] };
if (!moves[e.key]) return;
e.preventDefault();
graphView.x += moves[e.key][0];
graphView.y += moves[e.key][1];
graphState.applyView();
});
}
function initTraceUtilities() {
var legend = document.querySelector("[data-legend]");
var legendToggle = document.getElementById("legend-toggle");
if (legend && legendToggle) {
if (matchMedia("(max-width: 900px)").matches) {
legend.classList.add("collapsed");
legendToggle.setAttribute("aria-expanded", "false");
}
legendToggle.onclick = function () {
var collapsed = legend.classList.toggle("collapsed");
legendToggle.setAttribute("aria-expanded", collapsed ? "false" : "true");
};
}
var exportButton = document.getElementById("export-btn");
if (exportButton) {
exportButton.onclick = function () {
var svg = document.getElementById("graph-canvas");
if (!svg || !svg.childNodes.length) return;
var clone = svg.cloneNode(true);
clone.setAttribute("xmlns", "http://www.w3.org/2000/svg");
var originals = [svg].concat(Array.from(svg.querySelectorAll("*")));
var copies = [clone].concat(Array.from(clone.querySelectorAll("*")));
originals.forEach(function (element, index) {
var computed = getComputedStyle(element);
["fill", "stroke", "stroke-width", "stroke-dasharray", "stroke-linecap", "stroke-linejoin", "font-family", "font-size", "font-weight", "letter-spacing", "text-anchor"].forEach(function (property) {
var value = computed.getPropertyValue(property);
// SVG readers need legacy RGB even when the browser resolves color-mix to CSS Color 4.
var srgb = value.match(/^color\(srgb ([\d.e+-]+) ([\d.e+-]+) ([\d.e+-]+)(?: \/ ([\d.]+))?\)$/);
if (srgb) value = "rgba(" + srgb.slice(1,4).map(function (channel) { return Math.round(Number(channel) * 255); }).join(",") + "," + (srgb[4] || "1") + ")";
copies[index].style.setProperty(property, value);
if (property === "fill" || property === "stroke") copies[index].setAttribute(property, value);
});
copies[index].style.opacity = computed.opacity;
copies[index].removeAttribute("tabindex");
});
var nodes = graphState.nodes;
var minX = Math.min.apply(null,nodes.map(function (n) { return n._x; })) - 24;
var minY = Math.min.apply(null,nodes.map(function (n) { return n._y; })) - 24;
var width = Math.max.apply(null,nodes.map(function (n) { return n._x+n._w; })) - minX + 24;
var height = Math.max.apply(null,nodes.map(function (n) { return n._y+n._h; })) - minY + 24;
clone.setAttribute("viewBox", [minX,minY,width,height].join(" "));
clone.setAttribute("width",width);
clone.setAttribute("height",height);
clone.removeAttribute("id");
clone.querySelector("g").removeAttribute("transform");
var backdrop = document.createElementNS(svg.namespaceURI,"rect");
backdrop.setAttribute("x",minX); backdrop.setAttribute("y",minY);
backdrop.setAttribute("width",width); backdrop.setAttribute("height",height);
backdrop.setAttribute("fill",getComputedStyle(root).getPropertyValue("--canvas").trim());
clone.insertBefore(backdrop,clone.firstChild);
var blob = new Blob([new XMLSerializer().serializeToString(clone)], { type: "image/svg+xml" });
var href = URL.createObjectURL(blob);
var link = document.createElement("a");
link.href = href;
link.download = "candle-graph-trace.svg";
link.click();
announce("Graph exported as SVG.");
setTimeout(function () { URL.revokeObjectURL(href); }, 0);
};
}
}
function updateThemeButton() {
var next = root.getAttribute("data-theme") === "dark" ? "Light" : "Dark";
var button = document.getElementById("theme-btn");
button.textContent = next + " theme";
button.setAttribute("aria-label", "Switch to " + next.toLowerCase() + " theme");
document.querySelector('meta[name="theme-color"]').content = getComputedStyle(root).getPropertyValue("--bg").trim();
}
document.getElementById("theme-btn").onclick = function () {
var next = root.getAttribute("data-theme") === "dark" ? "light" : "dark";
root.setAttribute("data-theme", next);
try { localStorage.setItem("cg-theme", next); } catch (_) { /* Viewing stays available without storage. */ }
updateThemeButton();
};
document.querySelectorAll('input[name="heat-mode"]').forEach(function (input) {
if (input.value === "memory" && SUM.logical_peak_live_bytes == null) {
input.disabled = true;
input.parentElement.title = "Logical storage evidence was not captured.";
input.parentElement.append(" (unavailable)");
}
input.addEventListener("change", function () {
if (!input.checked) return;
heatMode = input.value;
if (graphState) graphState.applyHeat();
});
});
function closeInspector() {
selectedId = null;
setInspector(null);
if (graphState) graphState.updateHighlight();
buildSpanTree();
if (inspectorReturnFocus && inspectorReturnFocus.isConnected && inspectorReturnFocus.getClientRects().length) inspectorReturnFocus.focus();
else panelFor(currentView).focus();
}
document.getElementById("close-inspector").onclick = closeInspector;
document.getElementById("selection-graph-link").onclick = function (event) {
if (!matchMedia("(max-width: 900px)").matches) return;
event.preventDefault();
var nodeId = selectedId;
inspectorOpen = false;
hierarchyVisible = false;
selectView("trace", false);
history.pushState(null, "", "#trace?node=" + encodeURIComponent(nodeId));
highlightNode(nodeId);
centerOnNode(nodeId);
document.getElementById("graph-canvas").focus({ preventScroll: true });
};
document.getElementById("hierarchy-btn").onclick = function () {
hierarchyVisible = !hierarchyVisible;
updatePanes();
if (hierarchyVisible) document.getElementById("span-search").focus();
};
document.getElementById("close-hierarchy").onclick = function () {
hierarchyVisible = false; updatePanes(); document.getElementById("hierarchy-btn").focus();
};
matchMedia("(max-width: 900px)").addEventListener("change", function (event) {
hierarchyVisible = !event.matches; updatePanes();
document.querySelector("[data-legend]").classList.toggle("collapsed", event.matches);
document.getElementById("legend-toggle").setAttribute("aria-expanded", String(!event.matches));
});
var guide = document.getElementById("guide-dialog");
document.getElementById("help-btn").onclick = function () { guide.showModal(); };
document.getElementById("close-guide").onclick = function () { guide.close(); };
document.addEventListener("keydown", function (event) {
if (event.ctrlKey || event.metaKey || event.altKey || event.target.closest('input, select, textarea, [contenteditable="true"]')) return;
if (event.key === "?" && !guide.open) { event.preventDefault(); guide.showModal(); return; }
if (guide.open) return;
if (event.key === "Escape" && selectedId != null) { event.preventDefault(); closeInspector(); return; }
if (event.key === "/" && (currentView === "trace" || panelFor(currentView).querySelector('[data-table-filter]'))) {
event.preventDefault();
if (currentView === "trace") {
hierarchyVisible = true; updatePanes(); document.getElementById("span-search").focus();
} else panelFor(currentView).querySelector('[data-table-filter]')?.focus();
return;
}
if (currentView !== "trace" || !graphState || document.querySelector(".pane-canvas").inert) return;
if (event.key.toLowerCase() === "f") { event.preventDefault(); document.getElementById("fit-btn").click(); }
if (["+", "=", "-", "−"].includes(event.key)) {
event.preventDefault(); document.getElementById(["+", "="].includes(event.key) ? "zoom-in" : "zoom-out").click();
}
});
document.addEventListener("input", function (event) {
if (!event.target.matches('[data-table-filter]')) return;
var browser = event.target.closest('[data-table]');
var state = tableStates.get(browser.dataset.table);
state.query = event.target.value; state.page = 0;
updateTable(browser);
});
document.addEventListener("change", function (event) {
if (!event.target.matches('[data-table-kind]')) return;
var browser = event.target.closest('[data-table]');
var state = tableStates.get(browser.dataset.table);
state.kind = event.target.value; state.page = 0;
updateTable(browser);
});
document.addEventListener("click", function (event) {
var browser = event.target.closest('[data-table]');
if (!browser) return;
var state = tableStates.get(browser.dataset.table);
var sort = event.target.closest('[data-sort]');
var page = event.target.closest('[data-page]');
if (sort) {
state.descending = state.sort === sort.dataset.sort ? !state.descending : typeof state.rows[0][sort.dataset.sort] === "number";
state.sort = sort.dataset.sort; state.page = 0; updateTable(browser, state.sort);
} else if (page) {
state.page += Number(page.dataset.page); updateTable(browser);
var replacement = browser.querySelector('[data-page="' + page.dataset.page + '"]');
(replacement.disabled ? browser.querySelector('.table-wrap') : replacement).focus({ preventScroll: true });
} else if (event.target.closest('[data-table-clear]')) {
state.query = ""; state.kind = "all"; state.page = 0;
browser.querySelector('[data-table-filter]').value = "";
if (browser.querySelector('[data-table-kind]')) browser.querySelector('[data-table-kind]').value = "all";
updateTable(browser); browser.querySelector('[data-table-filter]').focus();
} else {
var button = event.target.closest('[data-span-cost-id]');
if (!button) return;
selectedId = button.dataset.spanCostId;
browser.querySelectorAll('tr.sel').forEach(function (tr) { tr.classList.remove('sel'); });
button.closest('tr').classList.add('sel');
setInspector(state.rows.find(function (row) { return idStr(row.id) === selectedId; }));
announce(button.textContent + " selected. Details opened.");
}
});
function followLocation() {
var parts = location.hash.slice(1).split("?");
var view = VIEW_META.find(function (m) { return m.id === parts[0]; });
var params = new URLSearchParams(parts[1] || "");
selectView(view ? view.id : P.default_view, false);
var node = params.get("node");
if (currentView === "trace" && node) {
var record = (V.trace.nodes || []).find(function (n) { return idStr(n.id) === node; });
if (record) { selectedId = node; setInspector(record); highlightNode(node); revealInTree(node); centerOnNode(node); }
}
var section = params.get("section");
var element = section && document.getElementById(section);
if (element && panelFor(currentView).contains(element)) {
if (element.tagName === "DETAILS") element.open = true;
element.scrollIntoView({ block: "start" });
var focus = element.querySelector('summary, input, button');
if (focus) focus.focus({ preventScroll: true });
}
}
window.addEventListener("hashchange", followLocation);
window.addEventListener("popstate", followLocation);
initResizers();
initTabs();
initCanvasControls();
initTraceUtilities();
renderCoverage();
renderPeakBreakdown();
buildSpanTree();
setInspector(null);
updateThemeButton();
graphView._fit = true;
followLocation();
})();