Skip to content

Commit 44599a4

Browse files
Add 'Full Paths/Module Names' toggle for flamegraph display
Users can now switch between module names and file paths using the toggle in the View Mode sidebar. Module names are concise, while file paths help locate the exact source file, both are useful depending on the debugging context.
1 parent 75e120b commit 44599a4

4 files changed

Lines changed: 82 additions & 13 deletions

File tree

Lib/profiling/sampling/_flamegraph_assets/flamegraph.css

Lines changed: 8 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -315,6 +315,12 @@ body.resizing-sidebar {
315315
}
316316

317317
/* View Mode Section */
318+
.view-mode-section {
319+
display: flex;
320+
flex-direction: column;
321+
gap: 8px;
322+
}
323+
318324
.view-mode-section .section-content {
319325
display: flex;
320326
flex-direction: column;
@@ -1067,7 +1073,8 @@ body.resizing-sidebar {
10671073
-------------------------------------------------------------------------- */
10681074

10691075
#toggle-invert .toggle-track.on,
1070-
#toggle-elided .toggle-track.on {
1076+
#toggle-elided .toggle-track.on,
1077+
#toggle-path-display .toggle-track.on {
10711078
background: #8e44ad;
10721079
border-color: #8e44ad;
10731080
box-shadow: 0 0 8px rgba(142, 68, 173, 0.3);

Lib/profiling/sampling/_flamegraph_assets/flamegraph.js

Lines changed: 48 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@ let normalData = null;
66
let invertedData = null;
77
let currentThreadFilter = 'all';
88
let isInverted = false;
9+
let useModuleNames = true;
910

1011
// Heat colors are now defined in CSS variables (--heat-1 through --heat-8)
1112
// and automatically switch with theme changes - no JS color arrays needed!
@@ -67,6 +68,9 @@ function resolveStringIndices(node, table) {
6768
if (typeof resolved.module_name === 'number') {
6869
resolved.module_name = resolveString(resolved.module_name);
6970
}
71+
if (typeof resolved.name_module === 'number') {
72+
resolved.name_module = resolveString(resolved.name_module);
73+
}
7074

7175
if (Array.isArray(resolved.source)) {
7276
resolved.source = resolved.source.map(index =>
@@ -86,6 +90,14 @@ function escapeHtml(str) {
8690
return str.replace(/&/g, "&amp;").replace(/</g, "&lt;").replace(/>/g, "&gt;");
8791
}
8892

93+
// Get display path based on user preference (module name or basename)
94+
function getDisplayName(moduleName, filename) {
95+
if (useModuleNames) {
96+
return moduleName || filename;
97+
}
98+
return filename ? filename.split('/').pop() : filename;
99+
}
100+
89101
function selectFlamegraphData() {
90102
const baseData = isShowingElided ? elidedFlamegraphData : normalData;
91103

@@ -258,7 +270,8 @@ function updateStatusBar(nodeData, rootValue) {
258270

259271
const fileEl = document.getElementById('status-file');
260272
if (fileEl && filename && filename !== "~") {
261-
fileEl.textContent = lineno ? `${moduleName}:${lineno}` : moduleName;
273+
const displayName = getDisplayName(moduleName, filename);
274+
fileEl.textContent = lineno ? `${displayName}:${lineno}` : displayName;
262275
}
263276

264277
const funcEl = document.getElementById('status-func');
@@ -309,7 +322,8 @@ function createPythonTooltip(data) {
309322

310323
const funcname = resolveString(d.data.funcname) || resolveString(d.data.name);
311324
const filename = resolveString(d.data.filename) || "";
312-
const moduleName = escapeHtml(resolveString(d.data.module_name) || "");
325+
const moduleName = resolveString(d.data.module_name) || "";
326+
const displayName = escapeHtml(useModuleNames ? (moduleName || filename) : filename);
313327
const isSpecialFrame = filename === "~";
314328

315329
// Build source section
@@ -378,7 +392,7 @@ function createPythonTooltip(data) {
378392
}
379393

380394
const fileLocationHTML = isSpecialFrame ? "" : `
381-
<div class="tooltip-location">${moduleName}${d.data.lineno ? ":" + d.data.lineno : ""}</div>`;
395+
<div class="tooltip-location">${displayName}${d.data.lineno ? ":" + d.data.lineno : ""}</div>`;
382396

383397
// Differential stats section
384398
let diffSection = "";
@@ -595,6 +609,7 @@ function createFlamegraph(tooltip, rootValue, data) {
595609
.minFrameSize(1)
596610
.tooltip(tooltip)
597611
.inverted(true)
612+
.getName(d => resolveString(useModuleNames ? d.data.name_module : d.data.name) || resolveString(d.data.name) || '')
598613
.setColorMapper(function (d) {
599614
if (d.depth === 0) return 'transparent';
600615

@@ -638,24 +653,24 @@ function updateSearchHighlight(searchTerm, searchInput) {
638653
const funcname = resolveString(d.data.funcname) || "";
639654
const filename = resolveString(d.data.filename) || "";
640655
const moduleName = resolveString(d.data.module_name) || "";
656+
const displayName = getDisplayName(moduleName, filename);
641657
const lineno = d.data.lineno;
642658
const term = searchTerm.toLowerCase();
643659

644-
// Check if search term looks like module:line pattern
660+
// Check if search term looks like path:line pattern
645661
const fileLineMatch = term.match(/^(.+):(\d+)$/);
646662
let matches = false;
647663

648664
if (fileLineMatch) {
649665
const searchFile = fileLineMatch[1];
650666
const searchLine = parseInt(fileLineMatch[2], 10);
651-
matches = moduleName.toLowerCase().includes(searchFile) && lineno === searchLine;
667+
matches = displayName.toLowerCase().includes(searchFile) && lineno === searchLine;
652668
} else {
653669
// Regular substring search
654670
matches =
655671
name.toLowerCase().includes(term) ||
656672
funcname.toLowerCase().includes(term) ||
657-
moduleName.toLowerCase().includes(term) ||
658-
filename.toLowerCase().includes(term);
673+
displayName.toLowerCase().includes(term);
659674
}
660675

661676
if (matches) {
@@ -1141,7 +1156,8 @@ function populateStats(data) {
11411156
if (isSpecialFrame) {
11421157
fileEl.textContent = '--';
11431158
} else {
1144-
fileEl.textContent = `${moduleName}:${lineno}`;
1159+
const displayName = getDisplayName(moduleName, filename);
1160+
fileEl.textContent = `${displayName}:${lineno}`;
11451161
}
11461162
}
11471163
if (percentEl) percentEl.textContent = `${h.directPercent.toFixed(1)}%`;
@@ -1158,8 +1174,10 @@ function populateStats(data) {
11581174
if (i < hotSpots.length && hotSpots[i]) {
11591175
const h = hotSpots[i];
11601176
const moduleName = h.module_name || 'unknown';
1161-
const hasValidLocation = moduleName !== 'unknown' && h.lineno !== '?';
1162-
const searchTerm = hasValidLocation ? `${moduleName}:${h.lineno}` : h.funcname;
1177+
const filename = h.filename || 'unknown';
1178+
const displayName = getDisplayName(moduleName, filename);
1179+
const hasValidLocation = displayName !== 'unknown' && h.lineno !== '?';
1180+
const searchTerm = hasValidLocation ? `${displayName}:${h.lineno}` : h.funcname;
11631181
card.dataset.searchterm = searchTerm;
11641182
card.onclick = () => searchForHotspot(searchTerm);
11651183
card.style.cursor = 'pointer';
@@ -1291,6 +1309,7 @@ function accumulateInvertedNode(parent, stackFrame, leaf, isDifferential) {
12911309
if (!parent.children[key]) {
12921310
const newNode = {
12931311
name: stackFrame.name,
1312+
name_module: stackFrame.name_module,
12941313
value: 0,
12951314
self: 0,
12961315
children: {},
@@ -1390,6 +1409,7 @@ function generateInvertedFlamegraph(data) {
13901409

13911410
const invertedRoot = {
13921411
name: data.name,
1412+
name_module: data.name_module,
13931413
value: data.value,
13941414
children: {},
13951415
stats: data.stats,
@@ -1414,6 +1434,19 @@ function toggleInvert() {
14141434
updateFlamegraphView();
14151435
}
14161436

1437+
function togglePathDisplay() {
1438+
useModuleNames = !useModuleNames;
1439+
updateToggleUI('toggle-path-display', useModuleNames);
1440+
const dataToRender = isInverted ? invertedData : normalData;
1441+
const filteredData = currentThreadFilter !== 'all'
1442+
? filterDataByThread(dataToRender, parseInt(currentThreadFilter))
1443+
: dataToRender;
1444+
1445+
const tooltip = createPythonTooltip(filteredData);
1446+
const chart = createFlamegraph(tooltip, filteredData.value);
1447+
renderFlamegraph(chart, filteredData);
1448+
}
1449+
14171450
// ============================================================================
14181451
// Initialization
14191452
// ============================================================================
@@ -1461,6 +1494,11 @@ function initFlamegraph() {
14611494
if (toggleInvertBtn) {
14621495
toggleInvertBtn.addEventListener('click', toggleInvert);
14631496
}
1497+
1498+
const togglePathDisplayBtn = document.getElementById('toggle-path-display');
1499+
if (togglePathDisplayBtn) {
1500+
togglePathDisplayBtn.addEventListener('click', togglePathDisplay);
1501+
}
14641502
}
14651503

14661504
// Keyboard shortcut: Enter/Space activates toggle switches

Lib/profiling/sampling/_flamegraph_assets/flamegraph_template.html

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -117,6 +117,12 @@ <h3 class="section-title">View Mode</h3>
117117
<span class="toggle-label" data-text="Elided" title="Code paths that existed in baseline but are missing from current profile">Elided</span>
118118
</div>
119119

120+
<div class="toggle-switch" id="toggle-path-display" title="Toggle between module names and full file paths" tabindex="0">
121+
<span class="toggle-label" data-text="Full Paths">Full Paths</span>
122+
<div class="toggle-track on"></div>
123+
<span class="toggle-label active" data-text="Module Names">Module Names</span>
124+
</div>
125+
120126
<div class="toggle-switch" id="toggle-invert" title="Toggle between standard and inverted flamegraph view" tabindex="0">
121127
<span class="toggle-label active" data-text="Flamegraph">Flamegraph</span>
122128
<div class="toggle-track"></div>

Lib/profiling/sampling/stack_collector.py

Lines changed: 20 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -170,7 +170,23 @@ def export(self, filename):
170170

171171
@staticmethod
172172
@functools.lru_cache(maxsize=None)
173-
def _format_function_name(func, module_name):
173+
def _format_function_name(func):
174+
filename, lineno, funcname = func
175+
176+
# Special frames like <GC> and <native> should not show file:line
177+
if filename == "~" and lineno == 0:
178+
return funcname
179+
180+
if len(filename) > 50:
181+
parts = filename.split("/")
182+
if len(parts) > 2:
183+
filename = f".../{'/'.join(parts[-2:])}"
184+
185+
return f"{funcname} ({filename}:{lineno})"
186+
187+
@staticmethod
188+
@functools.lru_cache(maxsize=None)
189+
def _format_module_name(func, module_name):
174190
filename, lineno, funcname = func
175191

176192
# Special frames like <GC> and <native> should not show file:line
@@ -209,10 +225,12 @@ def convert_children(children, min_samples, path_info):
209225
module_name = self._get_module_name(func[0], path_info)
210226

211227
module_name_idx = self._string_table.intern(module_name)
212-
name_idx = self._string_table.intern(self._format_function_name(func, module_name))
228+
name_idx = self._string_table.intern(self._format_function_name(func))
229+
name_module_idx = self._string_table.intern(self._format_module_name(func, module_name))
213230

214231
child_entry = {
215232
"name": name_idx,
233+
"name_module": name_module_idx,
216234
"value": samples,
217235
"self": node.get("self", 0),
218236
"children": [],

0 commit comments

Comments
 (0)