Files
Oxicloud/tests/load/compare.mjs
T
Edouard Vanbelle 0ef4c624c5 chore(load): start implementation of load tests
initial test from Ed's nuc:

    metric                                          pctl  baseline    current     delta    status
    -----------------------------------------------------------------------------------------------
    folder_cascade.list_depth1                      p50   0.3ms       0.3ms       -6.3%    ok
    folder_cascade.list_depth1                      p95   2.3ms       0.5ms       -75.7%   ok
    folder_cascade.list_depth1                      p99   4.7ms       2.5ms       -48.1%   ok
    folder_cascade.list_depth4                      p50   0.4ms       0.3ms       -10.0%   ok
    folder_cascade.list_depth4                      p95   0.9ms       0.6ms       -31.2%   ok
    folder_cascade.list_depth4                      p99   2.4ms       1.0ms       -56.7%   ok
    folder_cascade.list_depth8                      p50   0.3ms       0.3ms       -8.8%    ok
    folder_cascade.list_depth8                      p95   0.6ms       0.5ms       -22.2%   ok
    folder_cascade.list_depth8                      p99   1.9ms       0.5ms       -71.5%   ok
    folder_cascade.list_depth_deep                  p50   0.3ms       0.3ms       -5.0%    ok
    folder_cascade.list_depth_deep                  p95   0.6ms       0.5ms       -18.6%   ok
    folder_cascade.list_depth_deep                  p99   2.0ms       0.7ms       -67.2%   ok
    share_cascade_rebac.list_grants                 p50   0.4ms       0.3ms       -27.6%   ok
    share_cascade_rebac.list_grants                 p95   1.2ms       0.5ms       -57.0%   ok
    share_cascade_rebac.list_grants                 p99   1.7ms       1.1ms       -36.7%   ok
    share_cascade_rebac.fetch_as_grantee_depth1     p50   0.5ms       0.5ms       -11.1%   ok
    share_cascade_rebac.fetch_as_grantee_depth1     p95   1.1ms       0.7ms       -41.2%   ok
    share_cascade_rebac.fetch_as_grantee_depth1     p99   3.0ms       1.3ms       -58.3%   ok
    share_cascade_rebac.fetch_as_grantee_depth4     p50   0.5ms       0.5ms       -13.7%   ok
    share_cascade_rebac.fetch_as_grantee_depth4     p95   1.4ms       0.7ms       -50.5%   ok
    share_cascade_rebac.fetch_as_grantee_depth4     p99   2.2ms       1.1ms       -49.3%   ok
    share_cascade_rebac.fetch_as_grantee_depth8     p50   0.5ms       0.4ms       -16.0%   ok
    share_cascade_rebac.fetch_as_grantee_depth8     p95   0.9ms       0.7ms       -26.1%   ok
    share_cascade_rebac.fetch_as_grantee_depth8     p99   1.5ms       0.9ms       -40.1%   ok
    share_cascade_rebac.fetch_as_grantee_depth_deep p50   0.5ms       0.4ms       -17.3%   ok
    share_cascade_rebac.fetch_as_grantee_depth_deep p95   1.0ms       0.7ms       -31.2%   ok
    share_cascade_rebac.fetch_as_grantee_depth_deep p99   1.6ms       0.8ms       -49.4%   ok
    subject_group_nested.fetch_as_member_depth1     p50   0.5ms       0.4ms       -8.4%    ok
    subject_group_nested.fetch_as_member_depth1     p95   0.6ms       0.6ms       -10.2%   ok
    subject_group_nested.fetch_as_member_depth1     p99   1.4ms       0.6ms       -55.2%   ok
    subject_group_nested.fetch_as_member_depth4     p50   0.5ms       0.5ms       -7.9%    ok
    subject_group_nested.fetch_as_member_depth4     p95   0.6ms       0.6ms       -4.0%    ok
    subject_group_nested.fetch_as_member_depth4     p99   0.7ms       0.6ms       -2.2%    ok
    subject_group_nested.fetch_as_member_depth8     p50   0.5ms       0.4ms       -8.9%    ok
    subject_group_nested.fetch_as_member_depth8     p95   0.5ms       0.6ms       +7.1%    ok
    subject_group_nested.fetch_as_member_depth8     p99   0.6ms       0.7ms       +10.3%   ok
    subject_group_nested.fetch_as_member_depth_deep p50   0.5ms       0.4ms       -7.6%    ok
    subject_group_nested.fetch_as_member_depth_deep p95   0.6ms       0.5ms       -11.9%   ok
    subject_group_nested.fetch_as_member_depth_deep p99   0.6ms       0.7ms       +8.9%    ok
2026-06-18 09:38:25 +02:00

137 lines
4.5 KiB
JavaScript
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.
#!/usr/bin/env node
// compare.mjs — regression diff for k6 load-test runs.
//
// Reads a k6 --summary-export JSON and the committed baseline, diffs the
// p50/p95/p99 of every baseline metric against the current run, prints a
// human-readable table, and exits non-zero if any metric regresses beyond
// its tolerance.
//
// Usage:
// node compare.mjs <summary.json> <baseline.json>
//
// Exit codes:
// 0 — every metric within tolerance.
// 1 — one or more metrics regressed, or a baseline metric is absent from
// the current summary (suite drift — either a scenario was deleted
// or a tag was renamed without updating baseline.json).
import { readFileSync } from 'node:fs';
import { argv, exit } from 'node:process';
if (argv.length < 4) {
console.error('usage: compare.mjs <summary.json> <baseline.json>');
exit(2);
}
const summaryPath = argv[2];
const baselinePath = argv[3];
const summary = JSON.parse(readFileSync(summaryPath, 'utf8'));
const baseline = JSON.parse(readFileSync(baselinePath, 'utf8'));
// k6 summary metrics are keyed verbatim with the tag: e.g.
// "http_req_duration{op:smoke.login}".
const metricKeyFor = (op) => `http_req_duration{op:${op}}`;
// Map k6 percentile field names → baseline field names.
const PERCENTILES = [
{ baseline: 'p50', k6: 'med' },
{ baseline: 'p95', k6: 'p(95)' },
{ baseline: 'p99', k6: 'p(99)' },
];
const rows = [];
let anyRegression = false;
let anyMissing = false;
for (const [op, base] of Object.entries(baseline)) {
if (op.startsWith('_')) continue; // skip _comment etc.
const metric = summary.metrics?.[metricKeyFor(op)];
if (!metric) {
rows.push({ op, status: 'MISSING', detail: 'no data for this tag in current summary' });
anyMissing = true;
continue;
}
const tol = (base.tolerance_pct ?? 10) / 100;
// Noise-floor guard: at sub-millisecond percentiles, a 10% tolerance is
// smaller than a single context switch. Require the absolute delta to
// exceed `min_delta_ms` too — otherwise the swing is below the measurement
// floor and we don't flag it. Default 0.5ms is conservative for the load
// suite's typical 0.3–3ms range. Override per metric in baseline.json.
const minDelta = base.min_delta_ms ?? 0.5;
for (const p of PERCENTILES) {
const baseVal = base[p.baseline];
// k6 --summary-export puts percentile values directly on the metric, not
// inside a `.values` wrapper.
const curVal = metric[p.k6];
if (baseVal === undefined || curVal === undefined) continue;
const deltaPct = ((curVal - baseVal) / baseVal) * 100;
const deltaAbs = curVal - baseVal;
const limitPct = baseVal * (1 + tol);
// Regression only if BOTH the % rule AND the absolute floor are breached.
const regressed = curVal > limitPct && deltaAbs > minDelta;
if (regressed) anyRegression = true;
rows.push({
op,
percentile: p.baseline,
base: baseVal,
cur: curVal,
deltaPct,
regressed,
tol: base.tolerance_pct ?? 10,
});
}
}
// ── Render table ──────────────────────────────────────────────────────────
const ms = (v) => `${v.toFixed(1)}ms`;
const pct = (v) => `${v >= 0 ? '+' : ''}${v.toFixed(1)}%`;
function pad(s, n) {
if (s.length >= n) return s;
return s + ' '.repeat(n - s.length);
}
const colOp = Math.max(20, ...rows.map((r) => r.op.length));
const colP = 5;
const colVal = 11;
console.log();
console.log(
pad('metric', colOp + 1) +
pad('pctl', colP + 1) +
pad('baseline', colVal + 1) +
pad('current', colVal + 1) +
pad('delta', 9) +
'status',
);
console.log('-'.repeat(colOp + colP + colVal * 2 + 9 + 12));
for (const r of rows) {
if (r.status === 'MISSING') {
console.log(`${pad(r.op, colOp + 1)}${pad('-', colP + 1)}${pad('-', colVal + 1)}${pad('-', colVal + 1)}${pad('-', 9)}MISSING — ${r.detail}`);
continue;
}
const status = r.regressed ? `REGRESSION (tol ±${r.tol}%)` : 'ok';
console.log(
pad(r.op, colOp + 1) +
pad(r.percentile, colP + 1) +
pad(ms(r.base), colVal + 1) +
pad(ms(r.cur), colVal + 1) +
pad(pct(r.deltaPct), 9) +
status,
);
}
console.log();
if (anyRegression || anyMissing) {
if (anyRegression) console.error('compare.mjs: one or more metrics regressed beyond tolerance.');
if (anyMissing) console.error('compare.mjs: one or more baseline metrics are missing from the current summary.');
exit(1);
}
console.log('compare.mjs: all metrics within tolerance.');
exit(0);