Skip to content

Commit 4df8d3d

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

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, 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');
@@ -299,6 +307,7 @@ function createPythonTooltip(data) {
299307

300308
const funcname = resolveString(d.data.funcname) || resolveString(d.data.name);
301309
const filename = resolveString(d.data.filename) || "";
310+
const moduleName = escapeHtml(resolveString(d.data.module_name) || "");
302311
const isSpecialFrame = filename === "~";
303312

304313
// Build source section
@@ -307,7 +316,7 @@ function createPythonTooltip(data) {
307316
const sourceLines = source
308317
.map((line) => {
309318
const isCurrent = line.startsWith("→");
310-
const escaped = line.replace(/&/g, "&amp;").replace(/</g, "&lt;").replace(/>/g, "&gt;");
319+
const escaped = escapeHtml(line);
311320
return `<div class="tooltip-source-line${isCurrent ? ' current' : ''}">${escaped}</div>`;
312321
})
313322
.join("");
@@ -367,7 +376,7 @@ function createPythonTooltip(data) {
367376
}
368377

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

372381
// Differential stats section
373382
let diffSection = "";
@@ -621,24 +630,24 @@ function updateSearchHighlight(searchTerm, searchInput) {
621630
const name = resolveString(d.data.name) || "";
622631
const funcname = resolveString(d.data.funcname) || "";
623632
const filename = resolveString(d.data.filename) || "";
633+
const moduleName = resolveString(d.data.module_name) || "";
624634
const lineno = d.data.lineno;
625635
const term = searchTerm.toLowerCase();
626636

627-
// Check if search term looks like file:line pattern
637+
// Check if search term looks like module:line pattern
628638
const fileLineMatch = term.match(/^(.+):(\d+)$/);
629639
let matches = false;
630640

631641
if (fileLineMatch) {
632-
// Exact file:line matching
633642
const searchFile = fileLineMatch[1];
634643
const searchLine = parseInt(fileLineMatch[2], 10);
635-
const basename = filename.split('/').pop().toLowerCase();
636-
matches = basename.includes(searchFile) && lineno === searchLine;
644+
matches = moduleName.toLowerCase().includes(searchFile) && lineno === searchLine;
637645
} else {
638646
// Regular substring search
639647
matches =
640648
name.toLowerCase().includes(term) ||
641649
funcname.toLowerCase().includes(term) ||
650+
moduleName.toLowerCase().includes(term) ||
642651
filename.toLowerCase().includes(term);
643652
}
644653

@@ -1040,6 +1049,7 @@ function populateStats(data) {
10401049

10411050
let filename = resolveString(node.filename);
10421051
let funcname = resolveString(node.funcname);
1052+
let moduleName = resolveString(node.module_name);
10431053

10441054
if (!filename || !funcname) {
10451055
const nameStr = resolveString(node.name);
@@ -1054,6 +1064,7 @@ function populateStats(data) {
10541064

10551065
filename = filename || 'unknown';
10561066
funcname = funcname || 'unknown';
1067+
moduleName = moduleName || 'unknown';
10571068

10581069
if (filename !== 'unknown' && funcname !== 'unknown' && node.value > 0) {
10591070
let childrenValue = 0;
@@ -1070,12 +1081,14 @@ function populateStats(data) {
10701081
existing.directPercent = (existing.directSamples / totalSamples) * 100;
10711082
if (directSamples > existing.maxSingleSamples) {
10721083
existing.filename = filename;
1084+
existing.module_name = moduleName;
10731085
existing.lineno = node.lineno || '?';
10741086
existing.maxSingleSamples = directSamples;
10751087
}
10761088
} else {
10771089
functionMap.set(funcKey, {
10781090
filename: filename,
1091+
module_name: moduleName,
10791092
lineno: node.lineno || '?',
10801093
funcname: funcname,
10811094
directSamples,
@@ -1110,6 +1123,7 @@ function populateStats(data) {
11101123
const h = hotSpots[i];
11111124
const filename = h.filename || 'unknown';
11121125
const lineno = h.lineno ?? '?';
1126+
const moduleName = h.module_name || 'unknown';
11131127
const isSpecialFrame = filename === '~' && (lineno === 0 || lineno === '?');
11141128

11151129
let funcDisplay = h.funcname || 'unknown';
@@ -1120,8 +1134,7 @@ function populateStats(data) {
11201134
if (isSpecialFrame) {
11211135
fileEl.textContent = '--';
11221136
} else {
1123-
const basename = filename !== 'unknown' ? filename.split('/').pop() : 'unknown';
1124-
fileEl.textContent = `${basename}:${lineno}`;
1137+
fileEl.textContent = `${moduleName}:${lineno}`;
11251138
}
11261139
}
11271140
if (percentEl) percentEl.textContent = `${h.directPercent.toFixed(1)}%`;
@@ -1137,8 +1150,9 @@ function populateStats(data) {
11371150
if (card) {
11381151
if (i < hotSpots.length && hotSpots[i]) {
11391152
const h = hotSpots[i];
1140-
const basename = h.filename !== 'unknown' ? h.filename.split('/').pop() : '';
1141-
const searchTerm = basename && h.lineno !== '?' ? `${basename}:${h.lineno}` : h.funcname;
1153+
const moduleName = h.module_name || 'unknown';
1154+
const hasValidLocation = moduleName !== 'unknown' && h.lineno !== '?';
1155+
const searchTerm = hasValidLocation ? `${moduleName}:${h.lineno}` : h.funcname;
11421156
card.dataset.searchterm = searchTerm;
11431157
card.onclick = () => searchForHotspot(searchTerm);
11441158
card.style.cursor = 'pointer';
@@ -1273,6 +1287,7 @@ function accumulateInvertedNode(parent, stackFrame, leaf, isDifferential) {
12731287
value: 0,
12741288
children: {},
12751289
filename: stackFrame.filename,
1290+
module_name: stackFrame.module_name,
12761291
lineno: stackFrame.lineno,
12771292
funcname: stackFrame.funcname,
12781293
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
@@ -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.py:20)", name) # formatted name
438+
self.assertIn("func2 (file:20)", name)
439439
children = data.get("children", [])
440440
self.assertEqual(len(children), 1)
441441
child = children[0]
442-
self.assertIn("func1 (file.py:10)", resolve_name(child, strings))
442+
self.assertIn("func1 (file:10)", resolve_name(child, strings))
443443
self.assertEqual(child["value"], 1)
444444

445445
def test_flamegraph_collector_export(self):

0 commit comments

Comments
 (0)