2025-09-09 23:06:45 +01:00
|
|
|
const EMBEDDED_DATA = {{FLAMEGRAPH_DATA}};
|
|
|
|
|
|
2025-09-14 23:47:14 +01:00
|
|
|
// Global string table for resolving string indices
|
|
|
|
|
let stringTable = [];
|
2025-09-25 15:34:57 +01:00
|
|
|
let originalData = null;
|
|
|
|
|
let currentThreadFilter = 'all';
|
2025-09-14 23:47:14 +01:00
|
|
|
|
2025-12-01 17:34:14 +00:00
|
|
|
// Heat colors are now defined in CSS variables (--heat-1 through --heat-8)
|
|
|
|
|
// and automatically switch with theme changes - no JS color arrays needed!
|
|
|
|
|
|
|
|
|
|
// ============================================================================
|
|
|
|
|
// String Resolution
|
|
|
|
|
// ============================================================================
|
|
|
|
|
|
2025-09-14 23:47:14 +01:00
|
|
|
function resolveString(index) {
|
2025-12-01 17:34:14 +00:00
|
|
|
if (index === null || index === undefined) {
|
|
|
|
|
return null;
|
|
|
|
|
}
|
|
|
|
|
if (typeof index === 'number' && index >= 0 && index < stringTable.length) {
|
|
|
|
|
return stringTable[index];
|
|
|
|
|
}
|
|
|
|
|
return String(index);
|
2025-09-14 23:47:14 +01:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
function resolveStringIndices(node) {
|
2025-12-01 17:34:14 +00:00
|
|
|
if (!node) return node;
|
2025-09-14 23:47:14 +01:00
|
|
|
|
2025-12-01 17:34:14 +00:00
|
|
|
const resolved = { ...node };
|
2025-09-14 23:47:14 +01:00
|
|
|
|
2025-12-01 17:34:14 +00:00
|
|
|
if (typeof resolved.name === 'number') {
|
|
|
|
|
resolved.name = resolveString(resolved.name);
|
|
|
|
|
}
|
|
|
|
|
if (typeof resolved.filename === 'number') {
|
|
|
|
|
resolved.filename = resolveString(resolved.filename);
|
|
|
|
|
}
|
|
|
|
|
if (typeof resolved.funcname === 'number') {
|
|
|
|
|
resolved.funcname = resolveString(resolved.funcname);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
if (Array.isArray(resolved.source)) {
|
|
|
|
|
resolved.source = resolved.source.map(index =>
|
|
|
|
|
typeof index === 'number' ? resolveString(index) : index
|
|
|
|
|
);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
if (Array.isArray(resolved.children)) {
|
|
|
|
|
resolved.children = resolved.children.map(child => resolveStringIndices(child));
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
return resolved;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// ============================================================================
|
|
|
|
|
// Theme & UI Controls
|
|
|
|
|
// ============================================================================
|
|
|
|
|
|
|
|
|
|
function toggleTheme() {
|
|
|
|
|
const html = document.documentElement;
|
|
|
|
|
const current = html.getAttribute('data-theme') || 'light';
|
|
|
|
|
const next = current === 'light' ? 'dark' : 'light';
|
|
|
|
|
html.setAttribute('data-theme', next);
|
|
|
|
|
localStorage.setItem('flamegraph-theme', next);
|
|
|
|
|
|
|
|
|
|
// Update theme button icon
|
|
|
|
|
const btn = document.getElementById('theme-btn');
|
|
|
|
|
if (btn) {
|
|
|
|
|
btn.innerHTML = next === 'dark' ? '☼' : '☾'; // sun or moon
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// Re-render flamegraph with new theme colors
|
|
|
|
|
if (window.flamegraphData && originalData) {
|
|
|
|
|
const tooltip = createPythonTooltip(originalData);
|
|
|
|
|
const chart = createFlamegraph(tooltip, originalData.value);
|
|
|
|
|
renderFlamegraph(chart, window.flamegraphData);
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
function toggleSidebar() {
|
|
|
|
|
const sidebar = document.getElementById('sidebar');
|
|
|
|
|
if (sidebar) {
|
|
|
|
|
const isCollapsing = !sidebar.classList.contains('collapsed');
|
|
|
|
|
|
|
|
|
|
if (isCollapsing) {
|
|
|
|
|
// Save current width before collapsing
|
|
|
|
|
const currentWidth = sidebar.offsetWidth;
|
|
|
|
|
sidebar.dataset.expandedWidth = currentWidth;
|
|
|
|
|
localStorage.setItem('flamegraph-sidebar-width', currentWidth);
|
|
|
|
|
} else {
|
|
|
|
|
// Restore width when expanding
|
|
|
|
|
const savedWidth = sidebar.dataset.expandedWidth || localStorage.getItem('flamegraph-sidebar-width');
|
|
|
|
|
if (savedWidth) {
|
|
|
|
|
sidebar.style.width = savedWidth + 'px';
|
|
|
|
|
}
|
2025-09-14 23:47:14 +01:00
|
|
|
}
|
2025-12-01 17:34:14 +00:00
|
|
|
|
|
|
|
|
sidebar.classList.toggle('collapsed');
|
|
|
|
|
localStorage.setItem('flamegraph-sidebar', sidebar.classList.contains('collapsed') ? 'collapsed' : 'expanded');
|
|
|
|
|
|
|
|
|
|
// Resize chart after sidebar animation
|
|
|
|
|
setTimeout(() => {
|
|
|
|
|
resizeChart();
|
|
|
|
|
}, 300);
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
function resizeChart() {
|
|
|
|
|
if (window.flamegraphChart && window.flamegraphData) {
|
|
|
|
|
const chartArea = document.querySelector('.chart-area');
|
|
|
|
|
if (chartArea) {
|
|
|
|
|
window.flamegraphChart.width(chartArea.clientWidth - 32);
|
|
|
|
|
d3.select("#chart").datum(window.flamegraphData).call(window.flamegraphChart);
|
2025-09-14 23:47:14 +01:00
|
|
|
}
|
2025-12-01 17:34:14 +00:00
|
|
|
}
|
|
|
|
|
}
|
2025-09-14 23:47:14 +01:00
|
|
|
|
2025-12-01 17:34:14 +00:00
|
|
|
function toggleSection(sectionId) {
|
|
|
|
|
const section = document.getElementById(sectionId);
|
|
|
|
|
if (section) {
|
|
|
|
|
section.classList.toggle('collapsed');
|
|
|
|
|
// Save state
|
|
|
|
|
const collapsedSections = JSON.parse(localStorage.getItem('flamegraph-collapsed-sections') || '{}');
|
|
|
|
|
collapsedSections[sectionId] = section.classList.contains('collapsed');
|
|
|
|
|
localStorage.setItem('flamegraph-collapsed-sections', JSON.stringify(collapsedSections));
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
function restoreUIState() {
|
|
|
|
|
// Restore theme
|
|
|
|
|
const savedTheme = localStorage.getItem('flamegraph-theme');
|
|
|
|
|
if (savedTheme) {
|
|
|
|
|
document.documentElement.setAttribute('data-theme', savedTheme);
|
|
|
|
|
const btn = document.getElementById('theme-btn');
|
|
|
|
|
if (btn) {
|
|
|
|
|
btn.innerHTML = savedTheme === 'dark' ? '☼' : '☾';
|
2025-09-14 23:47:14 +01:00
|
|
|
}
|
2025-12-01 17:34:14 +00:00
|
|
|
}
|
2025-09-14 23:47:14 +01:00
|
|
|
|
2025-12-01 17:34:14 +00:00
|
|
|
// Restore sidebar state
|
|
|
|
|
const savedSidebar = localStorage.getItem('flamegraph-sidebar');
|
|
|
|
|
if (savedSidebar === 'collapsed') {
|
|
|
|
|
const sidebar = document.getElementById('sidebar');
|
|
|
|
|
if (sidebar) sidebar.classList.add('collapsed');
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// Restore sidebar width
|
|
|
|
|
const savedWidth = localStorage.getItem('flamegraph-sidebar-width');
|
|
|
|
|
if (savedWidth) {
|
|
|
|
|
const sidebar = document.getElementById('sidebar');
|
|
|
|
|
if (sidebar) {
|
|
|
|
|
sidebar.style.width = savedWidth + 'px';
|
2025-09-14 23:47:14 +01:00
|
|
|
}
|
2025-12-01 17:34:14 +00:00
|
|
|
}
|
2025-09-14 23:47:14 +01:00
|
|
|
|
2025-12-01 17:34:14 +00:00
|
|
|
// Restore collapsed sections
|
|
|
|
|
const collapsedSections = JSON.parse(localStorage.getItem('flamegraph-collapsed-sections') || '{}');
|
|
|
|
|
for (const [sectionId, isCollapsed] of Object.entries(collapsedSections)) {
|
|
|
|
|
if (isCollapsed) {
|
|
|
|
|
const section = document.getElementById(sectionId);
|
|
|
|
|
if (section) section.classList.add('collapsed');
|
|
|
|
|
}
|
|
|
|
|
}
|
2025-09-14 23:47:14 +01:00
|
|
|
}
|
|
|
|
|
|
2025-12-01 17:34:14 +00:00
|
|
|
// ============================================================================
|
|
|
|
|
// Status Bar
|
|
|
|
|
// ============================================================================
|
|
|
|
|
|
|
|
|
|
function updateStatusBar(nodeData, rootValue) {
|
|
|
|
|
const funcname = resolveString(nodeData.funcname) || resolveString(nodeData.name) || "--";
|
|
|
|
|
const filename = resolveString(nodeData.filename) || "";
|
|
|
|
|
const lineno = nodeData.lineno;
|
|
|
|
|
const timeMs = (nodeData.value / 1000).toFixed(2);
|
|
|
|
|
const percent = rootValue > 0 ? ((nodeData.value / rootValue) * 100).toFixed(1) : "0.0";
|
|
|
|
|
|
|
|
|
|
const locationEl = document.getElementById('status-location');
|
|
|
|
|
const funcItem = document.getElementById('status-func-item');
|
|
|
|
|
const timeItem = document.getElementById('status-time-item');
|
|
|
|
|
const percentItem = document.getElementById('status-percent-item');
|
|
|
|
|
|
|
|
|
|
if (locationEl) locationEl.style.display = filename && filename !== "~" ? 'flex' : 'none';
|
|
|
|
|
if (funcItem) funcItem.style.display = 'flex';
|
|
|
|
|
if (timeItem) timeItem.style.display = 'flex';
|
|
|
|
|
if (percentItem) percentItem.style.display = 'flex';
|
|
|
|
|
|
|
|
|
|
const fileEl = document.getElementById('status-file');
|
|
|
|
|
if (fileEl && filename && filename !== "~") {
|
|
|
|
|
const basename = filename.split('/').pop();
|
|
|
|
|
fileEl.textContent = lineno ? `${basename}:${lineno}` : basename;
|
2025-09-09 23:06:45 +01:00
|
|
|
}
|
2025-12-01 17:34:14 +00:00
|
|
|
|
|
|
|
|
const funcEl = document.getElementById('status-func');
|
|
|
|
|
if (funcEl) funcEl.textContent = funcname.length > 40 ? funcname.substring(0, 37) + '...' : funcname;
|
|
|
|
|
|
|
|
|
|
const timeEl = document.getElementById('status-time');
|
|
|
|
|
if (timeEl) timeEl.textContent = `${timeMs} ms`;
|
|
|
|
|
|
|
|
|
|
const percentEl = document.getElementById('status-percent');
|
|
|
|
|
if (percentEl) percentEl.textContent = `${percent}%`;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
function clearStatusBar() {
|
|
|
|
|
const ids = ['status-location', 'status-func-item', 'status-time-item', 'status-percent-item'];
|
|
|
|
|
ids.forEach(id => {
|
|
|
|
|
const el = document.getElementById(id);
|
|
|
|
|
if (el) el.style.display = 'none';
|
|
|
|
|
});
|
2025-09-09 23:06:45 +01:00
|
|
|
}
|
|
|
|
|
|
2025-12-01 17:34:14 +00:00
|
|
|
// ============================================================================
|
|
|
|
|
// Tooltip
|
|
|
|
|
// ============================================================================
|
|
|
|
|
|
2025-09-09 23:06:45 +01:00
|
|
|
function createPythonTooltip(data) {
|
|
|
|
|
const pythonTooltip = flamegraph.tooltip.defaultFlamegraphTooltip();
|
2025-12-01 17:34:14 +00:00
|
|
|
|
2025-09-09 23:06:45 +01:00
|
|
|
pythonTooltip.show = function (d, element) {
|
|
|
|
|
if (!this._tooltip) {
|
2025-12-01 17:34:14 +00:00
|
|
|
this._tooltip = d3.select("body")
|
2025-09-09 23:06:45 +01:00
|
|
|
.append("div")
|
|
|
|
|
.attr("class", "python-tooltip")
|
|
|
|
|
.style("opacity", 0);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
const timeMs = (d.data.value / 1000).toFixed(2);
|
|
|
|
|
const percentage = ((d.data.value / data.value) * 100).toFixed(2);
|
|
|
|
|
const calls = d.data.calls || 0;
|
|
|
|
|
const childCount = d.children ? d.children.length : 0;
|
|
|
|
|
const source = d.data.source;
|
|
|
|
|
|
2025-12-01 17:34:14 +00:00
|
|
|
const funcname = resolveString(d.data.funcname) || resolveString(d.data.name);
|
|
|
|
|
const filename = resolveString(d.data.filename) || "";
|
|
|
|
|
const isSpecialFrame = filename === "~";
|
|
|
|
|
|
|
|
|
|
// Build source section
|
2025-09-09 23:06:45 +01:00
|
|
|
let sourceSection = "";
|
|
|
|
|
if (source && Array.isArray(source) && source.length > 0) {
|
|
|
|
|
const sourceLines = source
|
2025-12-01 17:34:14 +00:00
|
|
|
.map((line) => {
|
|
|
|
|
const isCurrent = line.startsWith("→");
|
|
|
|
|
const escaped = line.replace(/&/g, "&").replace(/</g, "<").replace(/>/g, ">");
|
|
|
|
|
return `<div class="tooltip-source-line${isCurrent ? ' current' : ''}">${escaped}</div>`;
|
|
|
|
|
})
|
2025-09-09 23:06:45 +01:00
|
|
|
.join("");
|
|
|
|
|
|
|
|
|
|
sourceSection = `
|
2025-12-01 17:34:14 +00:00
|
|
|
<div class="tooltip-source">
|
|
|
|
|
<div class="tooltip-source-title">Source Code:</div>
|
|
|
|
|
<div class="tooltip-source-code">${sourceLines}</div>
|
2025-09-09 23:06:45 +01:00
|
|
|
</div>`;
|
|
|
|
|
}
|
|
|
|
|
|
2025-11-17 05:39:00 -08:00
|
|
|
const fileLocationHTML = isSpecialFrame ? "" : `
|
2025-12-01 17:34:14 +00:00
|
|
|
<div class="tooltip-location">${filename}${d.data.lineno ? ":" + d.data.lineno : ""}</div>`;
|
2025-11-17 05:39:00 -08:00
|
|
|
|
2025-09-09 23:06:45 +01:00
|
|
|
const tooltipHTML = `
|
2025-12-01 17:34:14 +00:00
|
|
|
<div class="tooltip-header">
|
|
|
|
|
<div class="tooltip-title">${funcname}</div>
|
2025-11-17 05:39:00 -08:00
|
|
|
${fileLocationHTML}
|
2025-12-01 17:34:14 +00:00
|
|
|
</div>
|
|
|
|
|
<div class="tooltip-stats">
|
|
|
|
|
<span class="tooltip-stat-label">Execution Time:</span>
|
|
|
|
|
<span class="tooltip-stat-value">${timeMs} ms</span>
|
|
|
|
|
|
|
|
|
|
<span class="tooltip-stat-label">Percentage:</span>
|
|
|
|
|
<span class="tooltip-stat-value accent">${percentage}%</span>
|
|
|
|
|
|
|
|
|
|
${calls > 0 ? `
|
|
|
|
|
<span class="tooltip-stat-label">Function Calls:</span>
|
|
|
|
|
<span class="tooltip-stat-value">${calls.toLocaleString()}</span>
|
|
|
|
|
` : ''}
|
|
|
|
|
|
|
|
|
|
${childCount > 0 ? `
|
|
|
|
|
<span class="tooltip-stat-label">Child Functions:</span>
|
|
|
|
|
<span class="tooltip-stat-value">${childCount}</span>
|
|
|
|
|
` : ''}
|
|
|
|
|
</div>
|
|
|
|
|
${sourceSection}
|
|
|
|
|
<div class="tooltip-hint">
|
|
|
|
|
${childCount > 0 ? "Click to zoom into this function" : "Leaf function - no children"}
|
2025-09-09 23:06:45 +01:00
|
|
|
</div>
|
|
|
|
|
`;
|
|
|
|
|
|
2025-12-01 17:34:14 +00:00
|
|
|
// Position tooltip
|
2025-09-09 23:06:45 +01:00
|
|
|
const event = d3.event || window.event;
|
|
|
|
|
const mouseX = event.pageX || event.clientX;
|
|
|
|
|
const mouseY = event.pageY || event.clientY;
|
2025-12-01 17:34:14 +00:00
|
|
|
const padding = 12;
|
2025-09-09 23:06:45 +01:00
|
|
|
|
2025-12-01 17:34:14 +00:00
|
|
|
this._tooltip.html(tooltipHTML);
|
2025-09-09 23:06:45 +01:00
|
|
|
|
2025-12-01 17:34:14 +00:00
|
|
|
// Measure tooltip
|
|
|
|
|
const node = this._tooltip.style("display", "block").style("opacity", 0).node();
|
|
|
|
|
const tooltipWidth = node.offsetWidth || 320;
|
|
|
|
|
const tooltipHeight = node.offsetHeight || 200;
|
|
|
|
|
|
|
|
|
|
// Calculate position
|
|
|
|
|
let left = mouseX + padding;
|
|
|
|
|
let top = mouseY + padding;
|
|
|
|
|
|
|
|
|
|
if (left + tooltipWidth > window.innerWidth) {
|
2025-09-09 23:06:45 +01:00
|
|
|
left = mouseX - tooltipWidth - padding;
|
2025-12-01 17:34:14 +00:00
|
|
|
if (left < 0) left = padding;
|
2025-09-09 23:06:45 +01:00
|
|
|
}
|
|
|
|
|
|
2025-12-01 17:34:14 +00:00
|
|
|
if (top + tooltipHeight > window.innerHeight) {
|
2025-09-09 23:06:45 +01:00
|
|
|
top = mouseY - tooltipHeight - padding;
|
2025-12-01 17:34:14 +00:00
|
|
|
if (top < 0) top = padding;
|
2025-09-09 23:06:45 +01:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
this._tooltip
|
|
|
|
|
.style("left", left + "px")
|
|
|
|
|
.style("top", top + "px")
|
|
|
|
|
.transition()
|
2025-12-01 17:34:14 +00:00
|
|
|
.duration(150)
|
2025-09-09 23:06:45 +01:00
|
|
|
.style("opacity", 1);
|
2025-12-01 17:34:14 +00:00
|
|
|
|
|
|
|
|
// Update status bar
|
|
|
|
|
updateStatusBar(d.data, data.value);
|
2025-09-09 23:06:45 +01:00
|
|
|
};
|
|
|
|
|
|
|
|
|
|
pythonTooltip.hide = function () {
|
|
|
|
|
if (this._tooltip) {
|
2025-12-01 17:34:14 +00:00
|
|
|
this._tooltip.transition().duration(150).style("opacity", 0);
|
2025-09-09 23:06:45 +01:00
|
|
|
}
|
2025-12-01 17:34:14 +00:00
|
|
|
clearStatusBar();
|
2025-09-09 23:06:45 +01:00
|
|
|
};
|
2025-12-01 17:34:14 +00:00
|
|
|
|
2025-09-09 23:06:45 +01:00
|
|
|
return pythonTooltip;
|
|
|
|
|
}
|
|
|
|
|
|
2025-12-01 17:34:14 +00:00
|
|
|
// ============================================================================
|
|
|
|
|
// Flamegraph Creation
|
|
|
|
|
// ============================================================================
|
|
|
|
|
|
|
|
|
|
function ensureLibraryLoaded() {
|
|
|
|
|
if (typeof flamegraph === "undefined") {
|
|
|
|
|
console.error("d3-flame-graph library not loaded");
|
|
|
|
|
document.getElementById("chart").innerHTML =
|
|
|
|
|
'<div style="padding: 40px; text-align: center; color: var(--text-muted);">Error: d3-flame-graph library failed to load</div>';
|
|
|
|
|
throw new Error("d3-flame-graph library failed to load");
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
const HEAT_THRESHOLDS = [
|
|
|
|
|
[0.6, 8],
|
|
|
|
|
[0.35, 7],
|
|
|
|
|
[0.18, 6],
|
|
|
|
|
[0.12, 5],
|
|
|
|
|
[0.06, 4],
|
|
|
|
|
[0.03, 3],
|
|
|
|
|
[0.01, 2],
|
|
|
|
|
];
|
|
|
|
|
|
|
|
|
|
function getHeatLevel(percentage) {
|
|
|
|
|
for (const [threshold, level] of HEAT_THRESHOLDS) {
|
|
|
|
|
if (percentage >= threshold) return level;
|
|
|
|
|
}
|
|
|
|
|
return 1;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
function getHeatColors() {
|
|
|
|
|
const style = getComputedStyle(document.documentElement);
|
|
|
|
|
const colors = {};
|
|
|
|
|
for (let i = 1; i <= 8; i++) {
|
|
|
|
|
colors[i] = style.getPropertyValue(`--heat-${i}`).trim();
|
|
|
|
|
}
|
|
|
|
|
return colors;
|
|
|
|
|
}
|
|
|
|
|
|
2025-09-09 23:06:45 +01:00
|
|
|
function createFlamegraph(tooltip, rootValue) {
|
2025-12-01 17:34:14 +00:00
|
|
|
const chartArea = document.querySelector('.chart-area');
|
|
|
|
|
const width = chartArea ? chartArea.clientWidth - 32 : window.innerWidth - 320;
|
|
|
|
|
const heatColors = getHeatColors();
|
|
|
|
|
|
2025-09-09 23:06:45 +01:00
|
|
|
let chart = flamegraph()
|
2025-12-01 17:34:14 +00:00
|
|
|
.width(width)
|
2025-09-09 23:06:45 +01:00
|
|
|
.cellHeight(20)
|
|
|
|
|
.transitionDuration(300)
|
|
|
|
|
.minFrameSize(1)
|
|
|
|
|
.tooltip(tooltip)
|
|
|
|
|
.inverted(true)
|
|
|
|
|
.setColorMapper(function (d) {
|
|
|
|
|
const percentage = d.data.value / rootValue;
|
2025-12-01 17:34:14 +00:00
|
|
|
const level = getHeatLevel(percentage);
|
|
|
|
|
return heatColors[level];
|
2025-09-09 23:06:45 +01:00
|
|
|
});
|
2025-12-01 17:34:14 +00:00
|
|
|
|
2025-09-09 23:06:45 +01:00
|
|
|
return chart;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
function renderFlamegraph(chart, data) {
|
|
|
|
|
d3.select("#chart").datum(data).call(chart);
|
2025-12-01 17:34:14 +00:00
|
|
|
window.flamegraphChart = chart;
|
|
|
|
|
window.flamegraphData = data;
|
2025-09-09 23:06:45 +01:00
|
|
|
populateStats(data);
|
|
|
|
|
}
|
|
|
|
|
|
2025-12-01 17:34:14 +00:00
|
|
|
// ============================================================================
|
|
|
|
|
// Search
|
|
|
|
|
// ============================================================================
|
2025-09-09 23:06:45 +01:00
|
|
|
|
|
|
|
|
function updateSearchHighlight(searchTerm, searchInput) {
|
|
|
|
|
d3.selectAll("#chart rect")
|
2025-12-01 17:34:14 +00:00
|
|
|
.classed("search-match", false)
|
|
|
|
|
.classed("search-dim", false);
|
|
|
|
|
|
|
|
|
|
// Clear active state from all hotspots
|
|
|
|
|
document.querySelectorAll('.hotspot').forEach(h => h.classList.remove('active'));
|
|
|
|
|
|
2025-09-09 23:06:45 +01:00
|
|
|
if (searchTerm && searchTerm.length > 0) {
|
|
|
|
|
let matchCount = 0;
|
2025-12-01 17:34:14 +00:00
|
|
|
|
2025-09-09 23:06:45 +01:00
|
|
|
d3.selectAll("#chart rect").each(function (d) {
|
|
|
|
|
if (d && d.data) {
|
2025-09-14 23:47:14 +01:00
|
|
|
const name = resolveString(d.data.name) || "";
|
|
|
|
|
const funcname = resolveString(d.data.funcname) || "";
|
|
|
|
|
const filename = resolveString(d.data.filename) || "";
|
2025-12-01 17:34:14 +00:00
|
|
|
const lineno = d.data.lineno;
|
2025-09-09 23:06:45 +01:00
|
|
|
const term = searchTerm.toLowerCase();
|
2025-12-01 17:34:14 +00:00
|
|
|
|
|
|
|
|
// Check if search term looks like file:line pattern
|
|
|
|
|
const fileLineMatch = term.match(/^(.+):(\d+)$/);
|
|
|
|
|
let matches = false;
|
|
|
|
|
|
|
|
|
|
if (fileLineMatch) {
|
|
|
|
|
// Exact file:line matching
|
|
|
|
|
const searchFile = fileLineMatch[1];
|
|
|
|
|
const searchLine = parseInt(fileLineMatch[2], 10);
|
|
|
|
|
const basename = filename.split('/').pop().toLowerCase();
|
|
|
|
|
matches = basename.includes(searchFile) && lineno === searchLine;
|
|
|
|
|
} else {
|
|
|
|
|
// Regular substring search
|
|
|
|
|
matches =
|
|
|
|
|
name.toLowerCase().includes(term) ||
|
|
|
|
|
funcname.toLowerCase().includes(term) ||
|
|
|
|
|
filename.toLowerCase().includes(term);
|
|
|
|
|
}
|
|
|
|
|
|
2025-09-09 23:06:45 +01:00
|
|
|
if (matches) {
|
|
|
|
|
matchCount++;
|
2025-12-01 17:34:14 +00:00
|
|
|
d3.select(this).classed("search-match", true);
|
|
|
|
|
} else {
|
|
|
|
|
d3.select(this).classed("search-dim", true);
|
2025-09-09 23:06:45 +01:00
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
});
|
2025-12-01 17:34:14 +00:00
|
|
|
|
2025-09-09 23:06:45 +01:00
|
|
|
if (searchInput) {
|
2025-12-01 17:34:14 +00:00
|
|
|
searchInput.classList.remove("has-matches", "no-matches");
|
|
|
|
|
searchInput.classList.add(matchCount > 0 ? "has-matches" : "no-matches");
|
2025-09-09 23:06:45 +01:00
|
|
|
}
|
2025-12-01 17:34:14 +00:00
|
|
|
|
|
|
|
|
// Mark matching hotspot as active
|
|
|
|
|
document.querySelectorAll('.hotspot').forEach(h => {
|
|
|
|
|
if (h.dataset.searchterm && h.dataset.searchterm.toLowerCase() === searchTerm.toLowerCase()) {
|
|
|
|
|
h.classList.add('active');
|
|
|
|
|
}
|
|
|
|
|
});
|
2025-09-09 23:06:45 +01:00
|
|
|
} else if (searchInput) {
|
2025-12-01 17:34:14 +00:00
|
|
|
searchInput.classList.remove("has-matches", "no-matches");
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
function searchForHotspot(funcname) {
|
|
|
|
|
const searchInput = document.getElementById('search-input');
|
|
|
|
|
const searchWrapper = document.querySelector('.search-wrapper');
|
|
|
|
|
if (searchInput) {
|
|
|
|
|
// Toggle: if already searching for this term, clear it
|
|
|
|
|
if (searchInput.value.trim() === funcname) {
|
|
|
|
|
clearSearch();
|
|
|
|
|
} else {
|
|
|
|
|
searchInput.value = funcname;
|
|
|
|
|
if (searchWrapper) {
|
|
|
|
|
searchWrapper.classList.add('has-value');
|
|
|
|
|
}
|
|
|
|
|
performSearch();
|
|
|
|
|
}
|
2025-09-09 23:06:45 +01:00
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
function initSearchHandlers() {
|
|
|
|
|
const searchInput = document.getElementById("search-input");
|
2025-12-01 17:34:14 +00:00
|
|
|
const searchWrapper = document.querySelector(".search-wrapper");
|
2025-09-09 23:06:45 +01:00
|
|
|
if (!searchInput) return;
|
2025-12-01 17:34:14 +00:00
|
|
|
|
2025-09-09 23:06:45 +01:00
|
|
|
let searchTimeout;
|
|
|
|
|
function performSearch() {
|
|
|
|
|
const term = searchInput.value.trim();
|
|
|
|
|
updateSearchHighlight(term, searchInput);
|
2025-12-01 17:34:14 +00:00
|
|
|
// Toggle has-value class for clear button visibility
|
|
|
|
|
if (searchWrapper) {
|
|
|
|
|
searchWrapper.classList.toggle("has-value", term.length > 0);
|
|
|
|
|
}
|
2025-09-09 23:06:45 +01:00
|
|
|
}
|
2025-12-01 17:34:14 +00:00
|
|
|
|
2025-09-09 23:06:45 +01:00
|
|
|
searchInput.addEventListener("input", function () {
|
|
|
|
|
clearTimeout(searchTimeout);
|
|
|
|
|
searchTimeout = setTimeout(performSearch, 150);
|
|
|
|
|
});
|
2025-12-01 17:34:14 +00:00
|
|
|
|
2025-09-09 23:06:45 +01:00
|
|
|
window.performSearch = performSearch;
|
|
|
|
|
}
|
|
|
|
|
|
2025-12-01 17:34:14 +00:00
|
|
|
function clearSearch() {
|
|
|
|
|
const searchInput = document.getElementById("search-input");
|
|
|
|
|
const searchWrapper = document.querySelector(".search-wrapper");
|
|
|
|
|
if (searchInput) {
|
|
|
|
|
searchInput.value = "";
|
|
|
|
|
searchInput.classList.remove("has-matches", "no-matches");
|
|
|
|
|
if (searchWrapper) {
|
|
|
|
|
searchWrapper.classList.remove("has-value");
|
2025-09-09 23:06:45 +01:00
|
|
|
}
|
2025-12-01 17:34:14 +00:00
|
|
|
// Clear highlights
|
|
|
|
|
d3.selectAll("#chart rect")
|
|
|
|
|
.classed("search-match", false)
|
|
|
|
|
.classed("search-dim", false);
|
|
|
|
|
// Clear active hotspot
|
|
|
|
|
document.querySelectorAll('.hotspot').forEach(h => h.classList.remove('active'));
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// ============================================================================
|
|
|
|
|
// Resize Handler
|
|
|
|
|
// ============================================================================
|
|
|
|
|
|
|
|
|
|
function handleResize() {
|
|
|
|
|
let resizeTimeout;
|
|
|
|
|
window.addEventListener("resize", function () {
|
|
|
|
|
clearTimeout(resizeTimeout);
|
|
|
|
|
resizeTimeout = setTimeout(resizeChart, 100);
|
2025-09-09 23:06:45 +01:00
|
|
|
});
|
|
|
|
|
}
|
|
|
|
|
|
2025-12-01 17:34:14 +00:00
|
|
|
function initSidebarResize() {
|
|
|
|
|
const sidebar = document.getElementById('sidebar');
|
|
|
|
|
const resizeHandle = document.getElementById('sidebar-resize-handle');
|
|
|
|
|
if (!sidebar || !resizeHandle) return;
|
|
|
|
|
|
|
|
|
|
let isResizing = false;
|
|
|
|
|
let startX = 0;
|
|
|
|
|
let startWidth = 0;
|
|
|
|
|
const minWidth = 200;
|
|
|
|
|
const maxWidth = 600;
|
|
|
|
|
|
|
|
|
|
resizeHandle.addEventListener('mousedown', function(e) {
|
|
|
|
|
isResizing = true;
|
|
|
|
|
startX = e.clientX;
|
|
|
|
|
startWidth = sidebar.offsetWidth;
|
|
|
|
|
resizeHandle.classList.add('resizing');
|
|
|
|
|
document.body.classList.add('resizing-sidebar');
|
|
|
|
|
e.preventDefault();
|
|
|
|
|
});
|
2025-09-14 23:47:14 +01:00
|
|
|
|
2025-12-01 17:34:14 +00:00
|
|
|
document.addEventListener('mousemove', function(e) {
|
|
|
|
|
if (!isResizing) return;
|
2025-09-14 23:47:14 +01:00
|
|
|
|
2025-12-01 17:34:14 +00:00
|
|
|
const deltaX = e.clientX - startX;
|
|
|
|
|
const newWidth = Math.min(Math.max(startWidth + deltaX, minWidth), maxWidth);
|
|
|
|
|
sidebar.style.width = newWidth + 'px';
|
|
|
|
|
e.preventDefault();
|
|
|
|
|
});
|
2025-09-25 15:34:57 +01:00
|
|
|
|
2025-12-01 17:34:14 +00:00
|
|
|
document.addEventListener('mouseup', function() {
|
|
|
|
|
if (isResizing) {
|
|
|
|
|
isResizing = false;
|
|
|
|
|
resizeHandle.classList.remove('resizing');
|
|
|
|
|
document.body.classList.remove('resizing-sidebar');
|
2025-09-25 15:34:57 +01:00
|
|
|
|
2025-12-01 17:34:14 +00:00
|
|
|
// Save the new width
|
|
|
|
|
const width = sidebar.offsetWidth;
|
|
|
|
|
localStorage.setItem('flamegraph-sidebar-width', width);
|
2025-09-09 23:06:45 +01:00
|
|
|
|
2025-12-01 17:34:14 +00:00
|
|
|
// Resize chart after sidebar resize
|
|
|
|
|
setTimeout(() => {
|
|
|
|
|
resizeChart();
|
|
|
|
|
}, 10);
|
|
|
|
|
}
|
|
|
|
|
});
|
2025-09-09 23:06:45 +01:00
|
|
|
}
|
|
|
|
|
|
2025-12-01 17:34:14 +00:00
|
|
|
// ============================================================================
|
|
|
|
|
// Thread Stats
|
|
|
|
|
// ============================================================================
|
|
|
|
|
|
2025-11-30 01:42:39 +00:00
|
|
|
// Mode constants (must match constants.py)
|
|
|
|
|
const PROFILING_MODE_WALL = 0;
|
|
|
|
|
const PROFILING_MODE_CPU = 1;
|
|
|
|
|
const PROFILING_MODE_GIL = 2;
|
|
|
|
|
const PROFILING_MODE_ALL = 3;
|
|
|
|
|
|
|
|
|
|
function populateThreadStats(data, selectedThreadId = null) {
|
|
|
|
|
const stats = data?.stats;
|
|
|
|
|
if (!stats || !stats.thread_stats) {
|
2025-12-01 17:34:14 +00:00
|
|
|
return;
|
2025-11-30 01:42:39 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
const mode = stats.mode !== undefined ? stats.mode : PROFILING_MODE_WALL;
|
|
|
|
|
let threadStats;
|
|
|
|
|
|
|
|
|
|
if (selectedThreadId !== null && stats.per_thread_stats && stats.per_thread_stats[selectedThreadId]) {
|
|
|
|
|
threadStats = stats.per_thread_stats[selectedThreadId];
|
|
|
|
|
} else {
|
|
|
|
|
threadStats = stats.thread_stats;
|
|
|
|
|
}
|
|
|
|
|
|
2025-12-01 17:34:14 +00:00
|
|
|
if (!threadStats || typeof threadStats.total !== 'number' || threadStats.total <= 0) {
|
|
|
|
|
return;
|
2025-11-30 01:42:39 +00:00
|
|
|
}
|
|
|
|
|
|
2025-12-01 17:34:14 +00:00
|
|
|
const section = document.getElementById('thread-stats-bar');
|
|
|
|
|
if (!section) {
|
|
|
|
|
return;
|
2025-11-30 01:42:39 +00:00
|
|
|
}
|
|
|
|
|
|
2025-12-01 17:34:14 +00:00
|
|
|
section.style.display = 'block';
|
2025-11-30 01:42:39 +00:00
|
|
|
|
2025-12-01 17:34:14 +00:00
|
|
|
const gilHeldStat = document.getElementById('gil-held-stat');
|
|
|
|
|
const gilReleasedStat = document.getElementById('gil-released-stat');
|
|
|
|
|
const gilWaitingStat = document.getElementById('gil-waiting-stat');
|
2025-11-30 01:42:39 +00:00
|
|
|
|
2025-12-01 17:34:14 +00:00
|
|
|
if (mode === PROFILING_MODE_GIL) {
|
|
|
|
|
// In GIL mode, hide GIL-related stats
|
|
|
|
|
if (gilHeldStat) gilHeldStat.style.display = 'none';
|
|
|
|
|
if (gilReleasedStat) gilReleasedStat.style.display = 'none';
|
|
|
|
|
if (gilWaitingStat) gilWaitingStat.style.display = 'none';
|
|
|
|
|
} else {
|
|
|
|
|
// Show all stats
|
|
|
|
|
if (gilHeldStat) gilHeldStat.style.display = 'block';
|
|
|
|
|
if (gilReleasedStat) gilReleasedStat.style.display = 'block';
|
|
|
|
|
if (gilWaitingStat) gilWaitingStat.style.display = 'block';
|
2025-11-30 01:42:39 +00:00
|
|
|
|
2025-12-01 17:34:14 +00:00
|
|
|
const gilHeldPctElem = document.getElementById('gil-held-pct');
|
|
|
|
|
if (gilHeldPctElem) gilHeldPctElem.textContent = `${(threadStats.has_gil_pct || 0).toFixed(1)}%`;
|
|
|
|
|
|
|
|
|
|
const gilReleasedPctElem = document.getElementById('gil-released-pct');
|
|
|
|
|
if (gilReleasedPctElem) gilReleasedPctElem.textContent = `${(threadStats.on_cpu_pct || 0).toFixed(1)}%`;
|
|
|
|
|
|
|
|
|
|
const gilWaitingPctElem = document.getElementById('gil-waiting-pct');
|
|
|
|
|
if (gilWaitingPctElem) gilWaitingPctElem.textContent = `${(threadStats.gil_requested_pct || 0).toFixed(1)}%`;
|
2025-11-30 01:42:39 +00:00
|
|
|
}
|
2025-12-01 17:34:14 +00:00
|
|
|
|
|
|
|
|
const gcPctElem = document.getElementById('gc-pct');
|
|
|
|
|
if (gcPctElem) gcPctElem.textContent = `${(threadStats.gc_pct || 0).toFixed(1)}%`;
|
2025-11-30 01:42:39 +00:00
|
|
|
}
|
|
|
|
|
|
2025-12-01 17:34:14 +00:00
|
|
|
// ============================================================================
|
|
|
|
|
// Profile Summary Stats
|
|
|
|
|
// ============================================================================
|
|
|
|
|
|
|
|
|
|
function formatNumber(num) {
|
|
|
|
|
if (num >= 1000000) return (num / 1000000).toFixed(1) + 'M';
|
|
|
|
|
if (num >= 1000) return (num / 1000).toFixed(1) + 'K';
|
|
|
|
|
return num.toLocaleString();
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
function formatDuration(seconds) {
|
|
|
|
|
if (seconds >= 3600) {
|
|
|
|
|
const h = Math.floor(seconds / 3600);
|
|
|
|
|
const m = Math.floor((seconds % 3600) / 60);
|
|
|
|
|
return `${h}h ${m}m`;
|
|
|
|
|
}
|
|
|
|
|
if (seconds >= 60) {
|
|
|
|
|
const m = Math.floor(seconds / 60);
|
|
|
|
|
const s = Math.floor(seconds % 60);
|
|
|
|
|
return `${m}m ${s}s`;
|
|
|
|
|
}
|
|
|
|
|
return seconds.toFixed(2) + 's';
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
function populateProfileSummary(data) {
|
|
|
|
|
const stats = data.stats || {};
|
|
|
|
|
const totalSamples = stats.total_samples || data.value || 0;
|
|
|
|
|
const duration = stats.duration_sec || 0;
|
|
|
|
|
const sampleRate = stats.sample_rate || (duration > 0 ? totalSamples / duration : 0);
|
|
|
|
|
const errorRate = stats.error_rate || 0;
|
|
|
|
|
const missedSamples= stats.missed_samples || 0;
|
|
|
|
|
|
|
|
|
|
const samplesEl = document.getElementById('stat-total-samples');
|
|
|
|
|
if (samplesEl) samplesEl.textContent = formatNumber(totalSamples);
|
|
|
|
|
|
|
|
|
|
const durationEl = document.getElementById('stat-duration');
|
|
|
|
|
if (durationEl) durationEl.textContent = duration > 0 ? formatDuration(duration) : '--';
|
|
|
|
|
|
|
|
|
|
const rateEl = document.getElementById('stat-sample-rate');
|
|
|
|
|
if (rateEl) rateEl.textContent = sampleRate > 0 ? formatNumber(Math.round(sampleRate)) : '--';
|
|
|
|
|
|
|
|
|
|
// Count unique functions
|
|
|
|
|
let functionCount = 0;
|
|
|
|
|
function countFunctions(node) {
|
|
|
|
|
if (!node) return;
|
|
|
|
|
functionCount++;
|
|
|
|
|
if (node.children) node.children.forEach(countFunctions);
|
|
|
|
|
}
|
|
|
|
|
countFunctions(data);
|
|
|
|
|
|
|
|
|
|
const functionsEl = document.getElementById('stat-functions');
|
|
|
|
|
if (functionsEl) functionsEl.textContent = formatNumber(functionCount);
|
|
|
|
|
|
|
|
|
|
// Efficiency bar
|
|
|
|
|
if (errorRate !== undefined && errorRate !== null) {
|
|
|
|
|
const efficiency = Math.max(0, Math.min(100, (100 - errorRate)));
|
|
|
|
|
|
|
|
|
|
const efficiencySection = document.getElementById('efficiency-section');
|
|
|
|
|
if (efficiencySection) efficiencySection.style.display = 'block';
|
|
|
|
|
|
|
|
|
|
const efficiencyValue = document.getElementById('stat-efficiency');
|
|
|
|
|
if (efficiencyValue) efficiencyValue.textContent = efficiency.toFixed(1) + '%';
|
|
|
|
|
|
|
|
|
|
const efficiencyFill = document.getElementById('efficiency-fill');
|
|
|
|
|
if (efficiencyFill) efficiencyFill.style.width = efficiency + '%';
|
|
|
|
|
}
|
|
|
|
|
// MissedSamples bar
|
|
|
|
|
if (missedSamples !== undefined && missedSamples !== null) {
|
|
|
|
|
const sampleEfficiency = Math.max(0, missedSamples);
|
|
|
|
|
|
|
|
|
|
const efficiencySection = document.getElementById('efficiency-section');
|
|
|
|
|
if (efficiencySection) efficiencySection.style.display = 'block';
|
|
|
|
|
|
|
|
|
|
const sampleEfficiencyValue = document.getElementById('stat-missed-samples');
|
|
|
|
|
if (sampleEfficiencyValue) sampleEfficiencyValue.textContent = sampleEfficiency.toFixed(1) + '%';
|
|
|
|
|
|
|
|
|
|
const sampleEfficiencyFill = document.getElementById('missed-samples-fill');
|
|
|
|
|
if (sampleEfficiencyFill) sampleEfficiencyFill.style.width = sampleEfficiency + '%';
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// ============================================================================
|
|
|
|
|
// Hotspot Stats
|
|
|
|
|
// ============================================================================
|
|
|
|
|
|
2025-09-09 23:06:45 +01:00
|
|
|
function populateStats(data) {
|
|
|
|
|
const totalSamples = data.value || 0;
|
|
|
|
|
|
2025-12-01 17:34:14 +00:00
|
|
|
// Populate profile summary
|
|
|
|
|
populateProfileSummary(data);
|
|
|
|
|
|
2025-11-30 01:42:39 +00:00
|
|
|
// Populate thread statistics if available
|
|
|
|
|
populateThreadStats(data);
|
|
|
|
|
|
2025-09-09 23:06:45 +01:00
|
|
|
const functionMap = new Map();
|
|
|
|
|
|
|
|
|
|
function collectFunctions(node) {
|
2025-09-25 15:34:57 +01:00
|
|
|
if (!node) return;
|
|
|
|
|
|
2025-12-01 17:34:14 +00:00
|
|
|
let filename = resolveString(node.filename);
|
|
|
|
|
let funcname = resolveString(node.funcname);
|
2025-09-25 15:34:57 +01:00
|
|
|
|
|
|
|
|
if (!filename || !funcname) {
|
2025-12-01 17:34:14 +00:00
|
|
|
const nameStr = resolveString(node.name);
|
2025-09-25 15:34:57 +01:00
|
|
|
if (nameStr?.includes('(')) {
|
|
|
|
|
const match = nameStr.match(/^(.+?)\s*\((.+?):(\d+)\)$/);
|
|
|
|
|
if (match) {
|
|
|
|
|
funcname = funcname || match[1];
|
|
|
|
|
filename = filename || match[2];
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|
2025-09-14 23:47:14 +01:00
|
|
|
|
2025-09-25 15:34:57 +01:00
|
|
|
filename = filename || 'unknown';
|
|
|
|
|
funcname = funcname || 'unknown';
|
|
|
|
|
|
|
|
|
|
if (filename !== 'unknown' && funcname !== 'unknown' && node.value > 0) {
|
2025-09-09 23:06:45 +01:00
|
|
|
let childrenValue = 0;
|
|
|
|
|
if (node.children) {
|
|
|
|
|
childrenValue = node.children.reduce((sum, child) => sum + child.value, 0);
|
|
|
|
|
}
|
|
|
|
|
const directSamples = Math.max(0, node.value - childrenValue);
|
|
|
|
|
|
2025-09-14 23:47:14 +01:00
|
|
|
const funcKey = `${filename}:${node.lineno || '?'}:${funcname}`;
|
2025-09-09 23:06:45 +01:00
|
|
|
|
|
|
|
|
if (functionMap.has(funcKey)) {
|
|
|
|
|
const existing = functionMap.get(funcKey);
|
|
|
|
|
existing.directSamples += directSamples;
|
|
|
|
|
existing.directPercent = (existing.directSamples / totalSamples) * 100;
|
|
|
|
|
if (directSamples > existing.maxSingleSamples) {
|
2025-09-14 23:47:14 +01:00
|
|
|
existing.filename = filename;
|
2025-09-09 23:06:45 +01:00
|
|
|
existing.lineno = node.lineno || '?';
|
|
|
|
|
existing.maxSingleSamples = directSamples;
|
|
|
|
|
}
|
|
|
|
|
} else {
|
|
|
|
|
functionMap.set(funcKey, {
|
2025-09-14 23:47:14 +01:00
|
|
|
filename: filename,
|
2025-09-09 23:06:45 +01:00
|
|
|
lineno: node.lineno || '?',
|
2025-09-14 23:47:14 +01:00
|
|
|
funcname: funcname,
|
2025-09-09 23:06:45 +01:00
|
|
|
directSamples,
|
|
|
|
|
directPercent: (directSamples / totalSamples) * 100,
|
|
|
|
|
maxSingleSamples: directSamples
|
|
|
|
|
});
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
if (node.children) {
|
|
|
|
|
node.children.forEach(child => collectFunctions(child));
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
collectFunctions(data);
|
|
|
|
|
|
|
|
|
|
const hotSpots = Array.from(functionMap.values())
|
2025-12-01 17:34:14 +00:00
|
|
|
.filter(f => f.directPercent > 0.5)
|
2025-09-09 23:06:45 +01:00
|
|
|
.sort((a, b) => b.directPercent - a.directPercent)
|
|
|
|
|
.slice(0, 3);
|
|
|
|
|
|
2025-12-01 17:34:14 +00:00
|
|
|
// Populate and animate hotspot cards
|
2025-09-09 23:06:45 +01:00
|
|
|
for (let i = 0; i < 3; i++) {
|
|
|
|
|
const num = i + 1;
|
2025-12-01 17:34:14 +00:00
|
|
|
const card = document.getElementById(`hotspot-${num}`);
|
|
|
|
|
const funcEl = document.getElementById(`hotspot-func-${num}`);
|
|
|
|
|
const fileEl = document.getElementById(`hotspot-file-${num}`);
|
|
|
|
|
const percentEl = document.getElementById(`hotspot-percent-${num}`);
|
|
|
|
|
const samplesEl = document.getElementById(`hotspot-samples-${num}`);
|
2025-09-09 23:06:45 +01:00
|
|
|
|
2025-12-01 17:34:14 +00:00
|
|
|
if (i < hotSpots.length && hotSpots[i]) {
|
|
|
|
|
const h = hotSpots[i];
|
|
|
|
|
const filename = h.filename || 'unknown';
|
|
|
|
|
const lineno = h.lineno ?? '?';
|
2025-11-17 05:39:00 -08:00
|
|
|
const isSpecialFrame = filename === '~' && (lineno === 0 || lineno === '?');
|
|
|
|
|
|
2025-12-01 17:34:14 +00:00
|
|
|
let funcDisplay = h.funcname || 'unknown';
|
|
|
|
|
if (funcDisplay.length > 28) funcDisplay = funcDisplay.substring(0, 25) + '...';
|
|
|
|
|
|
|
|
|
|
if (funcEl) funcEl.textContent = funcDisplay;
|
|
|
|
|
if (fileEl) {
|
|
|
|
|
if (isSpecialFrame) {
|
|
|
|
|
fileEl.textContent = '--';
|
|
|
|
|
} else {
|
|
|
|
|
const basename = filename !== 'unknown' ? filename.split('/').pop() : 'unknown';
|
|
|
|
|
fileEl.textContent = `${basename}:${lineno}`;
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
if (percentEl) percentEl.textContent = `${h.directPercent.toFixed(1)}%`;
|
|
|
|
|
if (samplesEl) samplesEl.textContent = ` (${h.directSamples.toLocaleString()})`;
|
2025-09-09 23:06:45 +01:00
|
|
|
} else {
|
2025-12-01 17:34:14 +00:00
|
|
|
if (funcEl) funcEl.textContent = '--';
|
|
|
|
|
if (fileEl) fileEl.textContent = '--';
|
|
|
|
|
if (percentEl) percentEl.textContent = '--';
|
|
|
|
|
if (samplesEl) samplesEl.textContent = '';
|
2025-09-09 23:06:45 +01:00
|
|
|
}
|
|
|
|
|
|
2025-12-01 17:34:14 +00:00
|
|
|
// Add click handler and animate entrance
|
|
|
|
|
if (card) {
|
|
|
|
|
if (i < hotSpots.length && hotSpots[i]) {
|
|
|
|
|
const h = hotSpots[i];
|
|
|
|
|
const basename = h.filename !== 'unknown' ? h.filename.split('/').pop() : '';
|
|
|
|
|
const searchTerm = basename && h.lineno !== '?' ? `${basename}:${h.lineno}` : h.funcname;
|
|
|
|
|
card.dataset.searchterm = searchTerm;
|
|
|
|
|
card.onclick = () => searchForHotspot(searchTerm);
|
|
|
|
|
card.style.cursor = 'pointer';
|
|
|
|
|
} else {
|
|
|
|
|
card.onclick = null;
|
|
|
|
|
delete card.dataset.searchterm;
|
|
|
|
|
card.style.cursor = 'default';
|
|
|
|
|
}
|
2025-09-09 23:06:45 +01:00
|
|
|
|
2025-12-01 17:34:14 +00:00
|
|
|
setTimeout(() => {
|
|
|
|
|
card.classList.add('visible');
|
|
|
|
|
}, 100 + i * 80);
|
2025-09-09 23:06:45 +01:00
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
2025-12-01 17:34:14 +00:00
|
|
|
// ============================================================================
|
|
|
|
|
// Thread Filter
|
|
|
|
|
// ============================================================================
|
|
|
|
|
|
2025-09-25 15:34:57 +01:00
|
|
|
function initThreadFilter(data) {
|
|
|
|
|
const threadFilter = document.getElementById('thread-filter');
|
2025-12-01 17:34:14 +00:00
|
|
|
const threadSection = document.getElementById('thread-section');
|
2025-09-25 15:34:57 +01:00
|
|
|
|
2025-12-01 17:34:14 +00:00
|
|
|
if (!threadFilter || !data.threads) return;
|
2025-09-25 15:34:57 +01:00
|
|
|
|
|
|
|
|
threadFilter.innerHTML = '<option value="all">All Threads</option>';
|
|
|
|
|
|
|
|
|
|
const threads = data.threads || [];
|
|
|
|
|
threads.forEach(threadId => {
|
|
|
|
|
const option = document.createElement('option');
|
|
|
|
|
option.value = threadId;
|
|
|
|
|
option.textContent = `Thread ${threadId}`;
|
|
|
|
|
threadFilter.appendChild(option);
|
|
|
|
|
});
|
|
|
|
|
|
2025-12-01 17:34:14 +00:00
|
|
|
if (threads.length > 1 && threadSection) {
|
|
|
|
|
threadSection.style.display = 'block';
|
2025-09-25 15:34:57 +01:00
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
function filterByThread() {
|
|
|
|
|
const threadFilter = document.getElementById('thread-filter');
|
|
|
|
|
if (!threadFilter || !originalData) return;
|
|
|
|
|
|
|
|
|
|
const selectedThread = threadFilter.value;
|
|
|
|
|
currentThreadFilter = selectedThread;
|
|
|
|
|
|
|
|
|
|
let filteredData;
|
2025-11-30 01:42:39 +00:00
|
|
|
let selectedThreadId = null;
|
|
|
|
|
|
2025-09-25 15:34:57 +01:00
|
|
|
if (selectedThread === 'all') {
|
|
|
|
|
filteredData = originalData;
|
|
|
|
|
} else {
|
2025-12-01 17:34:14 +00:00
|
|
|
selectedThreadId = parseInt(selectedThread, 10);
|
2025-11-30 01:42:39 +00:00
|
|
|
filteredData = filterDataByThread(originalData, selectedThreadId);
|
2025-09-25 15:34:57 +01:00
|
|
|
|
|
|
|
|
if (filteredData.strings) {
|
|
|
|
|
stringTable = filteredData.strings;
|
|
|
|
|
filteredData = resolveStringIndices(filteredData);
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
const tooltip = createPythonTooltip(filteredData);
|
|
|
|
|
const chart = createFlamegraph(tooltip, filteredData.value);
|
|
|
|
|
renderFlamegraph(chart, filteredData);
|
2025-11-30 01:42:39 +00:00
|
|
|
|
|
|
|
|
populateThreadStats(originalData, selectedThreadId);
|
2025-09-25 15:34:57 +01:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
function filterDataByThread(data, threadId) {
|
|
|
|
|
function filterNode(node) {
|
|
|
|
|
if (!node.threads || !node.threads.includes(threadId)) {
|
|
|
|
|
return null;
|
|
|
|
|
}
|
|
|
|
|
|
2025-12-01 17:34:14 +00:00
|
|
|
const filteredNode = { ...node, children: [] };
|
2025-09-25 15:34:57 +01:00
|
|
|
|
|
|
|
|
if (node.children && Array.isArray(node.children)) {
|
|
|
|
|
filteredNode.children = node.children
|
|
|
|
|
.map(child => filterNode(child))
|
|
|
|
|
.filter(child => child !== null);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
return filteredNode;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
function recalculateValue(node) {
|
|
|
|
|
if (!node.children || node.children.length === 0) {
|
|
|
|
|
return node.value || 0;
|
|
|
|
|
}
|
|
|
|
|
const childrenValue = node.children.reduce((sum, child) => sum + recalculateValue(child), 0);
|
|
|
|
|
node.value = Math.max(node.value || 0, childrenValue);
|
|
|
|
|
return node.value;
|
|
|
|
|
}
|
|
|
|
|
|
2025-12-01 17:34:14 +00:00
|
|
|
const filteredRoot = { ...data, children: [] };
|
2025-09-25 15:34:57 +01:00
|
|
|
|
2025-12-01 17:34:14 +00:00
|
|
|
if (data.children && Array.isArray(data.children)) {
|
|
|
|
|
filteredRoot.children = data.children
|
|
|
|
|
.map(child => filterNode(child))
|
|
|
|
|
.filter(child => child !== null);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
recalculateValue(filteredRoot);
|
2025-09-25 15:34:57 +01:00
|
|
|
return filteredRoot;
|
|
|
|
|
}
|
|
|
|
|
|
2025-12-01 17:34:14 +00:00
|
|
|
// ============================================================================
|
|
|
|
|
// Control Functions
|
|
|
|
|
// ============================================================================
|
|
|
|
|
|
|
|
|
|
function resetZoom() {
|
|
|
|
|
if (window.flamegraphChart) {
|
|
|
|
|
window.flamegraphChart.resetZoom();
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
function exportSVG() {
|
|
|
|
|
const svgElement = document.querySelector("#chart svg");
|
|
|
|
|
if (!svgElement) {
|
|
|
|
|
console.warn("Cannot export: No flamegraph SVG found");
|
|
|
|
|
return;
|
|
|
|
|
}
|
|
|
|
|
const serializer = new XMLSerializer();
|
|
|
|
|
const svgString = serializer.serializeToString(svgElement);
|
|
|
|
|
const blob = new Blob([svgString], { type: "image/svg+xml" });
|
|
|
|
|
const url = URL.createObjectURL(blob);
|
|
|
|
|
const a = document.createElement("a");
|
|
|
|
|
a.href = url;
|
|
|
|
|
a.download = "python-performance-flamegraph.svg";
|
|
|
|
|
a.click();
|
|
|
|
|
URL.revokeObjectURL(url);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// ============================================================================
|
|
|
|
|
// Initialization
|
|
|
|
|
// ============================================================================
|
|
|
|
|
|
|
|
|
|
function initFlamegraph() {
|
|
|
|
|
ensureLibraryLoaded();
|
|
|
|
|
restoreUIState();
|
|
|
|
|
|
|
|
|
|
let processedData = EMBEDDED_DATA;
|
|
|
|
|
if (EMBEDDED_DATA.strings) {
|
|
|
|
|
stringTable = EMBEDDED_DATA.strings;
|
|
|
|
|
processedData = resolveStringIndices(EMBEDDED_DATA);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
originalData = processedData;
|
|
|
|
|
initThreadFilter(processedData);
|
|
|
|
|
|
|
|
|
|
const tooltip = createPythonTooltip(processedData);
|
|
|
|
|
const chart = createFlamegraph(tooltip, processedData.value);
|
|
|
|
|
renderFlamegraph(chart, processedData);
|
|
|
|
|
initSearchHandlers();
|
|
|
|
|
initSidebarResize();
|
|
|
|
|
handleResize();
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
if (document.readyState === "loading") {
|
|
|
|
|
document.addEventListener("DOMContentLoaded", initFlamegraph);
|
|
|
|
|
} else {
|
|
|
|
|
initFlamegraph();
|
|
|
|
|
}
|