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
9 changes: 8 additions & 1 deletion Lib/profiling/sampling/_flamegraph_assets/flamegraph.css
Original file line number Diff line number Diff line change
Expand Up @@ -275,6 +275,12 @@ body.resizing-sidebar {
}

/* View Mode Section */
.view-mode-section {
display: flex;
flex-direction: column;
gap: 8px;
}

.view-mode-section .section-content {
display: flex;
justify-content: center;
Expand Down Expand Up @@ -989,7 +995,8 @@ body.resizing-sidebar {
Flamegraph-Specific Toggle Override
-------------------------------------------------------------------------- */

#toggle-invert .toggle-track.on {
#toggle-invert .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);
Expand Down
79 changes: 66 additions & 13 deletions Lib/profiling/sampling/_flamegraph_assets/flamegraph.js
Original file line number Diff line number Diff line change
Expand Up @@ -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!
Expand Down Expand Up @@ -64,6 +65,12 @@ 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 (typeof resolved.name_module === 'number') {
resolved.name_module = resolveString(resolved.name_module);
}

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

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

// Get display path based on user preference (module name or basename)
function getDisplayName(moduleName, filename) {
if (useModuleNames) {
return moduleName || filename;
}
return filename ? filename.split('/').pop() : filename;
}

// ============================================================================
// Theme & UI Controls
// ============================================================================
Expand Down Expand Up @@ -201,6 +221,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 +243,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');
Expand Down Expand Up @@ -272,6 +293,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_name) || "";
const displayName = escapeHtml(useModuleNames ? (moduleName || filename) : filename);
const isSpecialFrame = filename === "~";

// Build source section
Expand All @@ -280,7 +303,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 +363,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>`;

const tooltipHTML = `
<div class="tooltip-header">
Expand Down Expand Up @@ -470,6 +493,7 @@ function createFlamegraph(tooltip, rootValue) {
.minFrameSize(1)
.tooltip(tooltip)
.inverted(true)
.getName(d => resolveString(useModuleNames ? d.data.name_module : d.data.name) || resolveString(d.data.name) || '')
.setColorMapper(function (d) {
// Root node should be transparent
if (d.depth === 0) return 'transparent';
Expand Down Expand Up @@ -509,25 +533,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_name) || "";
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) {
Expand Down Expand Up @@ -894,6 +918,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 +933,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 +950,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 +992,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 +1003,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)}%`;
Expand All @@ -991,8 +1020,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_name || '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';
Expand Down Expand Up @@ -1144,9 +1176,11 @@ function accumulateInvertedNode(parent, stackFrame, leaf) {
if (!parent.children[key]) {
parent.children[key] = {
name: stackFrame.name,
name_module: stackFrame.name_module,
value: 0,
children: {},
filename: stackFrame.filename,
module_name: stackFrame.module_name,
lineno: stackFrame.lineno,
funcname: stackFrame.funcname,
source: stackFrame.source,
Expand Down Expand Up @@ -1205,6 +1239,7 @@ function convertInvertDictToArray(node) {
function generateInvertedFlamegraph(data) {
const invertedRoot = {
name: data.name,
name_module: data.name_module,
value: data.value,
children: {},
stats: data.stats,
Expand Down Expand Up @@ -1243,6 +1278,19 @@ function toggleInvert() {
renderFlamegraph(chart, dataToRender);
}

function togglePathDisplay() {
useModuleNames = !useModuleNames;
updateToggleUI('toggle-path-display', useModuleNames);
const dataToRender = isInverted ? invertedData : normalData;
const filteredData = currentThreadFilter !== 'all'
? filterDataByThread(dataToRender, parseInt(currentThreadFilter))
: dataToRender;

const tooltip = createPythonTooltip(filteredData);
const chart = createFlamegraph(tooltip, filteredData.value);
renderFlamegraph(chart, filteredData);
}

// ============================================================================
// Initialization
// ============================================================================
Expand Down Expand Up @@ -1278,6 +1326,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
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -109,6 +109,13 @@ <h3 class="section-title">View Mode</h3>
<path d="M3 4.5L6 7.5L9 4.5" stroke="currentColor" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"/>
</svg>
</button>
<div class="section-content">
<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="Full Paths">Full Paths</span>
<div class="toggle-track on"></div>
<span class="toggle-label active" data-text="Module Names">Module Names</span>
</div>
</div>
<div class="section-content">
<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>
Expand Down
Loading
Loading