Skip to content

Commit b5eeefb

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 dbe7521 commit b5eeefb

File tree

5 files changed

+85
-15
lines changed

5 files changed

+85
-15
lines changed

Lib/profiling/sampling/_flamegraph_assets/flamegraph.css

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

277277
/* View Mode Section */
278+
.view-mode-section {
279+
display: flex;
280+
flex-direction: column;
281+
gap: 8px;
282+
}
283+
278284
.view-mode-section .section-content {
279285
display: flex;
280286
justify-content: center;
@@ -989,7 +995,8 @@ body.resizing-sidebar {
989995
Flamegraph-Specific Toggle Override
990996
-------------------------------------------------------------------------- */
991997

992-
#toggle-invert .toggle-track.on {
998+
#toggle-invert .toggle-track.on,
999+
#toggle-path-display .toggle-track.on {
9931000
background: #8e44ad;
9941001
border-color: #8e44ad;
9951002
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) {
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
// ============================================================================
90102
// Theme & UI Controls
91103
// ============================================================================
@@ -231,7 +243,8 @@ function updateStatusBar(nodeData, rootValue) {
231243

232244
const fileEl = document.getElementById('status-file');
233245
if (fileEl && filename && filename !== "~") {
234-
fileEl.textContent = lineno ? `${moduleName}:${lineno}` : moduleName;
246+
const displayName = getDisplayName(moduleName, filename);
247+
fileEl.textContent = lineno ? `${displayName}:${lineno}` : displayName;
235248
}
236249

237250
const funcEl = document.getElementById('status-func');
@@ -280,7 +293,8 @@ function createPythonTooltip(data) {
280293

281294
const funcname = resolveString(d.data.funcname) || resolveString(d.data.name);
282295
const filename = resolveString(d.data.filename) || "";
283-
const moduleName = escapeHtml(resolveString(d.data.module_name) || "");
296+
const moduleName = resolveString(d.data.module_name) || "";
297+
const displayName = escapeHtml(useModuleNames ? (moduleName || filename) : filename);
284298
const isSpecialFrame = filename === "~";
285299

286300
// Build source section
@@ -349,7 +363,7 @@ function createPythonTooltip(data) {
349363
}
350364

351365
const fileLocationHTML = isSpecialFrame ? "" : `
352-
<div class="tooltip-location">${moduleName}${d.data.lineno ? ":" + d.data.lineno : ""}</div>`;
366+
<div class="tooltip-location">${displayName}${d.data.lineno ? ":" + d.data.lineno : ""}</div>`;
353367

354368
const tooltipHTML = `
355369
<div class="tooltip-header">
@@ -479,6 +493,7 @@ function createFlamegraph(tooltip, rootValue) {
479493
.minFrameSize(1)
480494
.tooltip(tooltip)
481495
.inverted(true)
496+
.getName(d => resolveString(useModuleNames ? d.data.name_module : d.data.name) || resolveString(d.data.name) || '')
482497
.setColorMapper(function (d) {
483498
// Root node should be transparent
484499
if (d.depth === 0) return 'transparent';
@@ -519,24 +534,24 @@ function updateSearchHighlight(searchTerm, searchInput) {
519534
const funcname = resolveString(d.data.funcname) || "";
520535
const filename = resolveString(d.data.filename) || "";
521536
const moduleName = resolveString(d.data.module_name) || "";
537+
const displayName = getDisplayName(moduleName, filename);
522538
const lineno = d.data.lineno;
523539
const term = searchTerm.toLowerCase();
524540

525-
// Check if search term looks like module:line pattern
541+
// Check if search term looks like path:line pattern
526542
const fileLineMatch = term.match(/^(.+):(\d+)$/);
527543
let matches = false;
528544

529545
if (fileLineMatch) {
530546
const searchFile = fileLineMatch[1];
531547
const searchLine = parseInt(fileLineMatch[2], 10);
532-
matches = moduleName.toLowerCase().includes(searchFile) && lineno === searchLine;
548+
matches = displayName.toLowerCase().includes(searchFile) && lineno === searchLine;
533549
} else {
534550
// Regular substring search
535551
matches =
536552
name.toLowerCase().includes(term) ||
537553
funcname.toLowerCase().includes(term) ||
538-
moduleName.toLowerCase().includes(term) ||
539-
filename.toLowerCase().includes(term);
554+
displayName.toLowerCase().includes(term);
540555
}
541556

542557
if (matches) {
@@ -988,7 +1003,8 @@ function populateStats(data) {
9881003
if (isSpecialFrame) {
9891004
fileEl.textContent = '--';
9901005
} else {
991-
fileEl.textContent = `${moduleName}:${lineno}`;
1006+
const displayName = getDisplayName(moduleName, filename);
1007+
fileEl.textContent = `${displayName}:${lineno}`;
9921008
}
9931009
}
9941010
if (percentEl) percentEl.textContent = `${h.directPercent.toFixed(1)}%`;
@@ -1005,8 +1021,10 @@ function populateStats(data) {
10051021
if (i < hotSpots.length && hotSpots[i]) {
10061022
const h = hotSpots[i];
10071023
const moduleName = h.module_name || 'unknown';
1008-
const hasValidLocation = moduleName !== 'unknown' && h.lineno !== '?';
1009-
const searchTerm = hasValidLocation ? `${moduleName}:${h.lineno}` : h.funcname;
1024+
const filename = h.filename || 'unknown';
1025+
const displayName = getDisplayName(moduleName, filename);
1026+
const hasValidLocation = displayName !== 'unknown' && h.lineno !== '?';
1027+
const searchTerm = hasValidLocation ? `${displayName}:${h.lineno}` : h.funcname;
10101028
card.dataset.searchterm = searchTerm;
10111029
card.onclick = () => searchForHotspot(searchTerm);
10121030
card.style.cursor = 'pointer';
@@ -1158,6 +1176,7 @@ function accumulateInvertedNode(parent, stackFrame, leaf) {
11581176
if (!parent.children[key]) {
11591177
parent.children[key] = {
11601178
name: stackFrame.name,
1179+
name_module: stackFrame.name_module,
11611180
value: 0,
11621181
children: {},
11631182
filename: stackFrame.filename,
@@ -1220,6 +1239,7 @@ function convertInvertDictToArray(node) {
12201239
function generateInvertedFlamegraph(data) {
12211240
const invertedRoot = {
12221241
name: data.name,
1242+
name_module: data.name_module,
12231243
value: data.value,
12241244
children: {},
12251245
stats: data.stats,
@@ -1258,6 +1278,19 @@ function toggleInvert() {
12581278
renderFlamegraph(chart, dataToRender);
12591279
}
12601280

1281+
function togglePathDisplay() {
1282+
useModuleNames = !useModuleNames;
1283+
updateToggleUI('toggle-path-display', useModuleNames);
1284+
const dataToRender = isInverted ? invertedData : normalData;
1285+
const filteredData = currentThreadFilter !== 'all'
1286+
? filterDataByThread(dataToRender, parseInt(currentThreadFilter))
1287+
: dataToRender;
1288+
1289+
const tooltip = createPythonTooltip(filteredData);
1290+
const chart = createFlamegraph(tooltip, filteredData.value);
1291+
renderFlamegraph(chart, filteredData);
1292+
}
1293+
12611294
// ============================================================================
12621295
// Initialization
12631296
// ============================================================================
@@ -1293,6 +1326,11 @@ function initFlamegraph() {
12931326
if (toggleInvertBtn) {
12941327
toggleInvertBtn.addEventListener('click', toggleInvert);
12951328
}
1329+
1330+
const togglePathDisplayBtn = document.getElementById('toggle-path-display');
1331+
if (togglePathDisplayBtn) {
1332+
togglePathDisplayBtn.addEventListener('click', togglePathDisplay);
1333+
}
12961334
}
12971335

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

Lib/profiling/sampling/_flamegraph_assets/flamegraph_template.html

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -109,6 +109,13 @@ <h3 class="section-title">View Mode</h3>
109109
<path d="M3 4.5L6 7.5L9 4.5" stroke="currentColor" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"/>
110110
</svg>
111111
</button>
112+
<div class="section-content">
113+
<div class="toggle-switch" id="toggle-path-display" title="Toggle between module names and full file paths" tabindex="0">
114+
<span class="toggle-label" data-text="Full Paths">Full Paths</span>
115+
<div class="toggle-track on"></div>
116+
<span class="toggle-label active" data-text="Module Names">Module Names</span>
117+
</div>
118+
</div>
112119
<div class="section-content">
113120
<div class="toggle-switch" id="toggle-invert" title="Toggle between standard and inverted flamegraph view" tabindex="0">
114121
<span class="toggle-label active" data-text="Flamegraph">Flamegraph</span>

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
"children": [],
218236
"filename": filename_idx,

Lib/test/test_profiling/test_sampling_profiler/test_collectors.py

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -430,7 +430,7 @@ def test_flamegraph_collector_basic(self):
430430
)
431431
self.assertIsInstance(name, str)
432432
self.assertTrue(name.startswith("Program Root: "))
433-
self.assertIn("func2 (file:20)", name)
433+
self.assertIn("func2 (file.py:20)", name)
434434
children = data.get("children", [])
435435
self.assertEqual(len(children), 1)
436436
child = children[0]
@@ -441,7 +441,7 @@ def test_flamegraph_collector_basic(self):
441441
and 0 <= child_name_index < len(strings)
442442
else str(child_name_index)
443443
)
444-
self.assertIn("func1 (file:10)", child_name)
444+
self.assertIn("func1 (file.py:10)", child_name)
445445
self.assertEqual(child["value"], 1)
446446

447447
def test_flamegraph_collector_export(self):

0 commit comments

Comments
 (0)