mirror of
https://github.com/python/cpython.git
synced 2026-05-04 09:31:02 +00:00
gh-142927: Show module names instead of file paths in flamegraph (#146040)
This commit is contained in:
parent
d71e3bc5a0
commit
d4eee16659
7 changed files with 215 additions and 141 deletions
|
|
@ -315,6 +315,12 @@ .section-title {
|
|||
}
|
||||
|
||||
/* View Mode Section */
|
||||
.view-mode-section {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 8px;
|
||||
}
|
||||
|
||||
.view-mode-section .section-content {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
|
|
@ -1067,7 +1073,8 @@ .d3-flame-graph g:first-of-type .d3-flame-graph-label {
|
|||
-------------------------------------------------------------------------- */
|
||||
|
||||
#toggle-invert .toggle-track.on,
|
||||
#toggle-elided .toggle-track.on {
|
||||
#toggle-elided .toggle-track.on,
|
||||
#toggle-path-display .toggle-track.on {
|
||||
background: #8e44ad;
|
||||
border-color: #8e44ad;
|
||||
box-shadow: 0 0 8px rgba(142, 68, 173, 0.3);
|
||||
|
|
|
|||
|
|
@ -6,6 +6,7 @@ let normalData = null;
|
|||
let invertedData = null;
|
||||
let currentThreadFilter = 'all';
|
||||
let isInverted = false;
|
||||
let useModuleNames = true;
|
||||
|
||||
// Heat colors are now defined in CSS variables (--heat-1 through --heat-8)
|
||||
// and automatically switch with theme changes - no JS color arrays needed!
|
||||
|
|
@ -64,6 +65,12 @@ function resolveStringIndices(node, table) {
|
|||
if (typeof resolved.funcname === 'number') {
|
||||
resolved.funcname = resolveString(resolved.funcname, table);
|
||||
}
|
||||
if (typeof resolved.module === 'number') {
|
||||
resolved.module = resolveString(resolved.module, table);
|
||||
}
|
||||
if (typeof resolved.label === 'number') {
|
||||
resolved.label = resolveString(resolved.label, table);
|
||||
}
|
||||
|
||||
if (Array.isArray(resolved.source)) {
|
||||
resolved.source = resolved.source.map(index =>
|
||||
|
|
@ -78,6 +85,19 @@ function resolveStringIndices(node, table) {
|
|||
return resolved;
|
||||
}
|
||||
|
||||
// Escape HTML special characters
|
||||
function escapeHtml(str) {
|
||||
return str.replace(/&/g, "&").replace(/</g, "<").replace(/>/g, ">");
|
||||
}
|
||||
|
||||
// Get display path based on user preference (module or full path)
|
||||
function getDisplayName(moduleName, filename) {
|
||||
if (useModuleNames) {
|
||||
return moduleName || filename;
|
||||
}
|
||||
return filename;
|
||||
}
|
||||
|
||||
function selectFlamegraphData() {
|
||||
const baseData = isShowingElided ? elidedFlamegraphData : normalData;
|
||||
|
||||
|
|
@ -228,6 +248,7 @@ function setupLogos() {
|
|||
function updateStatusBar(nodeData, rootValue) {
|
||||
const funcname = resolveString(nodeData.funcname) || resolveString(nodeData.name) || "--";
|
||||
const filename = resolveString(nodeData.filename) || "";
|
||||
const moduleName = resolveString(nodeData.module) || "";
|
||||
const lineno = nodeData.lineno;
|
||||
const timeMs = (nodeData.value / 1000).toFixed(2);
|
||||
const percent = rootValue > 0 ? ((nodeData.value / rootValue) * 100).toFixed(1) : "0.0";
|
||||
|
|
@ -249,8 +270,8 @@ function updateStatusBar(nodeData, rootValue) {
|
|||
|
||||
const fileEl = document.getElementById('status-file');
|
||||
if (fileEl && filename && filename !== "~") {
|
||||
const basename = filename.split('/').pop();
|
||||
fileEl.textContent = lineno ? `${basename}:${lineno}` : basename;
|
||||
const displayName = getDisplayName(moduleName, filename);
|
||||
fileEl.textContent = lineno ? `${displayName}:${lineno}` : displayName;
|
||||
}
|
||||
|
||||
const funcEl = document.getElementById('status-func');
|
||||
|
|
@ -301,6 +322,8 @@ function createPythonTooltip(data) {
|
|||
|
||||
const funcname = resolveString(d.data.funcname) || resolveString(d.data.name);
|
||||
const filename = resolveString(d.data.filename) || "";
|
||||
const moduleName = resolveString(d.data.module) || "";
|
||||
const displayName = escapeHtml(useModuleNames ? (moduleName || filename) : filename);
|
||||
const isSpecialFrame = filename === "~";
|
||||
|
||||
// Build source section
|
||||
|
|
@ -309,7 +332,7 @@ function createPythonTooltip(data) {
|
|||
const sourceLines = source
|
||||
.map((line) => {
|
||||
const isCurrent = line.startsWith("→");
|
||||
const escaped = line.replace(/&/g, "&").replace(/</g, "<").replace(/>/g, ">");
|
||||
const escaped = escapeHtml(line);
|
||||
return `<div class="tooltip-source-line${isCurrent ? ' current' : ''}">${escaped}</div>`;
|
||||
})
|
||||
.join("");
|
||||
|
|
@ -369,7 +392,7 @@ function createPythonTooltip(data) {
|
|||
}
|
||||
|
||||
const fileLocationHTML = isSpecialFrame ? "" : `
|
||||
<div class="tooltip-location">${filename}${d.data.lineno ? ":" + d.data.lineno : ""}</div>`;
|
||||
<div class="tooltip-location">${displayName}${d.data.lineno ? ":" + d.data.lineno : ""}</div>`;
|
||||
|
||||
// Differential stats section
|
||||
let diffSection = "";
|
||||
|
|
@ -586,6 +609,7 @@ function createFlamegraph(tooltip, rootValue, data) {
|
|||
.minFrameSize(1)
|
||||
.tooltip(tooltip)
|
||||
.inverted(true)
|
||||
.getName(d => resolveString(useModuleNames ? d.data.label : d.data.name) || resolveString(d.data.name) || '')
|
||||
.setColorMapper(function (d) {
|
||||
if (d.depth === 0) return 'transparent';
|
||||
|
||||
|
|
@ -628,25 +652,25 @@ function updateSearchHighlight(searchTerm, searchInput) {
|
|||
const name = resolveString(d.data.name) || "";
|
||||
const funcname = resolveString(d.data.funcname) || "";
|
||||
const filename = resolveString(d.data.filename) || "";
|
||||
const moduleName = resolveString(d.data.module) || "";
|
||||
const displayName = getDisplayName(moduleName, filename);
|
||||
const lineno = d.data.lineno;
|
||||
const term = searchTerm.toLowerCase();
|
||||
|
||||
// Check if search term looks like file:line pattern
|
||||
// Check if search term looks like path:line pattern
|
||||
const fileLineMatch = term.match(/^(.+):(\d+)$/);
|
||||
let matches = false;
|
||||
|
||||
if (fileLineMatch) {
|
||||
// Exact file:line matching
|
||||
const searchFile = fileLineMatch[1];
|
||||
const searchLine = parseInt(fileLineMatch[2], 10);
|
||||
const basename = filename.split('/').pop().toLowerCase();
|
||||
matches = basename.includes(searchFile) && lineno === searchLine;
|
||||
matches = displayName.toLowerCase().includes(searchFile) && lineno === searchLine;
|
||||
} else {
|
||||
// Regular substring search
|
||||
matches =
|
||||
name.toLowerCase().includes(term) ||
|
||||
funcname.toLowerCase().includes(term) ||
|
||||
filename.toLowerCase().includes(term);
|
||||
displayName.toLowerCase().includes(term);
|
||||
}
|
||||
|
||||
if (matches) {
|
||||
|
|
@ -1047,6 +1071,7 @@ function populateStats(data) {
|
|||
|
||||
let filename = resolveString(node.filename);
|
||||
let funcname = resolveString(node.funcname);
|
||||
let moduleName = resolveString(node.module);
|
||||
|
||||
if (!filename || !funcname) {
|
||||
const nameStr = resolveString(node.name);
|
||||
|
|
@ -1061,6 +1086,7 @@ function populateStats(data) {
|
|||
|
||||
filename = filename || 'unknown';
|
||||
funcname = funcname || 'unknown';
|
||||
moduleName = moduleName || 'unknown';
|
||||
|
||||
if (filename !== 'unknown' && funcname !== 'unknown' && node.value > 0) {
|
||||
const directSamples = node.self || 0;
|
||||
|
|
@ -1073,12 +1099,14 @@ function populateStats(data) {
|
|||
existing.directPercent = (existing.directSamples / totalSamples) * 100;
|
||||
if (directSamples > existing.maxSingleSamples) {
|
||||
existing.filename = filename;
|
||||
existing.module = moduleName;
|
||||
existing.lineno = node.lineno || '?';
|
||||
existing.maxSingleSamples = directSamples;
|
||||
}
|
||||
} else {
|
||||
functionMap.set(funcKey, {
|
||||
filename: filename,
|
||||
module: moduleName,
|
||||
lineno: node.lineno || '?',
|
||||
funcname: funcname,
|
||||
directSamples,
|
||||
|
|
@ -1113,6 +1141,7 @@ function populateStats(data) {
|
|||
const h = hotSpots[i];
|
||||
const filename = h.filename || 'unknown';
|
||||
const lineno = h.lineno ?? '?';
|
||||
const moduleName = h.module || 'unknown';
|
||||
const isSpecialFrame = filename === '~' && (lineno === 0 || lineno === '?');
|
||||
|
||||
let funcDisplay = h.funcname || 'unknown';
|
||||
|
|
@ -1123,8 +1152,8 @@ function populateStats(data) {
|
|||
if (isSpecialFrame) {
|
||||
fileEl.textContent = '--';
|
||||
} else {
|
||||
const basename = filename !== 'unknown' ? filename.split('/').pop() : 'unknown';
|
||||
fileEl.textContent = `${basename}:${lineno}`;
|
||||
const displayName = getDisplayName(moduleName, filename);
|
||||
fileEl.textContent = `${displayName}:${lineno}`;
|
||||
}
|
||||
}
|
||||
if (percentEl) percentEl.textContent = `${h.directPercent.toFixed(1)}%`;
|
||||
|
|
@ -1140,8 +1169,11 @@ function populateStats(data) {
|
|||
if (card) {
|
||||
if (i < hotSpots.length && hotSpots[i]) {
|
||||
const h = hotSpots[i];
|
||||
const basename = h.filename !== 'unknown' ? h.filename.split('/').pop() : '';
|
||||
const searchTerm = basename && h.lineno !== '?' ? `${basename}:${h.lineno}` : h.funcname;
|
||||
const moduleName = h.module || 'unknown';
|
||||
const filename = h.filename || 'unknown';
|
||||
const displayName = getDisplayName(moduleName, filename);
|
||||
const hasValidLocation = displayName !== 'unknown' && h.lineno !== '?';
|
||||
const searchTerm = hasValidLocation ? `${displayName}:${h.lineno}` : h.funcname;
|
||||
card.dataset.searchterm = searchTerm;
|
||||
card.onclick = () => searchForHotspot(searchTerm);
|
||||
card.style.cursor = 'pointer';
|
||||
|
|
@ -1273,10 +1305,12 @@ function accumulateInvertedNode(parent, stackFrame, leaf, isDifferential) {
|
|||
if (!parent.children[key]) {
|
||||
const newNode = {
|
||||
name: stackFrame.name,
|
||||
label: stackFrame.label,
|
||||
value: 0,
|
||||
self: 0,
|
||||
children: {},
|
||||
filename: stackFrame.filename,
|
||||
module: stackFrame.module,
|
||||
lineno: stackFrame.lineno,
|
||||
funcname: stackFrame.funcname,
|
||||
source: stackFrame.source,
|
||||
|
|
@ -1370,6 +1404,7 @@ function generateInvertedFlamegraph(data) {
|
|||
|
||||
const invertedRoot = {
|
||||
name: data.name,
|
||||
label: data.label,
|
||||
value: data.value,
|
||||
children: {},
|
||||
stats: data.stats,
|
||||
|
|
@ -1394,6 +1429,12 @@ function toggleInvert() {
|
|||
updateFlamegraphView();
|
||||
}
|
||||
|
||||
function togglePathDisplay() {
|
||||
useModuleNames = !useModuleNames;
|
||||
updateToggleUI('toggle-path-display', useModuleNames);
|
||||
updateFlamegraphView();
|
||||
}
|
||||
|
||||
// ============================================================================
|
||||
// Initialization
|
||||
// ============================================================================
|
||||
|
|
@ -1441,6 +1482,11 @@ function initFlamegraph() {
|
|||
if (toggleInvertBtn) {
|
||||
toggleInvertBtn.addEventListener('click', toggleInvert);
|
||||
}
|
||||
|
||||
const togglePathDisplayBtn = document.getElementById('toggle-path-display');
|
||||
if (togglePathDisplayBtn) {
|
||||
togglePathDisplayBtn.addEventListener('click', togglePathDisplay);
|
||||
}
|
||||
}
|
||||
|
||||
// Keyboard shortcut: Enter/Space activates toggle switches
|
||||
|
|
|
|||
|
|
@ -117,6 +117,12 @@ <h3 class="section-title">View Mode</h3>
|
|||
<span class="toggle-label" data-text="Elided" title="Code paths that existed in baseline but are missing from current profile">Elided</span>
|
||||
</div>
|
||||
|
||||
<div class="toggle-switch" id="toggle-path-display" title="Toggle between module names and full file paths" tabindex="0">
|
||||
<span class="toggle-label" data-text="File Paths">File Paths</span>
|
||||
<div class="toggle-track on"></div>
|
||||
<span class="toggle-label active" data-text="Module Names">Module Names</span>
|
||||
</div>
|
||||
|
||||
<div class="toggle-switch" id="toggle-invert" title="Toggle between standard and inverted flamegraph view" tabindex="0">
|
||||
<span class="toggle-label active" data-text="Flamegraph">Flamegraph</span>
|
||||
<div class="toggle-track"></div>
|
||||
|
|
|
|||
|
|
@ -20,6 +20,7 @@
|
|||
from .collector import normalize_location, extract_lineno
|
||||
from .opcode_utils import get_opcode_info, format_opcode
|
||||
from .stack_collector import StackTraceCollector
|
||||
from .module_utils import extract_module_name, get_python_path_info
|
||||
|
||||
|
||||
# ============================================================================
|
||||
|
|
@ -49,126 +50,6 @@ class TreeNode:
|
|||
children: Dict[str, 'TreeNode'] = field(default_factory=dict)
|
||||
|
||||
|
||||
# ============================================================================
|
||||
# Module Path Analysis
|
||||
# ============================================================================
|
||||
|
||||
def get_python_path_info():
|
||||
"""Get information about Python installation paths for module extraction.
|
||||
|
||||
Returns:
|
||||
dict: Dictionary containing stdlib path, site-packages paths, and sys.path entries.
|
||||
"""
|
||||
info = {
|
||||
'stdlib': None,
|
||||
'site_packages': [],
|
||||
'sys_path': []
|
||||
}
|
||||
|
||||
# Get standard library path from os module location
|
||||
try:
|
||||
if hasattr(os, '__file__') and os.__file__:
|
||||
info['stdlib'] = Path(os.__file__).parent
|
||||
except (AttributeError, OSError):
|
||||
pass # Silently continue if we can't determine stdlib path
|
||||
|
||||
# Get site-packages directories
|
||||
site_packages = []
|
||||
try:
|
||||
site_packages.extend(Path(p) for p in site.getsitepackages())
|
||||
except (AttributeError, OSError):
|
||||
pass # Continue without site packages if unavailable
|
||||
|
||||
# Get user site-packages
|
||||
try:
|
||||
user_site = site.getusersitepackages()
|
||||
if user_site and Path(user_site).exists():
|
||||
site_packages.append(Path(user_site))
|
||||
except (AttributeError, OSError):
|
||||
pass # Continue without user site packages
|
||||
|
||||
info['site_packages'] = site_packages
|
||||
info['sys_path'] = [Path(p) for p in sys.path if p]
|
||||
|
||||
return info
|
||||
|
||||
|
||||
def extract_module_name(filename, path_info):
|
||||
"""Extract Python module name and type from file path.
|
||||
|
||||
Args:
|
||||
filename: Path to the Python file
|
||||
path_info: Dictionary from get_python_path_info()
|
||||
|
||||
Returns:
|
||||
tuple: (module_name, module_type) where module_type is one of:
|
||||
'stdlib', 'site-packages', 'project', or 'other'
|
||||
"""
|
||||
if not filename:
|
||||
return ('unknown', 'other')
|
||||
|
||||
try:
|
||||
file_path = Path(filename)
|
||||
except (ValueError, OSError):
|
||||
return (str(filename), 'other')
|
||||
|
||||
# Check if it's in stdlib
|
||||
if path_info['stdlib'] and _is_subpath(file_path, path_info['stdlib']):
|
||||
try:
|
||||
rel_path = file_path.relative_to(path_info['stdlib'])
|
||||
return (_path_to_module(rel_path), 'stdlib')
|
||||
except ValueError:
|
||||
pass
|
||||
|
||||
# Check site-packages
|
||||
for site_pkg in path_info['site_packages']:
|
||||
if _is_subpath(file_path, site_pkg):
|
||||
try:
|
||||
rel_path = file_path.relative_to(site_pkg)
|
||||
return (_path_to_module(rel_path), 'site-packages')
|
||||
except ValueError:
|
||||
continue
|
||||
|
||||
# Check other sys.path entries (project files)
|
||||
if not str(file_path).startswith(('<', '[')): # Skip special files
|
||||
for path_entry in path_info['sys_path']:
|
||||
if _is_subpath(file_path, path_entry):
|
||||
try:
|
||||
rel_path = file_path.relative_to(path_entry)
|
||||
return (_path_to_module(rel_path), 'project')
|
||||
except ValueError:
|
||||
continue
|
||||
|
||||
# Fallback: just use the filename
|
||||
return (_path_to_module(file_path), 'other')
|
||||
|
||||
|
||||
def _is_subpath(file_path, parent_path):
|
||||
try:
|
||||
file_path.relative_to(parent_path)
|
||||
return True
|
||||
except (ValueError, OSError):
|
||||
return False
|
||||
|
||||
|
||||
def _path_to_module(path):
|
||||
if isinstance(path, str):
|
||||
path = Path(path)
|
||||
|
||||
# Remove .py extension
|
||||
if path.suffix == '.py':
|
||||
path = path.with_suffix('')
|
||||
|
||||
# Convert path separators to dots
|
||||
parts = path.parts
|
||||
|
||||
# Handle __init__ files - they represent the package itself
|
||||
if parts and parts[-1] == '__init__':
|
||||
parts = parts[:-1]
|
||||
|
||||
return '.'.join(parts) if parts else path.stem
|
||||
|
||||
|
||||
# ============================================================================
|
||||
# Helper Classes
|
||||
# ============================================================================
|
||||
|
|
|
|||
102
Lib/profiling/sampling/module_utils.py
Normal file
102
Lib/profiling/sampling/module_utils.py
Normal file
|
|
@ -0,0 +1,102 @@
|
|||
"""Utilities for extracting module names from file paths."""
|
||||
|
||||
import os
|
||||
import site
|
||||
import sys
|
||||
from pathlib import Path
|
||||
|
||||
|
||||
def get_python_path_info():
|
||||
"""Get information about Python's search paths.
|
||||
|
||||
Returns:
|
||||
dict: Dictionary containing stdlib path, site-packages paths, and sys.path entries.
|
||||
"""
|
||||
info = {
|
||||
'stdlib': None,
|
||||
'site_packages': [],
|
||||
'sys_path': []
|
||||
}
|
||||
|
||||
# Get standard library path from os module location
|
||||
try:
|
||||
if hasattr(os, '__file__') and os.__file__:
|
||||
info['stdlib'] = Path(os.__file__).parent
|
||||
except (AttributeError, OSError):
|
||||
pass # Silently continue if we can't determine stdlib path
|
||||
|
||||
# Get site-packages directories
|
||||
site_packages = []
|
||||
try:
|
||||
site_packages.extend(Path(p) for p in site.getsitepackages())
|
||||
except (AttributeError, OSError):
|
||||
pass # Continue without site packages if unavailable
|
||||
|
||||
# Get user site-packages
|
||||
try:
|
||||
user_site = site.getusersitepackages()
|
||||
if user_site and Path(user_site).exists():
|
||||
site_packages.append(Path(user_site))
|
||||
except (AttributeError, OSError):
|
||||
pass # Continue without user site packages
|
||||
|
||||
info['site_packages'] = site_packages
|
||||
info['sys_path'] = [Path(p) for p in sys.path if p]
|
||||
|
||||
return info
|
||||
|
||||
|
||||
def extract_module_name(filename, path_info):
|
||||
"""Extract Python module name and type from file path.
|
||||
|
||||
Args:
|
||||
filename: Path to the Python file
|
||||
path_info: Dictionary from get_python_path_info()
|
||||
|
||||
Returns:
|
||||
tuple: (module_name, module_type) where module_type is one of:
|
||||
'stdlib', 'site-packages', 'project', or 'other'
|
||||
"""
|
||||
if not filename:
|
||||
return ('unknown', 'other')
|
||||
|
||||
try:
|
||||
file_path = Path(filename)
|
||||
except (ValueError, OSError):
|
||||
return (str(filename), 'other')
|
||||
|
||||
# Check if it's in stdlib
|
||||
if path_info['stdlib'] and file_path.is_relative_to(path_info['stdlib']):
|
||||
return (_path_to_module(file_path.relative_to(path_info['stdlib'])), 'stdlib')
|
||||
|
||||
# Check site-packages
|
||||
for site_pkg in path_info['site_packages']:
|
||||
if file_path.is_relative_to(site_pkg):
|
||||
return (_path_to_module(file_path.relative_to(site_pkg)), 'site-packages')
|
||||
|
||||
# Check other sys.path entries (project files)
|
||||
if not str(file_path).startswith(('<', '[')): # Skip special files
|
||||
for path_entry in path_info['sys_path']:
|
||||
if file_path.is_relative_to(path_entry):
|
||||
return (_path_to_module(file_path.relative_to(path_entry)), 'project')
|
||||
|
||||
# Fallback: just use the filename
|
||||
return (_path_to_module(file_path), 'other')
|
||||
|
||||
|
||||
def _path_to_module(path):
|
||||
if isinstance(path, str):
|
||||
path = Path(path)
|
||||
|
||||
# Remove .py extension
|
||||
if path.suffix == '.py':
|
||||
path = path.with_suffix('')
|
||||
|
||||
# Convert path separators to dots, stripping root/drive (e.g. "/" or "C:\")
|
||||
parts = [p for p in path.parts if p != path.root and p != path.drive]
|
||||
|
||||
# Handle __init__ files - they represent the package itself
|
||||
if parts and parts[-1] == '__init__':
|
||||
parts = parts[:-1]
|
||||
|
||||
return '.'.join(parts) if parts else path.stem
|
||||
|
|
@ -12,6 +12,7 @@
|
|||
from .collector import Collector, extract_lineno
|
||||
from .opcode_utils import get_opcode_mapping
|
||||
from .string_table import StringTable
|
||||
from .module_utils import extract_module_name, get_python_path_info
|
||||
|
||||
|
||||
class StackTraceCollector(Collector):
|
||||
|
|
@ -72,6 +73,7 @@ def __init__(self, *args, **kwargs):
|
|||
self._sample_count = 0 # Track actual number of samples (not thread traces)
|
||||
self._func_intern = {}
|
||||
self._string_table = StringTable()
|
||||
self._module_cache = {}
|
||||
self._all_threads = set()
|
||||
|
||||
# Thread status statistics (similar to LiveStatsCollector)
|
||||
|
|
@ -182,6 +184,24 @@ def _format_function_name(func):
|
|||
|
||||
return f"{funcname} ({filename}:{lineno})"
|
||||
|
||||
@staticmethod
|
||||
@functools.lru_cache(maxsize=None)
|
||||
def _format_module_name(func, module_name):
|
||||
filename, lineno, funcname = func
|
||||
|
||||
# Special frames like <GC> and <native> should not show file:line
|
||||
if filename == "~" and lineno == 0:
|
||||
return funcname
|
||||
|
||||
return f"{funcname} ({module_name}:{lineno})"
|
||||
|
||||
def _get_module_name(self, filename, path_info):
|
||||
module_name = self._module_cache.get(filename)
|
||||
if module_name is None:
|
||||
module_name, _ = extract_module_name(filename, path_info)
|
||||
self._module_cache[filename] = module_name
|
||||
return module_name
|
||||
|
||||
def _convert_to_flamegraph_format(self):
|
||||
if self._total_samples == 0:
|
||||
return {
|
||||
|
|
@ -192,7 +212,7 @@ def _convert_to_flamegraph_format(self):
|
|||
"strings": self._string_table.get_strings()
|
||||
}
|
||||
|
||||
def convert_children(children, min_samples):
|
||||
def convert_children(children, min_samples, path_info):
|
||||
out = []
|
||||
for func, node in children.items():
|
||||
samples = node["samples"]
|
||||
|
|
@ -202,14 +222,20 @@ def convert_children(children, min_samples):
|
|||
# Intern all string components for maximum efficiency
|
||||
filename_idx = self._string_table.intern(func[0])
|
||||
funcname_idx = self._string_table.intern(func[2])
|
||||
module_name = self._get_module_name(func[0], path_info)
|
||||
|
||||
module_idx = self._string_table.intern(module_name)
|
||||
name_idx = self._string_table.intern(self._format_function_name(func))
|
||||
label_idx = self._string_table.intern(self._format_module_name(func, module_name))
|
||||
|
||||
child_entry = {
|
||||
"name": name_idx,
|
||||
"label": label_idx,
|
||||
"value": samples,
|
||||
"self": node.get("self", 0),
|
||||
"children": [],
|
||||
"filename": filename_idx,
|
||||
"module": module_idx,
|
||||
"lineno": func[1],
|
||||
"funcname": funcname_idx,
|
||||
"threads": sorted(list(node.get("threads", set()))),
|
||||
|
|
@ -228,7 +254,7 @@ def convert_children(children, min_samples):
|
|||
|
||||
# Recurse
|
||||
child_entry["children"] = convert_children(
|
||||
node["children"], min_samples
|
||||
node["children"], min_samples, path_info
|
||||
)
|
||||
out.append(child_entry)
|
||||
|
||||
|
|
@ -239,8 +265,9 @@ def convert_children(children, min_samples):
|
|||
# Filter out very small functions (less than 0.1% of total samples)
|
||||
total_samples = self._total_samples
|
||||
min_samples = max(1, int(total_samples * 0.001))
|
||||
path_info = get_python_path_info()
|
||||
|
||||
root_children = convert_children(self._root["children"], min_samples)
|
||||
root_children = convert_children(self._root["children"], min_samples, path_info)
|
||||
if not root_children:
|
||||
return {
|
||||
"name": self._string_table.intern("No significant data"),
|
||||
|
|
@ -282,10 +309,11 @@ def convert_children(children, min_samples):
|
|||
# If we only have one root child, make it the root to avoid redundant level
|
||||
if len(root_children) == 1:
|
||||
main_child = root_children[0]
|
||||
# Update the name to indicate it's the program root
|
||||
# Update name and label to indicate it's the program root
|
||||
old_name = self._string_table.get_string(main_child["name"])
|
||||
new_name = f"Program Root: {old_name}"
|
||||
main_child["name"] = self._string_table.intern(new_name)
|
||||
main_child["name"] = self._string_table.intern(f"Program Root: {old_name}")
|
||||
old_label = self._string_table.get_string(main_child["label"])
|
||||
main_child["label"] = self._string_table.intern(f"Program Root: {old_label}")
|
||||
main_child["stats"] = {
|
||||
**self.stats,
|
||||
"thread_stats": thread_stats,
|
||||
|
|
@ -296,8 +324,10 @@ def convert_children(children, min_samples):
|
|||
main_child["opcode_mapping"] = opcode_mapping
|
||||
return main_child
|
||||
|
||||
program_root_idx = self._string_table.intern("Program Root")
|
||||
return {
|
||||
"name": self._string_table.intern("Program Root"),
|
||||
"name": program_root_idx,
|
||||
"label": program_root_idx,
|
||||
"value": total_samples,
|
||||
"children": root_children,
|
||||
"stats": {
|
||||
|
|
|
|||
|
|
@ -436,6 +436,8 @@ def test_flamegraph_collector_basic(self):
|
|||
name = resolve_name(data, strings)
|
||||
self.assertTrue(name.startswith("Program Root: "))
|
||||
self.assertIn("func2 (file.py:20)", name)
|
||||
label = strings[data["label"]]
|
||||
self.assertTrue(label.startswith("Program Root: "))
|
||||
self.assertEqual(data["self"], 0) # non-leaf: no self time
|
||||
children = data.get("children", [])
|
||||
self.assertEqual(len(children), 1)
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue