velxio/frontend/src/__tests__/solver-perf-baseline.test.ts

88 lines
3.3 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

/**
* Solver performance baseline (Phase 1d-tests G — opt-in).
*
* For the same canonical examples that `solver-determinism` locks,
* measure `solveMs` 10 times each and assert the median solve fits
* inside a pre-defined ceiling. Detects perf regressions when
* someone refactors `NetlistBuilder`, swaps a model, or upgrades
* ngspice.
*
* Gated behind `CI_PERF=1` because:
* • Numerics are deterministic, but timings are CI-machine dependent.
* A GitHub-hosted runner can be 2-3× slower than a developer
* workstation; running this in every PR would flake.
* • 10× runs × 6 examples × ~50-500 ms each ≈ 30 s extra wall time.
*
* Enable on demand:
* CI_PERF=1 npx vitest run src/__tests__/solver-perf-baseline.test.ts
*
* Update ceilings in the table below after a deliberate solver
* change. The values are deliberately generous (2-3× expected) to
* tolerate CI variance while still catching 10× regressions.
*/
import { describe, it, expect } from 'vitest';
import { analogExamples } from '../data/examples-analog';
import { digitalExamples } from '../data/examples-digital';
import { exampleToBuildNetlistInput } from '../utils/exampleToBuildNetlistInput';
import { solveInput } from './helpers/solveInput';
import type { ExampleProject } from '../data/examples';
const PERF_ENABLED = process.env.CI_PERF === '1';
/**
* Per-example median-solve-time ceilings, ms. Generous to tolerate
* CI variance; tighten after empirical baselining on a real CI box.
*/
const CEILINGS_MS: Record<string, number> = {
'an-voltage-divider': 100,
'an-rc-low-pass': 100,
'an-half-wave-rectifier': 2000, // .tran with sine generator → slower
'an-bjt-switch': 200,
'digital-and-two-switches': 100,
'digital-not-inverter': 100,
};
function findExample(id: string): ExampleProject | undefined {
return [...analogExamples, ...digitalExamples].find((ex) => ex.id === id);
}
function median(values: number[]): number {
const sorted = [...values].sort((a, b) => a - b);
const mid = Math.floor(sorted.length / 2);
if (sorted.length % 2) return sorted[mid]!;
return ((sorted[mid - 1] ?? 0) + (sorted[mid] ?? 0)) / 2;
}
describe.skipIf(!PERF_ENABLED)('Solver performance baseline (CI_PERF=1)', () => {
for (const [id, ceiling] of Object.entries(CEILINGS_MS)) {
const example = findExample(id);
if (!example) {
it.skip(`${id} (not found — ceiling list out of sync)`, () => {});
continue;
}
it(`${id} — median solve ms ≤ ${ceiling}`, { timeout: 60_000 }, async () => {
const input = exampleToBuildNetlistInput(example);
const samples: number[] = [];
for (let i = 0; i < 10; i++) {
const r = await solveInput(input);
samples.push(r.solveMs);
}
const med = median(samples);
const all = samples.map((s) => s.toFixed(1)).join(', ');
expect(
med,
`median=${med.toFixed(1)} ms exceeds ${ceiling} ms ceiling for ${id}. Samples: [${all}]`,
).toBeLessThanOrEqual(ceiling);
});
}
});
// When the suite is disabled (default), expose a trivial it() so the
// file still registers in test discovery and a CI dashboard can show
// "skipped" instead of an empty file.
describe.skipIf(PERF_ENABLED)('Solver performance baseline (disabled — set CI_PERF=1 to enable)', () => {
it('placeholder', () => {
expect(PERF_ENABLED).toBe(false);
});
});