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, "&").replace(//g, ">"); return `
${escaped}
`; }) .join(""); sourceSection = `
Source Code:
${sourceLines}
`; } // 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 ? 'SPECIALIZED' : ''; const baseOpHint = opcodeInfo.isSpecialized ? `(${opcodeInfo.baseOpname})` : ''; const nameClass = opcodeInfo.isSpecialized ? 'tooltip-opcode-name specialized' : 'tooltip-opcode-name'; return `
${opcodeInfo.opname}${baseOpHint}${specializedBadge}
${count.toLocaleString()} (${pct}%)
`; }).join(''); opcodeSection = `
Bytecode Instructions:
${opcodeLines}
`; } const fileLocationHTML = isSpecialFrame ? "" : `
${filename}${d.data.lineno ? ":" + d.data.lineno : ""}
`; const tooltipHTML = `
${funcname}
${fileLocationHTML}
Execution Time: ${timeMs} ms Percentage: ${percentage}% ${calls > 0 ? ` Function Calls: ${calls.toLocaleString()} ` : ''} ${childCount > 0 ? ` Child Functions: ${childCount} ` : ''}
${sourceSection} ${opcodeSection}
${childCount > 0 ? "Click to zoom into this function" : "Leaf function - no children"}
`; // 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 = '
Error: d3-flame-graph library failed to load
'; 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 = ''; 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|" 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); } } // Keyboard shortcut: Enter/Space activates toggle switches document.addEventListener('keydown', function(e) { if (e.target.tagName === 'INPUT' || e.target.tagName === 'TEXTAREA') { return; } if ((e.key === 'Enter' || e.key === ' ') && e.target.classList.contains('toggle-switch')) { e.preventDefault(); e.target.click(); } }); if (document.readyState === "loading") { document.addEventListener("DOMContentLoaded", initFlamegraph); } else { initFlamegraph(); }