Files
meshcore-analyzer/test-perf-go-runtime.js
Kpa-clawbot 744702ccf6 feat(perf): show Go Runtime stats instead of Event Loop on Go backend
When engine=go, the perf page now renders Go-specific runtime stats
(goroutines, GC collections, GC pause times, heap breakdown, CPUs)
instead of the misleading Node.js Event Loop metrics. Falls back to
the existing Node UI when engine is not 'go' or goRuntime data is
missing. Includes color-coded GC pause thresholds.

fixes #153

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
2026-03-27 11:49:10 -07:00

254 lines
9.6 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');
});
console.log(`\n${passed} passed, ${failed} failed\n`);
process.exit(failed ? 1 : 0);