diff --git a/public/perf.js b/public/perf.js
index 8172914e..ba34b1ad 100644
--- a/public/perf.js
+++ b/public/perf.js
@@ -13,17 +13,17 @@
const el = document.getElementById('perfContent');
if (!el) return;
try {
- const [server, client, ioStats, sqliteStats, writeSources] = await Promise.all([
+ // #1258: /api/health was awaited AFTER Promise.all, adding a full RTT
+ // (~50-200ms) on every 5s refresh. Issue it in parallel with the rest.
+ const [server, client, ioStats, sqliteStats, writeSources, health] = await Promise.all([
fetch('/api/perf').then(r => r.json()),
Promise.resolve(window.apiPerf ? window.apiPerf() : null),
fetch('/api/perf/io').then(r => r.json()).catch(() => null),
fetch('/api/perf/sqlite').then(r => r.json()).catch(() => null),
- fetch('/api/perf/write-sources').then(r => r.json()).catch(() => null)
+ fetch('/api/perf/write-sources').then(r => r.json()).catch(() => null),
+ fetch('/api/health').then(r => r.json()).catch(() => null)
]);
- // Also fetch health telemetry
- const health = await fetch('/api/health').then(r => r.json()).catch(() => null);
-
let html = '';
// Server overview
@@ -230,8 +230,15 @@
html += ``;
}
- // Server endpoints table
- const eps = Object.entries(server.endpoints);
+ // Server endpoints table — sort by total time (count * avg) DESC.
+ // #1258: header claimed "sorted by total time" but JSON map order is
+ // undefined and the frontend was not sorting. Slow endpoints could
+ // appear anywhere in the table, defeating the section's whole purpose.
+ const eps = Object.entries(server.endpoints).sort((a, b) => {
+ const ta = (a[1].count || 0) * (a[1].avgMs || 0);
+ const tb = (b[1].count || 0) * (b[1].avgMs || 0);
+ return tb - ta;
+ });
if (eps.length) {
html += '
Server Endpoints (sorted by total time)
';
html += '| Endpoint | Count | Avg | P50 | P95 | Max | Total |
';
@@ -281,10 +288,28 @@
registerPage('perf', {
init(app) {
render(app);
- interval = setInterval(refresh, 5000);
+ // #1258: don't burn CPU/network rebuilding the page (and its many cards
+ // + 3 large tables) every 5s while the tab is hidden. Pause polling on
+ // visibilitychange and resume on focus. Reduces background fetch traffic
+ // to zero and prevents a returning user from seeing a 100+ms thrash as
+ // a backlog of refreshes flush.
+ const tick = () => {
+ if (document.hidden) return;
+ refresh();
+ };
+ interval = setInterval(tick, 5000);
+ const onVis = () => {
+ if (!document.hidden) refresh();
+ };
+ document.addEventListener('visibilitychange', onVis);
+ this._onVis = onVis;
},
destroy() {
if (interval) { clearInterval(interval); interval = null; }
+ if (this._onVis) {
+ document.removeEventListener('visibilitychange', this._onVis);
+ this._onVis = null;
+ }
}
});
})();
diff --git a/test-perf-render-1258.js b/test-perf-render-1258.js
new file mode 100644
index 00000000..3f4d8597
--- /dev/null
+++ b/test-perf-render-1258.js
@@ -0,0 +1,140 @@
+/* Tests for perf.js render performance (#1258).
+ *
+ * Failure modes we gate against:
+ * 1) /api/health awaited sequentially AFTER Promise.all → extra RTT
+ * 2) setInterval keeps polling even when document is hidden → wasted work
+ * 3) Endpoints table claims "sorted by total time" but renders in map order
+ */
+'use strict';
+const vm = require('vm');
+const fs = require('fs');
+const assert = require('assert');
+
+let passed = 0, failed = 0;
+function test(name, fn) {
+ const run = (r) => { if (r && typeof r.then === 'function') return r.then(() => { passed++; console.log(` ✅ ${name}`); }, e => { failed++; console.log(` ❌ ${name}: ${e.message}`); }); passed++; console.log(` ✅ ${name}`); };
+ try { const r = fn(); if (r && typeof r.then === 'function') return r.then(() => { passed++; console.log(` ✅ ${name}`); }, e => { failed++; console.log(` ❌ ${name}: ${e.message}`); }); else { passed++; console.log(` ✅ ${name}`); } }
+ catch (e) { failed++; console.log(` ❌ ${name}: ${e.message}`); }
+}
+
+function makeSandbox(opts = {}) {
+ let capturedHtml = '';
+ const pages = {};
+ let visState = opts.hidden ? 'hidden' : 'visible';
+ const visListeners = [];
+ const ctx = {
+ window: { addEventListener: () => {}, apiPerf: null },
+ document: {
+ getElementById: (id) => {
+ if (id === 'perfContent') return { set innerHTML(v) { capturedHtml = v; } };
+ if (id === 'perfReset' || id === 'perfRefresh') return { addEventListener: () => {} };
+ return null;
+ },
+ addEventListener: (ev, fn) => { if (ev === 'visibilitychange') visListeners.push(fn); },
+ removeEventListener: () => {},
+ get visibilityState() { return visState; },
+ get hidden() { return visState === 'hidden'; },
+ },
+ console,
+ Date, Math, Array, Object, String, Number, JSON, RegExp, Error, TypeError,
+ parseInt, parseFloat, isNaN, isFinite,
+ setTimeout: (fn, ms) => setTimeout(fn, ms), clearTimeout,
+ setInterval: (fn, ms) => { return setInterval(fn, ms); }, clearInterval,
+ performance: { now: () => Date.now() },
+ Map, Set, Promise,
+ registerPage: (name, handler) => { pages[name] = handler; },
+ _apiCache: { size: 0 },
+ fetch: () => Promise.resolve({ json: () => Promise.resolve({}) }),
+ };
+ ctx.window.document = ctx.document;
+ ctx.globalThis = ctx;
+ return { ctx, pages, getHtml: () => capturedHtml,
+ setVisibility(v) { visState = v; visListeners.forEach(fn => fn()); } };
+}
+
+function loadPerf() {
+ const sb = makeSandbox();
+ const code = fs.readFileSync('public/perf.js', 'utf8');
+ vm.runInNewContext(code, sb.ctx);
+ return sb;
+}
+
+// ---------- 1) Health fetched in parallel ----------
+test('all initial fetches (including /api/health) issued in parallel', async () => {
+ const sb = loadPerf();
+ const order = [];
+ let resolveAll;
+ const gate = new Promise(r => { resolveAll = r; });
+ sb.ctx.fetch = (url) => {
+ order.push(url);
+ // Don't resolve until all 5 calls have been issued — proves they're parallel
+ return gate.then(() => ({ json: () => Promise.resolve({}) }));
+ };
+ const p = sb.pages.perf.init({ set innerHTML(v) {} });
+ // Microtask flush
+ await Promise.resolve();
+ await Promise.resolve();
+ await Promise.resolve();
+ // Before any fetch resolves, all 5 URLs must have been started
+ assert.ok(order.includes('/api/health'),
+ `expected /api/health to be issued in parallel with the others, got: ${order.join(', ')}`);
+ resolveAll();
+ await p;
+});
+
+// ---------- 2) setInterval pauses when tab hidden ----------
+test('refresh interval does not fire when document is hidden', async () => {
+ const sb = loadPerf();
+ let fetchCount = 0;
+ sb.ctx.fetch = (url) => {
+ fetchCount++;
+ return Promise.resolve({ json: () => Promise.resolve({}) });
+ };
+ // Replace setInterval with fast firing
+ let timerFn = null;
+ sb.ctx.setInterval = (fn, ms) => { timerFn = fn; return 1; };
+ sb.ctx.clearInterval = () => { timerFn = null; };
+
+ await sb.pages.perf.init({ set innerHTML(v) {} });
+ await new Promise(r => setTimeout(r, 30));
+ const baseline = fetchCount;
+ // Hide the tab, then fire the interval — should NOT issue fresh fetches
+ sb.setVisibility('hidden');
+ if (timerFn) timerFn();
+ await new Promise(r => setTimeout(r, 30));
+ assert.strictEqual(fetchCount, baseline,
+ `refresh should be suppressed while hidden; baseline=${baseline} after=${fetchCount}`);
+});
+
+// ---------- 3) Endpoints table actually sorted by total time ----------
+test('endpoints table is sorted by total time descending', async () => {
+ const sb = loadPerf();
+ // Map insertion order is preserved in JS object literals — put SLOW endpoint
+ // LAST to ensure the renderer is actively sorting, not relying on input order.
+ const perfData = {
+ totalRequests: 100, avgMs: 5, uptime: 3600, slowQueries: [],
+ endpoints: {
+ '/api/fast': { count: 1, avgMs: 1, p50Ms: 1, p95Ms: 1, maxMs: 1 },
+ '/api/mid': { count: 10, avgMs: 10, p50Ms: 10, p95Ms: 10, maxMs: 10 },
+ '/api/SLOW': { count: 100, avgMs: 100, p50Ms: 100, p95Ms: 100, maxMs: 100 },
+ },
+ };
+ sb.ctx.fetch = (url) => {
+ if (url === '/api/perf') return Promise.resolve({ json: () => Promise.resolve(perfData) });
+ return Promise.resolve({ json: () => Promise.resolve(null) });
+ };
+ await sb.pages.perf.init({ set innerHTML(v) {} });
+ await new Promise(r => setTimeout(r, 30));
+ const html = sb.getHtml();
+ const iSlow = html.indexOf('/api/SLOW');
+ const iMid = html.indexOf('/api/mid');
+ const iFast = html.indexOf('/api/fast');
+ assert.ok(iSlow > -1 && iMid > -1 && iFast > -1, 'all three endpoints must render');
+ assert.ok(iSlow < iMid && iMid < iFast,
+ `expected SLOW < mid < fast in DOM order, got SLOW=${iSlow} mid=${iMid} fast=${iFast}`);
+});
+
+setTimeout(() => {
+ console.log(`\n${passed} passed, ${failed} failed\n`);
+ process.exit(failed ? 1 : 0);
+}, 500);