Skip to content
Draft
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
1,157 changes: 1,120 additions & 37 deletions Lib/profiling/sampling/flamegraph.css

Large diffs are not rendered by default.

718 changes: 718 additions & 0 deletions Lib/profiling/sampling/heatmap.css

Large diffs are not rendered by default.

184 changes: 184 additions & 0 deletions Lib/profiling/sampling/heatmap.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,184 @@
// Tachyon Profiler - Heatmap JavaScript
// Interactive features for the heatmap visualization

// Apply background colors on page load
document.addEventListener('DOMContentLoaded', function() {
// Apply background colors
document.querySelectorAll('.code-line[data-bg-color]').forEach(line => {
const bgColor = line.getAttribute('data-bg-color');
if (bgColor) {
line.style.background = bgColor;
}
});
});

// State management
let currentMenu = null;

// Utility: Create element with class and content
function createElement(tag, className, textContent = '') {
const el = document.createElement(tag);
if (className) el.className = className;
if (textContent) el.textContent = textContent;
return el;
}

// Utility: Calculate smart menu position
function calculateMenuPosition(buttonRect, menuWidth, menuHeight) {
const viewport = { width: window.innerWidth, height: window.innerHeight };
const scroll = {
x: window.pageXOffset || document.documentElement.scrollLeft,
y: window.pageYOffset || document.documentElement.scrollTop
};

const left = buttonRect.right + menuWidth + 10 < viewport.width
? buttonRect.right + scroll.x + 10
: Math.max(scroll.x + 10, buttonRect.left + scroll.x - menuWidth - 10);

const top = buttonRect.bottom + menuHeight + 10 < viewport.height
? buttonRect.bottom + scroll.y + 5
: Math.max(scroll.y + 10, buttonRect.top + scroll.y - menuHeight - 10);

return { left, top };
}

// Close and remove current menu
function closeMenu() {
if (currentMenu) {
currentMenu.remove();
currentMenu = null;
}
}

// Show navigation menu for multiple options
function showNavigationMenu(button, items, title) {
closeMenu();

const menu = createElement('div', 'callee-menu');
menu.appendChild(createElement('div', 'callee-menu-header', title));

items.forEach(linkData => {
const item = createElement('div', 'callee-menu-item');
item.appendChild(createElement('div', 'callee-menu-func', linkData.func));
item.appendChild(createElement('div', 'callee-menu-file', linkData.file));
item.addEventListener('click', () => window.location.href = linkData.link);
menu.appendChild(item);
});

const pos = calculateMenuPosition(button.getBoundingClientRect(), 350, 300);
menu.style.left = `${pos.left}px`;
menu.style.top = `${pos.top}px`;

document.body.appendChild(menu);
currentMenu = menu;
}

// Handle navigation button clicks
function handleNavigationClick(button, e) {
e.stopPropagation();

const navData = button.getAttribute('data-nav');
if (navData) {
window.location.href = JSON.parse(navData).link;
return;
}

const navMulti = button.getAttribute('data-nav-multi');
if (navMulti) {
const items = JSON.parse(navMulti);
const title = button.classList.contains('caller') ? 'Choose a caller:' : 'Choose a callee:';
showNavigationMenu(button, items, title);
}
}

// Initialize navigation buttons
document.querySelectorAll('.nav-btn').forEach(button => {
button.addEventListener('click', e => handleNavigationClick(button, e));
});

// Close menu when clicking outside
document.addEventListener('click', e => {
if (currentMenu && !currentMenu.contains(e.target) && !e.target.classList.contains('nav-btn')) {
closeMenu();
}
});

// Scroll to target line (centered using CSS scroll-margin-top)
function scrollToTargetLine() {
if (!window.location.hash) return;
const target = document.querySelector(window.location.hash);
if (target) {
target.scrollIntoView({ behavior: 'smooth', block: 'start' });
}
}

// Initialize line number permalink handlers
document.querySelectorAll('.line-number').forEach(lineNum => {
lineNum.style.cursor = 'pointer';
lineNum.addEventListener('click', e => {
window.location.hash = `line-${e.target.textContent.trim()}`;
});
});

// Setup scroll-to-line behavior
setTimeout(scrollToTargetLine, 100);
window.addEventListener('hashchange', () => setTimeout(scrollToTargetLine, 50));

// Get sample count from line element
function getSampleCount(line) {
const text = line.querySelector('.line-samples')?.textContent.trim().replace(/,/g, '');
return parseInt(text) || 0;
}

// Classify intensity based on ratio
function getIntensityClass(ratio) {
if (ratio > 0.75) return 'vhot';
if (ratio > 0.5) return 'hot';
if (ratio > 0.25) return 'warm';
return 'cold';
}

// Build scroll minimap showing hotspot locations
function buildScrollMarker() {
const existing = document.getElementById('scroll_marker');
if (existing) existing.remove();

if (document.body.scrollHeight <= window.innerHeight) return;

const lines = document.querySelectorAll('.code-line');
const markerScale = window.innerHeight / document.body.scrollHeight;
const lineHeight = Math.min(Math.max(3, window.innerHeight / lines.length), 10);
const maxSamples = Math.max(...Array.from(lines, getSampleCount));

const scrollMarker = createElement('div', '');
scrollMarker.id = 'scroll_marker';

let prevLine = -99, lastMark, lastTop;

lines.forEach((line, index) => {
const samples = getSampleCount(line);
if (samples === 0) return;

const lineTop = Math.floor(line.offsetTop * markerScale);
const lineNumber = index + 1;
const intensityClass = maxSamples > 0 ? getIntensityClass(samples / maxSamples) : 'cold';

if (lineNumber === prevLine + 1 && lastMark?.classList.contains(intensityClass)) {
lastMark.style.height = `${lineTop + lineHeight - lastTop}px`;
} else {
lastMark = createElement('div', `marker ${intensityClass}`);
lastMark.style.height = `${lineHeight}px`;
lastMark.style.top = `${lineTop}px`;
scrollMarker.appendChild(lastMark);
lastTop = lineTop;
}

prevLine = lineNumber;
});

document.body.appendChild(scrollMarker);
}

// Build scroll marker on load and resize
setTimeout(buildScrollMarker, 200);
window.addEventListener('resize', buildScrollMarker);
63 changes: 63 additions & 0 deletions Lib/profiling/sampling/heatmap_index.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,63 @@
function filterTable() {
const searchTerm = document.getElementById('searchInput').value.toLowerCase();
const moduleFilter = document.getElementById('moduleFilter').value;
const table = document.getElementById('fileTable');
const rows = table.getElementsByTagName('tr');

for (let i = 1; i < rows.length; i++) {
const row = rows[i];
const text = row.textContent.toLowerCase();
const moduleType = row.getAttribute('data-module-type');

const matchesSearch = text.includes(searchTerm);
const matchesModule = moduleFilter === 'all' || moduleType === moduleFilter;

row.style.display = (matchesSearch && matchesModule) ? '' : 'none';
}
}

// Track current sort state
let currentSortColumn = -1;
let currentSortAscending = true;

function sortTable(columnIndex) {
const table = document.getElementById('fileTable');
const tbody = table.querySelector('tbody');
const rows = Array.from(tbody.querySelectorAll('tr'));

// Determine sort direction
let ascending = true;
if (currentSortColumn === columnIndex) {
// Same column - toggle direction
ascending = !currentSortAscending;
} else {
// New column - default direction based on type
// For numeric columns (samples, lines, %), descending is default
// For text columns (file, module, type), ascending is default
ascending = columnIndex <= 2; // Columns 0-2 are text, 3+ are numeric
}

rows.sort((a, b) => {
let aVal = a.cells[columnIndex].textContent.trim();
let bVal = b.cells[columnIndex].textContent.trim();

// Try to parse as number
const aNum = parseFloat(aVal.replace(/,/g, '').replace('%', ''));
const bNum = parseFloat(bVal.replace(/,/g, '').replace('%', ''));

let result;
if (!isNaN(aNum) && !isNaN(bNum)) {
result = aNum - bNum; // Numeric comparison
} else {
result = aVal.localeCompare(bVal); // String comparison
}

return ascending ? result : -result;
});

rows.forEach(row => tbody.appendChild(row));

// Update sort state
currentSortColumn = columnIndex;
currentSortAscending = ascending;
}
80 changes: 80 additions & 0 deletions Lib/profiling/sampling/heatmap_index_template.html
Original file line number Diff line number Diff line change
@@ -0,0 +1,80 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Tachyon Profiler - Heatmap Report</title>
<!-- INLINE_CSS -->
</head>
<body>
<div class="page-wrapper">
<div class="container">
<header class="header">
<div class="header-content">
<!-- PYTHON_LOGO -->
<div class="header-text">
<h1>Tachyon Profiler Heatmap Report</h1>
<p class="subtitle">Line-by-line performance analysis</p>
</div>
</div>
</header>

<div class="stats-summary">
<div class="stat-card">
<span class="stat-value"><!-- NUM_FILES --></span>
<span class="stat-label">Files Profiled</span>
</div>
<div class="stat-card">
<span class="stat-value"><!-- TOTAL_SAMPLES --></span>
<span class="stat-label">Total Snapshots</span>
</div>
<div class="stat-card">
<span class="stat-value"><!-- DURATION --></span>
<span class="stat-label">Duration</span>
</div>
<div class="stat-card">
<span class="stat-value"><!-- SAMPLE_RATE --></span>
<span class="stat-label">Samples/sec</span>
</div>
</div>

<div class="file-list">
<h2>Profiled Files</h2>

<div class="filter-controls">
<input type="text" id="searchInput" placeholder="🔍 Search files..." onkeyup="filterTable()">
<select id="moduleFilter" onchange="filterTable()">
<option value="all">All Modules</option>
<option value="stdlib">Standard Library</option>
<option value="site-packages">Site Packages</option>
<option value="project">Project</option>
<option value="other">Other</option>
</select>
</div>

<table id="fileTable">
<thead>
<tr>
<th onclick="sortTable(0)">File ⇅</th>
<th onclick="sortTable(1)">Module ⇅</th>
<th onclick="sortTable(2)">Type ⇅</th>
<th onclick="sortTable(3)" style="text-align: right;">Line Samples ⇅</th>
<th onclick="sortTable(4)" style="text-align: right;">Lines Hit ⇅</th>
<th style="text-align: left;">Intensity</th>
</tr>
</thead>
<tbody>
<!-- TABLE_ROWS -->
</tbody>
</table>
</div>

<footer>
Generated by Tachyon Profiler | Python Sampling Profiler
</footer>
</div>
</div>

<!-- INLINE_JS -->
</body>
</html>
56 changes: 56 additions & 0 deletions Lib/profiling/sampling/heatmap_pyfile_template.html
Original file line number Diff line number Diff line change
@@ -0,0 +1,56 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title><!-- FILENAME --> - Heatmap</title>
<!-- INLINE_CSS -->
</head>
<body class="code-view">
<header class="code-header">
<div class="code-header-content">
<h1>📄 <!-- FILENAME --></h1>
<a href="index.html" class="back-link">← Back to Index</a>
</div>
</header>

<div class="file-stats">
<div class="stats-grid">
<div class="stat-item">
<div class="stat-value"><!-- TOTAL_SAMPLES --></div>
<div class="stat-label">Total Samples</div>
</div>
<div class="stat-item">
<div class="stat-value"><!-- NUM_LINES --></div>
<div class="stat-label">Lines Hit</div>
</div>
<div class="stat-item">
<div class="stat-value"><!-- PERCENTAGE -->%</div>
<div class="stat-label">% of Total</div>
</div>
<div class="stat-item">
<div class="stat-value"><!-- MAX_SAMPLES --></div>
<div class="stat-label">Max Samples/Line</div>
</div>
</div>
</div>

<div class="legend">
<div class="legend-content">
<span class="legend-title">🔥 Intensity:</span>
<div class="legend-gradient"></div>
<div class="legend-labels">
<span>Cold (0)</span>
<span>→</span>
<span>Hot (Max)</span>
</div>
</div>
</div>

<div class="code-container">
<!-- CODE_LINES -->
</div>

<!-- INLINE_JS -->
</body>
</html>
Loading
Loading