// MiniProfiler Web Application // Global state let socket = null; let isConnected = false; let isProfiling = false; let flamegraphChart = null; // Initialize on page load document.addEventListener('DOMContentLoaded', function() { initializeSocket(); initializeFlameGraph(); }); // Socket.IO Connection function initializeSocket() { socket = io(); socket.on('connect', function() { console.log('Connected to server'); updateStatus('Connected to server', false); }); socket.on('disconnect', function() { console.log('Disconnected from server'); updateStatus('Disconnected from server', false); isConnected = false; updateControlButtons(); }); socket.on('connected', function(data) { console.log('Connected to serial:', data); isConnected = true; updateStatus(`Connected to ${data.port}`, true); updateControlButtons(); document.getElementById('metadata-panel').style.display = 'block'; }); socket.on('disconnected', function() { console.log('Disconnected from serial'); isConnected = false; isProfiling = false; updateStatus('Disconnected', false); updateControlButtons(); document.getElementById('metadata-panel').style.display = 'none'; }); socket.on('profiling_started', function() { console.log('Profiling started'); isProfiling = true; updateStatus('Profiling active', true, true); updateControlButtons(); }); socket.on('profiling_stopped', function() { console.log('Profiling stopped'); isProfiling = false; updateStatus('Profiling stopped', true); updateControlButtons(); }); socket.on('metadata', function(data) { console.log('Metadata received:', data); displayMetadata(data); }); socket.on('device_status', function(data) { console.log('Device status:', data); if (data.buffer_overflows > 0) { console.warn(`Buffer overflows detected: ${data.buffer_overflows}`); } }); socket.on('summary_update', function(data) { updateSummary(data); }); socket.on('flamegraph_update', function(data) { updateFlameGraph(data); }); socket.on('statistics_update', function(data) { updateStatistics(data); }); socket.on('timeline_update', function(data) { updateTimeline(data); }); socket.on('error', function(data) { console.error('Error:', data.message); alert('Error: ' + data.message); }); } // Connection Control function toggleConnection() { if (isConnected) { socket.emit('disconnect_serial'); document.getElementById('connect-btn').textContent = 'Connect'; } else { const port = document.getElementById('serial-port').value; const baudrate = parseInt(document.getElementById('baudrate').value); const elfPath = document.getElementById('elf-path').value; if (!port) { alert('Please enter a serial port'); return; } socket.emit('connect_serial', { port: port, baudrate: baudrate, elf_path: elfPath || null }); document.getElementById('connect-btn').textContent = 'Disconnect'; } } function startProfiling() { socket.emit('start_profiling'); } function stopProfiling() { socket.emit('stop_profiling'); } function clearData() { if (confirm('Clear all profiling data?')) { socket.emit('clear_data'); } } function resetBuffers() { socket.emit('reset_buffers'); } // UI Updates function updateStatus(text, connected, profiling = false) { const statusText = document.getElementById('status-text'); const statusIndicator = document.getElementById('connection-status'); statusText.textContent = text; statusIndicator.classList.remove('connected', 'profiling'); if (profiling) { statusIndicator.classList.add('profiling'); } else if (connected) { statusIndicator.classList.add('connected'); } } function updateControlButtons() { document.getElementById('start-btn').disabled = !isConnected || isProfiling; document.getElementById('stop-btn').disabled = !isConnected || !isProfiling; document.getElementById('clear-btn').disabled = !isConnected; document.getElementById('reset-btn').disabled = !isConnected; } function displayMetadata(data) { const content = document.getElementById('metadata-content'); content.innerHTML = `
Firmware: ${data.fw_version}
MCU Clock: ${(data.mcu_clock_hz / 1e6).toFixed(1)} MHz
Timer Freq: ${(data.timer_freq / 1e6).toFixed(1)} MHz
Build ID: ${data.build_id}
`; } function updateSummary(data) { const content = document.getElementById('summary-content'); if (data.total_records === 0) { content.innerHTML = '

No profiling data available

'; return; } const totalTimeMs = (data.total_time_us / 1000).toFixed(2); const hottestTimeMs = (data.hottest_time_us / 1000).toFixed(2); content.innerHTML = `
Total Records: ${data.total_records}
Unique Functions: ${data.total_functions}
Total Time: ${totalTimeMs} ms
Hottest Function: ${data.hottest_function || 'N/A'}
Hottest Time: ${hottestTimeMs} ms
`; document.getElementById('record-count').textContent = `Records: ${data.total_records}`; } // Tab Management function showTab(tabName) { // Hide all tabs const tabs = document.querySelectorAll('.tab-content'); tabs.forEach(tab => tab.classList.remove('active')); const buttons = document.querySelectorAll('.tab-button'); buttons.forEach(btn => btn.classList.remove('active')); // Show selected tab document.getElementById(tabName + '-tab').classList.add('active'); event.target.classList.add('active'); } // Flame Graph function initializeFlameGraph() { const width = document.getElementById('flamegraph').offsetWidth; flamegraph = flamegraph() .width(width) .cellHeight(18) .transitionDuration(750) .minFrameSize(5) .transitionEase(d3.easeCubic) .sort(true); // Color scheme flamegraphChart.setColorMapper((d, originalColor) => { // Color by depth for better visualization const depth = d.depth || 0; const hue = (depth * 30) % 360; return d3.hsl(hue, 0.6, 0.5); }); d3.select("#flamegraph") .datum({name: "root", value: 0, children: []}) .call(flamegraphChart); } function updateFlameGraph(data) { // Initialize if not already created if (!flamegraphChart) { const width = document.getElementById('flamegraph').offsetWidth; flamegraphChart = flamegraph() .width(width) .cellHeight(18) .transitionDuration(750) .minFrameSize(5) .transitionEase(d3.easeCubic) .sort(true); // Color scheme flamegraphChart.setColorMapper((d, originalColor) => { // Color by depth for better visualization const depth = d.depth || 0; const hue = (depth * 30) % 360; return d3.hsl(hue, 0.6, 0.5); }); } // Update flame graph with new data d3.select("#flamegraph") .datum(data) .call(flamegraphChart); } // Timeline Visualization function updateTimeline(data) { if (!data || data.length === 0) { document.getElementById('timeline').innerHTML = '

No timeline data available

'; return; } // Convert to Plotly timeline format const traces = []; // Group by depth for better visualization const maxDepth = Math.max(...data.map(d => d.depth)); for (let depth = 0; depth <= maxDepth; depth++) { const depthData = data.filter(d => d.depth === depth); if (depthData.length === 0) continue; const trace = { x: depthData.map(d => d.start), y: depthData.map(d => depth), text: depthData.map(d => `${d.name}
Duration: ${(d.duration / 1000).toFixed(3)} ms`), mode: 'markers', marker: { size: depthData.map(d => Math.max(5, Math.log(d.duration + 1) * 2)), color: depthData.map(d => d.duration), colorscale: 'Viridis', showscale: depth === 0, colorbar: { title: 'Duration (μs)' } }, name: `Depth ${depth}`, hovertemplate: '%{text}' }; traces.push(trace); } const layout = { title: 'Function Call Timeline', xaxis: { title: 'Time (μs)', gridcolor: '#444' }, yaxis: { title: 'Call Depth', gridcolor: '#444' }, paper_bgcolor: '#252526', plot_bgcolor: '#1e1e1e', font: { color: '#d4d4d4' }, hovermode: 'closest', showlegend: false }; Plotly.newPlot('timeline', traces, layout, {responsive: true}); } // Statistics Table function updateStatistics(data) { const tbody = document.getElementById('statistics-body'); if (!data || data.length === 0) { tbody.innerHTML = 'No data available'; return; } let html = ''; data.forEach(stat => { html += ` ${stat.name} ${stat.address} ${stat.calls} ${stat.total_us.toLocaleString()} ${stat.avg_us.toFixed(2)} ${stat.min_us} ${stat.max_us} `; }); tbody.innerHTML = html; } // Load Sample Data async function loadSampleData() { try { updateStatus('Generating sample data...', false); // Generate and fetch sample data in one call const response = await fetch('/api/sample/generate'); if (!response.ok) { throw new Error('Failed to generate sample data'); } const data = await response.json(); const { flamegraph: flamegraphData, statistics: statsData, timeline: timelineData } = data; // Update all visualizations updateFlameGraph(flamegraphData); updateStatistics(statsData); updateTimeline(timelineData); // Update summary const totalRecords = timelineData.length; const totalFunctions = statsData.length; const totalTime = statsData.reduce((sum, s) => sum + s.total_us, 0); const hottest = statsData[0]; // Already sorted by total_us updateSummary({ total_records: totalRecords, total_functions: totalFunctions, total_time_us: totalTime, hottest_function: hottest ? hottest.name : null, hottest_time_us: hottest ? hottest.total_us : 0 }); updateStatus('Sample data loaded', false); console.log('Sample data loaded successfully'); } catch (error) { console.error('Error loading sample data:', error); alert(`Error loading sample data: ${error.message}`); updateStatus('Error loading sample data', false); } } // Handle window resize for flame graph window.addEventListener('resize', function() { if (flamegraph) { const width = document.getElementById('flamegraph').offsetWidth; flamegraphChart.width(width); flamegraphChart.update(); } });