From 483fd1909b1b23c4a0c592e1b2072e7670bb35ac Mon Sep 17 00:00:00 2001 From: Ivona Stojanovic Date: Fri, 5 Dec 2025 11:26:25 +0000 Subject: [PATCH 1/6] Add inverted flamegraph Introduce an inverted flamegraph view that aggregates all leaf nodes. In a standard flamegraph, if a hot function is called from multiple locations, it appears multiple times as separate leaf nodes. In the inverted flamegraph, all occurrences of the same leaf function are merged into a single aggregated node, showing the total hotness of that function in one place. In this inverted view, the children of each aggregated hot function represent its callers, the functions that led to that leaf in the original call tree. --- .../_flamegraph_assets/flamegraph.css | 27 +++ .../sampling/_flamegraph_assets/flamegraph.js | 170 ++++++++++++++++-- .../flamegraph_template.html | 10 ++ 3 files changed, 192 insertions(+), 15 deletions(-) diff --git a/Lib/profiling/sampling/_flamegraph_assets/flamegraph.css b/Lib/profiling/sampling/_flamegraph_assets/flamegraph.css index c75f2324b6d499..51ed5d8a6e3671 100644 --- a/Lib/profiling/sampling/_flamegraph_assets/flamegraph.css +++ b/Lib/profiling/sampling/_flamegraph_assets/flamegraph.css @@ -274,6 +274,20 @@ body.resizing-sidebar { flex: 1; } +/* View Mode Section */ +.view-mode-section { + padding-bottom: 20px; + border-bottom: 1px solid var(--border); +} + +.view-mode-section .section-title { + margin-bottom: 12px; +} + +.view-mode-section .toggle-switch { + justify-content: center; +} + /* Collapsible sections */ .collapsible .section-header { display: flex; @@ -899,3 +913,16 @@ body.resizing-sidebar { grid-template-columns: 1fr; } } + +/* -------------------------------------------------------------------------- + Flamegraph Root Node Styling + -------------------------------------------------------------------------- */ + +/* Style the root node - no border, themed text */ +.d3-flame-graph g:first-of-type rect { + stroke: none; +} + +.d3-flame-graph g:first-of-type .d3-flame-graph-label { + color: var(--text-muted); +} diff --git a/Lib/profiling/sampling/_flamegraph_assets/flamegraph.js b/Lib/profiling/sampling/_flamegraph_assets/flamegraph.js index 494d156a8dddfc..660ba0f22d713b 100644 --- a/Lib/profiling/sampling/_flamegraph_assets/flamegraph.js +++ b/Lib/profiling/sampling/_flamegraph_assets/flamegraph.js @@ -2,8 +2,10 @@ const EMBEDDED_DATA = {{FLAMEGRAPH_DATA}}; // Global string table for resolving string indices let stringTable = []; -let originalData = null; +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! @@ -68,9 +70,10 @@ function toggleTheme() { } // Re-render flamegraph with new theme colors - if (window.flamegraphData && originalData) { - const tooltip = createPythonTooltip(originalData); - const chart = createFlamegraph(tooltip, originalData.value); + if (window.flamegraphData && normalData) { + const currentData = isInverted ? invertedData : normalData; + const tooltip = createPythonTooltip(currentData); + const chart = createFlamegraph(tooltip, currentData.value); renderFlamegraph(chart, window.flamegraphData); } } @@ -380,6 +383,9 @@ function createFlamegraph(tooltip, rootValue) { .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]; @@ -888,19 +894,20 @@ function initThreadFilter(data) { function filterByThread() { const threadFilter = document.getElementById('thread-filter'); - if (!threadFilter || !originalData) return; + if (!threadFilter || !normalData) return; const selectedThread = threadFilter.value; currentThreadFilter = selectedThread; + const baseData = isInverted ? invertedData : normalData; let filteredData; let selectedThreadId = null; if (selectedThread === 'all') { - filteredData = originalData; + filteredData = baseData; } else { selectedThreadId = parseInt(selectedThread, 10); - filteredData = filterDataByThread(originalData, selectedThreadId); + filteredData = filterDataByThread(baseData, selectedThreadId); if (filteredData.strings) { stringTable = filteredData.strings; @@ -912,7 +919,7 @@ function filterByThread() { const chart = createFlamegraph(tooltip, filteredData.value); renderFlamegraph(chart, filteredData); - populateThreadStats(originalData, selectedThreadId); + populateThreadStats(baseData, selectedThreadId); } function filterDataByThread(data, threadId) { @@ -980,6 +987,131 @@ function exportSVG() { 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 traverseInvert(path, currentNode, invertedRoot) { + if (!currentNode.children || currentNode.children.length === 0) { + // We've reached a leaf node + if (!path || path.length === 0) { + return; + } + + let invertedParent = accumulateInvertedNode(invertedRoot, currentNode, currentNode); + + // Walk backwards through the call stack + for (let i = path.length - 2; i >= 0; i--) { + invertedParent = accumulateInvertedNode(invertedParent, path[i], currentNode); + } + } else { + // Not a leaf, continue traversing down the tree + for (const child of currentNode.children) { + 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 + }; + + data.children?.forEach(child => { + traverseInvert([child], child, invertedRoot); + }); + + // Convert children dictionaries to arrays for rendering + 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 // ============================================================================ @@ -988,21 +1120,29 @@ function initFlamegraph() { ensureLibraryLoaded(); restoreUIState(); - let processedData = EMBEDDED_DATA; if (EMBEDDED_DATA.strings) { stringTable = EMBEDDED_DATA.strings; - processedData = resolveStringIndices(EMBEDDED_DATA); + normalData = resolveStringIndices(EMBEDDED_DATA); + } else { + normalData = EMBEDDED_DATA; } - originalData = processedData; - initThreadFilter(processedData); + // Inverted data will be built on first toggle + invertedData = null; - const tooltip = createPythonTooltip(processedData); - const chart = createFlamegraph(tooltip, processedData.value); - renderFlamegraph(chart, processedData); + 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") { diff --git a/Lib/profiling/sampling/_flamegraph_assets/flamegraph_template.html b/Lib/profiling/sampling/_flamegraph_assets/flamegraph_template.html index 82102c229e7af9..fc322f65e5495a 100644 --- a/Lib/profiling/sampling/_flamegraph_assets/flamegraph_template.html +++ b/Lib/profiling/sampling/_flamegraph_assets/flamegraph_template.html @@ -75,6 +75,16 @@ + + +