Skip to content

Commit a5cda5b

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 4df8d3d commit a5cda5b

File tree

5 files changed

+84
-15
lines changed

5 files changed

+84
-15
lines changed

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');
@@ -307,7 +320,8 @@ function createPythonTooltip(data) {
307320

308321
const funcname = resolveString(d.data.funcname) || resolveString(d.data.name);
309322
const filename = resolveString(d.data.filename) || "";
310-
const moduleName = escapeHtml(resolveString(d.data.module_name) || "");
323+
const moduleName = resolveString(d.data.module_name) || "";
324+
const displayName = escapeHtml(useModuleNames ? (moduleName || filename) : filename);
311325
const isSpecialFrame = filename === "~";
312326

313327
// Build source section
@@ -376,7 +390,7 @@ function createPythonTooltip(data) {
376390
}
377391

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

381395
// Differential stats section
382396
let diffSection = "";
@@ -588,6 +602,7 @@ function createFlamegraph(tooltip, rootValue, data) {
588602
.minFrameSize(1)
589603
.tooltip(tooltip)
590604
.inverted(true)
605+
.getName(d => resolveString(useModuleNames ? d.data.name_module : d.data.name) || resolveString(d.data.name) || '')
591606
.setColorMapper(function (d) {
592607
if (d.depth === 0) return 'transparent';
593608

@@ -631,24 +646,24 @@ function updateSearchHighlight(searchTerm, searchInput) {
631646
const funcname = resolveString(d.data.funcname) || "";
632647
const filename = resolveString(d.data.filename) || "";
633648
const moduleName = resolveString(d.data.module_name) || "";
649+
const displayName = getDisplayName(moduleName, filename);
634650
const lineno = d.data.lineno;
635651
const term = searchTerm.toLowerCase();
636652

637-
// Check if search term looks like module:line pattern
653+
// Check if search term looks like path:line pattern
638654
const fileLineMatch = term.match(/^(.+):(\d+)$/);
639655
let matches = false;
640656

641657
if (fileLineMatch) {
642658
const searchFile = fileLineMatch[1];
643659
const searchLine = parseInt(fileLineMatch[2], 10);
644-
matches = moduleName.toLowerCase().includes(searchFile) && lineno === searchLine;
660+
matches = displayName.toLowerCase().includes(searchFile) && lineno === searchLine;
645661
} else {
646662
// Regular substring search
647663
matches =
648664
name.toLowerCase().includes(term) ||
649665
funcname.toLowerCase().includes(term) ||
650-
moduleName.toLowerCase().includes(term) ||
651-
filename.toLowerCase().includes(term);
666+
displayName.toLowerCase().includes(term);
652667
}
653668

654669
if (matches) {
@@ -1134,7 +1149,8 @@ function populateStats(data) {
11341149
if (isSpecialFrame) {
11351150
fileEl.textContent = '--';
11361151
} else {
1137-
fileEl.textContent = `${moduleName}:${lineno}`;
1152+
const displayName = getDisplayName(moduleName, filename);
1153+
fileEl.textContent = `${displayName}:${lineno}`;
11381154
}
11391155
}
11401156
if (percentEl) percentEl.textContent = `${h.directPercent.toFixed(1)}%`;
@@ -1151,8 +1167,10 @@ function populateStats(data) {
11511167
if (i < hotSpots.length && hotSpots[i]) {
11521168
const h = hotSpots[i];
11531169
const moduleName = h.module_name || 'unknown';
1154-
const hasValidLocation = moduleName !== 'unknown' && h.lineno !== '?';
1155-
const searchTerm = hasValidLocation ? `${moduleName}:${h.lineno}` : h.funcname;
1170+
const filename = h.filename || 'unknown';
1171+
const displayName = getDisplayName(moduleName, filename);
1172+
const hasValidLocation = displayName !== 'unknown' && h.lineno !== '?';
1173+
const searchTerm = hasValidLocation ? `${displayName}:${h.lineno}` : h.funcname;
11561174
card.dataset.searchterm = searchTerm;
11571175
card.onclick = () => searchForHotspot(searchTerm);
11581176
card.style.cursor = 'pointer';
@@ -1284,6 +1302,7 @@ function accumulateInvertedNode(parent, stackFrame, leaf, isDifferential) {
12841302
if (!parent.children[key]) {
12851303
const newNode = {
12861304
name: stackFrame.name,
1305+
name_module: stackFrame.name_module,
12871306
value: 0,
12881307
children: {},
12891308
filename: stackFrame.filename,
@@ -1381,6 +1400,7 @@ function generateInvertedFlamegraph(data) {
13811400

13821401
const invertedRoot = {
13831402
name: data.name,
1403+
name_module: data.name_module,
13841404
value: data.value,
13851405
children: {},
13861406
stats: data.stats,
@@ -1405,6 +1425,19 @@ function toggleInvert() {
14051425
updateFlamegraphView();
14061426
}
14071427

1428+
function togglePathDisplay() {
1429+
useModuleNames = !useModuleNames;
1430+
updateToggleUI('toggle-path-display', useModuleNames);
1431+
const dataToRender = isInverted ? invertedData : normalData;
1432+
const filteredData = currentThreadFilter !== 'all'
1433+
? filterDataByThread(dataToRender, parseInt(currentThreadFilter))
1434+
: dataToRender;
1435+
1436+
const tooltip = createPythonTooltip(filteredData);
1437+
const chart = createFlamegraph(tooltip, filteredData.value);
1438+
renderFlamegraph(chart, filteredData);
1439+
}
1440+
14081441
// ============================================================================
14091442
// Initialization
14101443
// ============================================================================
@@ -1452,6 +1485,11 @@ function initFlamegraph() {
14521485
if (toggleInvertBtn) {
14531486
toggleInvertBtn.addEventListener('click', toggleInvert);
14541487
}
1488+
1489+
const togglePathDisplayBtn = document.getElementById('toggle-path-display');
1490+
if (togglePathDisplayBtn) {
1491+
togglePathDisplayBtn.addEventListener('click', togglePathDisplay);
1492+
}
14551493
}
14561494

14571495
// 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
"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
@@ -435,11 +435,11 @@ def test_flamegraph_collector_basic(self):
435435
strings = data.get("strings", [])
436436
name = resolve_name(data, strings)
437437
self.assertTrue(name.startswith("Program Root: "))
438-
self.assertIn("func2 (file:20)", name)
438+
self.assertIn("func2 (file.py:20)", name)
439439
children = data.get("children", [])
440440
self.assertEqual(len(children), 1)
441441
child = children[0]
442-
self.assertIn("func1 (file:10)", resolve_name(child, strings))
442+
self.assertIn("func1 (file.py:10)", resolve_name(child, strings))
443443
self.assertEqual(child["value"], 1)
444444

445445
def test_flamegraph_collector_export(self):

0 commit comments

Comments
 (0)