// 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 = `