mirror of
https://github.com/Kpa-clawbot/meshcore-analyzer.git
synced 2026-10-15 03:39:53 +00:00
## Summary The nav bar's packet / node / observer counts were refreshed three ways at once: a 15 s timer, every batch of live WebSocket packets (`debouncedOnWS`, 250 ms), and the `/stats` client cache being cleared every 5 s while packets arrive. None of them checked whether the tab was visible. So every open tab, including hidden ones, asked `/api/stats` every few seconds for as long as it stayed open. On one production instance's nginx log for a single day, `/api/stats` was **135,654 of 187,411** CoreScope API requests (72%), about 20,000 a day for each browser that kept a tab open. Each response is cheap thanks to the 10 s server-side cache (#1910), so this is about request volume, bandwidth and log noise, not server CPU. This PR adds `createVisibleThrottle()` to `app.js` and routes all three triggers through one throttled refresh: - at most one `/api/stats` request per 15 s from a visible tab (the old timer's own period); - none from a hidden tab; - one immediate refresh when a hidden tab becomes visible again. No existing issue covers this. ## Performance Measured with headless Chromium, counting `/api/stats` requests from a real tab: 60 s visible, then 60 s with the page hidden (`document.hidden` and a `visibilitychange` event), then back to visible. | `/api/stats` requests | production (current release) | staging, `master` | staging, this PR | |---|---|---|---| | visible tab, per minute | 9 | 4 | 4 (at +10, +25, +40, +55 s) | | hidden tab, per minute | 6 | 5 | **0** | | on return to visible | n/a | n/a | 1, immediately | The same check confirmed the nav bar still renders its counts (`… pkts · … nodes · … obs`). ## Tests - Red commit `2c81a3e4`: `tests/unit/test-nav-stats-throttle.js` loads the real `public/app.js` in a `vm` context. `createVisibleThrottle` lands as a pass-through stub, so the test fails on its first assertion (40 refreshes where 1 is expected). - Green commit `f0ba92ab`: the throttle, and the nav stats timer, WS hook and a new `visibilitychange` listener calling one throttled trigger. All 6 cases pass: one refresh per burst, again after the interval, at most 241 an hour, none while hidden, one on return, and a source check that `updateNavStats` is wrapped and not called by the timer directly. - Added to `test-all.sh` in sorted order; `test-test-inventory.js` passes. - Frontend suites: 194 of 195 pass. The one failure, `test-issue-1956-release-routing.js`, needs CI's `GITHUB_REF_NAME` and fails the same way on `master` locally. ## Review notes - `home.js` keeps its own one-shot `/stats` load on the home page; only the nav bar's polling changed. - A tab opened in the background loads its counts when it is first shown. - The 15 s interval is a constant at the call site. Could be exposed in the customizer later (AGENTS.md rule 8); not added here. - No Playwright E2E added: the change is timing behaviour, covered by the unit test against the real file and by the browser measurement above. Happy to add an E2E if wanted. ## SonarLint SonarQube Community rules (the SonarLint engine) were run on the files this PR touches, and only issues on lines it adds are listed. - **Fixed** in `9eb94ea1`: `node:` specifiers (S7772). - **Kept on purpose:** - S7773 wants `Number.isFinite`/`Number.parseInt` for the `isFinite` and `parseInt` handed to the `vm` sandbox, but `app.js` calls the global functions, so those are the ones it needs. - S1523 flags `vm.runInContext`, which is how the frontend unit tests load the repo's own `public/app.js`. ## Checked against the repo rules - Rebased onto `master` (`673442cc`). Each red commit was re-run and still fails on its assertions; each green commit passes (TDD.md). - `make test`: all 16 modules pass. Frontend suites: all pass except `test-issue-1956-release-routing.js`, which needs CI's `GITHUB_REF_NAME` and fails the same way on `master` locally (#2160 addresses it). - No private data: test fixtures and examples use the codes the existing tests use (SJC/SFO/LAX, `#be`-style scopes). - No new `map[string]interface{}`, no hard-coded colours. `git add` was explicit, and there is one logical change per commit. 🤖 Generated with [Claude Code](https://claude.com/claude-code) https://claude.ai/code/session_01KaR4xu8gVykQBkH8yG8m6H --------- Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
92 lines
3.7 KiB
JavaScript
92 lines
3.7 KiB
JavaScript
/* test-nav-stats-throttle.js
|
|
*
|
|
* The nav bar's packet/node/observer counts were refreshed on a 15 s timer AND
|
|
* after every batch of live WebSocket packets, while the /stats client cache
|
|
* was cleared every 5 s during traffic. Every open tab, visible or not,
|
|
* therefore asked /api/stats about every 4-5 s. On one production instance
|
|
* that was 135,654 of 187,411 API requests in a day, about 20,000 per browser.
|
|
*
|
|
* createVisibleThrottle() gates those refreshes: at most one per interval,
|
|
* none while the tab is hidden, and one on return to a visible tab once the
|
|
* interval has passed. This test loads the real public/app.js in a vm context.
|
|
*/
|
|
'use strict';
|
|
const vm = require('node:vm');
|
|
const fs = require('node:fs');
|
|
const assert = require('node:assert');
|
|
|
|
function loadApp() {
|
|
const ctx = {
|
|
console, Date, Math, Promise, Error, isFinite, parseInt, JSON, Map, Set,
|
|
performance: { now: () => 0 },
|
|
window: {},
|
|
document: { readyState: 'complete', hidden: false, body: { appendChild: () => {} }, createElement: () => ({ style: {} }) },
|
|
fetch: async () => ({ ok: true, status: 200, headers: { get: () => null }, json: async () => ({}) }),
|
|
setTimeout: () => 0, clearTimeout: () => {}, setInterval: () => 0, clearInterval: () => {},
|
|
};
|
|
vm.createContext(ctx);
|
|
try { vm.runInContext(fs.readFileSync('public/app.js', 'utf8'), ctx); } catch (e) { /* DOM-only code past the helpers */ }
|
|
assert.strictEqual(typeof ctx.createVisibleThrottle, 'function', 'createVisibleThrottle must be defined at top level');
|
|
return ctx;
|
|
}
|
|
|
|
let passed = 0;
|
|
function test(name, fn) { fn(); passed++; console.log(' ✅ ' + name); }
|
|
|
|
const ctx = loadApp();
|
|
|
|
function harness(minIntervalMs) {
|
|
const state = { t: 1000000, hidden: false, calls: 0 };
|
|
const trigger = ctx.createVisibleThrottle(() => { state.calls++; }, {
|
|
minIntervalMs, now: () => state.t, isHidden: () => state.hidden,
|
|
});
|
|
return { state, trigger };
|
|
}
|
|
|
|
console.log('\n=== createVisibleThrottle (nav stats) ===');
|
|
|
|
test('a burst of triggers within the interval runs the refresh once', () => {
|
|
const { state, trigger } = harness(15000);
|
|
for (let i = 0; i < 40; i++) { trigger(); state.t += 250; } // 10 s of WS batches
|
|
assert.strictEqual(state.calls, 1);
|
|
});
|
|
|
|
test('runs again once the interval has passed', () => {
|
|
const { state, trigger } = harness(15000);
|
|
trigger();
|
|
state.t += 15000;
|
|
trigger();
|
|
assert.strictEqual(state.calls, 2);
|
|
});
|
|
|
|
test('an hour of WS batches and timer ticks in a visible tab costs at most 241 refreshes', () => {
|
|
const { state, trigger } = harness(15000);
|
|
for (let s = 0; s < 3600 * 4; s++) { trigger(); state.t += 250; }
|
|
assert.ok(state.calls <= 241, 'got ' + state.calls);
|
|
});
|
|
|
|
test('never runs while the tab is hidden', () => {
|
|
const { state, trigger } = harness(15000);
|
|
state.hidden = true;
|
|
for (let s = 0; s < 3600; s++) { trigger(); state.t += 1000; }
|
|
assert.strictEqual(state.calls, 0);
|
|
});
|
|
|
|
test('runs on return to a visible tab once the interval has passed', () => {
|
|
const { state, trigger } = harness(15000);
|
|
trigger(); // visible: 1
|
|
state.hidden = true;
|
|
state.t += 60000; trigger(); // hidden: skipped
|
|
state.hidden = false;
|
|
trigger(); // back: interval long passed, runs
|
|
assert.strictEqual(state.calls, 2);
|
|
});
|
|
|
|
test('app.js routes the nav stats refresh through the throttle', () => {
|
|
const src = fs.readFileSync('public/app.js', 'utf8');
|
|
assert.ok(/createVisibleThrottle\(\s*updateNavStats/.test(src), 'updateNavStats should be wrapped by createVisibleThrottle');
|
|
assert.ok(!/setInterval\(\s*updateNavStats\s*,/.test(src), 'the timer should call the throttled trigger, not updateNavStats directly');
|
|
});
|
|
|
|
console.log('\n' + passed + ' passed');
|