const EMBEDDED_DATA = {{FLAMEGRAPH_DATA}}; // Global string table for resolving string indices let stringTable = []; let originalData = null; let currentThreadFilter = 'all'; // 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 // ============================================================================ 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.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'; } } 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.innerHTML = savedTheme === 'dark' ? '☼' : '☾'; } } // 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'); } } } // ============================================================================ // 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; } 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'; }); } // ============================================================================ // 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}
`; } 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}
${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) { 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'); 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)}%`; } const gcPctElem = document.getElementById('gc-pct'); if (gcPctElem) gcPctElem.textContent = `${(threadStats.gc_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 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 // ============================================================================ function populateStats(data) { const totalSamples = data.value || 0; // Populate profile summary populateProfileSummary(data); // Populate thread statistics if available populateThreadStats(data); 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(data); 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 || !originalData) return; const selectedThread = threadFilter.value; currentThreadFilter = selectedThread; let filteredData; let selectedThreadId = null; if (selectedThread === 'all') { filteredData = originalData; } else { selectedThreadId = parseInt(selectedThread, 10); filteredData = filterDataByThread(originalData, selectedThreadId); if (filteredData.strings) { stringTable = filteredData.strings; filteredData = resolveStringIndices(filteredData); } } const tooltip = createPythonTooltip(filteredData); const chart = createFlamegraph(tooltip, filteredData.value); renderFlamegraph(chart, filteredData); populateThreadStats(originalData, 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); } // ============================================================================ // 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(); }