Initialized MiniProfiler project
- Contains the host code with a protocol implementation, data analyser and web-based visualiser
This commit is contained in:
340
host/web/static/js/app.js
Normal file
340
host/web/static/js/app.js
Normal file
@@ -0,0 +1,340 @@
|
||||
// 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();
|
||||
}
|
||||
});
|
||||
Reference in New Issue
Block a user