Initialized MiniProfiler project

- Contains the host code with a protocol implementation, data analyser and web-based visualiser
This commit is contained in:
Atharva Sawant
2025-11-27 20:34:41 +05:30
commit 852957a7de
20 changed files with 3845 additions and 0 deletions

View File

@@ -0,0 +1,282 @@
/* MiniProfiler Styles */
* {
margin: 0;
padding: 0;
box-sizing: border-box;
}
body {
font-family: 'Segoe UI', Tahoma, Geneva, Verdana, sans-serif;
background: #1e1e1e;
color: #d4d4d4;
line-height: 1.6;
}
.container {
max-width: 1400px;
margin: 0 auto;
padding: 20px;
}
/* Header */
header {
text-align: center;
margin-bottom: 30px;
padding-bottom: 20px;
border-bottom: 2px solid #3a3a3a;
}
header h1 {
font-size: 2.5em;
color: #569cd6;
margin-bottom: 5px;
}
.subtitle {
color: #9cdcfe;
font-size: 0.9em;
}
/* Control Panel */
.control-panel {
background: #252526;
padding: 20px;
border-radius: 8px;
margin-bottom: 20px;
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.3);
}
.connection-controls, .profiling-controls {
display: flex;
gap: 10px;
margin-bottom: 15px;
flex-wrap: wrap;
}
input[type="text"], input[type="number"] {
flex: 1;
min-width: 150px;
padding: 10px;
background: #3c3c3c;
border: 1px solid #555;
border-radius: 4px;
color: #d4d4d4;
font-size: 14px;
}
input[type="text"]:focus, input[type="number"]:focus {
outline: none;
border-color: #569cd6;
}
button {
padding: 10px 20px;
background: #0e639c;
color: white;
border: none;
border-radius: 4px;
cursor: pointer;
font-size: 14px;
transition: background 0.3s;
}
button:hover:not(:disabled) {
background: #1177bb;
}
button:disabled {
background: #3a3a3a;
color: #777;
cursor: not-allowed;
}
button#stop-btn {
background: #d16969;
}
button#stop-btn:hover:not(:disabled) {
background: #e67373;
}
button#clear-btn, button#reset-btn {
background: #4e4e4e;
}
button#clear-btn:hover:not(:disabled), button#reset-btn:hover:not(:disabled) {
background: #5e5e5e;
}
/* Status Display */
.status-display {
display: flex;
align-items: center;
gap: 15px;
padding-top: 10px;
border-top: 1px solid #3a3a3a;
}
.status-indicator {
font-size: 1.5em;
color: #6e6e6e;
}
.status-indicator.connected {
color: #4ec9b0;
}
.status-indicator.profiling {
color: #ce9178;
animation: pulse 1.5s infinite;
}
@keyframes pulse {
0%, 100% { opacity: 1; }
50% { opacity: 0.5; }
}
/* Metadata Panel */
.metadata-panel, .summary-panel {
background: #252526;
padding: 15px;
border-radius: 8px;
margin-bottom: 20px;
}
.metadata-panel h3, .summary-panel h3 {
color: #9cdcfe;
margin-bottom: 10px;
font-size: 1.1em;
}
#metadata-content, #summary-content {
display: grid;
grid-template-columns: repeat(auto-fit, minmax(200px, 1fr));
gap: 10px;
font-size: 0.9em;
}
#summary-content p {
margin: 5px 0;
}
/* Tabs */
.tabs {
display: flex;
gap: 5px;
margin-bottom: 0;
}
.tab-button {
padding: 12px 24px;
background: #2d2d30;
border: none;
border-radius: 8px 8px 0 0;
color: #9cdcfe;
cursor: pointer;
transition: background 0.3s;
}
.tab-button:hover {
background: #3e3e42;
}
.tab-button.active {
background: #252526;
color: #569cd6;
}
/* Tab Content */
.tab-content {
display: none;
background: #252526;
padding: 20px;
border-radius: 0 8px 8px 8px;
min-height: 500px;
}
.tab-content.active {
display: block;
}
/* Flame Graph */
#flamegraph {
width: 100%;
min-height: 500px;
}
/* Override d3-flamegraph default styles for dark theme */
.d3-flame-graph rect {
stroke: #1e1e1e;
stroke-width: 1;
}
.d3-flame-graph text {
fill: #fff;
font-size: 12px;
}
.d3-flame-graph .label {
pointer-events: none;
}
/* Timeline */
#timeline {
width: 100%;
min-height: 500px;
}
/* Statistics Table */
.table-container {
overflow-x: auto;
}
table {
width: 100%;
border-collapse: collapse;
font-size: 0.9em;
}
thead {
background: #2d2d30;
}
th {
padding: 12px;
text-align: left;
color: #9cdcfe;
font-weight: 600;
border-bottom: 2px solid #3a3a3a;
}
td {
padding: 10px 12px;
border-bottom: 1px solid #3a3a3a;
}
tbody tr:hover {
background: #2d2d30;
}
tbody tr:nth-child(even) {
background: #1e1e1e;
}
/* Responsive */
@media (max-width: 768px) {
.connection-controls, .profiling-controls {
flex-direction: column;
}
input[type="text"], input[type="number"], button {
width: 100%;
}
.tabs {
flex-direction: column;
}
.tab-button {
border-radius: 4px;
}
}

340
host/web/static/js/app.js Normal file
View 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();
}
});