Skip to content

Commit 51c3f34

Browse files
committed
Add heatmap visualization to Tachyon sampling profiler
Introduce a new --heatmap output format that provides line-by-line execution visualization. The heatmap shows: - Color-coded execution intensity for each line (cold → warm → hot → very hot) - Inline sample counts and percentages per line - Per-file statistics (total samples, hottest line) - Interactive call graph navigation with caller/callee buttons - Module type badges (stdlib, site-packages, project code) Unlike flamegraphs which show call stacks and time distribution, heatmaps excel at identifying hot code paths within files, understanding line-level execution patterns, and navigating through call relationships.
1 parent bcced02 commit 51c3f34

File tree

10 files changed

+2802
-43
lines changed

10 files changed

+2802
-43
lines changed

Lib/profiling/sampling/flamegraph.css

Lines changed: 1120 additions & 37 deletions
Large diffs are not rendered by default.

Lib/profiling/sampling/heatmap.css

Lines changed: 696 additions & 0 deletions
Large diffs are not rendered by default.

Lib/profiling/sampling/heatmap.js

Lines changed: 183 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,183 @@
1+
// Tachyon Profiler - Heatmap JavaScript
2+
// Interactive features for the heatmap visualization
3+
4+
// Apply background colors from data attributes on page load
5+
document.addEventListener('DOMContentLoaded', function() {
6+
document.querySelectorAll('.code-line[data-bg-color]').forEach(line => {
7+
const bgColor = line.getAttribute('data-bg-color');
8+
if (bgColor) {
9+
line.style.background = bgColor;
10+
}
11+
});
12+
});
13+
14+
// State management
15+
let currentMenu = null;
16+
17+
// Utility: Create element with class and content
18+
function createElement(tag, className, textContent = '') {
19+
const el = document.createElement(tag);
20+
if (className) el.className = className;
21+
if (textContent) el.textContent = textContent;
22+
return el;
23+
}
24+
25+
// Utility: Calculate smart menu position
26+
function calculateMenuPosition(buttonRect, menuWidth, menuHeight) {
27+
const viewport = { width: window.innerWidth, height: window.innerHeight };
28+
const scroll = {
29+
x: window.pageXOffset || document.documentElement.scrollLeft,
30+
y: window.pageYOffset || document.documentElement.scrollTop
31+
};
32+
33+
const left = buttonRect.right + menuWidth + 10 < viewport.width
34+
? buttonRect.right + scroll.x + 10
35+
: Math.max(scroll.x + 10, buttonRect.left + scroll.x - menuWidth - 10);
36+
37+
const top = buttonRect.bottom + menuHeight + 10 < viewport.height
38+
? buttonRect.bottom + scroll.y + 5
39+
: Math.max(scroll.y + 10, buttonRect.top + scroll.y - menuHeight - 10);
40+
41+
return { left, top };
42+
}
43+
44+
// Close and remove current menu
45+
function closeMenu() {
46+
if (currentMenu) {
47+
currentMenu.remove();
48+
currentMenu = null;
49+
}
50+
}
51+
52+
// Show navigation menu for multiple options
53+
function showNavigationMenu(button, items, title) {
54+
closeMenu();
55+
56+
const menu = createElement('div', 'callee-menu');
57+
menu.appendChild(createElement('div', 'callee-menu-header', title));
58+
59+
items.forEach(linkData => {
60+
const item = createElement('div', 'callee-menu-item');
61+
item.appendChild(createElement('div', 'callee-menu-func', linkData.func));
62+
item.appendChild(createElement('div', 'callee-menu-file', linkData.file));
63+
item.addEventListener('click', () => window.location.href = linkData.link);
64+
menu.appendChild(item);
65+
});
66+
67+
const pos = calculateMenuPosition(button.getBoundingClientRect(), 350, 300);
68+
menu.style.left = `${pos.left}px`;
69+
menu.style.top = `${pos.top}px`;
70+
71+
document.body.appendChild(menu);
72+
currentMenu = menu;
73+
}
74+
75+
// Handle navigation button clicks
76+
function handleNavigationClick(button, e) {
77+
e.stopPropagation();
78+
79+
const navData = button.getAttribute('data-nav');
80+
if (navData) {
81+
window.location.href = JSON.parse(navData).link;
82+
return;
83+
}
84+
85+
const navMulti = button.getAttribute('data-nav-multi');
86+
if (navMulti) {
87+
const items = JSON.parse(navMulti);
88+
const title = button.classList.contains('caller') ? 'Choose a caller:' : 'Choose a callee:';
89+
showNavigationMenu(button, items, title);
90+
}
91+
}
92+
93+
// Initialize navigation buttons
94+
document.querySelectorAll('.nav-btn').forEach(button => {
95+
button.addEventListener('click', e => handleNavigationClick(button, e));
96+
});
97+
98+
// Close menu when clicking outside
99+
document.addEventListener('click', e => {
100+
if (currentMenu && !currentMenu.contains(e.target) && !e.target.classList.contains('nav-btn')) {
101+
closeMenu();
102+
}
103+
});
104+
105+
// Scroll to target line (centered using CSS scroll-margin-top)
106+
function scrollToTargetLine() {
107+
if (!window.location.hash) return;
108+
const target = document.querySelector(window.location.hash);
109+
if (target) {
110+
target.scrollIntoView({ behavior: 'smooth', block: 'start' });
111+
}
112+
}
113+
114+
// Initialize line number permalink handlers
115+
document.querySelectorAll('.line-number').forEach(lineNum => {
116+
lineNum.style.cursor = 'pointer';
117+
lineNum.addEventListener('click', e => {
118+
window.location.hash = `line-${e.target.textContent.trim()}`;
119+
});
120+
});
121+
122+
// Setup scroll-to-line behavior
123+
setTimeout(scrollToTargetLine, 100);
124+
window.addEventListener('hashchange', () => setTimeout(scrollToTargetLine, 50));
125+
126+
// Get sample count from line element
127+
function getSampleCount(line) {
128+
const text = line.querySelector('.line-samples')?.textContent.trim().replace(/,/g, '');
129+
return parseInt(text) || 0;
130+
}
131+
132+
// Classify intensity based on ratio
133+
function getIntensityClass(ratio) {
134+
if (ratio > 0.75) return 'vhot';
135+
if (ratio > 0.5) return 'hot';
136+
if (ratio > 0.25) return 'warm';
137+
return 'cold';
138+
}
139+
140+
// Build scroll minimap showing hotspot locations
141+
function buildScrollMarker() {
142+
const existing = document.getElementById('scroll_marker');
143+
if (existing) existing.remove();
144+
145+
if (document.body.scrollHeight <= window.innerHeight) return;
146+
147+
const lines = document.querySelectorAll('.code-line');
148+
const markerScale = window.innerHeight / document.body.scrollHeight;
149+
const lineHeight = Math.min(Math.max(3, window.innerHeight / lines.length), 10);
150+
const maxSamples = Math.max(...Array.from(lines, getSampleCount));
151+
152+
const scrollMarker = createElement('div', '');
153+
scrollMarker.id = 'scroll_marker';
154+
155+
let prevLine = -99, lastMark, lastTop;
156+
157+
lines.forEach((line, index) => {
158+
const samples = getSampleCount(line);
159+
if (samples === 0) return;
160+
161+
const lineTop = Math.floor(line.offsetTop * markerScale);
162+
const lineNumber = index + 1;
163+
const intensityClass = maxSamples > 0 ? getIntensityClass(samples / maxSamples) : 'cold';
164+
165+
if (lineNumber === prevLine + 1 && lastMark?.classList.contains(intensityClass)) {
166+
lastMark.style.height = `${lineTop + lineHeight - lastTop}px`;
167+
} else {
168+
lastMark = createElement('div', `marker ${intensityClass}`);
169+
lastMark.style.height = `${lineHeight}px`;
170+
lastMark.style.top = `${lineTop}px`;
171+
scrollMarker.appendChild(lastMark);
172+
lastTop = lineTop;
173+
}
174+
175+
prevLine = lineNumber;
176+
});
177+
178+
document.body.appendChild(scrollMarker);
179+
}
180+
181+
// Build scroll marker on load and resize
182+
setTimeout(buildScrollMarker, 200);
183+
window.addEventListener('resize', buildScrollMarker);
Lines changed: 63 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,63 @@
1+
function filterTable() {
2+
const searchTerm = document.getElementById('searchInput').value.toLowerCase();
3+
const moduleFilter = document.getElementById('moduleFilter').value;
4+
const table = document.getElementById('fileTable');
5+
const rows = table.getElementsByTagName('tr');
6+
7+
for (let i = 1; i < rows.length; i++) {
8+
const row = rows[i];
9+
const text = row.textContent.toLowerCase();
10+
const moduleType = row.getAttribute('data-module-type');
11+
12+
const matchesSearch = text.includes(searchTerm);
13+
const matchesModule = moduleFilter === 'all' || moduleType === moduleFilter;
14+
15+
row.style.display = (matchesSearch && matchesModule) ? '' : 'none';
16+
}
17+
}
18+
19+
// Track current sort state
20+
let currentSortColumn = -1;
21+
let currentSortAscending = true;
22+
23+
function sortTable(columnIndex) {
24+
const table = document.getElementById('fileTable');
25+
const tbody = table.querySelector('tbody');
26+
const rows = Array.from(tbody.querySelectorAll('tr'));
27+
28+
// Determine sort direction
29+
let ascending = true;
30+
if (currentSortColumn === columnIndex) {
31+
// Same column - toggle direction
32+
ascending = !currentSortAscending;
33+
} else {
34+
// New column - default direction based on type
35+
// For numeric columns (samples, lines, %), descending is default
36+
// For text columns (file, module, type), ascending is default
37+
ascending = columnIndex <= 2; // Columns 0-2 are text, 3+ are numeric
38+
}
39+
40+
rows.sort((a, b) => {
41+
let aVal = a.cells[columnIndex].textContent.trim();
42+
let bVal = b.cells[columnIndex].textContent.trim();
43+
44+
// Try to parse as number
45+
const aNum = parseFloat(aVal.replace(/,/g, '').replace('%', ''));
46+
const bNum = parseFloat(bVal.replace(/,/g, '').replace('%', ''));
47+
48+
let result;
49+
if (!isNaN(aNum) && !isNaN(bNum)) {
50+
result = aNum - bNum; // Numeric comparison
51+
} else {
52+
result = aVal.localeCompare(bVal); // String comparison
53+
}
54+
55+
return ascending ? result : -result;
56+
});
57+
58+
rows.forEach(row => tbody.appendChild(row));
59+
60+
// Update sort state
61+
currentSortColumn = columnIndex;
62+
currentSortAscending = ascending;
63+
}
Lines changed: 81 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,81 @@
1+
<!DOCTYPE html>
2+
<html lang="en">
3+
<head>
4+
<meta charset="UTF-8">
5+
<meta name="viewport" content="width=device-width, initial-scale=1.0">
6+
<title>Tachyon Profiler - Heatmap Report</title>
7+
<!-- INLINE_CSS -->
8+
</head>
9+
<body>
10+
<div class="page-wrapper">
11+
<div class="container">
12+
<header class="header">
13+
<div class="header-content">
14+
<!-- PYTHON_LOGO -->
15+
<div class="header-text">
16+
<h1>Tachyon Profiler Heatmap Report</h1>
17+
<p class="subtitle">Line-by-line performance analysis</p>
18+
</div>
19+
</div>
20+
</header>
21+
22+
<div class="stats-summary">
23+
<div class="stat-card">
24+
<span class="stat-value"><!-- NUM_FILES --></span>
25+
<span class="stat-label">Files Profiled</span>
26+
</div>
27+
<div class="stat-card">
28+
<span class="stat-value"><!-- TOTAL_SAMPLES --></span>
29+
<span class="stat-label">Total Samples</span>
30+
</div>
31+
<div class="stat-card">
32+
<span class="stat-value"><!-- DURATION --></span>
33+
<span class="stat-label">Duration</span>
34+
</div>
35+
<div class="stat-card">
36+
<span class="stat-value"><!-- SAMPLE_RATE --></span>
37+
<span class="stat-label">Samples/sec</span>
38+
</div>
39+
</div>
40+
41+
<div class="file-list">
42+
<h2>Profiled Files</h2>
43+
44+
<div class="filter-controls">
45+
<input type="text" id="searchInput" placeholder="🔍 Search files..." onkeyup="filterTable()">
46+
<select id="moduleFilter" onchange="filterTable()">
47+
<option value="all">All Modules</option>
48+
<option value="stdlib">Standard Library</option>
49+
<option value="site-packages">Site Packages</option>
50+
<option value="project">Project</option>
51+
<option value="other">Other</option>
52+
</select>
53+
</div>
54+
55+
<table id="fileTable">
56+
<thead>
57+
<tr>
58+
<th onclick="sortTable(0)">File ⇅</th>
59+
<th onclick="sortTable(1)">Module ⇅</th>
60+
<th onclick="sortTable(2)">Type ⇅</th>
61+
<th onclick="sortTable(3)" style="text-align: right;">Samples ⇅</th>
62+
<th onclick="sortTable(4)" style="text-align: right;">Lines Hit ⇅</th>
63+
<th onclick="sortTable(5)" style="text-align: right;">% of Total ⇅</th>
64+
<th style="text-align: left;">Intensity</th>
65+
</tr>
66+
</thead>
67+
<tbody>
68+
<!-- TABLE_ROWS -->
69+
</tbody>
70+
</table>
71+
</div>
72+
73+
<footer>
74+
Generated by Tachyon Profiler | Python Sampling Profiler
75+
</footer>
76+
</div>
77+
</div>
78+
79+
<!-- INLINE_JS -->
80+
</body>
81+
</html>
Lines changed: 56 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,56 @@
1+
<!DOCTYPE html>
2+
<html lang="en">
3+
<head>
4+
<meta charset="UTF-8">
5+
<meta name="viewport" content="width=device-width, initial-scale=1.0">
6+
<title><!-- FILENAME --> - Heatmap</title>
7+
<!-- INLINE_CSS -->
8+
</head>
9+
<body class="code-view">
10+
<header class="code-header">
11+
<div class="code-header-content">
12+
<h1>📄 <!-- FILENAME --></h1>
13+
<a href="index.html" class="back-link">← Back to Index</a>
14+
</div>
15+
</header>
16+
17+
<div class="file-stats">
18+
<div class="stats-grid">
19+
<div class="stat-item">
20+
<div class="stat-value"><!-- TOTAL_SAMPLES --></div>
21+
<div class="stat-label">Total Samples</div>
22+
</div>
23+
<div class="stat-item">
24+
<div class="stat-value"><!-- NUM_LINES --></div>
25+
<div class="stat-label">Lines Hit</div>
26+
</div>
27+
<div class="stat-item">
28+
<div class="stat-value"><!-- PERCENTAGE -->%</div>
29+
<div class="stat-label">% of Total</div>
30+
</div>
31+
<div class="stat-item">
32+
<div class="stat-value"><!-- MAX_SAMPLES --></div>
33+
<div class="stat-label">Max Samples/Line</div>
34+
</div>
35+
</div>
36+
</div>
37+
38+
<div class="legend">
39+
<div class="legend-content">
40+
<span class="legend-title">🔥 Intensity:</span>
41+
<div class="legend-gradient"></div>
42+
<div class="legend-labels">
43+
<span>Cold (0)</span>
44+
<span></span>
45+
<span>Hot (Max)</span>
46+
</div>
47+
</div>
48+
</div>
49+
50+
<div class="code-container">
51+
<!-- CODE_LINES -->
52+
</div>
53+
54+
<!-- INLINE_JS -->
55+
</body>
56+
</html>

0 commit comments

Comments
 (0)