mirror of
https://github.com/4gray/iptvnator.git
synced 2026-10-08 17:06:15 -08:00
Third step of the performance-journeys ratchet, stacked on #1693 (which is stacked on #1692; merge in order, GitHub retargets each to `master`). - New `Initial bytes ratchet` job in `.github/workflows/ci.yml` (ubuntu-latest): install, `pnpm nx build web --skip-nx-cache` (production configuration, the one users download), then `pnpm run perf:initial-bytes:check`. The job fails when `renderer.initialBytes` exceeds `tools/performance/journey-baselines.json`. - `dist/performance/` is uploaded as the `performance-journey-summary` artifact on every run, so a failing or tightenable run carries its evidence. - After review: the job first runs the new `tools/performance/check-baseline-direction.mjs`, which compares `journey-baselines.json` with the revision the change is measured against (the target branch of a pull request, `github.event.before` for a `master` push, `master` for a manual dispatch) and fails on any raised enforced limit (`value × toleranceRatio`), any widened or newly added tolerance, or any removed entry, so a PR cannot grow the payload and raise the baseline to match (lowered limits and new entries pass; a target branch without the file has nothing to weaken). Node tests cover it. - Docs: the performance-journeys contract and the validation map name the job, and the contract now states that this runner is the canonical measurer (take baseline values from its output, not from a local build). Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
222 lines
6.9 KiB
JavaScript
222 lines
6.9 KiB
JavaScript
import assert from 'node:assert/strict';
|
|
import { spawnSync } from 'node:child_process';
|
|
import { mkdtemp, rm, writeFile } from 'node:fs/promises';
|
|
import os from 'node:os';
|
|
import path from 'node:path';
|
|
import { fileURLToPath } from 'node:url';
|
|
import { after, before, test } from 'node:test';
|
|
|
|
import {
|
|
DEFAULT_HEAD_PATH,
|
|
compareBaselineDirection,
|
|
formatDirectionResult,
|
|
parseArgs,
|
|
} from './check-baseline-direction.mjs';
|
|
|
|
const scriptPath = fileURLToPath(
|
|
new URL('./check-baseline-direction.mjs', import.meta.url)
|
|
);
|
|
|
|
const file = (initialBytes, extra = {}) => ({
|
|
version: 1,
|
|
journeys: {
|
|
launch: {
|
|
'renderer.initialBytes': { value: initialBytes, unit: 'bytes' },
|
|
...extra,
|
|
},
|
|
},
|
|
});
|
|
|
|
let workDir;
|
|
before(async () => {
|
|
workDir = await mkdtemp(
|
|
path.join(os.tmpdir(), 'check-baseline-direction-')
|
|
);
|
|
});
|
|
after(async () => {
|
|
await rm(workDir, { recursive: true, force: true });
|
|
});
|
|
|
|
test('an unchanged or lowered baseline passes', () => {
|
|
const same = compareBaselineDirection({ base: file(100), head: file(100) });
|
|
assert.deepEqual(same.failures, []);
|
|
assert.deepEqual(same.unchanged, ['launch/renderer.initialBytes']);
|
|
|
|
const lowered = compareBaselineDirection({
|
|
base: file(100),
|
|
head: file(90),
|
|
});
|
|
assert.deepEqual(lowered.failures, []);
|
|
assert.match(lowered.lowered[0], /100 -> 90 bytes/);
|
|
});
|
|
|
|
test('a raised baseline fails with both values', () => {
|
|
const result = compareBaselineDirection({
|
|
base: file(100),
|
|
head: file(101),
|
|
});
|
|
assert.equal(result.failures.length, 1);
|
|
assert.match(
|
|
result.failures[0],
|
|
/raised from 100 to 101 bytes\. Baselines only move down/
|
|
);
|
|
});
|
|
|
|
test('a removed baseline fails; a new one is reported and passes', () => {
|
|
const removed = compareBaselineDirection({
|
|
base: file(100, { cdTicks: { value: 5 } }),
|
|
head: file(100),
|
|
});
|
|
assert.equal(removed.failures.length, 1);
|
|
assert.match(
|
|
removed.failures[0],
|
|
/launch\/cdTicks: baseline 5 was removed/
|
|
);
|
|
|
|
const added = compareBaselineDirection({
|
|
base: file(100),
|
|
head: file(100, { cdTicks: { value: 5 } }),
|
|
});
|
|
assert.deepEqual(added.failures, []);
|
|
assert.deepEqual(added.added, ['launch/cdTicks']);
|
|
});
|
|
|
|
test('a widened or newly added tolerance fails even when the value went down', () => {
|
|
const widened = compareBaselineDirection({
|
|
base: file(100, {
|
|
firstCardMs: { value: 100, unit: 'ms', toleranceRatio: 1.1 },
|
|
}),
|
|
head: file(100, {
|
|
firstCardMs: { value: 99, unit: 'ms', toleranceRatio: 2 },
|
|
}),
|
|
});
|
|
assert.equal(widened.failures.length, 1);
|
|
assert.match(
|
|
widened.failures[0],
|
|
/firstCardMs: toleranceRatio widened from 1\.1 to 2/
|
|
);
|
|
|
|
const added = compareBaselineDirection({
|
|
base: file(100),
|
|
head: file(100, undefined) && {
|
|
version: 1,
|
|
journeys: {
|
|
launch: {
|
|
'renderer.initialBytes': {
|
|
value: 100,
|
|
unit: 'bytes',
|
|
toleranceRatio: 1.25,
|
|
},
|
|
},
|
|
},
|
|
},
|
|
});
|
|
assert.equal(added.failures.length, 1);
|
|
assert.match(added.failures[0], /toleranceRatio widened from 1 to 1\.25/);
|
|
});
|
|
|
|
test('the enforced limit is what is compared for wall-clock entries', () => {
|
|
const narrowed = compareBaselineDirection({
|
|
base: file(100, {
|
|
firstCardMs: { value: 100, unit: 'ms', toleranceRatio: 1.25 },
|
|
}),
|
|
head: file(100, {
|
|
firstCardMs: { value: 110, unit: 'ms', toleranceRatio: 1 },
|
|
}),
|
|
});
|
|
assert.deepEqual(narrowed.failures, []);
|
|
assert.match(narrowed.lowered[0], /firstCardMs: 125 -> 110 ms/);
|
|
|
|
const raisedLimit = compareBaselineDirection({
|
|
base: file(100, {
|
|
firstCardMs: { value: 100, unit: 'ms', toleranceRatio: 1.25 },
|
|
}),
|
|
head: file(100, {
|
|
firstCardMs: { value: 130, unit: 'ms', toleranceRatio: 1.25 },
|
|
}),
|
|
});
|
|
assert.equal(raisedLimit.failures.length, 1);
|
|
assert.match(
|
|
raisedLimit.failures[0],
|
|
/firstCardMs: baseline raised from 125 to 162\.5 ms/
|
|
);
|
|
});
|
|
|
|
test('an empty target-branch file cannot be weakened', () => {
|
|
const result = compareBaselineDirection({
|
|
base: { journeys: {} },
|
|
head: file(100),
|
|
});
|
|
assert.deepEqual(result.failures, []);
|
|
assert.deepEqual(result.added, ['launch/renderer.initialBytes']);
|
|
});
|
|
|
|
test('formats the outcome', () => {
|
|
assert.match(
|
|
formatDirectionResult(
|
|
compareBaselineDirection({ base: file(100), head: file(90) })
|
|
),
|
|
/^lowered {2}launch\/renderer\.initialBytes: 100 -> 90 bytes\.\nBaseline direction OK: 0 unchanged, 1 lowered, 0 added\.$/
|
|
);
|
|
assert.match(
|
|
formatDirectionResult(
|
|
compareBaselineDirection({ base: file(100), head: file(200) })
|
|
),
|
|
/^FAIL {5}launch.*\nBaseline direction check failed: 1 entries raised or removed\.$/
|
|
);
|
|
});
|
|
|
|
test('parses arguments and requires --base', () => {
|
|
assert.deepEqual(parseArgs(['--base', 'b.json']), {
|
|
base: 'b.json',
|
|
head: DEFAULT_HEAD_PATH,
|
|
});
|
|
assert.deepEqual(parseArgs(['--', '--base=b.json', '--head=h.json']), {
|
|
base: 'b.json',
|
|
head: 'h.json',
|
|
});
|
|
assert.throws(
|
|
() => parseArgs([]),
|
|
/--base <target-branch-journey-baselines\.json> is required/
|
|
);
|
|
assert.throws(() => parseArgs(['--base']), /Missing value for --base/);
|
|
assert.throws(
|
|
() => parseArgs(['--base', 'b', '--force']),
|
|
/Unknown argument: --force/
|
|
);
|
|
});
|
|
|
|
async function runCli(base, head) {
|
|
const basePath =
|
|
base === null
|
|
? path.join(workDir, 'absent.json')
|
|
: path.join(workDir, `base-${Math.random()}.json`);
|
|
const headPath = path.join(workDir, `head-${Math.random()}.json`);
|
|
if (base !== null) await writeFile(basePath, JSON.stringify(base));
|
|
await writeFile(headPath, JSON.stringify(head));
|
|
return spawnSync(
|
|
process.execPath,
|
|
[scriptPath, '--base', basePath, '--head', headPath],
|
|
{
|
|
encoding: 'utf8',
|
|
}
|
|
);
|
|
}
|
|
|
|
test('CLI exits 0 for a lowered baseline, 1 for a raised one, 0 when the target branch has no file', async () => {
|
|
const lowered = await runCli(file(100), file(90));
|
|
assert.equal(lowered.status, 0, lowered.stderr);
|
|
assert.match(lowered.stdout, /Baseline direction OK/);
|
|
|
|
const raised = await runCli(file(100), file(101));
|
|
assert.equal(raised.status, 1);
|
|
assert.match(
|
|
raised.stderr,
|
|
/FAIL {5}launch\/renderer\.initialBytes: baseline raised/
|
|
);
|
|
|
|
const noBase = await runCli(null, file(100));
|
|
assert.equal(noBase.status, 0, noBase.stderr);
|
|
assert.match(noBase.stdout, /nothing to weaken/);
|
|
});
|