127 lines
6.1 KiB
JavaScript
127 lines
6.1 KiB
JavaScript
// Counter-derived chart regression: pauses must not become fake 0/s samples.
|
|
// Real Cache / Transform chart functions run against simulated DOM and clock.
|
|
import assert from 'node:assert/strict';
|
|
import fs from 'node:fs';
|
|
import vm from 'node:vm';
|
|
|
|
const samplingSource = fs.readFileSync('src/admin/web/app/rate-sampling.js', 'utf8');
|
|
const panels = {
|
|
cache: {
|
|
path: 'src/admin/web/cache-panel.js', stop: ' function configure()',
|
|
counters: (hit, miss) => ({ hits: hit, misses: miss, bypasses: 0 }),
|
|
graph: 'cache-live-hit-line', rate: 'cache-live-hit-rate',
|
|
keys: ['hits', 'misses', 'bypasses'], seconds: 5,
|
|
},
|
|
transform: {
|
|
path: 'src/admin/web/transform-panel.js', stop: ' function countersFor(data)',
|
|
counters: (hit, miss) => ({ applied: hit, cache_hits: miss, skipped: 0, errors: 0 }),
|
|
graph: 'transform-live-applied-line', rate: 'transform-live-applied-rate',
|
|
keys: ['applied', 'cache_hits', 'skipped', 'errors'], seconds: 3,
|
|
},
|
|
};
|
|
|
|
function fixture(name, config) {
|
|
const source = fs.readFileSync(config.path, 'utf8');
|
|
const end = source.indexOf(config.stop);
|
|
assert(end > 0, `unable to isolate ${name} live chart`);
|
|
const documentListeners = new Map(), windowListeners = new Map(), nodes = new Map();
|
|
let now = 1000;
|
|
const node = id => {
|
|
if (!nodes.has(id)) nodes.set(id, {
|
|
value: id.endsWith('-scope') ? '*' : '', textContent: '',
|
|
attrs: {}, setAttribute(key, value) { this.attrs[key] = value; },
|
|
});
|
|
return nodes.get(id);
|
|
};
|
|
const document = {
|
|
hidden: false,
|
|
addEventListener(name, listener) { documentListeners.set(name, listener); },
|
|
};
|
|
const window = {
|
|
addEventListener(name, listener) { windowListeners.set(name, listener); },
|
|
rustProxyRefresh: { current: () => ({ overview_seconds: 2, cache_seconds: config.seconds }) },
|
|
};
|
|
const ctx = {
|
|
document, window, $: node, performance: { now: () => now },
|
|
sparklinePoints: samples => samples.join(','),
|
|
};
|
|
vm.createContext(ctx);
|
|
vm.runInContext(samplingSource, ctx);
|
|
vm.runInContext(source.slice(0, end) + '\nwindow.liveChartTest = { renderLiveChart, resetLiveChart, liveChartSamples };\n})();', ctx);
|
|
const chart = window.liveChartTest;
|
|
const line = () => node(config.graph).attrs.points;
|
|
const rate = () => node(config.rate).textContent;
|
|
const length = () => chart.liveChartSamples[config.keys[0]].length;
|
|
const sample = (stamp, hits, misses) => {
|
|
now = stamp;
|
|
chart.renderLiveChart(config.counters(hits, misses));
|
|
};
|
|
return { ctx, chart, line, rate, length, sample, document, documentListeners, windowListeners };
|
|
}
|
|
|
|
for (const [name, config] of Object.entries(panels)) {
|
|
const f = fixture(name, config);
|
|
f.sample(1000, 100, 50); // no deltas exist before the first observation
|
|
assert.equal(f.length(), 0, `${name}: first reading is a baseline, not 0/s`);
|
|
assert.equal(f.rate(), '-');
|
|
f.sample(1000 + config.seconds * 1000, 130, 65);
|
|
assert.equal(f.rate(), `${(30 / config.seconds).toFixed(1)}/s`);
|
|
assert.equal(f.length(), 1);
|
|
f.sample(1000 + config.seconds * 2000, 130, 65);
|
|
assert.equal(f.rate(), '0.0/s', `${name}: real idle interval is still zero`);
|
|
assert.equal(f.length(), 2);
|
|
|
|
const graphBefore = f.line();
|
|
const delayed = 1000 + config.seconds * 2000 + 120000;
|
|
f.sample(delayed, 500, 200);
|
|
assert.equal(f.rate(), '-', `${name}: long pause cannot be averaged into a misleading rate`);
|
|
assert.equal(f.length(), 2, `${name}: do not append fake drop`);
|
|
assert.equal(f.line(), graphBefore, `${name}: keep old chart path`);
|
|
f.sample(delayed + config.seconds * 1000, 530, 215);
|
|
assert.equal(f.rate(), `${(30 / config.seconds).toFixed(1)}/s`, `${name}: next fresh interval resumes`);
|
|
assert.equal(f.length(), 3);
|
|
|
|
f.document.hidden = true;
|
|
f.documentListeners.get('visibilitychange')?.();
|
|
f.document.hidden = false;
|
|
f.documentListeners.get('visibilitychange')?.();
|
|
const afterBackground = delayed + config.seconds * 1000 + 200;
|
|
f.sample(afterBackground, 533, 219);
|
|
assert.equal(f.length(), 3, `${name}: even a short background hide must not append a rate`);
|
|
assert.equal(f.rate(), '-');
|
|
f.sample(afterBackground + config.seconds * 1000, 533, 219);
|
|
assert.equal(f.rate(), '0.0/s', `${name}: genuine zero after resume`);
|
|
assert.equal(f.length(), 4);
|
|
|
|
f.windowListeners.get('pagehide')?.();
|
|
f.sample(afterBackground + config.seconds * 2000, 540, 220);
|
|
assert.equal(f.length(), 4, `${name}: pagehide / BFCache must rebase`);
|
|
f.sample(afterBackground + config.seconds * 3000, 550, 230);
|
|
assert.equal(f.length(), 5);
|
|
f.sample(afterBackground + config.seconds * 4000, 1, 1);
|
|
assert.equal(f.length(), 5, `${name}: counters reset must not be interpreted as 0/s`);
|
|
f.sample(afterBackground + config.seconds * 5000, 3, 3);
|
|
assert.equal(f.length(), 6, `${name}: counter reset recovery`);
|
|
}
|
|
|
|
// All remaining rate charts share this exact continuity check, including
|
|
// configured 30-second Cache refresh (not falsely treated as a pause).
|
|
{
|
|
const listeners = new Map(), document = { hidden: false, addEventListener(key, fn) { listeners.set(key, fn); } };
|
|
const context = { window: {}, document };
|
|
vm.createContext(context);
|
|
vm.runInContext(samplingSource, context);
|
|
const sample = vm.runInContext('rustProxyRateSampling', context);
|
|
let baseline = sample.difference(null, [100], 1000, 30).next;
|
|
let value = sample.difference(baseline, [130], 31000, 30);
|
|
assert.equal(value.deltas[0], 30, '30-second chart interval is valid');
|
|
baseline = value.next;
|
|
value = sample.difference(baseline, [200], 124000, 30);
|
|
assert.equal(value.deltas, null, 'large delay is a gap');
|
|
document.hidden = true;
|
|
listeners.get('visibilitychange')?.();
|
|
document.hidden = false;
|
|
assert.equal(sample.continuous(value.next, 124001, 30), false, 'hide invalidates even a short elapsed period');
|
|
}
|
|
console.log('PASS: Cache/Transform history survives hidden tabs, long pauses, BFCache and counter resets; real 0/s and configured cadence remain intact.');
|