Skip to content

Commit dbe7521

Browse files
Show module names instead of file paths in flamegraph
Display module names instead of full file paths (/home/user/project/pkg/mod.py → pkg.mod) in flamegraph for readability.
1 parent d1de077 commit dbe7521

File tree

3 files changed

+48
-24
lines changed

3 files changed

+48
-24
lines changed

Lib/profiling/sampling/_flamegraph_assets/flamegraph.js

Lines changed: 27 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -64,6 +64,9 @@ function resolveStringIndices(node) {
6464
if (typeof resolved.funcname === 'number') {
6565
resolved.funcname = resolveString(resolved.funcname);
6666
}
67+
if (typeof resolved.module_name === 'number') {
68+
resolved.module_name = resolveString(resolved.module_name);
69+
}
6770

6871
if (Array.isArray(resolved.source)) {
6972
resolved.source = resolved.source.map(index =>
@@ -78,6 +81,11 @@ function resolveStringIndices(node) {
7881
return resolved;
7982
}
8083

84+
// Escape HTML special characters
85+
function escapeHtml(str) {
86+
return str.replace(/&/g, "&amp;").replace(/</g, "&lt;").replace(/>/g, "&gt;");
87+
}
88+
8189
// ============================================================================
8290
// Theme & UI Controls
8391
// ============================================================================
@@ -201,6 +209,7 @@ function setupLogos() {
201209
function updateStatusBar(nodeData, rootValue) {
202210
const funcname = resolveString(nodeData.funcname) || resolveString(nodeData.name) || "--";
203211
const filename = resolveString(nodeData.filename) || "";
212+
const moduleName = resolveString(nodeData.module_name) || "";
204213
const lineno = nodeData.lineno;
205214
const timeMs = (nodeData.value / 1000).toFixed(2);
206215
const percent = rootValue > 0 ? ((nodeData.value / rootValue) * 100).toFixed(1) : "0.0";
@@ -222,8 +231,7 @@ function updateStatusBar(nodeData, rootValue) {
222231

223232
const fileEl = document.getElementById('status-file');
224233
if (fileEl && filename && filename !== "~") {
225-
const basename = filename.split('/').pop();
226-
fileEl.textContent = lineno ? `${basename}:${lineno}` : basename;
234+
fileEl.textContent = lineno ? `${moduleName}:${lineno}` : moduleName;
227235
}
228236

229237
const funcEl = document.getElementById('status-func');
@@ -272,6 +280,7 @@ function createPythonTooltip(data) {
272280

273281
const funcname = resolveString(d.data.funcname) || resolveString(d.data.name);
274282
const filename = resolveString(d.data.filename) || "";
283+
const moduleName = escapeHtml(resolveString(d.data.module_name) || "");
275284
const isSpecialFrame = filename === "~";
276285

277286
// Build source section
@@ -280,7 +289,7 @@ function createPythonTooltip(data) {
280289
const sourceLines = source
281290
.map((line) => {
282291
const isCurrent = line.startsWith("→");
283-
const escaped = line.replace(/&/g, "&amp;").replace(/</g, "&lt;").replace(/>/g, "&gt;");
292+
const escaped = escapeHtml(line);
284293
return `<div class="tooltip-source-line${isCurrent ? ' current' : ''}">${escaped}</div>`;
285294
})
286295
.join("");
@@ -340,7 +349,7 @@ function createPythonTooltip(data) {
340349
}
341350

342351
const fileLocationHTML = isSpecialFrame ? "" : `
343-
<div class="tooltip-location">${filename}${d.data.lineno ? ":" + d.data.lineno : ""}</div>`;
352+
<div class="tooltip-location">${moduleName}${d.data.lineno ? ":" + d.data.lineno : ""}</div>`;
344353

345354
const tooltipHTML = `
346355
<div class="tooltip-header">
@@ -509,24 +518,24 @@ function updateSearchHighlight(searchTerm, searchInput) {
509518
const name = resolveString(d.data.name) || "";
510519
const funcname = resolveString(d.data.funcname) || "";
511520
const filename = resolveString(d.data.filename) || "";
521+
const moduleName = resolveString(d.data.module_name) || "";
512522
const lineno = d.data.lineno;
513523
const term = searchTerm.toLowerCase();
514524

515-
// Check if search term looks like file:line pattern
525+
// Check if search term looks like module:line pattern
516526
const fileLineMatch = term.match(/^(.+):(\d+)$/);
517527
let matches = false;
518528

519529
if (fileLineMatch) {
520-
// Exact file:line matching
521530
const searchFile = fileLineMatch[1];
522531
const searchLine = parseInt(fileLineMatch[2], 10);
523-
const basename = filename.split('/').pop().toLowerCase();
524-
matches = basename.includes(searchFile) && lineno === searchLine;
532+
matches = moduleName.toLowerCase().includes(searchFile) && lineno === searchLine;
525533
} else {
526534
// Regular substring search
527535
matches =
528536
name.toLowerCase().includes(term) ||
529537
funcname.toLowerCase().includes(term) ||
538+
moduleName.toLowerCase().includes(term) ||
530539
filename.toLowerCase().includes(term);
531540
}
532541

@@ -894,6 +903,7 @@ function populateStats(data) {
894903

895904
let filename = resolveString(node.filename);
896905
let funcname = resolveString(node.funcname);
906+
let moduleName = resolveString(node.module_name);
897907

898908
if (!filename || !funcname) {
899909
const nameStr = resolveString(node.name);
@@ -908,6 +918,7 @@ function populateStats(data) {
908918

909919
filename = filename || 'unknown';
910920
funcname = funcname || 'unknown';
921+
moduleName = moduleName || 'unknown';
911922

912923
if (filename !== 'unknown' && funcname !== 'unknown' && node.value > 0) {
913924
let childrenValue = 0;
@@ -924,12 +935,14 @@ function populateStats(data) {
924935
existing.directPercent = (existing.directSamples / totalSamples) * 100;
925936
if (directSamples > existing.maxSingleSamples) {
926937
existing.filename = filename;
938+
existing.module_name = moduleName;
927939
existing.lineno = node.lineno || '?';
928940
existing.maxSingleSamples = directSamples;
929941
}
930942
} else {
931943
functionMap.set(funcKey, {
932944
filename: filename,
945+
module_name: moduleName,
933946
lineno: node.lineno || '?',
934947
funcname: funcname,
935948
directSamples,
@@ -964,6 +977,7 @@ function populateStats(data) {
964977
const h = hotSpots[i];
965978
const filename = h.filename || 'unknown';
966979
const lineno = h.lineno ?? '?';
980+
const moduleName = h.module_name || 'unknown';
967981
const isSpecialFrame = filename === '~' && (lineno === 0 || lineno === '?');
968982

969983
let funcDisplay = h.funcname || 'unknown';
@@ -974,8 +988,7 @@ function populateStats(data) {
974988
if (isSpecialFrame) {
975989
fileEl.textContent = '--';
976990
} else {
977-
const basename = filename !== 'unknown' ? filename.split('/').pop() : 'unknown';
978-
fileEl.textContent = `${basename}:${lineno}`;
991+
fileEl.textContent = `${moduleName}:${lineno}`;
979992
}
980993
}
981994
if (percentEl) percentEl.textContent = `${h.directPercent.toFixed(1)}%`;
@@ -991,8 +1004,9 @@ function populateStats(data) {
9911004
if (card) {
9921005
if (i < hotSpots.length && hotSpots[i]) {
9931006
const h = hotSpots[i];
994-
const basename = h.filename !== 'unknown' ? h.filename.split('/').pop() : '';
995-
const searchTerm = basename && h.lineno !== '?' ? `${basename}:${h.lineno}` : h.funcname;
1007+
const moduleName = h.module_name || 'unknown';
1008+
const hasValidLocation = moduleName !== 'unknown' && h.lineno !== '?';
1009+
const searchTerm = hasValidLocation ? `${moduleName}:${h.lineno}` : h.funcname;
9961010
card.dataset.searchterm = searchTerm;
9971011
card.onclick = () => searchForHotspot(searchTerm);
9981012
card.style.cursor = 'pointer';
@@ -1147,6 +1161,7 @@ function accumulateInvertedNode(parent, stackFrame, leaf) {
11471161
value: 0,
11481162
children: {},
11491163
filename: stackFrame.filename,
1164+
module_name: stackFrame.module_name,
11501165
lineno: stackFrame.lineno,
11511166
funcname: stackFrame.funcname,
11521167
source: stackFrame.source,

Lib/profiling/sampling/stack_collector.py

Lines changed: 19 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,7 @@
1212
from .collector import Collector, extract_lineno
1313
from .opcode_utils import get_opcode_mapping
1414
from .string_table import StringTable
15+
from .module_utils import extract_module_name, get_python_path_info
1516

1617

1718
class StackTraceCollector(Collector):
@@ -72,6 +73,7 @@ def __init__(self, *args, **kwargs):
7273
self._sample_count = 0 # Track actual number of samples (not thread traces)
7374
self._func_intern = {}
7475
self._string_table = StringTable()
76+
self._module_cache = {}
7577
self._all_threads = set()
7678

7779
# Thread status statistics (similar to LiveStatsCollector)
@@ -168,19 +170,21 @@ def export(self, filename):
168170

169171
@staticmethod
170172
@functools.lru_cache(maxsize=None)
171-
def _format_function_name(func):
173+
def _format_function_name(func, module_name):
172174
filename, lineno, funcname = func
173175

174176
# Special frames like <GC> and <native> should not show file:line
175177
if filename == "~" and lineno == 0:
176178
return funcname
177179

178-
if len(filename) > 50:
179-
parts = filename.split("/")
180-
if len(parts) > 2:
181-
filename = f".../{'/'.join(parts[-2:])}"
180+
return f"{funcname} ({module_name}:{lineno})"
182181

183-
return f"{funcname} ({filename}:{lineno})"
182+
def _get_module_name(self, filename, path_info):
183+
module_name = self._module_cache.get(filename)
184+
if module_name is None:
185+
module_name, _ = extract_module_name(filename, path_info)
186+
self._module_cache[filename] = module_name
187+
return module_name
184188

185189
def _convert_to_flamegraph_format(self):
186190
if self._total_samples == 0:
@@ -192,7 +196,7 @@ def _convert_to_flamegraph_format(self):
192196
"strings": self._string_table.get_strings()
193197
}
194198

195-
def convert_children(children, min_samples):
199+
def convert_children(children, min_samples, path_info):
196200
out = []
197201
for func, node in children.items():
198202
samples = node["samples"]
@@ -202,13 +206,17 @@ def convert_children(children, min_samples):
202206
# Intern all string components for maximum efficiency
203207
filename_idx = self._string_table.intern(func[0])
204208
funcname_idx = self._string_table.intern(func[2])
205-
name_idx = self._string_table.intern(self._format_function_name(func))
209+
module_name = self._get_module_name(func[0], path_info)
210+
211+
module_name_idx = self._string_table.intern(module_name)
212+
name_idx = self._string_table.intern(self._format_function_name(func, module_name))
206213

207214
child_entry = {
208215
"name": name_idx,
209216
"value": samples,
210217
"children": [],
211218
"filename": filename_idx,
219+
"module_name": module_name_idx,
212220
"lineno": func[1],
213221
"funcname": funcname_idx,
214222
"threads": sorted(list(node.get("threads", set()))),
@@ -227,7 +235,7 @@ def convert_children(children, min_samples):
227235

228236
# Recurse
229237
child_entry["children"] = convert_children(
230-
node["children"], min_samples
238+
node["children"], min_samples, path_info
231239
)
232240
out.append(child_entry)
233241

@@ -238,8 +246,9 @@ def convert_children(children, min_samples):
238246
# Filter out very small functions (less than 0.1% of total samples)
239247
total_samples = self._total_samples
240248
min_samples = max(1, int(total_samples * 0.001))
249+
path_info = get_python_path_info()
241250

242-
root_children = convert_children(self._root["children"], min_samples)
251+
root_children = convert_children(self._root["children"], min_samples, path_info)
243252
if not root_children:
244253
return {
245254
"name": self._string_table.intern("No significant data"),

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.py:20)", name) # formatted name
433+
self.assertIn("func2 (file: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.py:10)", child_name) # formatted name
444+
self.assertIn("func1 (file:10)", child_name)
445445
self.assertEqual(child["value"], 1)
446446

447447
def test_flamegraph_collector_export(self):

0 commit comments

Comments
 (0)