// 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();
}
});