Skip to content
Open
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 @@ -315,6 +315,12 @@ body.resizing-sidebar {
}

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

.view-mode-section .section-content {
display: flex;
flex-direction: column;
Expand Down Expand Up @@ -1067,7 +1073,8 @@ body.resizing-sidebar {
-------------------------------------------------------------------------- */

#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);
Expand Down
72 changes: 59 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, 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 =>
Expand All @@ -78,6 +85,19 @@ function resolveStringIndices(node, table) {
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 or full path)
function getDisplayName(moduleName, filename) {
if (useModuleNames) {
return moduleName || filename;
}
return filename;
}

function selectFlamegraphData() {
const baseData = isShowingElided ? elidedFlamegraphData : normalData;

Expand Down Expand Up @@ -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";
Expand All @@ -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');
Expand Down Expand Up @@ -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
Expand All @@ -309,7 +332,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 @@ -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 = "";
Expand Down Expand Up @@ -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';

Expand Down Expand Up @@ -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) {
Expand Down Expand Up @@ -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);
Expand All @@ -1061,6 +1086,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 @@ -1077,12 +1103,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,
Expand Down Expand Up @@ -1117,6 +1145,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';
Expand All @@ -1127,8 +1156,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 @@ -1144,8 +1173,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';
Expand Down Expand Up @@ -1277,10 +1309,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,
Expand Down Expand Up @@ -1375,6 +1409,7 @@ function generateInvertedFlamegraph(data) {

const invertedRoot = {
name: data.name,
label: data.label,
value: data.value,
children: {},
stats: data.stats,
Expand All @@ -1399,6 +1434,12 @@ function toggleInvert() {
updateFlamegraphView();
}

function togglePathDisplay() {
useModuleNames = !useModuleNames;
updateToggleUI('toggle-path-display', useModuleNames);
updateFlamegraphView();
}

// ============================================================================
// Initialization
// ============================================================================
Expand Down Expand Up @@ -1446,6 +1487,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 @@ -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>
Expand Down
Loading
Loading