Skip to content

Commit 75e120b

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 e0e7d03 commit 75e120b

3 files changed

Lines changed: 48 additions & 24 deletions

File tree

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, table) {
6464
if (typeof resolved.funcname === 'number') {
6565
resolved.funcname = resolveString(resolved.funcname, table);
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, table) {
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
function selectFlamegraphData() {
8290
const baseData = isShowingElided ? elidedFlamegraphData : normalData;
8391

@@ -228,6 +236,7 @@ function setupLogos() {
228236
function updateStatusBar(nodeData, rootValue) {
229237
const funcname = resolveString(nodeData.funcname) || resolveString(nodeData.name) || "--";
230238
const filename = resolveString(nodeData.filename) || "";
239+
const moduleName = resolveString(nodeData.module_name) || "";
231240
const lineno = nodeData.lineno;
232241
const timeMs = (nodeData.value / 1000).toFixed(2);
233242
const percent = rootValue > 0 ? ((nodeData.value / rootValue) * 100).toFixed(1) : "0.0";
@@ -249,8 +258,7 @@ function updateStatusBar(nodeData, rootValue) {
249258

250259
const fileEl = document.getElementById('status-file');
251260
if (fileEl && filename && filename !== "~") {
252-
const basename = filename.split('/').pop();
253-
fileEl.textContent = lineno ? `${basename}:${lineno}` : basename;
261+
fileEl.textContent = lineno ? `${moduleName}:${lineno}` : moduleName;
254262
}
255263

256264
const funcEl = document.getElementById('status-func');
@@ -301,6 +309,7 @@ function createPythonTooltip(data) {
301309

302310
const funcname = resolveString(d.data.funcname) || resolveString(d.data.name);
303311
const filename = resolveString(d.data.filename) || "";
312+
const moduleName = escapeHtml(resolveString(d.data.module_name) || "");
304313
const isSpecialFrame = filename === "~";
305314

306315
// Build source section
@@ -309,7 +318,7 @@ function createPythonTooltip(data) {
309318
const sourceLines = source
310319
.map((line) => {
311320
const isCurrent = line.startsWith("→");
312-
const escaped = line.replace(/&/g, "&amp;").replace(/</g, "&lt;").replace(/>/g, "&gt;");
321+
const escaped = escapeHtml(line);
313322
return `<div class="tooltip-source-line${isCurrent ? ' current' : ''}">${escaped}</div>`;
314323
})
315324
.join("");
@@ -369,7 +378,7 @@ function createPythonTooltip(data) {
369378
}
370379

371380
const fileLocationHTML = isSpecialFrame ? "" : `
372-
<div class="tooltip-location">${filename}${d.data.lineno ? ":" + d.data.lineno : ""}</div>`;
381+
<div class="tooltip-location">${moduleName}${d.data.lineno ? ":" + d.data.lineno : ""}</div>`;
373382

374383
// Differential stats section
375384
let diffSection = "";
@@ -628,24 +637,24 @@ function updateSearchHighlight(searchTerm, searchInput) {
628637
const name = resolveString(d.data.name) || "";
629638
const funcname = resolveString(d.data.funcname) || "";
630639
const filename = resolveString(d.data.filename) || "";
640+
const moduleName = resolveString(d.data.module_name) || "";
631641
const lineno = d.data.lineno;
632642
const term = searchTerm.toLowerCase();
633643

634-
// Check if search term looks like file:line pattern
644+
// Check if search term looks like module:line pattern
635645
const fileLineMatch = term.match(/^(.+):(\d+)$/);
636646
let matches = false;
637647

638648
if (fileLineMatch) {
639-
// Exact file:line matching
640649
const searchFile = fileLineMatch[1];
641650
const searchLine = parseInt(fileLineMatch[2], 10);
642-
const basename = filename.split('/').pop().toLowerCase();
643-
matches = basename.includes(searchFile) && lineno === searchLine;
651+
matches = moduleName.toLowerCase().includes(searchFile) && lineno === searchLine;
644652
} else {
645653
// Regular substring search
646654
matches =
647655
name.toLowerCase().includes(term) ||
648656
funcname.toLowerCase().includes(term) ||
657+
moduleName.toLowerCase().includes(term) ||
649658
filename.toLowerCase().includes(term);
650659
}
651660

@@ -1047,6 +1056,7 @@ function populateStats(data) {
10471056

10481057
let filename = resolveString(node.filename);
10491058
let funcname = resolveString(node.funcname);
1059+
let moduleName = resolveString(node.module_name);
10501060

10511061
if (!filename || !funcname) {
10521062
const nameStr = resolveString(node.name);
@@ -1061,6 +1071,7 @@ function populateStats(data) {
10611071

10621072
filename = filename || 'unknown';
10631073
funcname = funcname || 'unknown';
1074+
moduleName = moduleName || 'unknown';
10641075

10651076
if (filename !== 'unknown' && funcname !== 'unknown' && node.value > 0) {
10661077
let childrenValue = 0;
@@ -1077,12 +1088,14 @@ function populateStats(data) {
10771088
existing.directPercent = (existing.directSamples / totalSamples) * 100;
10781089
if (directSamples > existing.maxSingleSamples) {
10791090
existing.filename = filename;
1091+
existing.module_name = moduleName;
10801092
existing.lineno = node.lineno || '?';
10811093
existing.maxSingleSamples = directSamples;
10821094
}
10831095
} else {
10841096
functionMap.set(funcKey, {
10851097
filename: filename,
1098+
module_name: moduleName,
10861099
lineno: node.lineno || '?',
10871100
funcname: funcname,
10881101
directSamples,
@@ -1117,6 +1130,7 @@ function populateStats(data) {
11171130
const h = hotSpots[i];
11181131
const filename = h.filename || 'unknown';
11191132
const lineno = h.lineno ?? '?';
1133+
const moduleName = h.module_name || 'unknown';
11201134
const isSpecialFrame = filename === '~' && (lineno === 0 || lineno === '?');
11211135

11221136
let funcDisplay = h.funcname || 'unknown';
@@ -1127,8 +1141,7 @@ function populateStats(data) {
11271141
if (isSpecialFrame) {
11281142
fileEl.textContent = '--';
11291143
} else {
1130-
const basename = filename !== 'unknown' ? filename.split('/').pop() : 'unknown';
1131-
fileEl.textContent = `${basename}:${lineno}`;
1144+
fileEl.textContent = `${moduleName}:${lineno}`;
11321145
}
11331146
}
11341147
if (percentEl) percentEl.textContent = `${h.directPercent.toFixed(1)}%`;
@@ -1144,8 +1157,9 @@ function populateStats(data) {
11441157
if (card) {
11451158
if (i < hotSpots.length && hotSpots[i]) {
11461159
const h = hotSpots[i];
1147-
const basename = h.filename !== 'unknown' ? h.filename.split('/').pop() : '';
1148-
const searchTerm = basename && h.lineno !== '?' ? `${basename}:${h.lineno}` : h.funcname;
1160+
const moduleName = h.module_name || 'unknown';
1161+
const hasValidLocation = moduleName !== 'unknown' && h.lineno !== '?';
1162+
const searchTerm = hasValidLocation ? `${moduleName}:${h.lineno}` : h.funcname;
11491163
card.dataset.searchterm = searchTerm;
11501164
card.onclick = () => searchForHotspot(searchTerm);
11511165
card.style.cursor = 'pointer';
@@ -1281,6 +1295,7 @@ function accumulateInvertedNode(parent, stackFrame, leaf, isDifferential) {
12811295
self: 0,
12821296
children: {},
12831297
filename: stackFrame.filename,
1298+
module_name: stackFrame.module_name,
12841299
lineno: stackFrame.lineno,
12851300
funcname: stackFrame.funcname,
12861301
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,14 +206,18 @@ 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
"self": node.get("self", 0),
211218
"children": [],
212219
"filename": filename_idx,
220+
"module_name": module_name_idx,
213221
"lineno": func[1],
214222
"funcname": funcname_idx,
215223
"threads": sorted(list(node.get("threads", set()))),
@@ -228,7 +236,7 @@ def convert_children(children, min_samples):
228236

229237
# Recurse
230238
child_entry["children"] = convert_children(
231-
node["children"], min_samples
239+
node["children"], min_samples, path_info
232240
)
233241
out.append(child_entry)
234242

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

243-
root_children = convert_children(self._root["children"], min_samples)
252+
root_children = convert_children(self._root["children"], min_samples, path_info)
244253
if not root_children:
245254
return {
246255
"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
@@ -435,12 +435,12 @@ 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.py:20)", name)
438+
self.assertIn("func2 (file:20)", name)
439439
self.assertEqual(data["self"], 0) # non-leaf: no self time
440440
children = data.get("children", [])
441441
self.assertEqual(len(children), 1)
442442
child = children[0]
443-
self.assertIn("func1 (file.py:10)", resolve_name(child, strings))
443+
self.assertIn("func1 (file:10)", resolve_name(child, strings))
444444
self.assertEqual(child["value"], 1)
445445
self.assertEqual(child["self"], 1) # leaf: all time is self
446446

0 commit comments

Comments
 (0)