// Tachyon Profiler - Heatmap JavaScript // Interactive features for the heatmap visualization // Aligned with Flamegraph viewer design patterns // ============================================================================ // State Management // ============================================================================ let currentMenu = null; let colorMode = 'self'; // 'self' or 'cumulative' - default to self let coldCodeHidden = false; // ============================================================================ // Theme Support // ============================================================================ function toggleTheme() { toggleAndSaveTheme(); applyLineColors(); // Rebuild scroll marker with new theme colors buildScrollMarker(); } // ============================================================================ // Utility Functions // ============================================================================ function createElement(tag, className, textContent = '') { const el = document.createElement(tag); if (className) el.className = className; if (textContent) el.textContent = textContent; return el; } 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 }; } // ============================================================================ // Menu Management // ============================================================================ function closeMenu() { if (currentMenu) { currentMenu.remove(); currentMenu = null; } } 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'); const funcDiv = createElement('div', 'callee-menu-func'); funcDiv.textContent = linkData.func; if (linkData.count !== undefined && linkData.count > 0) { const countBadge = createElement('span', 'count-badge'); countBadge.textContent = linkData.count.toLocaleString(); countBadge.title = `${linkData.count.toLocaleString()} samples`; funcDiv.appendChild(document.createTextNode(' ')); funcDiv.appendChild(countBadge); } item.appendChild(funcDiv); 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; } // ============================================================================ // Navigation // ============================================================================ 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); } } function scrollToTargetLine() { if (!window.location.hash) return; const target = document.querySelector(window.location.hash); if (target) { target.scrollIntoView({ behavior: 'smooth', block: 'start' }); } } // ============================================================================ // Sample Count & Intensity // ============================================================================ function getSampleCount(line) { let text; if (colorMode === 'self') { text = line.querySelector('.line-samples-self')?.textContent.trim().replace(/,/g, ''); } else { text = line.querySelector('.line-samples-cumulative')?.textContent.trim().replace(/,/g, ''); } return parseInt(text) || 0; } // ============================================================================ // Scroll Minimap // ============================================================================ function buildScrollMarker() { const existing = document.getElementById('scroll_marker'); if (existing) existing.remove(); if (document.body.scrollHeight <= window.innerHeight) return; const allLines = document.querySelectorAll('.code-line'); const lines = Array.from(allLines).filter(line => line.style.display !== 'none'); 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 ? (intensityToClass(samples / maxSamples) || 'cold') : '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); } function applyLineColors() { const lines = document.querySelectorAll('.code-line'); lines.forEach(line => { let intensity; if (colorMode === 'self') { intensity = parseFloat(line.getAttribute('data-self-intensity')) || 0; } else { intensity = parseFloat(line.getAttribute('data-cumulative-intensity')) || 0; } const color = intensityToColor(intensity); line.style.background = color; }); } // ============================================================================ // Toggle Controls // ============================================================================ function toggleColdCode() { coldCodeHidden = !coldCodeHidden; applyHotFilter(); updateToggleUI('toggle-cold', coldCodeHidden); buildScrollMarker(); } function applyHotFilter() { const lines = document.querySelectorAll('.code-line'); lines.forEach(line => { const selfSamples = line.querySelector('.line-samples-self')?.textContent.trim(); const cumulativeSamples = line.querySelector('.line-samples-cumulative')?.textContent.trim(); let isCold; if (colorMode === 'self') { isCold = !selfSamples || selfSamples === ''; } else { isCold = !cumulativeSamples || cumulativeSamples === ''; } if (isCold) { line.style.display = coldCodeHidden ? 'none' : 'flex'; } else { line.style.display = 'flex'; } }); } function toggleColorMode() { colorMode = colorMode === 'self' ? 'cumulative' : 'self'; applyLineColors(); updateToggleUI('toggle-color-mode', colorMode === 'cumulative'); if (coldCodeHidden) { applyHotFilter(); } buildScrollMarker(); } // ============================================================================ // Initialization // ============================================================================ document.addEventListener('DOMContentLoaded', function() { restoreUIState(); applyLineColors(); // Initialize navigation buttons document.querySelectorAll('.nav-btn').forEach(button => { button.addEventListener('click', e => handleNavigationClick(button, e)); }); // 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()}`; }); }); // Initialize toggle buttons const toggleColdBtn = document.getElementById('toggle-cold'); if (toggleColdBtn) toggleColdBtn.addEventListener('click', toggleColdCode); const colorModeBtn = document.getElementById('toggle-color-mode'); if (colorModeBtn) colorModeBtn.addEventListener('click', toggleColorMode); // Initialize specialization view toggle (hide if no bytecode data) const hasBytecode = document.querySelectorAll('.bytecode-toggle').length > 0; const specViewBtn = document.getElementById('toggle-spec-view'); if (specViewBtn) { if (hasBytecode) { specViewBtn.addEventListener('click', toggleSpecView); } else { specViewBtn.style.display = 'none'; } } // Initialize expand-all bytecode button const expandAllBtn = document.getElementById('toggle-all-bytecode'); if (expandAllBtn) { if (hasBytecode) { expandAllBtn.addEventListener('click', toggleAllBytecode); } else { expandAllBtn.style.display = 'none'; } } // Initialize span tooltips initSpanTooltips(); // Build scroll marker and scroll to target setTimeout(buildScrollMarker, 200); setTimeout(scrollToTargetLine, 100); }); // Close menu when clicking outside document.addEventListener('click', e => { if (currentMenu && !currentMenu.contains(e.target) && !e.target.classList.contains('nav-btn')) { closeMenu(); } }); // ======================================== // SPECIALIZATION VIEW TOGGLE // ======================================== let specViewEnabled = false; /** * Calculate heat color for given intensity (0-1) * Hot spans (>30%) get warm orange, cold spans get dimmed gray * @param {number} intensity - Value between 0 and 1 * @returns {string} rgba color string */ function calculateHeatColor(intensity) { // Hot threshold: only spans with >30% of max samples get color if (intensity > 0.3) { // Normalize intensity above threshold to 0-1 const normalizedIntensity = (intensity - 0.3) / 0.7; // Warm orange-red with increasing opacity for hotter spans const alpha = 0.25 + normalizedIntensity * 0.35; // 0.25 to 0.6 const hotColor = getComputedStyle(document.documentElement).getPropertyValue('--span-hot-base').trim(); return `rgba(${hotColor}, ${alpha})`; } else if (intensity > 0) { // Cold spans: very subtle gray, almost invisible const coldColor = getComputedStyle(document.documentElement).getPropertyValue('--span-cold-base').trim(); return `rgba(${coldColor}, 0.1)`; } return 'transparent'; } /** * Apply intensity-based heat colors to source spans * Hot spans get orange highlight, cold spans get dimmed * @param {boolean} enable - Whether to enable or disable span coloring */ function applySpanHeatColors(enable) { document.querySelectorAll('.instr-span').forEach(span => { const samples = enable ? (parseInt(span.dataset.samples) || 0) : 0; if (samples > 0) { const intensity = samples / (parseInt(span.dataset.maxSamples) || 1); span.style.backgroundColor = calculateHeatColor(intensity); span.style.borderRadius = '2px'; span.style.padding = '0 1px'; span.style.cursor = 'pointer'; } else { span.style.cssText = ''; } }); } // ======================================== // SPAN TOOLTIPS // ======================================== let activeTooltip = null; /** * Create and show tooltip for a span */ function showSpanTooltip(span) { hideSpanTooltip(); const samples = parseInt(span.dataset.samples) || 0; const maxSamples = parseInt(span.dataset.maxSamples) || 1; const pct = span.dataset.pct || '0'; const opcodes = span.dataset.opcodes || ''; if (samples === 0) return; const intensity = samples / maxSamples; const isHot = intensity > 0.7; const isWarm = intensity > 0.3; const hotnessText = isHot ? 'Hot' : isWarm ? 'Warm' : 'Cold'; const hotnessClass = isHot ? 'hot' : isWarm ? 'warm' : 'cold'; // Build opcodes rows - each opcode on its own row let opcodesHtml = ''; if (opcodes) { const opcodeList = opcodes.split(',').map(op => op.trim()).filter(op => op); if (opcodeList.length > 0) { opcodesHtml = `
Opcodes:
${opcodeList.map(op => `
${op}
`).join('')} `; } } const tooltip = document.createElement('div'); tooltip.className = 'span-tooltip'; tooltip.innerHTML = `
${hotnessText}
Samples: ${samples.toLocaleString()}
% of line: ${pct}%
${opcodesHtml} `; document.body.appendChild(tooltip); activeTooltip = tooltip; // Position tooltip above the span const rect = span.getBoundingClientRect(); const tooltipRect = tooltip.getBoundingClientRect(); let left = rect.left + (rect.width / 2) - (tooltipRect.width / 2); let top = rect.top - tooltipRect.height - 8; // Keep tooltip in viewport if (left < 5) left = 5; if (left + tooltipRect.width > window.innerWidth - 5) { left = window.innerWidth - tooltipRect.width - 5; } if (top < 5) { top = rect.bottom + 8; // Show below if no room above } tooltip.style.left = `${left + window.scrollX}px`; tooltip.style.top = `${top + window.scrollY}px`; } /** * Hide active tooltip */ function hideSpanTooltip() { if (activeTooltip) { activeTooltip.remove(); activeTooltip = null; } } /** * Initialize span tooltip handlers */ function initSpanTooltips() { document.addEventListener('mouseover', (e) => { const span = e.target.closest('.instr-span'); if (span && specViewEnabled) { showSpanTooltip(span); } }); document.addEventListener('mouseout', (e) => { const span = e.target.closest('.instr-span'); if (span) { hideSpanTooltip(); } }); } function toggleSpecView() { specViewEnabled = !specViewEnabled; const lines = document.querySelectorAll('.code-line'); if (specViewEnabled) { lines.forEach(line => { const specColor = line.getAttribute('data-spec-color'); line.style.background = specColor || 'transparent'; }); } else { applyLineColors(); } applySpanHeatColors(specViewEnabled); updateToggleUI('toggle-spec-view', specViewEnabled); // Disable/enable color mode toggle based on spec view state const colorModeToggle = document.getElementById('toggle-color-mode'); if (colorModeToggle) { colorModeToggle.classList.toggle('disabled', specViewEnabled); } buildScrollMarker(); } // ======================================== // BYTECODE PANEL TOGGLE // ======================================== /** * Toggle bytecode panel visibility for a source line * @param {HTMLElement} button - The toggle button that was clicked */ function toggleBytecode(button) { const lineDiv = button.closest('.code-line'); const lineId = lineDiv.id; const lineNum = lineId.replace('line-', ''); const panel = document.getElementById(`bytecode-${lineNum}`); const wrapper = document.getElementById(`bytecode-wrapper-${lineNum}`); if (!panel || !wrapper) return; const isExpanded = panel.classList.contains('expanded'); if (isExpanded) { panel.classList.remove('expanded'); wrapper.classList.remove('expanded'); button.classList.remove('expanded'); button.innerHTML = '▶'; // Right arrow } else { if (!panel.dataset.populated) { populateBytecodePanel(panel, button); } panel.classList.add('expanded'); wrapper.classList.add('expanded'); button.classList.add('expanded'); button.innerHTML = '▼'; // Down arrow } } /** * Populate bytecode panel with instruction data * @param {HTMLElement} panel - The panel element to populate * @param {HTMLElement} button - The button containing the bytecode data */ function populateBytecodePanel(panel, button) { const bytecodeJson = button.getAttribute('data-bytecode'); if (!bytecodeJson) return; // Get line number from parent const lineDiv = button.closest('.code-line'); const lineNum = lineDiv ? lineDiv.id.replace('line-', '') : null; try { const instructions = JSON.parse(bytecodeJson); if (!instructions.length) { panel.innerHTML = '
No bytecode data
'; panel.dataset.populated = 'true'; return; } const maxSamples = Math.max(...instructions.map(i => i.samples), 1); // Calculate specialization stats const totalSamples = instructions.reduce((sum, i) => sum + i.samples, 0); const specializedSamples = instructions .filter(i => i.is_specialized) .reduce((sum, i) => sum + i.samples, 0); const specPct = totalSamples > 0 ? Math.round(100 * specializedSamples / totalSamples) : 0; const specializedCount = instructions.filter(i => i.is_specialized).length; // Determine specialization level class let specClass = 'low'; if (specPct >= 67) specClass = 'high'; else if (specPct >= 33) specClass = 'medium'; // Build specialization summary const instruction_word = instructions.length === 1 ? 'instruction' : 'instructions'; const sample_word = totalSamples === 1 ? 'sample' : 'samples'; let html = `
${specPct}% specialized (${specializedCount}/${instructions.length} ${instruction_word}, ${specializedSamples.toLocaleString()}/${totalSamples.toLocaleString()} ${sample_word})
`; html += '
' + 'Instruction' + 'Samples' + 'Heat
'; for (const instr of instructions) { const heatPct = (instr.samples / maxSamples) * 100; const isHot = heatPct > 50; const specializedClass = instr.is_specialized ? ' specialized' : ''; const baseOpHtml = instr.is_specialized ? `(${escapeHtml(instr.base_opname)})` : ''; const badge = instr.is_specialized ? 'SPECIALIZED' : ''; // Build location data attributes for cross-referencing with source spans const hasLocations = instr.locations && instr.locations.length > 0; const locationData = hasLocations ? `data-locations='${JSON.stringify(instr.locations)}' data-line="${lineNum}" data-opcode="${instr.opcode}"` : ''; html += `
${escapeHtml(instr.opname)}${baseOpHtml}${badge} ${instr.samples.toLocaleString()}
`; } panel.innerHTML = html; panel.dataset.populated = 'true'; // Add hover handlers for bytecode instructions to highlight source spans panel.querySelectorAll('.bytecode-instruction[data-locations]').forEach(instrEl => { instrEl.addEventListener('mouseenter', highlightSourceFromBytecode); instrEl.addEventListener('mouseleave', unhighlightSourceFromBytecode); }); } catch (e) { panel.innerHTML = '
Error loading bytecode
'; console.error('Error parsing bytecode data:', e); } } /** * Highlight source spans when hovering over bytecode instruction */ function highlightSourceFromBytecode(e) { const instrEl = e.currentTarget; const lineNum = instrEl.dataset.line; const locationsStr = instrEl.dataset.locations; if (!lineNum) return; const lineDiv = document.getElementById(`line-${lineNum}`); if (!lineDiv) return; // Parse locations and highlight matching spans by column range try { const locations = JSON.parse(locationsStr || '[]'); const spans = lineDiv.querySelectorAll('.instr-span'); spans.forEach(span => { const spanStart = parseInt(span.dataset.colStart); const spanEnd = parseInt(span.dataset.colEnd); for (const loc of locations) { // Match if span's range matches instruction's location if (spanStart === loc.col_offset && spanEnd === loc.end_col_offset) { span.classList.add('highlight-from-bytecode'); break; } } }); } catch (err) { console.error('Error parsing locations:', err); } // Also highlight the instruction row itself instrEl.classList.add('highlight'); } /** * Remove highlighting from source spans */ function unhighlightSourceFromBytecode(e) { const instrEl = e.currentTarget; const lineNum = instrEl.dataset.line; if (!lineNum) return; const lineDiv = document.getElementById(`line-${lineNum}`); if (!lineDiv) return; const spans = lineDiv.querySelectorAll('.instr-span.highlight-from-bytecode'); spans.forEach(span => { span.classList.remove('highlight-from-bytecode'); }); instrEl.classList.remove('highlight'); } /** * Escape HTML special characters * @param {string} text - Text to escape * @returns {string} Escaped HTML */ function escapeHtml(text) { const div = document.createElement('div'); div.textContent = text; return div.innerHTML; } /** * Toggle all bytecode panels at once */ function toggleAllBytecode() { const buttons = document.querySelectorAll('.bytecode-toggle'); if (buttons.length === 0) return; const someExpanded = Array.from(buttons).some(b => b.classList.contains('expanded')); const expandAllBtn = document.getElementById('toggle-all-bytecode'); buttons.forEach(button => { const isExpanded = button.classList.contains('expanded'); if (someExpanded ? isExpanded : !isExpanded) { toggleBytecode(button); } }); // Update the expand-all button state if (expandAllBtn) { expandAllBtn.classList.toggle('expanded', !someExpanded); } } // Keyboard shortcut: 'b' toggles all bytecode panels, Enter/Space activates toggle switches document.addEventListener('keydown', function(e) { if (e.target.tagName === 'INPUT' || e.target.tagName === 'TEXTAREA') { return; } if (e.key === 'b' && !e.ctrlKey && !e.altKey && !e.metaKey) { toggleAllBytecode(); } if ((e.key === 'Enter' || e.key === ' ') && e.target.classList.contains('toggle-switch')) { e.preventDefault(); e.target.click(); } }); // Handle hash changes window.addEventListener('hashchange', () => setTimeout(scrollToTargetLine, 50)); // Rebuild scroll marker on resize window.addEventListener('resize', buildScrollMarker);