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
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>
195 lines
7.2 KiB
TypeScript
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',
|
|
]);
|
|
});
|
|
});
|