mirror of
https://github.com/Kpa-clawbot/meshcore-analyzer.git
synced 2026-03-30 12:25:40 +00:00
Compare commits
3 Commits
mobile-pac
...
perf-instr
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
36bf6eac82 | ||
|
|
af94065399 | ||
|
|
95db662c5a |
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "meshcore-analyzer",
|
||||
"version": "2.0.0",
|
||||
"version": "2.0.1",
|
||||
"description": "Community-run alternative to the closed-source `analyzer.letsmesh.net`. MQTT packet collection + open-source web analyzer for the Bay Area MeshCore mesh.",
|
||||
"main": "index.js",
|
||||
"scripts": {
|
||||
|
||||
@@ -11,11 +11,36 @@ function payloadTypeName(n) { return PAYLOAD_TYPES[n] || 'UNKNOWN'; }
|
||||
function payloadTypeColor(n) { return PAYLOAD_COLORS[n] || 'unknown'; }
|
||||
|
||||
// --- Utilities ---
|
||||
const _apiPerf = { calls: 0, totalMs: 0, log: [] };
|
||||
async function api(path) {
|
||||
const t0 = performance.now();
|
||||
const res = await fetch('/api' + path);
|
||||
if (!res.ok) throw new Error(`API ${res.status}: ${path}`);
|
||||
return res.json();
|
||||
const data = await res.json();
|
||||
const ms = performance.now() - t0;
|
||||
_apiPerf.calls++;
|
||||
_apiPerf.totalMs += ms;
|
||||
_apiPerf.log.push({ path, ms: Math.round(ms), time: Date.now() });
|
||||
if (_apiPerf.log.length > 200) _apiPerf.log.shift();
|
||||
if (ms > 500) console.warn(`[SLOW API] ${path} took ${Math.round(ms)}ms`);
|
||||
return data;
|
||||
}
|
||||
// Expose for console debugging: apiPerf()
|
||||
window.apiPerf = function() {
|
||||
const byPath = {};
|
||||
_apiPerf.log.forEach(e => {
|
||||
if (!byPath[e.path]) byPath[e.path] = { count: 0, totalMs: 0, maxMs: 0 };
|
||||
byPath[e.path].count++;
|
||||
byPath[e.path].totalMs += e.ms;
|
||||
if (e.ms > byPath[e.path].maxMs) byPath[e.path].maxMs = e.ms;
|
||||
});
|
||||
const rows = Object.entries(byPath).map(([p, s]) => ({
|
||||
path: p, count: s.count, avgMs: Math.round(s.totalMs / s.count), maxMs: s.maxMs,
|
||||
totalMs: Math.round(s.totalMs)
|
||||
})).sort((a, b) => b.totalMs - a.totalMs);
|
||||
console.table(rows);
|
||||
return { calls: _apiPerf.calls, avgMs: Math.round(_apiPerf.totalMs / _apiPerf.calls), endpoints: rows };
|
||||
};
|
||||
|
||||
function timeAgo(iso) {
|
||||
if (!iso) return '—';
|
||||
@@ -217,7 +242,10 @@ function navigate() {
|
||||
|
||||
const app = document.getElementById('app');
|
||||
if (pages[basePage]?.init) {
|
||||
const t0 = performance.now();
|
||||
pages[basePage].init(app, routeParam);
|
||||
const ms = performance.now() - t0;
|
||||
if (ms > 100) console.warn(`[SLOW PAGE] ${basePage} init took ${Math.round(ms)}ms`);
|
||||
app.classList.remove('page-enter'); void app.offsetWidth; app.classList.add('page-enter');
|
||||
} else {
|
||||
app.innerHTML = `<div style="padding:40px;text-align:center;color:#6b7280"><h2>${route}</h2><p>Page not yet implemented.</p></div>`;
|
||||
|
||||
@@ -20,7 +20,7 @@
|
||||
<meta name="twitter:title" content="MeshCore Analyzer">
|
||||
<meta name="twitter:description" content="Real-time MeshCore LoRa mesh network analyzer — live packet visualization, node tracking, channel decryption, and route analysis.">
|
||||
<meta name="twitter:image" content="https://raw.githubusercontent.com/Kpa-clawbot/meshcore-analyzer/master/public/og-image.png">
|
||||
<link rel="stylesheet" href="style.css?v=1773969261">
|
||||
<link rel="stylesheet" href="style.css?v=1773970465">
|
||||
<link rel="stylesheet" href="home.css">
|
||||
<link rel="stylesheet" href="live.css?v=1773966856">
|
||||
<link rel="stylesheet" href="https://unpkg.com/leaflet@1.9.4/dist/leaflet.css"
|
||||
@@ -76,7 +76,7 @@
|
||||
<main id="app" role="main"></main>
|
||||
|
||||
<script src="vendor/qrcode.js"></script>
|
||||
<script src="app.js?v=1774079160"></script>
|
||||
<script src="app.js?v=1773970465"></script>
|
||||
<script src="home.js?v=1774079160"></script>
|
||||
<script src="packets.js?v=1773969349"></script>
|
||||
<script src="map.js?v=1774079160" onerror="console.error('Failed to load:', this.src)"></script>
|
||||
@@ -87,5 +87,6 @@
|
||||
<script src="live.js?v=1773964458" onerror="console.error('Failed to load:', this.src)"></script>
|
||||
<script src="observers.js?v=1774079160" onerror="console.error('Failed to load:', this.src)"></script>
|
||||
<script src="node-analytics.js?v=1773961276" onerror="console.error('Failed to load:', this.src)"></script>
|
||||
<script src="perf.js?v=1" onerror="console.error('Failed to load:', this.src)"></script>
|
||||
</body>
|
||||
</html>
|
||||
|
||||
88
public/perf.js
Normal file
88
public/perf.js
Normal file
@@ -0,0 +1,88 @@
|
||||
/* === MeshCore Analyzer — perf.js === */
|
||||
'use strict';
|
||||
|
||||
(function () {
|
||||
let interval = null;
|
||||
|
||||
async function render(app) {
|
||||
app.innerHTML = '<div style="height:100%;overflow-y:auto;padding:16px 24px;"><h2>⚡ Performance Dashboard</h2><div id="perfContent">Loading...</div></div>';
|
||||
await refresh();
|
||||
}
|
||||
|
||||
async function refresh() {
|
||||
const el = document.getElementById('perfContent');
|
||||
if (!el) return;
|
||||
try {
|
||||
const [server, client] = await Promise.all([
|
||||
fetch('/api/perf').then(r => r.json()),
|
||||
Promise.resolve(window.apiPerf ? window.apiPerf() : null)
|
||||
]);
|
||||
|
||||
let html = '';
|
||||
|
||||
// Server overview
|
||||
html += `<div style="display:flex;gap:16px;flex-wrap:wrap;margin:16px 0;">
|
||||
<div class="perf-card"><div class="perf-num">${server.totalRequests}</div><div class="perf-label">Total Requests</div></div>
|
||||
<div class="perf-card"><div class="perf-num">${server.avgMs}ms</div><div class="perf-label">Avg Response</div></div>
|
||||
<div class="perf-card"><div class="perf-num">${Math.round(server.uptime / 60)}m</div><div class="perf-label">Uptime</div></div>
|
||||
<div class="perf-card"><div class="perf-num">${server.slowQueries.length}</div><div class="perf-label">Slow (>100ms)</div></div>
|
||||
</div>`;
|
||||
|
||||
// Server endpoints table
|
||||
const eps = Object.entries(server.endpoints);
|
||||
if (eps.length) {
|
||||
html += '<h3>Server Endpoints (sorted by total time)</h3>';
|
||||
html += '<div style="overflow-x:auto"><table class="perf-table"><thead><tr><th>Endpoint</th><th>Count</th><th>Avg</th><th>P50</th><th>P95</th><th>Max</th><th>Total</th></tr></thead><tbody>';
|
||||
for (const [path, s] of eps) {
|
||||
const total = Math.round(s.count * s.avgMs);
|
||||
const cls = s.p95Ms > 200 ? ' class="perf-slow"' : s.p95Ms > 50 ? ' class="perf-warn"' : '';
|
||||
html += `<tr${cls}><td><code>${path}</code></td><td>${s.count}</td><td>${s.avgMs}ms</td><td>${s.p50Ms}ms</td><td>${s.p95Ms}ms</td><td>${s.maxMs}ms</td><td>${total}ms</td></tr>`;
|
||||
}
|
||||
html += '</tbody></table></div>';
|
||||
}
|
||||
|
||||
// Client API calls
|
||||
if (client && client.endpoints.length) {
|
||||
html += '<h3>Client API Calls (this session)</h3>';
|
||||
html += '<div style="overflow-x:auto"><table class="perf-table"><thead><tr><th>Endpoint</th><th>Count</th><th>Avg</th><th>Max</th><th>Total</th></tr></thead><tbody>';
|
||||
for (const s of client.endpoints) {
|
||||
const cls = s.maxMs > 500 ? ' class="perf-slow"' : s.avgMs > 200 ? ' class="perf-warn"' : '';
|
||||
html += `<tr${cls}><td><code>${s.path}</code></td><td>${s.count}</td><td>${s.avgMs}ms</td><td>${s.maxMs}ms</td><td>${s.totalMs}ms</td></tr>`;
|
||||
}
|
||||
html += '</tbody></table></div>';
|
||||
}
|
||||
|
||||
// Slow queries
|
||||
if (server.slowQueries.length) {
|
||||
html += '<h3>Recent Slow Queries (>100ms)</h3>';
|
||||
html += '<div style="overflow-x:auto"><table class="perf-table"><thead><tr><th>Time</th><th>Path</th><th>Duration</th><th>Status</th></tr></thead><tbody>';
|
||||
for (const q of server.slowQueries.slice().reverse()) {
|
||||
html += `<tr class="perf-slow"><td>${new Date(q.time).toLocaleTimeString()}</td><td><code>${q.path}</code></td><td>${q.ms}ms</td><td>${q.status}</td></tr>`;
|
||||
}
|
||||
html += '</tbody></table></div>';
|
||||
}
|
||||
|
||||
html += `<div style="margin-top:16px"><button id="perfReset" style="padding:8px 16px;cursor:pointer">Reset Stats</button> <button id="perfRefresh" style="padding:8px 16px;cursor:pointer">Refresh</button></div>`;
|
||||
el.innerHTML = html;
|
||||
|
||||
document.getElementById('perfReset')?.addEventListener('click', async () => {
|
||||
await fetch('/api/perf/reset', { method: 'POST' });
|
||||
if (window._apiPerf) { window._apiPerf = { calls: 0, totalMs: 0, log: [] }; }
|
||||
refresh();
|
||||
});
|
||||
document.getElementById('perfRefresh')?.addEventListener('click', refresh);
|
||||
} catch (err) {
|
||||
el.innerHTML = `<p style="color:red">Error: ${err.message}</p>`;
|
||||
}
|
||||
}
|
||||
|
||||
registerPage('perf', {
|
||||
init(app) {
|
||||
render(app);
|
||||
interval = setInterval(refresh, 5000);
|
||||
},
|
||||
destroy() {
|
||||
if (interval) { clearInterval(interval); interval = null; }
|
||||
}
|
||||
});
|
||||
})();
|
||||
@@ -1413,3 +1413,16 @@ tr[data-hops]:hover { background: rgba(59,130,246,0.1); }
|
||||
}
|
||||
.mobile-sheet-close:hover { color: var(--text); }
|
||||
.mobile-sheet-content { padding-top: 4px; }
|
||||
|
||||
/* Perf dashboard */
|
||||
.perf-card { background: var(--surface-1); border: 1px solid var(--border); border-radius: 8px; padding: 12px 20px; min-width: 120px; text-align: center; }
|
||||
.perf-num { font-size: 24px; font-weight: 800; color: var(--text); font-variant-numeric: tabular-nums; }
|
||||
.perf-label { font-size: 11px; color: var(--text-muted); text-transform: uppercase; letter-spacing: 0.5px; margin-top: 4px; }
|
||||
.perf-table { width: 100%; border-collapse: collapse; font-size: 13px; }
|
||||
.perf-table th { text-align: left; padding: 6px 10px; border-bottom: 2px solid var(--border); color: var(--text-muted); font-size: 11px; text-transform: uppercase; }
|
||||
.perf-table td { padding: 5px 10px; border-bottom: 1px solid var(--border); font-variant-numeric: tabular-nums; }
|
||||
.perf-table code { font-size: 12px; color: var(--text); }
|
||||
.perf-table .perf-slow { background: rgba(239, 68, 68, 0.08); }
|
||||
.perf-table .perf-slow td { color: #ef4444; }
|
||||
.perf-table .perf-warn { background: rgba(251, 191, 36, 0.06); }
|
||||
.perf-table .perf-warn td { color: #f59e0b; }
|
||||
|
||||
65
server.js
65
server.js
@@ -34,6 +34,71 @@ db.seed();
|
||||
const app = express();
|
||||
const server = http.createServer(app);
|
||||
|
||||
// --- Performance Instrumentation ---
|
||||
const perfStats = {
|
||||
requests: 0,
|
||||
totalMs: 0,
|
||||
endpoints: {}, // { path: { count, totalMs, maxMs, avgMs, p95: [], lastSlow } }
|
||||
slowQueries: [], // last 50 requests > 100ms
|
||||
startedAt: Date.now(),
|
||||
reset() {
|
||||
this.requests = 0; this.totalMs = 0; this.endpoints = {}; this.slowQueries = []; this.startedAt = Date.now();
|
||||
}
|
||||
};
|
||||
|
||||
app.use((req, res, next) => {
|
||||
if (!req.path.startsWith('/api/')) return next();
|
||||
const start = process.hrtime.bigint();
|
||||
const origEnd = res.end;
|
||||
res.end = function(...args) {
|
||||
const ms = Number(process.hrtime.bigint() - start) / 1e6;
|
||||
perfStats.requests++;
|
||||
perfStats.totalMs += ms;
|
||||
// Normalize parameterized routes
|
||||
const key = req.route ? req.route.path : req.path.replace(/[0-9a-f]{8,}/gi, ':id');
|
||||
if (!perfStats.endpoints[key]) perfStats.endpoints[key] = { count: 0, totalMs: 0, maxMs: 0, recent: [] };
|
||||
const ep = perfStats.endpoints[key];
|
||||
ep.count++;
|
||||
ep.totalMs += ms;
|
||||
if (ms > ep.maxMs) ep.maxMs = ms;
|
||||
ep.recent.push(ms);
|
||||
if (ep.recent.length > 100) ep.recent.shift();
|
||||
if (ms > 100) {
|
||||
perfStats.slowQueries.push({ path: req.path, ms: Math.round(ms * 10) / 10, time: new Date().toISOString(), status: res.statusCode });
|
||||
if (perfStats.slowQueries.length > 50) perfStats.slowQueries.shift();
|
||||
}
|
||||
origEnd.apply(res, args);
|
||||
};
|
||||
next();
|
||||
});
|
||||
|
||||
app.get('/api/perf', (req, res) => {
|
||||
const summary = {};
|
||||
for (const [path, ep] of Object.entries(perfStats.endpoints)) {
|
||||
const sorted = [...ep.recent].sort((a, b) => a - b);
|
||||
const p95 = sorted[Math.floor(sorted.length * 0.95)] || 0;
|
||||
const p50 = sorted[Math.floor(sorted.length * 0.5)] || 0;
|
||||
summary[path] = {
|
||||
count: ep.count,
|
||||
avgMs: Math.round(ep.totalMs / ep.count * 10) / 10,
|
||||
p50Ms: Math.round(p50 * 10) / 10,
|
||||
p95Ms: Math.round(p95 * 10) / 10,
|
||||
maxMs: Math.round(ep.maxMs * 10) / 10,
|
||||
};
|
||||
}
|
||||
// Sort by total time spent (count * avg) descending
|
||||
const sorted = Object.entries(summary).sort((a, b) => (b[1].count * b[1].avgMs) - (a[1].count * a[1].avgMs));
|
||||
res.json({
|
||||
uptime: Math.round((Date.now() - perfStats.startedAt) / 1000),
|
||||
totalRequests: perfStats.requests,
|
||||
avgMs: perfStats.requests ? Math.round(perfStats.totalMs / perfStats.requests * 10) / 10 : 0,
|
||||
endpoints: Object.fromEntries(sorted),
|
||||
slowQueries: perfStats.slowQueries.slice(-20),
|
||||
});
|
||||
});
|
||||
|
||||
app.post('/api/perf/reset', (req, res) => { perfStats.reset(); res.json({ ok: true }); });
|
||||
|
||||
// --- WebSocket ---
|
||||
const wss = new WebSocketServer({ server });
|
||||
|
||||
|
||||
Reference in New Issue
Block a user