Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
39 changes: 27 additions & 12 deletions Lib/profiling/sampling/_flamegraph_assets/flamegraph.js
Original file line number Diff line number Diff line change
Expand Up @@ -64,6 +64,9 @@ function resolveStringIndices(node) {
if (typeof resolved.funcname === 'number') {
resolved.funcname = resolveString(resolved.funcname);
}
if (typeof resolved.module_name === 'number') {
resolved.module_name = resolveString(resolved.module_name);
}

if (Array.isArray(resolved.source)) {
resolved.source = resolved.source.map(index =>
Expand All @@ -78,6 +81,11 @@ function resolveStringIndices(node) {
return resolved;
}

// Escape HTML special characters
function escapeHtml(str) {
return str.replace(/&/g, "&amp;").replace(/</g, "&lt;").replace(/>/g, "&gt;");
}

// ============================================================================
// Theme & UI Controls
// ============================================================================
Expand Down Expand Up @@ -201,6 +209,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_name) || "";
const lineno = nodeData.lineno;
const timeMs = (nodeData.value / 1000).toFixed(2);
const percent = rootValue > 0 ? ((nodeData.value / rootValue) * 100).toFixed(1) : "0.0";
Expand All @@ -222,8 +231,7 @@ 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;
fileEl.textContent = lineno ? `${moduleName}:${lineno}` : moduleName;
}

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

const funcname = resolveString(d.data.funcname) || resolveString(d.data.name);
const filename = resolveString(d.data.filename) || "";
const moduleName = escapeHtml(resolveString(d.data.module_name) || "");
const isSpecialFrame = filename === "~";

// Build source section
Expand All @@ -280,7 +289,7 @@ function createPythonTooltip(data) {
const sourceLines = source
.map((line) => {
const isCurrent = line.startsWith("→");
const escaped = line.replace(/&/g, "&amp;").replace(/</g, "&lt;").replace(/>/g, "&gt;");
const escaped = escapeHtml(line);
return `<div class="tooltip-source-line${isCurrent ? ' current' : ''}">${escaped}</div>`;
})
.join("");
Expand Down Expand Up @@ -340,7 +349,7 @@ function createPythonTooltip(data) {
}

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

const tooltipHTML = `
<div class="tooltip-header">
Expand Down Expand Up @@ -509,24 +518,24 @@ 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_name) || "";
const lineno = d.data.lineno;
const term = searchTerm.toLowerCase();

// Check if search term looks like file:line pattern
// Check if search term looks like module: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 = moduleName.toLowerCase().includes(searchFile) && lineno === searchLine;
} else {
// Regular substring search
matches =
name.toLowerCase().includes(term) ||
funcname.toLowerCase().includes(term) ||
moduleName.toLowerCase().includes(term) ||
filename.toLowerCase().includes(term);
}

Expand Down Expand Up @@ -894,6 +903,7 @@ function populateStats(data) {

let filename = resolveString(node.filename);
let funcname = resolveString(node.funcname);
let moduleName = resolveString(node.module_name);

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

filename = filename || 'unknown';
funcname = funcname || 'unknown';
moduleName = moduleName || 'unknown';

if (filename !== 'unknown' && funcname !== 'unknown' && node.value > 0) {
let childrenValue = 0;
Expand All @@ -924,12 +935,14 @@ function populateStats(data) {
existing.directPercent = (existing.directSamples / totalSamples) * 100;
if (directSamples > existing.maxSingleSamples) {
existing.filename = filename;
existing.module_name = moduleName;
existing.lineno = node.lineno || '?';
existing.maxSingleSamples = directSamples;
}
} else {
functionMap.set(funcKey, {
filename: filename,
module_name: moduleName,
lineno: node.lineno || '?',
funcname: funcname,
directSamples,
Expand Down Expand Up @@ -964,6 +977,7 @@ function populateStats(data) {
const h = hotSpots[i];
const filename = h.filename || 'unknown';
const lineno = h.lineno ?? '?';
const moduleName = h.module_name || 'unknown';
const isSpecialFrame = filename === '~' && (lineno === 0 || lineno === '?');

let funcDisplay = h.funcname || 'unknown';
Expand All @@ -974,8 +988,7 @@ function populateStats(data) {
if (isSpecialFrame) {
fileEl.textContent = '--';
} else {
const basename = filename !== 'unknown' ? filename.split('/').pop() : 'unknown';
fileEl.textContent = `${basename}:${lineno}`;
fileEl.textContent = `${moduleName}:${lineno}`;
}
}
if (percentEl) percentEl.textContent = `${h.directPercent.toFixed(1)}%`;
Expand All @@ -991,8 +1004,9 @@ 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_name || 'unknown';
const hasValidLocation = moduleName !== 'unknown' && h.lineno !== '?';
const searchTerm = hasValidLocation ? `${moduleName}:${h.lineno}` : h.funcname;
card.dataset.searchterm = searchTerm;
card.onclick = () => searchForHotspot(searchTerm);
card.style.cursor = 'pointer';
Expand Down Expand Up @@ -1147,6 +1161,7 @@ function accumulateInvertedNode(parent, stackFrame, leaf) {
value: 0,
children: {},
filename: stackFrame.filename,
module_name: stackFrame.module_name,
lineno: stackFrame.lineno,
funcname: stackFrame.funcname,
source: stackFrame.source,
Expand Down
121 changes: 1 addition & 120 deletions Lib/profiling/sampling/heatmap_collector.py
Original file line number Diff line number Diff line change
Expand Up @@ -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


# ============================================================================
Expand Down Expand Up @@ -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
# ============================================================================
Expand Down
Loading
Loading