mirror of
https://github.com/Kpa-clawbot/meshcore-analyzer.git
synced 2026-06-27 05:11:44 +00:00
788a509e73
## Summary The version/commit badge currently rendered in the nav stats bar (alongside packet counts, node counts, and observer counts) is operator-facing diagnostic information — not something end users need visible on every page load. For most visitors, it adds visual noise without adding value. ## Changes - **perf.js**: Add a **Version** card to the Perf dashboard overview row. Shows `version` + short `commit` hash, both already available from `/api/health` (no new API surface needed). Card renders conditionally — if neither field is set it stays hidden. - **app.js**: Remove `formatVersionBadge()` and `formatEngineBadge()` helper functions (now unused); strip the badge call from `updateNavStats()` so the navbar shows only packet/node/observer counts. - **style.css**: Remove now-dead `.nav-stats .version-badge`, `.nav-stats .engine-badge`, and their link sub-rules. ## Rationale The Perf page is explicitly the right place for this information — it's already scoped to operators and developers who want to know what version is running. The navbar is a high-visibility surface shared by all users; version strings belong in a diagnostic context, not a navigation bar. Net result: navbar is cleaner for end users; operators can still find version info immediately on the Perf tab.
342 lines
13 KiB
JavaScript
342 lines
13 KiB
JavaScript
/* Tests for perf.js Go runtime vs Node event loop rendering (fixes #153) */
|
|
'use strict';
|
|
const vm = require('vm');
|
|
const fs = require('fs');
|
|
const assert = require('assert');
|
|
|
|
let passed = 0, failed = 0;
|
|
function test(name, fn) {
|
|
try { fn(); passed++; console.log(` ✅ ${name}`); }
|
|
catch (e) { failed++; console.log(` ❌ ${name}: ${e.message}`); }
|
|
}
|
|
|
|
// Minimal sandbox to run perf.js in a browser-like context
|
|
function makeSandbox() {
|
|
let capturedHtml = '';
|
|
const pages = {};
|
|
const ctx = {
|
|
window: { addEventListener: () => {}, apiPerf: null },
|
|
document: {
|
|
getElementById: (id) => {
|
|
if (id === 'perfContent') return { set innerHTML(v) { capturedHtml = v; } };
|
|
return null;
|
|
},
|
|
addEventListener: () => {},
|
|
},
|
|
console,
|
|
Date, Math, Array, Object, String, Number, JSON, RegExp, Error, TypeError,
|
|
parseInt, parseFloat, isNaN, isFinite,
|
|
setTimeout: () => {}, clearTimeout: () => {},
|
|
setInterval: () => 0, clearInterval: () => {},
|
|
performance: { now: () => Date.now() },
|
|
Map, Set, Promise,
|
|
registerPage: (name, handler) => { pages[name] = handler; },
|
|
_apiCache: null,
|
|
fetch: () => Promise.resolve({ json: () => Promise.resolve({}) }),
|
|
};
|
|
ctx.window.document = ctx.document;
|
|
ctx.globalThis = ctx;
|
|
return { ctx, pages, getHtml: () => capturedHtml };
|
|
}
|
|
|
|
// Load perf.js into sandbox
|
|
function loadPerf() {
|
|
const sb = makeSandbox();
|
|
const code = fs.readFileSync('public/perf.js', 'utf8');
|
|
vm.runInNewContext(code, sb.ctx);
|
|
return sb;
|
|
}
|
|
|
|
// Stub fetch to return controlled data
|
|
function stubFetch(sb, perfData, healthData) {
|
|
sb.ctx.fetch = (url) => {
|
|
if (url === '/api/perf') return Promise.resolve({ json: () => Promise.resolve(perfData) });
|
|
if (url === '/api/health') return Promise.resolve({ json: () => Promise.resolve(healthData) });
|
|
return Promise.resolve({ json: () => Promise.resolve({}) });
|
|
};
|
|
}
|
|
|
|
const basePerf = {
|
|
totalRequests: 100, avgMs: 5, uptime: 3600,
|
|
slowQueries: [], endpoints: {}, cache: null, packetStore: null, sqlite: null
|
|
};
|
|
|
|
const nodeHealth = {
|
|
engine: 'node',
|
|
uptimeHuman: '1h',
|
|
memory: { heapUsed: 100, heapTotal: 200, rss: 250 },
|
|
eventLoop: { p95Ms: 10, maxLagMs: 20, currentLagMs: 1 },
|
|
websocket: { clients: 3 }
|
|
};
|
|
|
|
const goRuntime = {
|
|
goroutines: 17, numGC: 31, pauseTotalMs: 2.1, lastPauseMs: 0.03,
|
|
heapAllocMB: 473, heapSysMB: 1035, heapInuseMB: 663, heapIdleMB: 371, numCPU: 2
|
|
};
|
|
|
|
const goHealth = {
|
|
engine: 'go',
|
|
uptimeHuman: '2h',
|
|
websocket: { clients: 5 }
|
|
};
|
|
|
|
console.log('\n🧪 perf.js — Go Runtime vs Node Event Loop\n');
|
|
|
|
// --- Node engine tests ---
|
|
|
|
test('Node engine shows Event Loop labels', async () => {
|
|
const sb = loadPerf();
|
|
stubFetch(sb, basePerf, nodeHealth);
|
|
await sb.pages.perf.init({ set innerHTML(v) {} });
|
|
// Wait for async refresh
|
|
await new Promise(r => setTimeout(r, 50));
|
|
const html = sb.getHtml();
|
|
assert.ok(html.includes('Event Loop p95'), 'should show Event Loop p95');
|
|
assert.ok(html.includes('EL Max Lag'), 'should show EL Max Lag');
|
|
assert.ok(html.includes('EL Current'), 'should show EL Current');
|
|
assert.ok(html.includes('System Health'), 'should show System Health heading');
|
|
});
|
|
|
|
test('Node engine does NOT show Go Runtime heading', async () => {
|
|
const sb = loadPerf();
|
|
stubFetch(sb, basePerf, nodeHealth);
|
|
await sb.pages.perf.init({ set innerHTML(v) {} });
|
|
await new Promise(r => setTimeout(r, 50));
|
|
const html = sb.getHtml();
|
|
assert.ok(!html.includes('Go Runtime'), 'should not show Go Runtime');
|
|
assert.ok(!html.includes('Goroutines'), 'should not show Goroutines');
|
|
});
|
|
|
|
test('Node engine shows memory stats', async () => {
|
|
const sb = loadPerf();
|
|
stubFetch(sb, basePerf, nodeHealth);
|
|
await sb.pages.perf.init({ set innerHTML(v) {} });
|
|
await new Promise(r => setTimeout(r, 50));
|
|
const html = sb.getHtml();
|
|
assert.ok(html.includes('Heap Used'), 'should show Heap Used');
|
|
assert.ok(html.includes('RSS'), 'should show RSS');
|
|
});
|
|
|
|
// --- Go engine tests ---
|
|
|
|
test('Go engine shows Go Runtime heading', async () => {
|
|
const sb = loadPerf();
|
|
stubFetch(sb, { ...basePerf, goRuntime }, goHealth);
|
|
await sb.pages.perf.init({ set innerHTML(v) {} });
|
|
await new Promise(r => setTimeout(r, 50));
|
|
const html = sb.getHtml();
|
|
assert.ok(html.includes('Go Runtime'), 'should show Go Runtime heading');
|
|
});
|
|
|
|
test('Go engine shows all goRuntime fields', async () => {
|
|
const sb = loadPerf();
|
|
stubFetch(sb, { ...basePerf, goRuntime }, goHealth);
|
|
await sb.pages.perf.init({ set innerHTML(v) {} });
|
|
await new Promise(r => setTimeout(r, 50));
|
|
const html = sb.getHtml();
|
|
assert.ok(html.includes('Goroutines'), 'should show Goroutines');
|
|
assert.ok(html.includes('GC Collections'), 'should show GC Collections');
|
|
assert.ok(html.includes('GC Pause Total'), 'should show GC Pause Total');
|
|
assert.ok(html.includes('Last GC Pause'), 'should show Last GC Pause');
|
|
assert.ok(html.includes('Heap Alloc'), 'should show Heap Alloc');
|
|
assert.ok(html.includes('Heap Sys'), 'should show Heap Sys');
|
|
assert.ok(html.includes('Heap Inuse'), 'should show Heap Inuse');
|
|
assert.ok(html.includes('Heap Idle'), 'should show Heap Idle');
|
|
assert.ok(html.includes('CPUs'), 'should show CPUs');
|
|
});
|
|
|
|
test('Go engine shows goRuntime values', async () => {
|
|
const sb = loadPerf();
|
|
stubFetch(sb, { ...basePerf, goRuntime }, goHealth);
|
|
await sb.pages.perf.init({ set innerHTML(v) {} });
|
|
await new Promise(r => setTimeout(r, 50));
|
|
const html = sb.getHtml();
|
|
assert.ok(html.includes('17'), 'goroutines value');
|
|
assert.ok(html.includes('31'), 'numGC value');
|
|
assert.ok(html.includes('2.1ms'), 'pauseTotalMs value');
|
|
assert.ok(html.includes('0.03ms'), 'lastPauseMs value');
|
|
assert.ok(html.includes('473MB'), 'heapAllocMB value');
|
|
assert.ok(html.includes('1035MB'), 'heapSysMB value');
|
|
assert.ok(html.includes('663MB'), 'heapInuseMB value');
|
|
assert.ok(html.includes('371MB'), 'heapIdleMB value');
|
|
});
|
|
|
|
test('Go engine does NOT show Event Loop labels', async () => {
|
|
const sb = loadPerf();
|
|
stubFetch(sb, { ...basePerf, goRuntime }, goHealth);
|
|
await sb.pages.perf.init({ set innerHTML(v) {} });
|
|
await new Promise(r => setTimeout(r, 50));
|
|
const html = sb.getHtml();
|
|
assert.ok(!html.includes('Event Loop'), 'should not show Event Loop');
|
|
assert.ok(!html.includes('EL Max Lag'), 'should not show EL Max Lag');
|
|
assert.ok(!html.includes('EL Current'), 'should not show EL Current');
|
|
});
|
|
|
|
test('Go engine still shows WS Clients', async () => {
|
|
const sb = loadPerf();
|
|
stubFetch(sb, { ...basePerf, goRuntime }, goHealth);
|
|
await sb.pages.perf.init({ set innerHTML(v) {} });
|
|
await new Promise(r => setTimeout(r, 50));
|
|
const html = sb.getHtml();
|
|
assert.ok(html.includes('WS Clients'), 'should show WS Clients');
|
|
assert.ok(html.includes('>5<'), 'should show 5 WS clients');
|
|
});
|
|
|
|
// --- GC color threshold tests ---
|
|
|
|
test('Go GC pause green when lastPauseMs <= 1', async () => {
|
|
const sb = loadPerf();
|
|
const gr = { ...goRuntime, lastPauseMs: 0.5 };
|
|
stubFetch(sb, { ...basePerf, goRuntime: gr }, goHealth);
|
|
await sb.pages.perf.init({ set innerHTML(v) {} });
|
|
await new Promise(r => setTimeout(r, 50));
|
|
const html = sb.getHtml();
|
|
assert.ok(html.includes('var(--status-green)'), 'should use green for low GC pause');
|
|
});
|
|
|
|
test('Go GC pause yellow when lastPauseMs > 1 and <= 5', async () => {
|
|
const sb = loadPerf();
|
|
const gr = { ...goRuntime, lastPauseMs: 3 };
|
|
stubFetch(sb, { ...basePerf, goRuntime: gr }, goHealth);
|
|
await sb.pages.perf.init({ set innerHTML(v) {} });
|
|
await new Promise(r => setTimeout(r, 50));
|
|
const html = sb.getHtml();
|
|
assert.ok(html.includes('var(--status-yellow)'), 'should use yellow for moderate GC pause');
|
|
});
|
|
|
|
test('Go GC pause red when lastPauseMs > 5', async () => {
|
|
const sb = loadPerf();
|
|
const gr = { ...goRuntime, lastPauseMs: 10 };
|
|
stubFetch(sb, { ...basePerf, goRuntime: gr }, goHealth);
|
|
await sb.pages.perf.init({ set innerHTML(v) {} });
|
|
await new Promise(r => setTimeout(r, 50));
|
|
const html = sb.getHtml();
|
|
assert.ok(html.includes('var(--status-red)'), 'should use red for high GC pause');
|
|
});
|
|
|
|
// --- Fallback: engine=go but no goRuntime falls back to Node UI ---
|
|
|
|
test('engine=go but missing goRuntime falls back to Node UI', async () => {
|
|
const sb = loadPerf();
|
|
const goHealthWithMemory = {
|
|
...goHealth,
|
|
memory: { heapUsed: 50, heapTotal: 100, rss: 80 },
|
|
eventLoop: { p95Ms: 5, maxLagMs: 10, currentLagMs: 1 }
|
|
};
|
|
stubFetch(sb, basePerf, goHealthWithMemory);
|
|
await sb.pages.perf.init({ set innerHTML(v) {} });
|
|
await new Promise(r => setTimeout(r, 50));
|
|
const html = sb.getHtml();
|
|
assert.ok(html.includes('Event Loop p95'), 'should fall back to Event Loop');
|
|
assert.ok(!html.includes('Go Runtime'), 'should not show Go Runtime');
|
|
});
|
|
|
|
// --- Missing engine field ---
|
|
|
|
test('Missing engine field shows Node UI', async () => {
|
|
const sb = loadPerf();
|
|
const healthNoEngine = {
|
|
uptimeHuman: '1h',
|
|
memory: { heapUsed: 100, heapTotal: 200, rss: 250 },
|
|
eventLoop: { p95Ms: 10, maxLagMs: 20, currentLagMs: 1 },
|
|
websocket: { clients: 2 }
|
|
};
|
|
stubFetch(sb, basePerf, healthNoEngine);
|
|
await sb.pages.perf.init({ set innerHTML(v) {} });
|
|
await new Promise(r => setTimeout(r, 50));
|
|
const html = sb.getHtml();
|
|
assert.ok(html.includes('Event Loop p95'), 'should show Event Loop');
|
|
assert.ok(!html.includes('Go Runtime'), 'should not show Go Runtime');
|
|
});
|
|
|
|
// --- Version card ---
|
|
|
|
test('Version card shows version tag when health.version is set', async () => {
|
|
const sb = loadPerf();
|
|
const health = { ...goHealth, version: '3.8.2', commit: 'unknown' };
|
|
stubFetch(sb, basePerf, health);
|
|
await sb.pages.perf.init({ set innerHTML(v) {} });
|
|
await new Promise(r => setTimeout(r, 50));
|
|
const html = sb.getHtml();
|
|
assert.ok(html.includes('v3.8.2'), 'should show version tag');
|
|
assert.ok(html.includes('Version'), 'should show Version label');
|
|
});
|
|
|
|
test('Version card shows commit hash when version is unknown', async () => {
|
|
const sb = loadPerf();
|
|
const health = { ...goHealth, version: 'unknown', commit: 'abc1234def' };
|
|
stubFetch(sb, basePerf, health);
|
|
await sb.pages.perf.init({ set innerHTML(v) {} });
|
|
await new Promise(r => setTimeout(r, 50));
|
|
const html = sb.getHtml();
|
|
assert.ok(html.includes('abc1234'), 'should show short commit hash');
|
|
assert.ok(!html.includes('unknown'), 'should not render literal "unknown"');
|
|
});
|
|
|
|
test('Version card shows both version and commit when both are known', async () => {
|
|
const sb = loadPerf();
|
|
const health = { ...goHealth, version: 'v3.8.2', commit: 'deadbeef1234' };
|
|
stubFetch(sb, basePerf, health);
|
|
await sb.pages.perf.init({ set innerHTML(v) {} });
|
|
await new Promise(r => setTimeout(r, 50));
|
|
const html = sb.getHtml();
|
|
assert.ok(html.includes('v3.8.2'), 'should show version');
|
|
assert.ok(html.includes('deadbee'), 'should show short commit');
|
|
});
|
|
|
|
test('Version card is absent when health has no version or commit', async () => {
|
|
const sb = loadPerf();
|
|
const health = { ...goHealth }; // no version/commit fields
|
|
stubFetch(sb, basePerf, health);
|
|
await sb.pages.perf.init({ set innerHTML(v) {} });
|
|
await new Promise(r => setTimeout(r, 50));
|
|
const html = sb.getHtml();
|
|
assert.ok(!html.includes('perf-label">Version<'), 'should not show Version card when fields absent');
|
|
});
|
|
|
|
test('Version card is absent when both version and commit are "unknown"', async () => {
|
|
const sb = loadPerf();
|
|
const health = { ...goHealth, version: 'unknown', commit: 'unknown' };
|
|
stubFetch(sb, basePerf, health);
|
|
await sb.pages.perf.init({ set innerHTML(v) {} });
|
|
await new Promise(r => setTimeout(r, 50));
|
|
const html = sb.getHtml();
|
|
assert.ok(!html.includes('perf-label">Version<'), 'should not show Version card when all values are unknown');
|
|
});
|
|
|
|
// --- renderVersionCard unit tests (direct helper) ---
|
|
|
|
test('renderVersionCard: version link points to release tag', () => {
|
|
const sb = loadPerf();
|
|
const card = sb.ctx.renderVersionCard({ version: '3.8.2', commit: 'unknown' });
|
|
assert.ok(card.includes('releases/tag/v3.8.2'), 'version should link to release tag');
|
|
assert.ok(card.includes('href='), 'should contain an anchor tag');
|
|
});
|
|
|
|
test('renderVersionCard: commit link points to commit URL', () => {
|
|
const sb = loadPerf();
|
|
const card = sb.ctx.renderVersionCard({ version: 'unknown', commit: 'deadbeef1234' });
|
|
assert.ok(card.includes('/commit/deadbeef1234'), 'commit should link to commit URL');
|
|
assert.ok(card.includes('href='), 'should contain an anchor tag');
|
|
});
|
|
|
|
test('renderVersionCard: returns empty string for null health', () => {
|
|
const sb = loadPerf();
|
|
assert.strictEqual(sb.ctx.renderVersionCard(null), '');
|
|
});
|
|
|
|
test('renderVersionCard: returns empty string when both fields are unknown', () => {
|
|
const sb = loadPerf();
|
|
assert.strictEqual(sb.ctx.renderVersionCard({ version: 'unknown', commit: 'unknown' }), '');
|
|
});
|
|
|
|
test('renderVersionCard: uses perf-num--small class (no inline style)', () => {
|
|
const sb = loadPerf();
|
|
const card = sb.ctx.renderVersionCard({ version: '1.0.0', commit: 'abc1234' });
|
|
assert.ok(card.includes('perf-num--small'), 'should use perf-num--small class');
|
|
assert.ok(!card.includes('font-size'), 'should not use inline font-size style');
|
|
});
|
|
|
|
console.log(`\n${passed} passed, ${failed} failed\n`);
|
|
process.exit(failed ? 1 : 0);
|