Skip to content

Commit 483fd19

Browse files
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.
1 parent 53ec7c8 commit 483fd19

File tree

3 files changed

+192
-15
lines changed

3 files changed

+192
-15
lines changed

Lib/profiling/sampling/_flamegraph_assets/flamegraph.css

Lines changed: 27 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -274,6 +274,20 @@ body.resizing-sidebar {
274274
flex: 1;
275275
}
276276

277+
/* View Mode Section */
278+
.view-mode-section {
279+
padding-bottom: 20px;
280+
border-bottom: 1px solid var(--border);
281+
}
282+
283+
.view-mode-section .section-title {
284+
margin-bottom: 12px;
285+
}
286+
287+
.view-mode-section .toggle-switch {
288+
justify-content: center;
289+
}
290+
277291
/* Collapsible sections */
278292
.collapsible .section-header {
279293
display: flex;
@@ -899,3 +913,16 @@ body.resizing-sidebar {
899913
grid-template-columns: 1fr;
900914
}
901915
}
916+
917+
/* --------------------------------------------------------------------------
918+
Flamegraph Root Node Styling
919+
-------------------------------------------------------------------------- */
920+
921+
/* Style the root node - no border, themed text */
922+
.d3-flame-graph g:first-of-type rect {
923+
stroke: none;
924+
}
925+
926+
.d3-flame-graph g:first-of-type .d3-flame-graph-label {
927+
color: var(--text-muted);
928+
}

Lib/profiling/sampling/_flamegraph_assets/flamegraph.js

Lines changed: 155 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -2,8 +2,10 @@ const EMBEDDED_DATA = {{FLAMEGRAPH_DATA}};
22

33
// Global string table for resolving string indices
44
let stringTable = [];
5-
let originalData = null;
5+
let normalData = null;
6+
let invertedData = null;
67
let currentThreadFilter = 'all';
8+
let isInverted = false;
79

810
// Heat colors are now defined in CSS variables (--heat-1 through --heat-8)
911
// and automatically switch with theme changes - no JS color arrays needed!
@@ -68,9 +70,10 @@ function toggleTheme() {
6870
}
6971

7072
// Re-render flamegraph with new theme colors
71-
if (window.flamegraphData && originalData) {
72-
const tooltip = createPythonTooltip(originalData);
73-
const chart = createFlamegraph(tooltip, originalData.value);
73+
if (window.flamegraphData && normalData) {
74+
const currentData = isInverted ? invertedData : normalData;
75+
const tooltip = createPythonTooltip(currentData);
76+
const chart = createFlamegraph(tooltip, currentData.value);
7477
renderFlamegraph(chart, window.flamegraphData);
7578
}
7679
}
@@ -380,6 +383,9 @@ function createFlamegraph(tooltip, rootValue) {
380383
.tooltip(tooltip)
381384
.inverted(true)
382385
.setColorMapper(function (d) {
386+
// Root node should be transparent
387+
if (d.depth === 0) return 'transparent';
388+
383389
const percentage = d.data.value / rootValue;
384390
const level = getHeatLevel(percentage);
385391
return heatColors[level];
@@ -888,19 +894,20 @@ function initThreadFilter(data) {
888894

889895
function filterByThread() {
890896
const threadFilter = document.getElementById('thread-filter');
891-
if (!threadFilter || !originalData) return;
897+
if (!threadFilter || !normalData) return;
892898

893899
const selectedThread = threadFilter.value;
894900
currentThreadFilter = selectedThread;
901+
const baseData = isInverted ? invertedData : normalData;
895902

896903
let filteredData;
897904
let selectedThreadId = null;
898905

899906
if (selectedThread === 'all') {
900-
filteredData = originalData;
907+
filteredData = baseData;
901908
} else {
902909
selectedThreadId = parseInt(selectedThread, 10);
903-
filteredData = filterDataByThread(originalData, selectedThreadId);
910+
filteredData = filterDataByThread(baseData, selectedThreadId);
904911

905912
if (filteredData.strings) {
906913
stringTable = filteredData.strings;
@@ -912,7 +919,7 @@ function filterByThread() {
912919
const chart = createFlamegraph(tooltip, filteredData.value);
913920
renderFlamegraph(chart, filteredData);
914921

915-
populateThreadStats(originalData, selectedThreadId);
922+
populateThreadStats(baseData, selectedThreadId);
916923
}
917924

918925
function filterDataByThread(data, threadId) {
@@ -980,6 +987,131 @@ function exportSVG() {
980987
URL.revokeObjectURL(url);
981988
}
982989

990+
// ============================================================================
991+
// Inverted Flamegraph
992+
// ============================================================================
993+
994+
// Example: "file.py|10|foo" or "~|0|<GC>" for special frames
995+
function getInvertNodeKey(node) {
996+
return `${node.filename || '~'}|${node.lineno || 0}|${node.funcname || node.name}`;
997+
}
998+
999+
function accumulateInvertedNode(parent, stackFrame, leaf) {
1000+
const key = getInvertNodeKey(stackFrame);
1001+
1002+
if (!parent.children[key]) {
1003+
parent.children[key] = {
1004+
name: stackFrame.name,
1005+
value: 0,
1006+
children: {},
1007+
filename: stackFrame.filename,
1008+
lineno: stackFrame.lineno,
1009+
funcname: stackFrame.funcname,
1010+
source: stackFrame.source,
1011+
threads: new Set()
1012+
};
1013+
}
1014+
1015+
const node = parent.children[key];
1016+
node.value += leaf.value;
1017+
if (leaf.threads) {
1018+
leaf.threads.forEach(t => node.threads.add(t));
1019+
}
1020+
1021+
return node;
1022+
}
1023+
1024+
function traverseInvert(path, currentNode, invertedRoot) {
1025+
if (!currentNode.children || currentNode.children.length === 0) {
1026+
// We've reached a leaf node
1027+
if (!path || path.length === 0) {
1028+
return;
1029+
}
1030+
1031+
let invertedParent = accumulateInvertedNode(invertedRoot, currentNode, currentNode);
1032+
1033+
// Walk backwards through the call stack
1034+
for (let i = path.length - 2; i >= 0; i--) {
1035+
invertedParent = accumulateInvertedNode(invertedParent, path[i], currentNode);
1036+
}
1037+
} else {
1038+
// Not a leaf, continue traversing down the tree
1039+
for (const child of currentNode.children) {
1040+
traverseInvert(path.concat([child]), child, invertedRoot);
1041+
}
1042+
}
1043+
}
1044+
1045+
function convertInvertDictToArray(node) {
1046+
if (node.threads instanceof Set) {
1047+
node.threads = Array.from(node.threads).sort((a, b) => a - b);
1048+
}
1049+
1050+
const children = node.children;
1051+
if (children && typeof children === 'object' && !Array.isArray(children)) {
1052+
node.children = Object.values(children);
1053+
node.children.sort((a, b) => b.value - a.value || a.name.localeCompare(b.name));
1054+
node.children.forEach(convertInvertDictToArray);
1055+
}
1056+
return node;
1057+
}
1058+
1059+
function generateInvertedFlamegraph(data) {
1060+
const invertedRoot = {
1061+
name: data.name,
1062+
value: data.value,
1063+
children: {},
1064+
stats: data.stats,
1065+
threads: data.threads
1066+
};
1067+
1068+
data.children?.forEach(child => {
1069+
traverseInvert([child], child, invertedRoot);
1070+
});
1071+
1072+
// Convert children dictionaries to arrays for rendering
1073+
convertInvertDictToArray(invertedRoot);
1074+
1075+
return invertedRoot;
1076+
}
1077+
1078+
function updateToggleUI(toggleId, isOn) {
1079+
const toggle = document.getElementById(toggleId);
1080+
if (toggle) {
1081+
const track = toggle.querySelector('.toggle-track');
1082+
const labels = toggle.querySelectorAll('.toggle-label');
1083+
if (isOn) {
1084+
track.classList.add('on');
1085+
labels[0].classList.remove('active');
1086+
labels[1].classList.add('active');
1087+
} else {
1088+
track.classList.remove('on');
1089+
labels[0].classList.add('active');
1090+
labels[1].classList.remove('active');
1091+
}
1092+
}
1093+
}
1094+
1095+
function toggleInvert() {
1096+
isInverted = !isInverted;
1097+
updateToggleUI('toggle-invert', isInverted);
1098+
1099+
// Build inverted data on first use
1100+
if (isInverted && !invertedData) {
1101+
invertedData = generateInvertedFlamegraph(normalData);
1102+
}
1103+
1104+
let dataToRender = isInverted ? invertedData : normalData;
1105+
1106+
if (currentThreadFilter !== 'all') {
1107+
dataToRender = filterDataByThread(dataToRender, parseInt(currentThreadFilter));
1108+
}
1109+
1110+
const tooltip = createPythonTooltip(dataToRender);
1111+
const chart = createFlamegraph(tooltip, dataToRender.value);
1112+
renderFlamegraph(chart, dataToRender);
1113+
}
1114+
9831115
// ============================================================================
9841116
// Initialization
9851117
// ============================================================================
@@ -988,21 +1120,29 @@ function initFlamegraph() {
9881120
ensureLibraryLoaded();
9891121
restoreUIState();
9901122

991-
let processedData = EMBEDDED_DATA;
9921123
if (EMBEDDED_DATA.strings) {
9931124
stringTable = EMBEDDED_DATA.strings;
994-
processedData = resolveStringIndices(EMBEDDED_DATA);
1125+
normalData = resolveStringIndices(EMBEDDED_DATA);
1126+
} else {
1127+
normalData = EMBEDDED_DATA;
9951128
}
9961129

997-
originalData = processedData;
998-
initThreadFilter(processedData);
1130+
// Inverted data will be built on first toggle
1131+
invertedData = null;
9991132

1000-
const tooltip = createPythonTooltip(processedData);
1001-
const chart = createFlamegraph(tooltip, processedData.value);
1002-
renderFlamegraph(chart, processedData);
1133+
initThreadFilter(normalData);
1134+
1135+
const tooltip = createPythonTooltip(normalData);
1136+
const chart = createFlamegraph(tooltip, normalData.value);
1137+
renderFlamegraph(chart, normalData);
10031138
initSearchHandlers();
10041139
initSidebarResize();
10051140
handleResize();
1141+
1142+
const toggleInvertBtn = document.getElementById('toggle-invert');
1143+
if (toggleInvertBtn) {
1144+
toggleInvertBtn.addEventListener('click', toggleInvert);
1145+
}
10061146
}
10071147

10081148
if (document.readyState === "loading") {

Lib/profiling/sampling/_flamegraph_assets/flamegraph_template.html

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -75,6 +75,16 @@
7575
<div class="sidebar-logo-img"><!-- INLINE_LOGO --></div>
7676
</div>
7777

78+
<!-- View Mode Section -->
79+
<section class="sidebar-section view-mode-section">
80+
<h3 class="section-title">View Mode</h3>
81+
<div class="toggle-switch" id="toggle-invert">
82+
<span class="toggle-label active">Flamegraph</span>
83+
<div class="toggle-track"></div>
84+
<span class="toggle-label">Inverted Flamegraph</span>
85+
</div>
86+
</section>
87+
7888
<!-- Profile Summary Section -->
7989
<section class="sidebar-section collapsible" id="summary-section">
8090
<button class="section-header" onclick="toggleSection('summary-section')">

0 commit comments

Comments
 (0)