Skip to content
Merged
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
37 changes: 37 additions & 0 deletions Lib/profiling/sampling/_flamegraph_assets/flamegraph.css
Original file line number Diff line number Diff line change
Expand Up @@ -274,6 +274,20 @@ body.resizing-sidebar {
flex: 1;
}

/* View Mode Section */
.view-mode-section {
padding-bottom: 20px;
border-bottom: 1px solid var(--border);
}

.view-mode-section .section-title {
margin-bottom: 12px;
}

.view-mode-section .toggle-switch {
justify-content: center;
}

/* Collapsible sections */
.collapsible .section-header {
display: flex;
Expand Down Expand Up @@ -986,3 +1000,26 @@ body.resizing-sidebar {
grid-template-columns: 1fr;
}
}

/* --------------------------------------------------------------------------
Flamegraph Root Node Styling
-------------------------------------------------------------------------- */

/* Style the root node - no border, themed text */
.d3-flame-graph g:first-of-type rect {
stroke: none;
}

.d3-flame-graph g:first-of-type .d3-flame-graph-label {
color: var(--text-muted);
}

/* --------------------------------------------------------------------------
Flamegraph-Specific Toggle Override
-------------------------------------------------------------------------- */

#toggle-invert .toggle-track.on {
background: #8e44ad;
border-color: #8e44ad;
box-shadow: 0 0 8px rgba(142, 68, 173, 0.3);
}
230 changes: 206 additions & 24 deletions Lib/profiling/sampling/_flamegraph_assets/flamegraph.js
Original file line number Diff line number Diff line change
Expand Up @@ -2,8 +2,10 @@ const EMBEDDED_DATA = {{FLAMEGRAPH_DATA}};

// Global string table for resolving string indices
let stringTable = [];
let originalData = null;
let normalData = null;
let invertedData = null;
let currentThreadFilter = 'all';
let isInverted = false;

// 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 @@ -94,9 +96,10 @@ function toggleTheme() {
}

// Re-render flamegraph with new theme colors
if (window.flamegraphData && originalData) {
const tooltip = createPythonTooltip(originalData);
const chart = createFlamegraph(tooltip, originalData.value);
if (window.flamegraphData && normalData) {
const currentData = isInverted ? invertedData : normalData;
const tooltip = createPythonTooltip(currentData);
const chart = createFlamegraph(tooltip, currentData.value);
renderFlamegraph(chart, window.flamegraphData);
}
}
Expand Down Expand Up @@ -485,6 +488,9 @@ function createFlamegraph(tooltip, rootValue) {
.tooltip(tooltip)
.inverted(true)
.setColorMapper(function (d) {
// Root node should be transparent
if (d.depth === 0) return 'transparent';

const percentage = d.data.value / rootValue;
const level = getHeatLevel(percentage);
return heatColors[level];
Expand Down Expand Up @@ -796,16 +802,35 @@ function populateProfileSummary(data) {
if (rateEl) rateEl.textContent = sampleRate > 0 ? formatNumber(Math.round(sampleRate)) : '--';

// Count unique functions
let functionCount = 0;
function countFunctions(node) {
// Use normal (non-inverted) tree structure, but respect thread filtering
const uniqueFunctions = new Set();
function collectUniqueFunctions(node) {
if (!node) return;
functionCount++;
if (node.children) node.children.forEach(countFunctions);
const filename = resolveString(node.filename) || 'unknown';
const funcname = resolveString(node.funcname) || resolveString(node.name) || 'unknown';
const lineno = node.lineno || 0;
const key = `${filename}|${lineno}|${funcname}`;
uniqueFunctions.add(key);
if (node.children) node.children.forEach(collectUniqueFunctions);
}
// In inverted mode, use normalData (with thread filter if active)
// In normal mode, use the passed data (already has thread filter applied if any)
let functionCountSource;
if (!normalData) {
functionCountSource = data;
} else if (isInverted) {
if (currentThreadFilter !== 'all') {
functionCountSource = filterDataByThread(normalData, parseInt(currentThreadFilter));
} else {
functionCountSource = normalData;
}
} else {
functionCountSource = data;
}
countFunctions(data);
collectUniqueFunctions(functionCountSource);

const functionsEl = document.getElementById('stat-functions');
if (functionsEl) functionsEl.textContent = formatNumber(functionCount);
if (functionsEl) functionsEl.textContent = formatNumber(uniqueFunctions.size);

// Efficiency bar
if (errorRate !== undefined && errorRate !== null) {
Expand Down Expand Up @@ -840,14 +865,31 @@ function populateProfileSummary(data) {
// ============================================================================

function populateStats(data) {
const totalSamples = data.value || 0;

// Populate profile summary
populateProfileSummary(data);

// Populate thread statistics if available
populateThreadStats(data);

// For hotspots: use normal (non-inverted) tree structure, but respect thread filtering.
// In inverted view, the tree structure changes but the hottest functions remain the same.
// However, if a thread filter is active, we need to show that thread's hotspots.
let hotspotSource;
if (!normalData) {
hotspotSource = data;
} else if (isInverted) {
// In inverted mode, use normalData (with thread filter if active)
if (currentThreadFilter !== 'all') {
hotspotSource = filterDataByThread(normalData, parseInt(currentThreadFilter));
} else {
hotspotSource = normalData;
}
} else {
// In normal mode, use the passed data (already has thread filter applied if any)
hotspotSource = data;
}
const totalSamples = hotspotSource.value || 0;

const functionMap = new Map();

function collectFunctions(node) {
Expand Down Expand Up @@ -905,7 +947,7 @@ function populateStats(data) {
}
}

collectFunctions(data);
collectFunctions(hotspotSource);

const hotSpots = Array.from(functionMap.values())
.filter(f => f.directPercent > 0.5)
Expand Down Expand Up @@ -997,19 +1039,20 @@ function initThreadFilter(data) {

function filterByThread() {
const threadFilter = document.getElementById('thread-filter');
if (!threadFilter || !originalData) return;
if (!threadFilter || !normalData) return;

const selectedThread = threadFilter.value;
currentThreadFilter = selectedThread;
const baseData = isInverted ? invertedData : normalData;

let filteredData;
let selectedThreadId = null;

if (selectedThread === 'all') {
filteredData = originalData;
filteredData = baseData;
} else {
selectedThreadId = parseInt(selectedThread, 10);
filteredData = filterDataByThread(originalData, selectedThreadId);
filteredData = filterDataByThread(baseData, selectedThreadId);

if (filteredData.strings) {
stringTable = filteredData.strings;
Expand All @@ -1021,7 +1064,7 @@ function filterByThread() {
const chart = createFlamegraph(tooltip, filteredData.value);
renderFlamegraph(chart, filteredData);

populateThreadStats(originalData, selectedThreadId);
populateThreadStats(baseData, selectedThreadId);
}

function filterDataByThread(data, threadId) {
Expand Down Expand Up @@ -1089,6 +1132,137 @@ function exportSVG() {
URL.revokeObjectURL(url);
}

// ============================================================================
// Inverted Flamegraph
// ============================================================================

// Example: "file.py|10|foo" or "~|0|<GC>" for special frames
function getInvertNodeKey(node) {
return `${node.filename || '~'}|${node.lineno || 0}|${node.funcname || node.name}`;
}

function accumulateInvertedNode(parent, stackFrame, leaf) {
const key = getInvertNodeKey(stackFrame);

if (!parent.children[key]) {
parent.children[key] = {
name: stackFrame.name,
value: 0,
children: {},
filename: stackFrame.filename,
lineno: stackFrame.lineno,
funcname: stackFrame.funcname,
source: stackFrame.source,
threads: new Set()
};
}

const node = parent.children[key];
node.value += leaf.value;
if (leaf.threads) {
leaf.threads.forEach(t => node.threads.add(t));
}

return node;
}

function processLeaf(invertedRoot, path, leafNode) {
if (!path || path.length === 0) {
return;
}

let invertedParent = accumulateInvertedNode(invertedRoot, leafNode, leafNode);

// Walk backwards through the call stack
for (let i = path.length - 2; i >= 0; i--) {
invertedParent = accumulateInvertedNode(invertedParent, path[i], leafNode);
}
}

function traverseInvert(path, currentNode, invertedRoot) {
const children = currentNode.children || [];
const childThreads = new Set(children.flatMap(c => c.threads || []));
const selfThreads = (currentNode.threads || []).filter(t => !childThreads.has(t));

if (selfThreads.length > 0) {
processLeaf(invertedRoot, path, { ...currentNode, threads: selfThreads });
}

children.forEach(child => traverseInvert(path.concat([child]), child, invertedRoot));
}

function convertInvertDictToArray(node) {
if (node.threads instanceof Set) {
node.threads = Array.from(node.threads).sort((a, b) => a - b);
}

const children = node.children;
if (children && typeof children === 'object' && !Array.isArray(children)) {
node.children = Object.values(children);
node.children.sort((a, b) => b.value - a.value || a.name.localeCompare(b.name));
node.children.forEach(convertInvertDictToArray);
}
return node;
}

function generateInvertedFlamegraph(data) {
const invertedRoot = {
name: data.name,
value: data.value,
children: {},
stats: data.stats,
threads: data.threads
};

const children = data.children || [];
if (children.length === 0) {
// Single-frame tree: the root is its own leaf
processLeaf(invertedRoot, [data], data);
} else {
children.forEach(child => traverseInvert([child], child, invertedRoot));
}

convertInvertDictToArray(invertedRoot);
return invertedRoot;
}

function updateToggleUI(toggleId, isOn) {
const toggle = document.getElementById(toggleId);
if (toggle) {
const track = toggle.querySelector('.toggle-track');
const labels = toggle.querySelectorAll('.toggle-label');
if (isOn) {
track.classList.add('on');
labels[0].classList.remove('active');
labels[1].classList.add('active');
} else {
track.classList.remove('on');
labels[0].classList.add('active');
labels[1].classList.remove('active');
}
}
}

function toggleInvert() {
isInverted = !isInverted;
updateToggleUI('toggle-invert', isInverted);

// Build inverted data on first use
if (isInverted && !invertedData) {
invertedData = generateInvertedFlamegraph(normalData);
}

let dataToRender = isInverted ? invertedData : normalData;

if (currentThreadFilter !== 'all') {
dataToRender = filterDataByThread(dataToRender, parseInt(currentThreadFilter));
}

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

// ============================================================================
// Initialization
// ============================================================================
Expand All @@ -1098,24 +1272,32 @@ function initFlamegraph() {
restoreUIState();
setupLogos();

let processedData = EMBEDDED_DATA;
if (EMBEDDED_DATA.strings) {
stringTable = EMBEDDED_DATA.strings;
processedData = resolveStringIndices(EMBEDDED_DATA);
normalData = resolveStringIndices(EMBEDDED_DATA);
} else {
normalData = EMBEDDED_DATA;
}

// Initialize opcode mapping from embedded data
initOpcodeMapping(EMBEDDED_DATA);

originalData = processedData;
initThreadFilter(processedData);
// Inverted data will be built on first toggle
invertedData = null;

initThreadFilter(normalData);

const tooltip = createPythonTooltip(processedData);
const chart = createFlamegraph(tooltip, processedData.value);
renderFlamegraph(chart, processedData);
const tooltip = createPythonTooltip(normalData);
const chart = createFlamegraph(tooltip, normalData.value);
renderFlamegraph(chart, normalData);
initSearchHandlers();
initSidebarResize();
handleResize();

const toggleInvertBtn = document.getElementById('toggle-invert');
if (toggleInvertBtn) {
toggleInvertBtn.addEventListener('click', toggleInvert);
}
}

if (document.readyState === "loading") {
Expand Down
10 changes: 10 additions & 0 deletions Lib/profiling/sampling/_flamegraph_assets/flamegraph_template.html
Original file line number Diff line number Diff line change
Expand Up @@ -76,6 +76,16 @@
<div class="sidebar-logo-img"><!-- INLINE_LOGO --></div>
</div>

<!-- View Mode Section -->
<section class="sidebar-section view-mode-section">
<h3 class="section-title">View Mode</h3>
<div class="toggle-switch" id="toggle-invert">
<span class="toggle-label active">Flamegraph</span>
<div class="toggle-track"></div>
<span class="toggle-label">Inverted Flamegraph</span>
</div>
</section>

<!-- Profile Summary Section -->
<section class="sidebar-section collapsible" id="summary-section">
<button class="section-header" onclick="toggleSection('summary-section')">
Expand Down
Loading
Loading