Skip to content
Closed
Show file tree
Hide file tree
Changes from 1 commit
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
Prev Previous commit
Next Next commit
fixup! fixup! Update license.rst
  • Loading branch information
pablogsal authored and lkollar committed Sep 9, 2025
commit fb48a88581d5b0fe054798264e40aab45e67e18f
117 changes: 117 additions & 0 deletions Lib/profiling/sampling/flamegraph.css
Original file line number Diff line number Diff line change
Expand Up @@ -61,6 +61,95 @@ body {
font-weight: 300;
}

.stats-section {
background: #ffffff;
padding: 24px 0;
border-bottom: 1px solid #e9ecef;
}

.stats-container {
max-width: 1200px;
margin: 0 auto;
padding: 0 24px;
display: grid;
grid-template-columns: repeat(3, 1fr);
gap: 20px;
}

.stat-card {
background: #ffffff;
border: 1px solid #e9ecef;
border-radius: 12px;
padding: 20px;
display: flex;
align-items: flex-start;
gap: 16px;
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.06);
transition: all 0.2s ease;
min-height: 120px;
}

.stat-card:hover {
box-shadow: 0 4px 16px rgba(0, 0, 0, 0.1);
transform: translateY(-2px);
}

.stat-icon {
font-size: 32px;
width: 56px;
height: 56px;
display: flex;
align-items: center;
justify-content: center;
background: linear-gradient(135deg, #3776ab 0%, #4584bb 100%);
border-radius: 50%;
flex-shrink: 0;
box-shadow: 0 2px 8px rgba(55, 118, 171, 0.3);
}

.stat-content {
flex: 1;
}

.stat-label {
font-size: 14px;
color: #5a6c7d;
font-weight: 500;
margin-bottom: 4px;
text-transform: uppercase;
letter-spacing: 0.5px;
}

.stat-value {
font-size: 16px;
font-weight: 700;
color: #2e3338;
line-height: 1.3;
margin-bottom: 4px;
word-break: break-word;
overflow-wrap: break-word;
}

.stat-file {
font-size: 12px;
color: #8b949e;
font-weight: 400;
margin-bottom: 2px;
font-family: 'SF Mono', 'Monaco', 'Consolas', monospace;
word-break: break-word;
overflow-wrap: break-word;
}

.stat-detail {
font-size: 12px;
color: #5a6c7d;
font-weight: 400;
line-height: 1.4;
font-family: 'SF Mono', 'Monaco', 'Consolas', monospace;
word-break: break-word;
overflow-wrap: break-word;
}

.controls {
background: #f8f9fa;
border-bottom: 1px solid #e9ecef;
Expand All @@ -72,6 +161,34 @@ body {
max-width: 1200px;
margin: 0 auto;
padding: 0 24px;
text-align: center;
}

.search-container {
max-width: 1200px;
margin: 16px auto 0 auto;
padding: 0 24px;
text-align: center;
}

#search-input {
padding: 12px 20px;
border: 2px solid #e9ecef;
border-radius: 6px;
font-size: 14px;
font-family: inherit;
background: white;
transition: all 0.2s ease;
box-shadow: 0 2px 4px rgba(0, 0, 0, 0.06);
display: inline-block;
width: auto;
min-width: 300px;
}

#search-input:focus {
outline: none;
border-color: #3776ab;
box-shadow: 0 4px 12px rgba(55, 118, 171, 0.2);
}

.controls button {
Expand Down
156 changes: 156 additions & 0 deletions Lib/profiling/sampling/flamegraph.js
Original file line number Diff line number Diff line change
Expand Up @@ -211,6 +211,9 @@ function main() {

// Make chart globally accessible for controls
window.flamegraphChart = chart;

// Populate stats cards
populateStats(data);
}

// Wait for libraries to load
Expand All @@ -220,6 +223,8 @@ document.addEventListener("DOMContentLoaded", function () {
const infoBtn = document.getElementById("show-info-btn");
const infoPanel = document.getElementById("info-panel");
const closeBtn = document.getElementById("close-info-btn");
const searchInput = document.getElementById("search-input");

if (infoBtn && infoPanel) {
infoBtn.addEventListener("click", function () {
const isOpen = infoPanel.style.display === "block";
Expand All @@ -231,6 +236,72 @@ document.addEventListener("DOMContentLoaded", function () {
infoPanel.style.display = "none";
});
}

// Add search functionality - wait for chart to be ready
if (searchInput) {

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Could this block be extracted to a helper function?

let searchTimeout;

function performSearch() {
const searchTerm = searchInput.value.trim();

// Clear previous search highlighting
d3.selectAll("#chart rect")
.style("stroke", null)
.style("stroke-width", null)
.style("opacity", null);

if (searchTerm && searchTerm.length > 0) {
// First, dim all rectangles
d3.selectAll("#chart rect")
.style("opacity", 0.3);

// Then highlight and restore opacity for matching nodes
let matchCount = 0;
d3.selectAll("#chart rect")
.each(function(d) {
if (d && d.data) {
const name = d.data.name || "";
const funcname = d.data.funcname || "";
const filename = d.data.filename || "";

const matches = name.toLowerCase().includes(searchTerm.toLowerCase()) ||
funcname.toLowerCase().includes(searchTerm.toLowerCase()) ||
filename.toLowerCase().includes(searchTerm.toLowerCase());

if (matches) {
matchCount++;
d3.select(this)
.style("opacity", 1)
.style("stroke", "#ff6b35")
.style("stroke-width", "2px")
.style("stroke-dasharray", "3,3");
}
}
});

// Update search input style based on results
if (matchCount > 0) {
searchInput.style.borderColor = "#28a745";
searchInput.style.boxShadow = "0 0 0 3px rgba(40, 167, 69, 0.1)";
} else {
searchInput.style.borderColor = "#dc3545";
searchInput.style.boxShadow = "0 0 0 3px rgba(220, 53, 69, 0.1)";
}
} else {
// Reset search input style
searchInput.style.borderColor = "#e9ecef";
searchInput.style.boxShadow = "0 2px 4px rgba(0, 0, 0, 0.06)";
}
}

searchInput.addEventListener("input", function() {
clearTimeout(searchTimeout);
searchTimeout = setTimeout(performSearch, 150);
});

// Make search function globally accessible
window.performSearch = performSearch;
}
});

// Python color palette - cold to hot
Expand All @@ -245,6 +316,81 @@ const pythonColors = [
"#3776ab", // Hottest - Python blue (≥60%)
];

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

// Collect all functions with their metrics, aggregated by function name
const functionMap = new Map();

function collectFunctions(node) {
if (node.filename && node.funcname) {
// Calculate direct samples (this node's value minus children's values)
let childrenValue = 0;
if (node.children) {
childrenValue = node.children.reduce((sum, child) => sum + child.value, 0);
}
const directSamples = Math.max(0, node.value - childrenValue);

// Use file:line:funcname as key to ensure uniqueness
const funcKey = `${node.filename}:${node.lineno || '?'}:${node.funcname}`;

if (functionMap.has(funcKey)) {
const existing = functionMap.get(funcKey);
existing.directSamples += directSamples;
existing.directPercent = (existing.directSamples / totalSamples) * 100;
// Keep the most representative file/line (the one with more samples)
if (directSamples > existing.maxSingleSamples) {
existing.filename = node.filename;
existing.lineno = node.lineno || '?';
existing.maxSingleSamples = directSamples;
}
} else {
functionMap.set(funcKey, {
filename: node.filename,
lineno: node.lineno || '?',
funcname: node.funcname,
directSamples,
directPercent: (directSamples / totalSamples) * 100,
maxSingleSamples: directSamples
});
}
}

if (node.children) {
node.children.forEach(child => collectFunctions(child));
}
}

collectFunctions(data);

// Convert map to array and get top 3 hotspots
const hotSpots = Array.from(functionMap.values())
.filter(f => f.directPercent > 0.5) // At least 0.5% to be significant
.sort((a, b) => b.directPercent - a.directPercent)
.slice(0, 3);

// Populate the 3 cards
for (let i = 0; i < 3; i++) {
const num = i + 1;
if (i < hotSpots.length) {
const hotspot = hotSpots[i];
const basename = hotspot.filename.split('/').pop();
let funcDisplay = hotspot.funcname;
if (funcDisplay.length > 35) {
funcDisplay = funcDisplay.substring(0, 32) + '...';
}

document.getElementById(`hotspot-file-${num}`).textContent = `${basename}:${hotspot.lineno}`;
document.getElementById(`hotspot-func-${num}`).textContent = funcDisplay;
document.getElementById(`hotspot-detail-${num}`).textContent = `${hotspot.directPercent.toFixed(1)}% samples (${hotspot.directSamples.toLocaleString()})`;
} else {
document.getElementById(`hotspot-file-${num}`).textContent = '--';
document.getElementById(`hotspot-func-${num}`).textContent = '--';
document.getElementById(`hotspot-detail-${num}`).textContent = '--';
}
}
}

// Control functions
function resetZoom() {
if (window.flamegraphChart) {
Expand Down Expand Up @@ -274,6 +420,16 @@ function toggleLegend() {
legendPanel.style.display = isHidden ? "block" : "none";
}

function clearSearch() {
const searchInput = document.getElementById("search-input");
if (searchInput) {
searchInput.value = "";
if (window.flamegraphChart) {
window.flamegraphChart.clear();
}
}
}

// Handle window resize
window.addEventListener("resize", function () {
if (window.flamegraphChart && window.flamegraphData) {
Expand Down
35 changes: 35 additions & 0 deletions Lib/profiling/sampling/flamegraph_template.html
Original file line number Diff line number Diff line change
Expand Up @@ -23,12 +23,47 @@ <h1>Python Performance Flamegraph</h1>
</div>
</div>

<div class="stats-section">
<div class="stats-container">
<div class="stat-card hotspot-card">
<div class="stat-icon">🥇</div>
<div class="stat-content">
<div class="stat-label">#1 Hot Spot</div>
<div class="stat-file" id="hotspot-file-1">--</div>
<div class="stat-value" id="hotspot-func-1">--</div>
<div class="stat-detail" id="hotspot-detail-1">--</div>
</div>
</div>
<div class="stat-card hotspot-card">
<div class="stat-icon">🥈</div>
<div class="stat-content">
<div class="stat-label">#2 Hot Spot</div>
<div class="stat-file" id="hotspot-file-2">--</div>
<div class="stat-value" id="hotspot-func-2">--</div>
<div class="stat-detail" id="hotspot-detail-2">--</div>
</div>
</div>
<div class="stat-card hotspot-card">
<div class="stat-icon">🥉</div>
<div class="stat-content">
<div class="stat-label">#3 Hot Spot</div>
<div class="stat-file" id="hotspot-file-3">--</div>
<div class="stat-value" id="hotspot-func-3">--</div>
<div class="stat-detail" id="hotspot-detail-3">--</div>
</div>
</div>
</div>
</div>

<div class="controls">
<div class="controls-content">
<button onclick="resetZoom()">🏠 Reset Zoom</button>
<button onclick="exportSVG()" class="secondary">📁 Export SVG</button>
<button onclick="toggleLegend()">🔥 Heat Map Legend</button>
</div>
<div class="search-container">
<input type="text" id="search-input" placeholder="🔍 Search functions..." />
</div>
</div>

<button id="show-info-btn" title="Show navigation guide">&#8505;</button>
Expand Down
11 changes: 9 additions & 2 deletions Lib/profiling/sampling/sample.py
Original file line number Diff line number Diff line change
Expand Up @@ -191,9 +191,16 @@ def sample(self, collector, duration_sec=10):
if self.realtime_stats and len(self.sample_intervals) > 0:
print() # Add newline after real-time stats

sample_rate = num_samples / running_time
error_rate = (errors / num_samples) * 100 if num_samples > 0 else 0

print(f"Captured {num_samples} samples in {running_time:.2f} seconds")
print(f"Sample rate: {num_samples / running_time:.2f} samples/sec")
print(f"Error rate: {(errors / num_samples) * 100:.2f}%")
print(f"Sample rate: {sample_rate:.2f} samples/sec")
print(f"Error rate: {error_rate:.2f}%")

# Pass stats to flamegraph collector if it's the right type
if hasattr(collector, 'set_stats'):
collector.set_stats(self.sample_interval_usec, running_time, sample_rate, error_rate)

expected_samples = int(duration_sec / sample_interval_sec)
if num_samples < expected_samples:
Expand Down
Loading