test(performance): add a local pokédex benchmark and run it in CI

Measures the pokédex load against a local Supabase with a generated fixture, and
compares runs so a regression shows up as a number rather than a hunch. Wired
into test:ci and given its own job so the artifacts survive a failure.
This commit is contained in:
Josh Creek
2026-09-15 17:47:21 +01:00
parent 2766e5ea67
commit 2042e5a65a
8 changed files with 766 additions and 2 deletions
+105
View File
@@ -0,0 +1,105 @@
import { spawn } from 'node:child_process';
import { mkdtemp, mkdir, cp, rm, readFile, writeFile } from 'node:fs/promises';
import os from 'node:os';
import path from 'node:path';
const directory = await mkdtemp(path.join(os.tmpdir(), 'livingdex-grid-tests-'));
const port = process.env.PERF_PORT ?? '4185';
const env = {
...process.env,
NODE_ADAPTER: 'true',
PUBLIC_USE_LOCAL_POKEMON_SPRITE_FOLDER: 'true',
POKEDEX_PERFORMANCE: 'true',
PERF_FIXTURE_DIR: directory,
PERF_BASE_URL: `http://127.0.0.1:${port}`,
PORT: port,
HOST: '127.0.0.1'
};
function run(command, args) {
return new Promise((resolve, reject) => {
const process = spawn(command, args, { env, stdio: 'inherit' });
process.once('error', reject);
process.once('exit', (code) =>
code === 0 ? resolve() : reject(new Error(`${command} failed (${code})`))
);
});
}
let server;
let fixture = false;
try {
await run('npm', ['run', 'build-inject-manifest-node']);
fixture = true;
await run('node', ['scripts/performance/fixture.mjs']);
server = spawn('node', ['build'], { env, stdio: 'inherit' });
await new Promise((resolve, reject) => {
const timer = setInterval(async () => {
try {
if ((await fetch(env.PERF_BASE_URL)).ok) {
clearInterval(timer);
clearTimeout(timeout);
resolve();
}
} catch {}
}, 200);
const timeout = setTimeout(() => {
clearInterval(timer);
reject(new Error('Local production server did not start'));
}, 15000);
server.once('error', (error) => {
clearInterval(timer);
clearTimeout(timeout);
reject(error);
});
server.once('exit', (code) => {
clearInterval(timer);
clearTimeout(timeout);
reject(new Error(`Local server exited (${code})`));
});
});
if (process.env.PERF_BENCHMARK === 'true') {
const data = JSON.parse(await readFile(path.join(directory, 'fixture.json'), 'utf8'));
await writeFile(
path.join(directory, 'benchmark-config.json'),
JSON.stringify({
samples: 30,
revision: process.env.PERF_REVISION ?? 'local-working-tree',
databaseLabel: 'local-seeded-supabase',
clientLocation: 'local-loopback',
environment: 'local-node',
hosts: [
{
label: 'node',
url: env.PERF_BASE_URL,
storageState: path.join(directory, 'storage-state.json'),
fixtures: data.dexes.map((dex) => ({
id: dex.id,
name: dex.name,
label: dex.gameScope ? 'scoped-forms' : 'national'
}))
}
]
})
);
await run('node', [
'scripts/performance/benchmark.mjs',
path.join(directory, 'benchmark-config.json'),
path.join(directory, 'benchmark.json')
]);
await mkdir('test-results/performance', { recursive: true });
await cp(path.join(directory, 'benchmark.json'), 'test-results/performance/benchmark.json');
}
await run('node', ['scripts/performance/verify.mjs']);
await run('node', ['scripts/performance/verify-behavior.mjs']);
await mkdir('test-results/performance', { recursive: true });
for (const file of ['verification.json', 'behavior.json', 'national.png', 'scoped.png'])
await cp(path.join(directory, file), path.join('test-results/performance', file));
} finally {
server?.kill('SIGTERM');
if (fixture)
await run('node', ['scripts/performance/fixture.mjs', '--cleanup']).catch((error) => {
throw new Error(`Fixture cleanup failed; retained recovery files in ${directory}`, {
cause: error
});
});
await rm(directory, { recursive: true, force: true });
}