Initialized MiniProfiler project
- Contains the host code with a protocol implementation, data analyser and web-based visualiser
This commit is contained in:
282
host/web/static/css/style.css
Normal file
282
host/web/static/css/style.css
Normal 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
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