- Contains the host code with a protocol implementation, data analyser and web-based visualiser
341 lines
9.7 KiB
JavaScript
341 lines
9.7 KiB
JavaScript
// MiniProfiler Web Application
|
|
|
|
// Global state
|
|
let socket = null;
|
|
let isConnected = false;
|
|
let isProfiling = false;
|
|
let flamegraph = 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 = `
|
|
<div><strong>Firmware:</strong> ${data.fw_version}</div>
|
|
<div><strong>MCU Clock:</strong> ${(data.mcu_clock_hz / 1e6).toFixed(1)} MHz</div>
|
|
<div><strong>Timer Freq:</strong> ${(data.timer_freq / 1e6).toFixed(1)} MHz</div>
|
|
<div><strong>Build ID:</strong> ${data.build_id}</div>
|
|
`;
|
|
}
|
|
|
|
function updateSummary(data) {
|
|
const content = document.getElementById('summary-content');
|
|
|
|
if (data.total_records === 0) {
|
|
content.innerHTML = '<p>No profiling data available</p>';
|
|
return;
|
|
}
|
|
|
|
const totalTimeMs = (data.total_time_us / 1000).toFixed(2);
|
|
const hottestTimeMs = (data.hottest_time_us / 1000).toFixed(2);
|
|
|
|
content.innerHTML = `
|
|
<div><strong>Total Records:</strong> ${data.total_records}</div>
|
|
<div><strong>Unique Functions:</strong> ${data.total_functions}</div>
|
|
<div><strong>Total Time:</strong> ${totalTimeMs} ms</div>
|
|
<div><strong>Hottest Function:</strong> ${data.hottest_function || 'N/A'}</div>
|
|
<div><strong>Hottest Time:</strong> ${hottestTimeMs} ms</div>
|
|
`;
|
|
|
|
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 = d3.flamegraph()
|
|
.width(width)
|
|
.cellHeight(18)
|
|
.transitionDuration(750)
|
|
.minFrameSize(5)
|
|
.transitionEase(d3.easeCubic)
|
|
.sort(true)
|
|
.title("")
|
|
.differential(false)
|
|
.selfValue(false);
|
|
|
|
// Color scheme
|
|
flamegraph.setColorMapper((d, originalColor) => {
|
|
// Color by depth for better visualization
|
|
const hue = (d.data.depth * 30) % 360;
|
|
return d3.hsl(hue, 0.6, 0.5);
|
|
});
|
|
|
|
d3.select("#flamegraph")
|
|
.datum({name: "root", value: 0, children: []})
|
|
.call(flamegraph);
|
|
}
|
|
|
|
function updateFlameGraph(data) {
|
|
if (!flamegraph) {
|
|
initializeFlameGraph();
|
|
}
|
|
|
|
// Update flame graph with new data
|
|
d3.select("#flamegraph")
|
|
.datum(data)
|
|
.call(flamegraph);
|
|
}
|
|
|
|
// Timeline Visualization
|
|
function updateTimeline(data) {
|
|
if (!data || data.length === 0) {
|
|
document.getElementById('timeline').innerHTML = '<p>No timeline data available</p>';
|
|
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}<br>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}<extra></extra>'
|
|
};
|
|
|
|
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 = '<tr><td colspan="7">No data available</td></tr>';
|
|
return;
|
|
}
|
|
|
|
let html = '';
|
|
data.forEach(stat => {
|
|
html += `
|
|
<tr>
|
|
<td>${stat.name}</td>
|
|
<td><code>${stat.address}</code></td>
|
|
<td>${stat.calls}</td>
|
|
<td>${stat.total_us.toLocaleString()}</td>
|
|
<td>${stat.avg_us.toFixed(2)}</td>
|
|
<td>${stat.min_us}</td>
|
|
<td>${stat.max_us}</td>
|
|
</tr>
|
|
`;
|
|
});
|
|
|
|
tbody.innerHTML = html;
|
|
}
|
|
|
|
// Handle window resize for flame graph
|
|
window.addEventListener('resize', function() {
|
|
if (flamegraph) {
|
|
const width = document.getElementById('flamegraph').offsetWidth;
|
|
flamegraph.width(width);
|
|
flamegraph.update();
|
|
}
|
|
});
|