blob: 6345320bd2555de8ed3ccace6e0277d8895b9dd0 [file] [log] [blame]
const EMBEDDED_DATA = {{FLAMEGRAPH_DATA}};
// Global string table for resolving string indices
let stringTable = [];
let normalData = null;
let invertedData = null;
let currentThreadFilter = 'all';
let isInverted = false;
// Heat colors are now defined in CSS variables (--heat-1 through --heat-8)
// and automatically switch with theme changes - no JS color arrays needed!
// Opcode mappings - loaded from embedded data (generated by Python)
let OPCODE_NAMES = {};
let DEOPT_MAP = {};
// Initialize opcode mappings from embedded data
function initOpcodeMapping(data) {
if (data && data.opcode_mapping) {
OPCODE_NAMES = data.opcode_mapping.names || {};
DEOPT_MAP = data.opcode_mapping.deopt || {};
}
}
// Get opcode info from opcode number
function getOpcodeInfo(opcode) {
const opname = OPCODE_NAMES[opcode] || `<${opcode}>`;
const baseOpcode = DEOPT_MAP[opcode];
const isSpecialized = baseOpcode !== undefined;
const baseOpname = isSpecialized ? (OPCODE_NAMES[baseOpcode] || `<${baseOpcode}>`) : opname;
return {
opname: opname,
baseOpname: baseOpname,
isSpecialized: isSpecialized
};
}
// ============================================================================
// String Resolution
// ============================================================================
function resolveString(index) {
if (index === null || index === undefined) {
return null;
}
if (typeof index === 'number' && index >= 0 && index < stringTable.length) {
return stringTable[index];
}
return String(index);
}
function resolveStringIndices(node) {
if (!node) return node;
const resolved = { ...node };
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.querySelector('.icon-moon').style.display = next === 'dark' ? 'none' : '';
btn.querySelector('.icon-sun').style.display = next === 'dark' ? '' : 'none';
}
// Re-render flamegraph with new theme colors
if (window.flamegraphData && normalData) {
const currentData = isInverted ? invertedData : normalData;
const tooltip = createPythonTooltip(currentData);
const chart = createFlamegraph(tooltip, currentData.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';
}
}
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);
}
}
}
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.querySelector('.icon-moon').style.display = savedTheme === 'dark' ? 'none' : '';
btn.querySelector('.icon-sun').style.display = savedTheme === 'dark' ? '' : 'none';
}
}
// 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';
}
}
// 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');
}
}
}
// ============================================================================
// Logo/Favicon Setup
// ============================================================================
function setupLogos() {
const logo = document.querySelector('.sidebar-logo-img img');
if (!logo) return;
const navbarLogoContainer = document.getElementById('navbar-logo');
if (navbarLogoContainer) {
const navbarLogo = logo.cloneNode(true);
navbarLogoContainer.appendChild(navbarLogo);
}
const favicon = document.createElement('link');
favicon.rel = 'icon';
favicon.type = 'image/png';
favicon.href = logo.src;
document.head.appendChild(favicon);
}
// ============================================================================
// 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 brandEl = document.getElementById('status-brand');
const taglineEl = document.getElementById('status-tagline');
if (brandEl) brandEl.style.display = 'none';
if (taglineEl) taglineEl.style.display = 'none';
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;
}
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';
});
const brandEl = document.getElementById('status-brand');
const taglineEl = document.getElementById('status-tagline');
if (brandEl) brandEl.style.display = 'flex';
if (taglineEl) taglineEl.style.display = 'flex';
}
// ============================================================================
// Tooltip
// ============================================================================
function createPythonTooltip(data) {
const pythonTooltip = flamegraph.tooltip.defaultFlamegraphTooltip();
pythonTooltip.show = function (d, element) {
if (!this._tooltip) {
this._tooltip = d3.select("body")
.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;
const funcname = resolveString(d.data.funcname) || resolveString(d.data.name);
const filename = resolveString(d.data.filename) || "";
const isSpecialFrame = filename === "~";
// Build source section
let sourceSection = "";
if (source && Array.isArray(source) && source.length > 0) {
const sourceLines = source
.map((line) => {
const isCurrent = line.startsWith("→");
const escaped = line.replace(/&/g, "&amp;").replace(/</g, "&lt;").replace(/>/g, "&gt;");
return `<div class="tooltip-source-line${isCurrent ? ' current' : ''}">${escaped}</div>`;
})
.join("");
sourceSection = `
<div class="tooltip-source">
<div class="tooltip-source-title">Source Code:</div>
<div class="tooltip-source-code">${sourceLines}</div>
</div>`;
}
// Create bytecode/opcode section if available
let opcodeSection = "";
const opcodes = d.data.opcodes;
if (opcodes && typeof opcodes === 'object' && Object.keys(opcodes).length > 0) {
// Sort opcodes by sample count (descending)
const sortedOpcodes = Object.entries(opcodes)
.sort((a, b) => b[1] - a[1])
.slice(0, 8); // Limit to top 8
const totalOpcodeSamples = sortedOpcodes.reduce((sum, [, count]) => sum + count, 0);
const maxCount = sortedOpcodes[0][1] || 1;
const opcodeLines = sortedOpcodes.map(([opcode, count]) => {
const opcodeInfo = getOpcodeInfo(parseInt(opcode, 10));
const pct = ((count / totalOpcodeSamples) * 100).toFixed(1);
const barWidth = (count / maxCount) * 100;
const specializedBadge = opcodeInfo.isSpecialized
? '<span class="tooltip-opcode-badge">SPECIALIZED</span>'
: '';
const baseOpHint = opcodeInfo.isSpecialized
? `<span class="tooltip-opcode-base-hint">(${opcodeInfo.baseOpname})</span>`
: '';
const nameClass = opcodeInfo.isSpecialized
? 'tooltip-opcode-name specialized'
: 'tooltip-opcode-name';
return `
<div class="tooltip-opcode-row">
<div class="${nameClass}">
${opcodeInfo.opname}${baseOpHint}${specializedBadge}
</div>
<div class="tooltip-opcode-count">${count.toLocaleString()} (${pct}%)</div>
<div class="tooltip-opcode-bar">
<div class="tooltip-opcode-bar-fill" style="width: ${barWidth}%;"></div>
</div>
</div>`;
}).join('');
opcodeSection = `
<div class="tooltip-opcodes">
<div class="tooltip-opcodes-title">Bytecode Instructions:</div>
<div class="tooltip-opcodes-list">
${opcodeLines}
</div>
</div>`;
}
const fileLocationHTML = isSpecialFrame ? "" : `
<div class="tooltip-location">${filename}${d.data.lineno ? ":" + d.data.lineno : ""}</div>`;
const tooltipHTML = `
<div class="tooltip-header">
<div class="tooltip-title">${funcname}</div>
${fileLocationHTML}
</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}
${opcodeSection}
<div class="tooltip-hint">
${childCount > 0 ? "Click to zoom into this function" : "Leaf function - no children"}
</div>
`;
// Position tooltip
const event = d3.event || window.event;
const mouseX = event.pageX || event.clientX;
const mouseY = event.pageY || event.clientY;
const padding = 12;
this._tooltip.html(tooltipHTML);
// 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) {
left = mouseX - tooltipWidth - padding;
if (left < 0) left = padding;
}
if (top + tooltipHeight > window.innerHeight) {
top = mouseY - tooltipHeight - padding;
if (top < 0) top = padding;
}
this._tooltip
.style("left", left + "px")
.style("top", top + "px")
.transition()
.duration(150)
.style("opacity", 1);
// Update status bar
updateStatusBar(d.data, data.value);
};
pythonTooltip.hide = function () {
if (this._tooltip) {
this._tooltip.transition().duration(150).style("opacity", 0);
}
clearStatusBar();
};
return pythonTooltip;
}
// ============================================================================
// 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;
}
function createFlamegraph(tooltip, rootValue) {
const chartArea = document.querySelector('.chart-area');
const width = chartArea ? chartArea.clientWidth - 32 : window.innerWidth - 320;
const heatColors = getHeatColors();
let chart = flamegraph()
.width(width)
.cellHeight(20)
.transitionDuration(300)
.minFrameSize(1)
.tooltip(tooltip)
.inverted(true)
.setColorMapper(function (d) {
// Root node should be transparent
if (d.depth === 0) return 'transparent';
const percentage = d.data.value / rootValue;
const level = getHeatLevel(percentage);
return heatColors[level];
});
return chart;
}
function renderFlamegraph(chart, data) {
d3.select("#chart").datum(data).call(chart);
window.flamegraphChart = chart;
window.flamegraphData = data;
populateStats(data);
}
// ============================================================================
// Search
// ============================================================================
function updateSearchHighlight(searchTerm, searchInput) {
d3.selectAll("#chart rect")
.classed("search-match", false)
.classed("search-dim", false);
// Clear active state from all hotspots
document.querySelectorAll('.hotspot').forEach(h => h.classList.remove('active'));
if (searchTerm && searchTerm.length > 0) {
let matchCount = 0;
d3.selectAll("#chart rect").each(function (d) {
if (d && d.data) {
const name = resolveString(d.data.name) || "";
const funcname = resolveString(d.data.funcname) || "";
const filename = resolveString(d.data.filename) || "";
const lineno = d.data.lineno;
const term = searchTerm.toLowerCase();
// 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);
}
if (matches) {
matchCount++;
d3.select(this).classed("search-match", true);
} else {
d3.select(this).classed("search-dim", true);
}
}
});
if (searchInput) {
searchInput.classList.remove("has-matches", "no-matches");
searchInput.classList.add(matchCount > 0 ? "has-matches" : "no-matches");
}
// Mark matching hotspot as active
document.querySelectorAll('.hotspot').forEach(h => {
if (h.dataset.searchterm && h.dataset.searchterm.toLowerCase() === searchTerm.toLowerCase()) {
h.classList.add('active');
}
});
} else if (searchInput) {
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();
}
}
}
function initSearchHandlers() {
const searchInput = document.getElementById("search-input");
const searchWrapper = document.querySelector(".search-wrapper");
if (!searchInput) return;
let searchTimeout;
function performSearch() {
const term = searchInput.value.trim();
updateSearchHighlight(term, searchInput);
// Toggle has-value class for clear button visibility
if (searchWrapper) {
searchWrapper.classList.toggle("has-value", term.length > 0);
}
}
searchInput.addEventListener("input", function () {
clearTimeout(searchTimeout);
searchTimeout = setTimeout(performSearch, 150);
});
window.performSearch = performSearch;
}
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");
}
// 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);
});
}
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();
});
document.addEventListener('mousemove', function(e) {
if (!isResizing) return;
const deltaX = e.clientX - startX;
const newWidth = Math.min(Math.max(startWidth + deltaX, minWidth), maxWidth);
sidebar.style.width = newWidth + 'px';
e.preventDefault();
});
document.addEventListener('mouseup', function() {
if (isResizing) {
isResizing = false;
resizeHandle.classList.remove('resizing');
document.body.classList.remove('resizing-sidebar');
// Save the new width
const width = sidebar.offsetWidth;
localStorage.setItem('flamegraph-sidebar-width', width);
// Resize chart after sidebar resize
setTimeout(() => {
resizeChart();
}, 10);
}
});
}
// ============================================================================
// Thread Stats
// ============================================================================
// 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) {
return;
}
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;
}
if (!threadStats || typeof threadStats.total !== 'number' || threadStats.total <= 0) {
return;
}
const section = document.getElementById('thread-stats-bar');
if (!section) {
return;
}
section.style.display = 'block';
const gilHeldStat = document.getElementById('gil-held-stat');
const gilReleasedStat = document.getElementById('gil-released-stat');
const gilWaitingStat = document.getElementById('gil-waiting-stat');
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';
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');
// GIL Released = not holding GIL and not waiting for it
const gilReleasedPct = Math.max(0, 100 - (threadStats.has_gil_pct || 0) - (threadStats.gil_requested_pct || 0));
if (gilReleasedPctElem) gilReleasedPctElem.textContent = `${gilReleasedPct.toFixed(1)}%`;
const gilWaitingPctElem = document.getElementById('gil-waiting-pct');
if (gilWaitingPctElem) gilWaitingPctElem.textContent = `${(threadStats.gil_requested_pct || 0).toFixed(1)}%`;
}
const gcPctElem = document.getElementById('gc-pct');
if (gcPctElem) gcPctElem.textContent = `${(threadStats.gc_pct || 0).toFixed(1)}%`;
// Exception stats
const excPctElem = document.getElementById('exc-pct');
if (excPctElem) excPctElem.textContent = `${(threadStats.has_exception_pct || 0).toFixed(1)}%`;
}
// ============================================================================
// 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
// Use normal (non-inverted) tree structure, but respect thread filtering
const uniqueFunctions = new Set();
function collectUniqueFunctions(node) {
if (!node) return;
const filename = resolveString(node.filename) || 'unknown';
const funcname = resolveString(node.funcname) || resolveString(node.name) || 'unknown';
const lineno = node.lineno || 0;
const key = `${filename}|${lineno}|${funcname}`;
uniqueFunctions.add(key);
if (node.children) node.children.forEach(collectUniqueFunctions);
}
// In inverted mode, use normalData (with thread filter if active)
// In normal mode, use the passed data (already has thread filter applied if any)
let functionCountSource;
if (!normalData) {
functionCountSource = data;
} else if (isInverted) {
if (currentThreadFilter !== 'all') {
functionCountSource = filterDataByThread(normalData, parseInt(currentThreadFilter));
} else {
functionCountSource = normalData;
}
} else {
functionCountSource = data;
}
collectUniqueFunctions(functionCountSource);
const functionsEl = document.getElementById('stat-functions');
if (functionsEl) functionsEl.textContent = formatNumber(uniqueFunctions.size);
// 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
// ============================================================================
function populateStats(data) {
// Populate profile summary
populateProfileSummary(data);
// Populate thread statistics if available
populateThreadStats(data);
// For hotspots: use normal (non-inverted) tree structure, but respect thread filtering.
// In inverted view, the tree structure changes but the hottest functions remain the same.
// However, if a thread filter is active, we need to show that thread's hotspots.
let hotspotSource;
if (!normalData) {
hotspotSource = data;
} else if (isInverted) {
// In inverted mode, use normalData (with thread filter if active)
if (currentThreadFilter !== 'all') {
hotspotSource = filterDataByThread(normalData, parseInt(currentThreadFilter));
} else {
hotspotSource = normalData;
}
} else {
// In normal mode, use the passed data (already has thread filter applied if any)
hotspotSource = data;
}
const totalSamples = hotspotSource.value || 0;
const functionMap = new Map();
function collectFunctions(node) {
if (!node) return;
let filename = resolveString(node.filename);
let funcname = resolveString(node.funcname);
if (!filename || !funcname) {
const nameStr = resolveString(node.name);
if (nameStr?.includes('(')) {
const match = nameStr.match(/^(.+?)\s*\((.+?):(\d+)\)$/);
if (match) {
funcname = funcname || match[1];
filename = filename || match[2];
}
}
}
filename = filename || 'unknown';
funcname = funcname || 'unknown';
if (filename !== 'unknown' && funcname !== 'unknown' && node.value > 0) {
let childrenValue = 0;
if (node.children) {
childrenValue = node.children.reduce((sum, child) => sum + child.value, 0);
}
const directSamples = Math.max(0, node.value - childrenValue);
const funcKey = `${filename}:${node.lineno || '?'}:${funcname}`;
if (functionMap.has(funcKey)) {
const existing = functionMap.get(funcKey);
existing.directSamples += directSamples;
existing.directPercent = (existing.directSamples / totalSamples) * 100;
if (directSamples > existing.maxSingleSamples) {
existing.filename = filename;
existing.lineno = node.lineno || '?';
existing.maxSingleSamples = directSamples;
}
} else {
functionMap.set(funcKey, {
filename: filename,
lineno: node.lineno || '?',
funcname: funcname,
directSamples,
directPercent: (directSamples / totalSamples) * 100,
maxSingleSamples: directSamples
});
}
}
if (node.children) {
node.children.forEach(child => collectFunctions(child));
}
}
collectFunctions(hotspotSource);
const hotSpots = Array.from(functionMap.values())
.filter(f => f.directPercent > 0.5)
.sort((a, b) => b.directPercent - a.directPercent)
.slice(0, 3);
// Populate and animate hotspot cards
for (let i = 0; i < 3; i++) {
const num = i + 1;
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}`);
if (i < hotSpots.length && hotSpots[i]) {
const h = hotSpots[i];
const filename = h.filename || 'unknown';
const lineno = h.lineno ?? '?';
const isSpecialFrame = filename === '~' && (lineno === 0 || lineno === '?');
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()})`;
} else {
if (funcEl) funcEl.textContent = '--';
if (fileEl) fileEl.textContent = '--';
if (percentEl) percentEl.textContent = '--';
if (samplesEl) samplesEl.textContent = '';
}
// 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';
}
setTimeout(() => {
card.classList.add('visible');
}, 100 + i * 80);
}
}
}
// ============================================================================
// Thread Filter
// ============================================================================
function initThreadFilter(data) {
const threadFilter = document.getElementById('thread-filter');
const threadSection = document.getElementById('thread-section');
if (!threadFilter || !data.threads) return;
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);
});
if (threads.length > 1 && threadSection) {
threadSection.style.display = 'block';
}
}
function filterByThread() {
const threadFilter = document.getElementById('thread-filter');
if (!threadFilter || !normalData) return;
const selectedThread = threadFilter.value;
currentThreadFilter = selectedThread;
const baseData = isInverted ? invertedData : normalData;
let filteredData;
let selectedThreadId = null;
if (selectedThread === 'all') {
filteredData = baseData;
} else {
selectedThreadId = parseInt(selectedThread, 10);
filteredData = filterDataByThread(baseData, selectedThreadId);
if (filteredData.strings) {
stringTable = filteredData.strings;
filteredData = resolveStringIndices(filteredData);
}
}
const tooltip = createPythonTooltip(filteredData);
const chart = createFlamegraph(tooltip, filteredData.value);
renderFlamegraph(chart, filteredData);
populateThreadStats(baseData, selectedThreadId);
}
function filterDataByThread(data, threadId) {
function filterNode(node) {
if (!node.threads || !node.threads.includes(threadId)) {
return null;
}
const filteredNode = { ...node, children: [] };
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;
}
const filteredRoot = { ...data, children: [] };
if (data.children && Array.isArray(data.children)) {
filteredRoot.children = data.children
.map(child => filterNode(child))
.filter(child => child !== null);
}
recalculateValue(filteredRoot);
return filteredRoot;
}
// ============================================================================
// 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);
}
// ============================================================================
// Inverted Flamegraph
// ============================================================================
// Example: "file.py|10|foo" or "~|0|<GC>" for special frames
function getInvertNodeKey(node) {
return `${node.filename || '~'}|${node.lineno || 0}|${node.funcname || node.name}`;
}
function accumulateInvertedNode(parent, stackFrame, leaf) {
const key = getInvertNodeKey(stackFrame);
if (!parent.children[key]) {
parent.children[key] = {
name: stackFrame.name,
value: 0,
children: {},
filename: stackFrame.filename,
lineno: stackFrame.lineno,
funcname: stackFrame.funcname,
source: stackFrame.source,
threads: new Set()
};
}
const node = parent.children[key];
node.value += leaf.value;
if (leaf.threads) {
leaf.threads.forEach(t => node.threads.add(t));
}
return node;
}
function processLeaf(invertedRoot, path, leafNode) {
if (!path || path.length === 0) {
return;
}
let invertedParent = accumulateInvertedNode(invertedRoot, leafNode, leafNode);
// Walk backwards through the call stack
for (let i = path.length - 2; i >= 0; i--) {
invertedParent = accumulateInvertedNode(invertedParent, path[i], leafNode);
}
}
function traverseInvert(path, currentNode, invertedRoot) {
const children = currentNode.children || [];
const childThreads = new Set(children.flatMap(c => c.threads || []));
const selfThreads = (currentNode.threads || []).filter(t => !childThreads.has(t));
if (selfThreads.length > 0) {
processLeaf(invertedRoot, path, { ...currentNode, threads: selfThreads });
}
children.forEach(child => traverseInvert(path.concat([child]), child, invertedRoot));
}
function convertInvertDictToArray(node) {
if (node.threads instanceof Set) {
node.threads = Array.from(node.threads).sort((a, b) => a - b);
}
const children = node.children;
if (children && typeof children === 'object' && !Array.isArray(children)) {
node.children = Object.values(children);
node.children.sort((a, b) => b.value - a.value || a.name.localeCompare(b.name));
node.children.forEach(convertInvertDictToArray);
}
return node;
}
function generateInvertedFlamegraph(data) {
const invertedRoot = {
name: data.name,
value: data.value,
children: {},
stats: data.stats,
threads: data.threads
};
const children = data.children || [];
if (children.length === 0) {
// Single-frame tree: the root is its own leaf
processLeaf(invertedRoot, [data], data);
} else {
children.forEach(child => traverseInvert([child], child, invertedRoot));
}
convertInvertDictToArray(invertedRoot);
return invertedRoot;
}
function updateToggleUI(toggleId, isOn) {
const toggle = document.getElementById(toggleId);
if (toggle) {
const track = toggle.querySelector('.toggle-track');
const labels = toggle.querySelectorAll('.toggle-label');
if (isOn) {
track.classList.add('on');
labels[0].classList.remove('active');
labels[1].classList.add('active');
} else {
track.classList.remove('on');
labels[0].classList.add('active');
labels[1].classList.remove('active');
}
}
}
function toggleInvert() {
isInverted = !isInverted;
updateToggleUI('toggle-invert', isInverted);
// Build inverted data on first use
if (isInverted && !invertedData) {
invertedData = generateInvertedFlamegraph(normalData);
}
let dataToRender = isInverted ? invertedData : normalData;
if (currentThreadFilter !== 'all') {
dataToRender = filterDataByThread(dataToRender, parseInt(currentThreadFilter));
}
const tooltip = createPythonTooltip(dataToRender);
const chart = createFlamegraph(tooltip, dataToRender.value);
renderFlamegraph(chart, dataToRender);
}
// ============================================================================
// Initialization
// ============================================================================
function initFlamegraph() {
ensureLibraryLoaded();
restoreUIState();
setupLogos();
if (EMBEDDED_DATA.strings) {
stringTable = EMBEDDED_DATA.strings;
normalData = resolveStringIndices(EMBEDDED_DATA);
} else {
normalData = EMBEDDED_DATA;
}
// Initialize opcode mapping from embedded data
initOpcodeMapping(EMBEDDED_DATA);
// Inverted data will be built on first toggle
invertedData = null;
initThreadFilter(normalData);
const tooltip = createPythonTooltip(normalData);
const chart = createFlamegraph(tooltip, normalData.value);
renderFlamegraph(chart, normalData);
initSearchHandlers();
initSidebarResize();
handleResize();
const toggleInvertBtn = document.getElementById('toggle-invert');
if (toggleInvertBtn) {
toggleInvertBtn.addEventListener('click', toggleInvert);
}
}
if (document.readyState === "loading") {
document.addEventListener("DOMContentLoaded", initFlamegraph);
} else {
initFlamegraph();
}