Files
0pt/tests/analytics/benchmark-harness.test.ts
T
Claude Opus 5 f33cc8d32f
CI (SIAX Cloud) / security (push) Successful in 30s
CI (SIAX Cloud) / integration (push) Successful in 44s
Security Scan (SIAX Cloud) / security (push) Successful in 36s
CI (SIAX Cloud) / quality (push) Successful in 1m14s
CI (SIAX Cloud) / security (pull_request) Successful in 34s
CI (SIAX Cloud) / integration (pull_request) Successful in 50s
Security Scan (SIAX Cloud) / security (pull_request) Successful in 27s
CI (SIAX Cloud) / quality (pull_request) Successful in 1m7s
feat(analytics): BI-P104 — a benchmark harness that refuses to quote one number
Six dataset profiles (small SaaS, medium business, VehIQ market, large event,
wide table, geospatial), one representative question each, run on ClickHouse
26.3.10.60 and DuckDB 1.5.5 with 2 warmups discarded and 9 timed repetitions.
evidence/bi-p104/benchmark.json.

The harness's job is to stop the benchmark from lying:

  - overlapping ranges are INDISTINGUISHABLE whatever the medians say. Three
    of the six real comparisons came out that way, one of them with a 1.6x
    median gap — which is exactly the headline a noisy host manufactures.
  - every sentence carries the range, the sample size and the stability, so it
    cannot be trimmed to "4.4x faster" without visibly deleting the caveats.
  - under 5 samples nothing is comparable at all. A three-run benchmark is an
    anecdote.
  - every engine BI-P104 names appears in the evidence, and the five nothing
    measured carry a reason each. The running Trino belongs to another lane,
    so benchmarking through it would have measured their configuration.

Measured shape, warm cache, one idle host, these query shapes only: DuckDB
faster on small_saas (1.6-8.2x) and vehiq_market (2.4-13.3x), SLOWER on
large_event (0.2-0.9x, count DISTINCT over 5M rows), indistinguishable on
medium_business, wide_table and geospatial.

One finding from the harness's own tests: an engine declared available that
produced no run came out of buildEvidence() with no reason — the dangerous
case, since it reads as "we could have and chose not to say". It now gets a
generated reason. A second from the runs: DuckDB's progress bar writes to
STDOUT and corrupted the first transcript; it is disabled explicitly and the
JSON is read from a sentinel.

10 unit tests + 4 against real engines.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-09-07 04:06:43 +02:00

195 lines
7.2 KiB
TypeScript

/**
* tests/analytics/benchmark-harness.test.ts
* ==========================================
* BI-P104 — the harness's job is to stop a benchmark from lying.
*
* The tests are therefore attacks on the reporting, not on the timing:
*
* - two overlapping distributions whose MEDIANS differ 2x must come back
* `indistinguishable`, because that is the claim a noisy host manufactures;
* - the real 150x/130x pair from yesterday's budget-enforcement lane is fed
* in as data, and the harness must describe a RANGE rather than pick one;
* - a three-sample run must refuse to be compared at all;
* - every sentence `describeComparison` produces must carry the range, the
* sample size and the stability, so it cannot be quoted as a bare figure;
* - an engine that was not run must appear in the evidence with a reason.
*/
import { describe, expect, it } from 'vitest';
import {
BENCH_ENGINES,
buildEvidence,
compareDistributions,
describeComparison,
measure,
MIN_SAMPLES,
summarizeSamples,
} from '@/src/analytics/bench/benchmark-harness';
describe('BI-P104 distribution summary', () => {
it('reports the shape, and never a single number', () => {
const d = summarizeSamples([10, 11, 12, 13, 14, 15, 16, 40]);
expect(d.n).toBe(8);
expect(d.min).toBe(10);
expect(d.max).toBe(40);
expect(d.p50).toBe(13);
expect(d.spread_ratio).toBe(4);
expect(d.cv).toBeGreaterThan(0);
// Every field a reader needs to judge the p50 is present next to it.
for (const key of [
'min',
'p50',
'p95',
'p99',
'max',
'stdev',
'cv',
'iqr',
'spread_ratio',
'stability',
]) {
expect(Object.keys(d)).toContain(key);
}
});
it('calls a tight run stable and a spread run unstable', () => {
expect(summarizeSamples([100, 101, 99, 100, 102, 98, 100]).stability).toBe('stable');
expect(summarizeSamples([10, 200, 15, 400, 12, 350, 11]).stability).toBe('unstable');
});
it('refuses to characterise fewer than MIN_SAMPLES', () => {
const d = summarizeSamples([10, 11, 12]);
expect(d.n).toBe(3);
expect(d.stability).toBe('insufficient_samples');
expect(MIN_SAMPLES).toBeGreaterThan(3);
});
});
describe('BI-P104 comparison — variance beats a flattering median', () => {
it('overlapping ranges are INDISTINGUISHABLE even when medians differ 2x', () => {
const baseline = summarizeSamples([50, 60, 200, 70, 300, 55, 65, 280]);
const candidate = summarizeSamples([25, 30, 190, 35, 260, 28, 33, 240]);
const c = compareDistributions(baseline, candidate);
expect(c.p50_ratio).toBeGreaterThan(1.5); // the tempting headline
expect(c.ranges_overlap).toBe(true);
expect(c.verdict).toBe('indistinguishable'); // the honest verdict
expect(describeComparison(c)).toMatch(/INDISTINGUISHABLE/);
});
it('clean separation is reported as faster, WITH the range', () => {
const baseline = summarizeSamples([500, 520, 510, 530, 505, 515, 525]);
const candidate = summarizeSamples([50, 55, 52, 58, 51, 53, 54]);
const c = compareDistributions(baseline, candidate);
expect(c.verdict).toBe('faster');
const sentence = describeComparison(c, 'pre-aggregate');
expect(sentence).toMatch(/FASTER/);
expect(sentence).toMatch(/x to /); // a range, not a number
expect(sentence).toMatch(/n=7\/7/); // sample sizes
expect(sentence).toMatch(/stability/);
});
it('the real 150x / 130x overshoot pair is described as a range, not a figure', () => {
// Two runs of the same measurement on the same machine, from the BI-P021
// budget-enforcement lane. A harness that picks one of these is lying.
const overshoot = summarizeSamples([153, 131, 149, 128, 144, 137, 152, 126, 141]);
// Worth being precise about: by the CV test this run is STABLE (0.07). The
// problem was never instability — it is that a stable run still spans
// 126x-153x, so any single figure quoted from it is ~20% wrong. The
// harness's answer is to keep the ends attached to the middle.
expect(overshoot.stability).toBe('stable');
expect(overshoot.min).toBe(126);
expect(overshoot.p50).toBe(141);
expect(overshoot.max).toBe(153);
expect(overshoot.spread_ratio).toBeCloseTo(1.21, 2);
const sentence = describeComparison(
compareDistributions(summarizeSamples([1, 1, 1, 1, 1, 1, 1, 1, 1]), overshoot),
'budget overshoot factor',
);
expect(sentence).toMatch(/x to /); // a range survives into the prose
expect(sentence).toMatch(/n=9\/9/);
});
it('a three-sample run cannot be compared at all', () => {
const c = compareDistributions(summarizeSamples([10, 11, 12]), summarizeSamples([1, 2, 3]));
expect(c.verdict).toBe('not_comparable');
expect(describeComparison(c)).toMatch(/anecdote/);
});
it('slower is reported as slower — the harness has no favourite', () => {
const baseline = summarizeSamples([50, 51, 52, 53, 54, 55, 56]);
const candidate = summarizeSamples([500, 510, 520, 530, 540, 550, 560]);
expect(compareDistributions(baseline, candidate).verdict).toBe('slower');
});
});
describe('BI-P104 measurement', () => {
it('records warmups but excludes them from the statistics', async () => {
let call = 0;
let clock = 0;
const run = await measure({
engine: 'duckdb',
dataset: 'small_saas',
query_label: 'group by day',
warmups: 2,
repetitions: 6,
// A cold first pair (1000ms) then a warm steady state (10ms).
now: () => {
const t = clock;
clock += call <= 2 ? 1000 : 10;
call += 1;
return t;
},
run: async () => ({ rows: 42 }),
});
expect(run.warmup_ms).toHaveLength(2);
expect(run.samples_ms).toHaveLength(6);
expect(run.distribution.n).toBe(6);
expect(run.distribution.max).toBeLessThan(1000); // the cold pair is not in it
expect(run.rows_returned).toBe(42);
});
});
describe('BI-P104 evidence', () => {
it('lists every engine BI-P104 names, and says why the unrun ones were not run', () => {
const evidence = buildEvidence({
host_note: 'one idle host, warm cache',
engines: [
{ engine: 'clickhouse', available: true, version: '26.3' },
{ engine: 'duckdb', available: true, version: '1.5.5' },
{ engine: 'trino', available: false, reason: 'the running Trino belongs to another lane' },
],
runs: [
{
engine: 'clickhouse',
dataset: 'small_saas',
query_label: 'q',
warmup_ms: [1],
samples_ms: [1, 2, 3, 4, 5],
distribution: summarizeSamples([1, 2, 3, 4, 5]),
},
],
});
expect(evidence.engines.map((e) => e.engine)).toEqual([...BENCH_ENGINES]);
const notRun = evidence.not_exercised.map((e) => e.engine);
expect(notRun).toContain('superset');
expect(notRun).toContain('perspective');
expect(notRun).toContain('duckdb'); // declared available but produced no run
for (const e of evidence.not_exercised) {
expect(e.reason, e.engine).toBeTruthy();
}
expect(evidence.datasets.map((d) => d.id)).toEqual([
'small_saas',
'medium_business',
'vehiq_market',
'large_event',
'wide_table',
'geospatial',
]);
});
});