@kiwa-test/perf-harness 0.1.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md ADDED
@@ -0,0 +1,55 @@
1
+ # @kiwa-test/perf-harness
2
+
3
+ Generic performance harness for kiwa packages and dogfood apps. It measures p50/p95/p99 latency, persists baselines, detects regressions, and feeds perf data into `@kiwa-test/quality-metrics`.
4
+
5
+ ## Single measure
6
+
7
+ ```ts
8
+ import { measure } from '@kiwa-test/perf-harness';
9
+
10
+ const result = await measure({
11
+ name: 'reply',
12
+ iterations: 100,
13
+ warmup: 5,
14
+ fn: async () => {
15
+ await adapter.reply({ userMessage: 'Say hi.' });
16
+ },
17
+ });
18
+ ```
19
+
20
+ ## Baseline compare
21
+
22
+ ```ts
23
+ import {
24
+ defaultBaselinePath,
25
+ detectRegression,
26
+ loadBaseline,
27
+ measure,
28
+ saveBaseline,
29
+ } from '@kiwa-test/perf-harness';
30
+
31
+ const path = defaultBaselinePath('dogfood-anthropic-chatbot');
32
+ const current = await measure({ name: 'reply', iterations: 100, warmup: 5, fn });
33
+ const baseline = await loadBaseline(path);
34
+
35
+ if (baseline) {
36
+ const regression = detectRegression({ current, baseline, threshold: 0.2 });
37
+ console.log(regression.verdict);
38
+ }
39
+
40
+ await saveBaseline(path, current);
41
+ ```
42
+
43
+ ## Release-gate integration
44
+
45
+ ```ts
46
+ import { evaluatePerfGate, measure } from '@kiwa-test/perf-harness';
47
+
48
+ const result = await measure({ name: 'evaluateReleaseGate', iterations: 100, warmup: 5, fn });
49
+ const gate = evaluatePerfGate({
50
+ result,
51
+ thresholds: { p95Ms: 100 },
52
+ });
53
+
54
+ console.log(gate.verdict.passed, gate.breaches);
55
+ ```
package/dist/index.cjs ADDED
@@ -0,0 +1,387 @@
1
+ "use strict";
2
+ var __defProp = Object.defineProperty;
3
+ var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
4
+ var __getOwnPropNames = Object.getOwnPropertyNames;
5
+ var __hasOwnProp = Object.prototype.hasOwnProperty;
6
+ var __export = (target, all) => {
7
+ for (var name in all)
8
+ __defProp(target, name, { get: all[name], enumerable: true });
9
+ };
10
+ var __copyProps = (to, from, except, desc) => {
11
+ if (from && typeof from === "object" || typeof from === "function") {
12
+ for (let key of __getOwnPropNames(from))
13
+ if (!__hasOwnProp.call(to, key) && key !== except)
14
+ __defProp(to, key, { get: () => from[key], enumerable: !(desc = __getOwnPropDesc(from, key)) || desc.enumerable });
15
+ }
16
+ return to;
17
+ };
18
+ var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: true }), mod);
19
+
20
+ // src/index.ts
21
+ var index_exports = {};
22
+ __export(index_exports, {
23
+ buildMeasureResult: () => buildMeasureResult,
24
+ defaultBaselinePath: () => defaultBaselinePath,
25
+ detectRegression: () => detectRegression,
26
+ emitPerfReport: () => emitPerfReport,
27
+ evaluatePerfGate: () => evaluatePerfGate,
28
+ loadBaseline: () => loadBaseline,
29
+ measure: () => measure,
30
+ saveBaseline: () => saveBaseline
31
+ });
32
+ module.exports = __toCommonJS(index_exports);
33
+
34
+ // src/measure.ts
35
+ async function measure(input) {
36
+ const warmup = input.warmup ?? 0;
37
+ if (input.iterations < 1) {
38
+ throw new Error(`measure: iterations must be >= 1, got ${input.iterations}`);
39
+ }
40
+ if (warmup < 0) {
41
+ throw new Error(`measure: warmup must be >= 0, got ${warmup}`);
42
+ }
43
+ for (let index = 0; index < warmup; index += 1) {
44
+ await input.fn();
45
+ }
46
+ const samples = [];
47
+ for (let index = 0; index < input.iterations; index += 1) {
48
+ const start = process.hrtime.bigint();
49
+ await input.fn();
50
+ const end = process.hrtime.bigint();
51
+ samples.push(Number(end - start) / 1e6);
52
+ }
53
+ return buildMeasureResult(input.name, input.iterations, warmup, samples);
54
+ }
55
+ function buildMeasureResult(name, iterations, warmup, samples) {
56
+ const sorted = [...samples].sort((left, right) => left - right);
57
+ const totalMs = samples.reduce((sum, sample) => sum + sample, 0);
58
+ const mean = totalMs / samples.length;
59
+ const variance = samples.length > 1 ? samples.reduce((sum, sample) => {
60
+ const delta = sample - mean;
61
+ return sum + delta * delta;
62
+ }, 0) / (samples.length - 1) : 0;
63
+ return {
64
+ name,
65
+ iterations,
66
+ warmup,
67
+ samples,
68
+ p50: percentile(sorted, 0.5),
69
+ p95: percentile(sorted, 0.95),
70
+ p99: percentile(sorted, 0.99),
71
+ mean,
72
+ stdev: Math.sqrt(variance),
73
+ minMs: sorted[0] ?? 0,
74
+ maxMs: sorted[sorted.length - 1] ?? 0,
75
+ totalMs
76
+ };
77
+ }
78
+ function percentile(sorted, ratio) {
79
+ if (sorted.length === 0) return 0;
80
+ const rank = Math.max(0, Math.ceil(sorted.length * ratio) - 1);
81
+ return sorted[rank] ?? sorted[sorted.length - 1] ?? 0;
82
+ }
83
+
84
+ // src/regression.ts
85
+ function detectRegression(input) {
86
+ const threshold = input.threshold ?? 0.2;
87
+ const current = normalize(input.current);
88
+ const baseline = normalize(input.baseline);
89
+ const deltaPct = baseline.p95 === 0 ? current.p95 === 0 ? 0 : Number.POSITIVE_INFINITY : (current.p95 - baseline.p95) / baseline.p95;
90
+ const welchT = welchTScore(current.samples, baseline.samples);
91
+ const significant = Math.abs(welchT) > 2;
92
+ let verdict = "stable";
93
+ if (significant && deltaPct >= threshold) {
94
+ verdict = "regressed";
95
+ } else if (significant && deltaPct <= -threshold) {
96
+ verdict = "improved";
97
+ }
98
+ return {
99
+ regressed: verdict === "regressed",
100
+ deltaPct,
101
+ welchT,
102
+ significant,
103
+ verdict
104
+ };
105
+ }
106
+ function normalize(result) {
107
+ if (result.samples.length === 0) {
108
+ return result;
109
+ }
110
+ return buildMeasureResult(result.name, result.iterations, result.warmup, result.samples);
111
+ }
112
+ function welchTScore(current, baseline) {
113
+ if (current.length < 2 || baseline.length < 2) {
114
+ return 0;
115
+ }
116
+ const currentStats = sampleStats(current);
117
+ const baselineStats = sampleStats(baseline);
118
+ const numerator = currentStats.mean - baselineStats.mean;
119
+ const denominator = Math.sqrt(
120
+ currentStats.variance / current.length + baselineStats.variance / baseline.length
121
+ );
122
+ if (!Number.isFinite(denominator) || denominator === 0) {
123
+ return 0;
124
+ }
125
+ return numerator / denominator;
126
+ }
127
+ function sampleStats(samples) {
128
+ const mean = samples.reduce((sum, sample) => sum + sample, 0) / samples.length;
129
+ const variance = samples.reduce((sum, sample) => {
130
+ const delta = sample - mean;
131
+ return sum + delta * delta;
132
+ }, 0) / (samples.length - 1);
133
+ return { mean, variance };
134
+ }
135
+
136
+ // src/baseline.ts
137
+ var import_promises = require("fs/promises");
138
+ var import_node_path = require("path");
139
+ async function loadBaseline(path) {
140
+ try {
141
+ const body = await (0, import_promises.readFile)(path, "utf8");
142
+ return JSON.parse(body);
143
+ } catch (error) {
144
+ if (isMissingFile(error)) {
145
+ return null;
146
+ }
147
+ throw error;
148
+ }
149
+ }
150
+ async function saveBaseline(path, result) {
151
+ await (0, import_promises.mkdir)((0, import_node_path.dirname)(path), { recursive: true });
152
+ await (0, import_promises.writeFile)(path, `${JSON.stringify(result, null, 2)}
153
+ `, "utf8");
154
+ }
155
+ function defaultBaselinePath(moduleName) {
156
+ return `${process.cwd()}/.perf-baseline/${moduleName}.json`;
157
+ }
158
+ function isMissingFile(error) {
159
+ return typeof error === "object" && error !== null && "code" in error && error.code === "ENOENT";
160
+ }
161
+
162
+ // src/gate.ts
163
+ var import_quality_metrics = require("@kiwa-test/quality-metrics");
164
+ function evaluatePerfGate(input) {
165
+ const thresholds = input.thresholds ?? {};
166
+ const enabledAxes = countThresholds(thresholds);
167
+ const report = buildReport(input, thresholds, enabledAxes === 0);
168
+ const relaxedVerdict = (0, import_quality_metrics.evaluateReleaseGate)(report, {
169
+ coverageLine: 0,
170
+ coverageBranch: 0,
171
+ coverageFunction: 0,
172
+ fidelityRatio: 0,
173
+ perfP95Ms: thresholds.p95Ms ?? Number.POSITIVE_INFINITY,
174
+ mutationKillRate: 0,
175
+ behaviorTests: 0
176
+ });
177
+ if (enabledAxes === 0) {
178
+ return {
179
+ report,
180
+ verdict: { passed: true, blockers: [], axesEvaluated: 0 },
181
+ breaches: []
182
+ };
183
+ }
184
+ const breaches = [];
185
+ if (thresholds.p95Ms !== void 0 && relaxedVerdict.blockers.some((blocker) => blocker.axis === "perf.p95Ms")) {
186
+ breaches.push({
187
+ axis: "perf.p95Ms",
188
+ threshold: thresholds.p95Ms,
189
+ actual: input.result.p95,
190
+ op: "<="
191
+ });
192
+ }
193
+ appendOptionalBreach(
194
+ breaches,
195
+ "cost.perRequestUsd",
196
+ "<=",
197
+ thresholds.costUsd,
198
+ input.metrics?.costUsd,
199
+ Number.POSITIVE_INFINITY
200
+ );
201
+ appendOptionalBreach(
202
+ breaches,
203
+ "token.totalTokens",
204
+ "<=",
205
+ thresholds.tokens,
206
+ input.metrics?.tokens,
207
+ Number.POSITIVE_INFINITY
208
+ );
209
+ appendOptionalBreach(
210
+ breaches,
211
+ "accuracy.score",
212
+ ">=",
213
+ thresholds.accuracy,
214
+ input.metrics?.accuracy,
215
+ Number.NEGATIVE_INFINITY
216
+ );
217
+ const verdict = {
218
+ passed: breaches.length === 0,
219
+ blockers: breaches,
220
+ axesEvaluated: enabledAxes
221
+ };
222
+ return { report, verdict, breaches };
223
+ }
224
+ function buildReport(input, thresholds, empty) {
225
+ const perf = empty || thresholds.p95Ms === void 0 ? { p50Ms: 0, p95Ms: 0, p99Ms: 0, samples: 0 } : {
226
+ p50Ms: input.result.p50,
227
+ p95Ms: input.result.p95,
228
+ p99Ms: input.result.p99,
229
+ samples: input.result.samples.length
230
+ };
231
+ const report = {
232
+ provider: `@kiwa-test/perf-harness/${input.result.name}`,
233
+ version: "0.1.0",
234
+ reportedAt: (/* @__PURE__ */ new Date()).toISOString(),
235
+ coverage: { line: 0, branch: 0, function: 0 },
236
+ testCount: { behavior: 0, integration: 0, e2e: 0, total: 0 },
237
+ fidelity: { mockCoveredMethods: 0, realTotalMethods: 0, ratio: 0 },
238
+ perf,
239
+ mutation: { mutations: 0, killed: 0, survived: 0, killRate: 0 }
240
+ };
241
+ if (!empty && thresholds.costUsd !== void 0) {
242
+ const actual = input.metrics?.costUsd ?? 0;
243
+ report.cost = { perRequestUsd: actual, totalUsd: actual, requests: 1 };
244
+ }
245
+ if (!empty && thresholds.tokens !== void 0) {
246
+ const actual = input.metrics?.tokens ?? 0;
247
+ report.token = {
248
+ promptTokens: actual,
249
+ completionTokens: 0,
250
+ totalTokens: actual,
251
+ requests: 1
252
+ };
253
+ }
254
+ if (!empty && thresholds.accuracy !== void 0) {
255
+ report.accuracy = {
256
+ score: input.metrics?.accuracy ?? 0,
257
+ samples: 1,
258
+ method: "provided"
259
+ };
260
+ }
261
+ return report;
262
+ }
263
+ function appendOptionalBreach(breaches, axis, op, threshold, actual, missingFallback) {
264
+ if (threshold === void 0) {
265
+ return;
266
+ }
267
+ const resolvedActual = actual ?? missingFallback;
268
+ const passed = op === "<=" ? resolvedActual <= threshold : resolvedActual >= threshold;
269
+ if (!passed) {
270
+ breaches.push({
271
+ axis,
272
+ threshold,
273
+ actual: resolvedActual,
274
+ op
275
+ });
276
+ }
277
+ }
278
+ function countThresholds(thresholds) {
279
+ return [
280
+ thresholds.p95Ms,
281
+ thresholds.costUsd,
282
+ thresholds.tokens,
283
+ thresholds.accuracy
284
+ ].filter((value) => value !== void 0).length;
285
+ }
286
+
287
+ // src/report.ts
288
+ function emitPerfReport(result, opts = {}) {
289
+ const lines = [];
290
+ lines.push(`# Perf Report \u2014 ${result.name}`);
291
+ lines.push("");
292
+ lines.push("| metric | value |");
293
+ lines.push("|---|---|");
294
+ lines.push(`| iterations | ${result.iterations} |`);
295
+ lines.push(`| warmup | ${result.warmup} |`);
296
+ lines.push(`| p50 | ${formatMs(result.p50)} |`);
297
+ lines.push(`| p95 | ${formatMs(result.p95)} |`);
298
+ lines.push(`| p99 | ${formatMs(result.p99)} |`);
299
+ lines.push(`| mean | ${formatMs(result.mean)} |`);
300
+ lines.push(`| stdev | ${formatMs(result.stdev)} |`);
301
+ lines.push(`| min | ${formatMs(result.minMs)} |`);
302
+ lines.push(`| max | ${formatMs(result.maxMs)} |`);
303
+ lines.push(`| total | ${formatMs(result.totalMs)} |`);
304
+ lines.push("");
305
+ if (opts.baseline) {
306
+ const metrics = [
307
+ { label: "p50", current: result.p50, baseline: opts.baseline.p50 },
308
+ { label: "p95", current: result.p95, baseline: opts.baseline.p95 },
309
+ { label: "p99", current: result.p99, baseline: opts.baseline.p99 },
310
+ { label: "mean", current: result.mean, baseline: opts.baseline.mean },
311
+ { label: "min", current: result.minMs, baseline: opts.baseline.minMs },
312
+ { label: "max", current: result.maxMs, baseline: opts.baseline.maxMs },
313
+ { label: "total", current: result.totalMs, baseline: opts.baseline.totalMs }
314
+ ];
315
+ lines.push("## Baseline diff");
316
+ lines.push("");
317
+ lines.push("| metric | current | baseline | delta ms | delta % |");
318
+ lines.push("|---|---|---|---|---|");
319
+ for (const metric of metrics) {
320
+ const deltaMs = metric.current - metric.baseline;
321
+ const deltaPct = metric.baseline === 0 ? 0 : deltaMs / metric.baseline * 100;
322
+ lines.push(
323
+ `| ${metric.label} | ${formatMs(metric.current)} | ${formatMs(metric.baseline)} | ${formatSignedMs(deltaMs)} | ${formatSignedPct(deltaPct)} |`
324
+ );
325
+ }
326
+ lines.push("");
327
+ }
328
+ if (opts.includeSamples) {
329
+ lines.push("## Samples histogram");
330
+ lines.push("");
331
+ lines.push("| bin | range ms | count | bar |");
332
+ lines.push("|---|---|---|---|");
333
+ for (const row of histogramRows(result.samples, 10)) {
334
+ lines.push(`| ${row.index} | ${row.range} | ${row.count} | ${row.bar} |`);
335
+ }
336
+ lines.push("");
337
+ }
338
+ return lines.join("\n");
339
+ }
340
+ function histogramRows(samples, bins) {
341
+ if (samples.length === 0) {
342
+ return [];
343
+ }
344
+ const min = Math.min(...samples);
345
+ const max = Math.max(...samples);
346
+ const width = max === min ? 1 : (max - min) / bins;
347
+ const counts = new Array(bins).fill(0);
348
+ for (const sample of samples) {
349
+ const rawIndex = width === 0 ? 0 : Math.floor((sample - min) / width);
350
+ const index = Math.min(bins - 1, Math.max(0, rawIndex));
351
+ counts[index] = (counts[index] ?? 0) + 1;
352
+ }
353
+ const peak = Math.max(...counts, 1);
354
+ return counts.map((count, index) => {
355
+ const start = min + index * width;
356
+ const end = index === bins - 1 ? max : start + width;
357
+ return {
358
+ index: index + 1,
359
+ range: `${start.toFixed(2)}-${end.toFixed(2)}`,
360
+ count,
361
+ bar: "#".repeat(count === 0 ? 0 : Math.max(1, Math.round(count / peak * 10)))
362
+ };
363
+ });
364
+ }
365
+ function formatMs(value) {
366
+ return `${value.toFixed(2)}ms`;
367
+ }
368
+ function formatSignedMs(value) {
369
+ const sign = value > 0 ? "+" : "";
370
+ return `${sign}${value.toFixed(2)}ms`;
371
+ }
372
+ function formatSignedPct(value) {
373
+ const sign = value > 0 ? "+" : "";
374
+ return `${sign}${value.toFixed(2)}%`;
375
+ }
376
+ // Annotate the CommonJS export names for ESM import in node:
377
+ 0 && (module.exports = {
378
+ buildMeasureResult,
379
+ defaultBaselinePath,
380
+ detectRegression,
381
+ emitPerfReport,
382
+ evaluatePerfGate,
383
+ loadBaseline,
384
+ measure,
385
+ saveBaseline
386
+ });
387
+ //# sourceMappingURL=index.cjs.map
@@ -0,0 +1 @@
1
+ {"version":3,"sources":["../src/index.ts","../src/measure.ts","../src/regression.ts","../src/baseline.ts","../src/gate.ts","../src/report.ts"],"sourcesContent":["export type {\n MeasureInput,\n MeasureResult,\n PerfGateInput,\n PerfGateResult,\n RegressionInput,\n RegressionResult,\n Thresholds,\n} from './types.js';\n\nexport { buildMeasureResult, measure } from './measure.js';\nexport { detectRegression } from './regression.js';\nexport { defaultBaselinePath, loadBaseline, saveBaseline } from './baseline.js';\nexport { evaluatePerfGate } from './gate.js';\nexport { emitPerfReport } from './report.js';\n","import type { MeasureInput, MeasureResult } from './types.js';\n\nexport async function measure(input: MeasureInput): Promise<MeasureResult> {\n const warmup = input.warmup ?? 0;\n if (input.iterations < 1) {\n throw new Error(`measure: iterations must be >= 1, got ${input.iterations}`);\n }\n if (warmup < 0) {\n throw new Error(`measure: warmup must be >= 0, got ${warmup}`);\n }\n\n for (let index = 0; index < warmup; index += 1) {\n await input.fn();\n }\n\n const samples: number[] = [];\n for (let index = 0; index < input.iterations; index += 1) {\n const start = process.hrtime.bigint();\n await input.fn();\n const end = process.hrtime.bigint();\n samples.push(Number(end - start) / 1_000_000);\n }\n\n return buildMeasureResult(input.name, input.iterations, warmup, samples);\n}\n\nexport function buildMeasureResult(\n name: string,\n iterations: number,\n warmup: number,\n samples: number[],\n): MeasureResult {\n const sorted = [...samples].sort((left, right) => left - right);\n const totalMs = samples.reduce((sum, sample) => sum + sample, 0);\n const mean = totalMs / samples.length;\n const variance = samples.length > 1\n ? samples.reduce((sum, sample) => {\n const delta = sample - mean;\n return sum + (delta * delta);\n }, 0) / (samples.length - 1)\n : 0;\n\n return {\n name,\n iterations,\n warmup,\n samples,\n p50: percentile(sorted, 0.5),\n p95: percentile(sorted, 0.95),\n p99: percentile(sorted, 0.99),\n mean,\n stdev: Math.sqrt(variance),\n minMs: sorted[0] ?? 0,\n maxMs: sorted[sorted.length - 1] ?? 0,\n totalMs,\n };\n}\n\nfunction percentile(sorted: number[], ratio: number): number {\n if (sorted.length === 0) return 0;\n const rank = Math.max(0, Math.ceil(sorted.length * ratio) - 1);\n return sorted[rank] ?? sorted[sorted.length - 1] ?? 0;\n}\n","import { buildMeasureResult } from './measure.js';\nimport type {\n MeasureResult,\n RegressionInput,\n RegressionResult,\n} from './types.js';\n\nexport function detectRegression(input: RegressionInput): RegressionResult {\n const threshold = input.threshold ?? 0.2;\n const current = normalize(input.current);\n const baseline = normalize(input.baseline);\n\n const deltaPct = baseline.p95 === 0\n ? current.p95 === 0\n ? 0\n : Number.POSITIVE_INFINITY\n : (current.p95 - baseline.p95) / baseline.p95;\n const welchT = welchTScore(current.samples, baseline.samples);\n const significant = Math.abs(welchT) > 2;\n\n let verdict: RegressionResult['verdict'] = 'stable';\n if (significant && deltaPct >= threshold) {\n verdict = 'regressed';\n } else if (significant && deltaPct <= -threshold) {\n verdict = 'improved';\n }\n\n return {\n regressed: verdict === 'regressed',\n deltaPct,\n welchT,\n significant,\n verdict,\n };\n}\n\nfunction normalize(result: MeasureResult): MeasureResult {\n if (result.samples.length === 0) {\n return result;\n }\n return buildMeasureResult(result.name, result.iterations, result.warmup, result.samples);\n}\n\nfunction welchTScore(current: number[], baseline: number[]): number {\n if (current.length < 2 || baseline.length < 2) {\n return 0;\n }\n\n const currentStats = sampleStats(current);\n const baselineStats = sampleStats(baseline);\n const numerator = currentStats.mean - baselineStats.mean;\n const denominator = Math.sqrt(\n (currentStats.variance / current.length) +\n (baselineStats.variance / baseline.length),\n );\n\n if (!Number.isFinite(denominator) || denominator === 0) {\n return 0;\n }\n return numerator / denominator;\n}\n\nfunction sampleStats(samples: number[]): { mean: number; variance: number } {\n const mean = samples.reduce((sum, sample) => sum + sample, 0) / samples.length;\n const variance = samples.reduce((sum, sample) => {\n const delta = sample - mean;\n return sum + (delta * delta);\n }, 0) / (samples.length - 1);\n return { mean, variance };\n}\n","import { mkdir, readFile, writeFile } from 'node:fs/promises';\nimport { dirname } from 'node:path';\nimport type { MeasureResult } from './types.js';\n\nexport async function loadBaseline(path: string): Promise<MeasureResult | null> {\n try {\n const body = await readFile(path, 'utf8');\n return JSON.parse(body) as MeasureResult;\n } catch (error) {\n if (isMissingFile(error)) {\n return null;\n }\n throw error;\n }\n}\n\nexport async function saveBaseline(path: string, result: MeasureResult): Promise<void> {\n await mkdir(dirname(path), { recursive: true });\n await writeFile(path, `${JSON.stringify(result, null, 2)}\\n`, 'utf8');\n}\n\nexport function defaultBaselinePath(moduleName: string): string {\n return `${process.cwd()}/.perf-baseline/${moduleName}.json`;\n}\n\nfunction isMissingFile(error: unknown): error is NodeJS.ErrnoException {\n return typeof error === 'object' && error !== null && 'code' in error && error.code === 'ENOENT';\n}\n","import {\n evaluateReleaseGate,\n type QualityReport,\n type ReleaseGateBlocker,\n type ReleaseGateVerdict,\n} from '@kiwa-test/quality-metrics';\nimport type {\n PerfGateInput,\n PerfGateResult,\n Thresholds,\n} from './types.js';\n\nexport function evaluatePerfGate(input: PerfGateInput): PerfGateResult {\n const thresholds = input.thresholds ?? {};\n const enabledAxes = countThresholds(thresholds);\n const report = buildReport(input, thresholds, enabledAxes === 0);\n const relaxedVerdict = evaluateReleaseGate(report, {\n coverageLine: 0,\n coverageBranch: 0,\n coverageFunction: 0,\n fidelityRatio: 0,\n perfP95Ms: thresholds.p95Ms ?? Number.POSITIVE_INFINITY,\n mutationKillRate: 0,\n behaviorTests: 0,\n });\n\n if (enabledAxes === 0) {\n return {\n report,\n verdict: { passed: true, blockers: [], axesEvaluated: 0 },\n breaches: [],\n };\n }\n\n const breaches: ReleaseGateBlocker[] = [];\n if (thresholds.p95Ms !== undefined && relaxedVerdict.blockers.some((blocker) => blocker.axis === 'perf.p95Ms')) {\n breaches.push({\n axis: 'perf.p95Ms',\n threshold: thresholds.p95Ms,\n actual: input.result.p95,\n op: '<=',\n });\n }\n\n appendOptionalBreach(\n breaches,\n 'cost.perRequestUsd',\n '<=',\n thresholds.costUsd,\n input.metrics?.costUsd,\n Number.POSITIVE_INFINITY,\n );\n appendOptionalBreach(\n breaches,\n 'token.totalTokens',\n '<=',\n thresholds.tokens,\n input.metrics?.tokens,\n Number.POSITIVE_INFINITY,\n );\n appendOptionalBreach(\n breaches,\n 'accuracy.score',\n '>=',\n thresholds.accuracy,\n input.metrics?.accuracy,\n Number.NEGATIVE_INFINITY,\n );\n\n const verdict: ReleaseGateVerdict = {\n passed: breaches.length === 0,\n blockers: breaches,\n axesEvaluated: enabledAxes,\n };\n\n return { report, verdict, breaches };\n}\n\nfunction buildReport(\n input: PerfGateInput,\n thresholds: Thresholds,\n empty: boolean,\n): QualityReport {\n const perf = empty || thresholds.p95Ms === undefined\n ? { p50Ms: 0, p95Ms: 0, p99Ms: 0, samples: 0 }\n : {\n p50Ms: input.result.p50,\n p95Ms: input.result.p95,\n p99Ms: input.result.p99,\n samples: input.result.samples.length,\n };\n const report: QualityReport = {\n provider: `@kiwa-test/perf-harness/${input.result.name}`,\n version: '0.1.0',\n reportedAt: new Date().toISOString(),\n coverage: { line: 0, branch: 0, function: 0 },\n testCount: { behavior: 0, integration: 0, e2e: 0, total: 0 },\n fidelity: { mockCoveredMethods: 0, realTotalMethods: 0, ratio: 0 },\n perf,\n mutation: { mutations: 0, killed: 0, survived: 0, killRate: 0 },\n };\n if (!empty && thresholds.costUsd !== undefined) {\n const actual = input.metrics?.costUsd ?? 0;\n report.cost = { perRequestUsd: actual, totalUsd: actual, requests: 1 };\n }\n if (!empty && thresholds.tokens !== undefined) {\n const actual = input.metrics?.tokens ?? 0;\n report.token = {\n promptTokens: actual,\n completionTokens: 0,\n totalTokens: actual,\n requests: 1,\n };\n }\n if (!empty && thresholds.accuracy !== undefined) {\n report.accuracy = {\n score: input.metrics?.accuracy ?? 0,\n samples: 1,\n method: 'provided',\n };\n }\n return report;\n}\n\nfunction appendOptionalBreach(\n breaches: ReleaseGateBlocker[],\n axis: string,\n op: '>=' | '<=',\n threshold: number | undefined,\n actual: number | undefined,\n missingFallback: number,\n): void {\n if (threshold === undefined) {\n return;\n }\n const resolvedActual = actual ?? missingFallback;\n const passed = op === '<=' ? resolvedActual <= threshold : resolvedActual >= threshold;\n if (!passed) {\n breaches.push({\n axis,\n threshold,\n actual: resolvedActual,\n op,\n });\n }\n}\n\nfunction countThresholds(thresholds: Thresholds): number {\n return [\n thresholds.p95Ms,\n thresholds.costUsd,\n thresholds.tokens,\n thresholds.accuracy,\n ].filter((value) => value !== undefined).length;\n}\n","import type { MeasureResult } from './types.js';\n\nexport function emitPerfReport(\n result: MeasureResult,\n opts: {\n baseline?: MeasureResult;\n includeSamples?: boolean;\n } = {},\n): string {\n const lines: string[] = [];\n lines.push(`# Perf Report — ${result.name}`);\n lines.push('');\n lines.push('| metric | value |');\n lines.push('|---|---|');\n lines.push(`| iterations | ${result.iterations} |`);\n lines.push(`| warmup | ${result.warmup} |`);\n lines.push(`| p50 | ${formatMs(result.p50)} |`);\n lines.push(`| p95 | ${formatMs(result.p95)} |`);\n lines.push(`| p99 | ${formatMs(result.p99)} |`);\n lines.push(`| mean | ${formatMs(result.mean)} |`);\n lines.push(`| stdev | ${formatMs(result.stdev)} |`);\n lines.push(`| min | ${formatMs(result.minMs)} |`);\n lines.push(`| max | ${formatMs(result.maxMs)} |`);\n lines.push(`| total | ${formatMs(result.totalMs)} |`);\n lines.push('');\n\n if (opts.baseline) {\n const metrics = [\n { label: 'p50', current: result.p50, baseline: opts.baseline.p50 },\n { label: 'p95', current: result.p95, baseline: opts.baseline.p95 },\n { label: 'p99', current: result.p99, baseline: opts.baseline.p99 },\n { label: 'mean', current: result.mean, baseline: opts.baseline.mean },\n { label: 'min', current: result.minMs, baseline: opts.baseline.minMs },\n { label: 'max', current: result.maxMs, baseline: opts.baseline.maxMs },\n { label: 'total', current: result.totalMs, baseline: opts.baseline.totalMs },\n ];\n lines.push('## Baseline diff');\n lines.push('');\n lines.push('| metric | current | baseline | delta ms | delta % |');\n lines.push('|---|---|---|---|---|');\n for (const metric of metrics) {\n const deltaMs = metric.current - metric.baseline;\n const deltaPct = metric.baseline === 0 ? 0 : (deltaMs / metric.baseline) * 100;\n lines.push(\n `| ${metric.label} | ${formatMs(metric.current)} | ${formatMs(metric.baseline)} | ${formatSignedMs(deltaMs)} | ${formatSignedPct(deltaPct)} |`,\n );\n }\n lines.push('');\n }\n\n if (opts.includeSamples) {\n lines.push('## Samples histogram');\n lines.push('');\n lines.push('| bin | range ms | count | bar |');\n lines.push('|---|---|---|---|');\n for (const row of histogramRows(result.samples, 10)) {\n lines.push(`| ${row.index} | ${row.range} | ${row.count} | ${row.bar} |`);\n }\n lines.push('');\n }\n\n return lines.join('\\n');\n}\n\nfunction histogramRows(samples: number[], bins: number): Array<{\n index: number;\n range: string;\n count: number;\n bar: string;\n}> {\n if (samples.length === 0) {\n return [];\n }\n\n const min = Math.min(...samples);\n const max = Math.max(...samples);\n const width = max === min ? 1 : (max - min) / bins;\n const counts = new Array<number>(bins).fill(0);\n\n for (const sample of samples) {\n const rawIndex = width === 0 ? 0 : Math.floor((sample - min) / width);\n const index = Math.min(bins - 1, Math.max(0, rawIndex));\n counts[index] = (counts[index] ?? 0) + 1;\n }\n\n const peak = Math.max(...counts, 1);\n return counts.map((count, index) => {\n const start = min + (index * width);\n const end = index === bins - 1 ? max : start + width;\n return {\n index: index + 1,\n range: `${start.toFixed(2)}-${end.toFixed(2)}`,\n count,\n bar: '#'.repeat(count === 0 ? 0 : Math.max(1, Math.round((count / peak) * 10))),\n };\n });\n}\n\nfunction formatMs(value: number): string {\n return `${value.toFixed(2)}ms`;\n}\n\nfunction formatSignedMs(value: number): string {\n const sign = value > 0 ? '+' : '';\n return `${sign}${value.toFixed(2)}ms`;\n}\n\nfunction formatSignedPct(value: number): string {\n const sign = value > 0 ? '+' : '';\n return `${sign}${value.toFixed(2)}%`;\n}\n"],"mappings":"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","names":[]}
@@ -0,0 +1,73 @@
1
+ import { QualityReport, ReleaseGateVerdict, ReleaseGateBlocker } from '@kiwa-test/quality-metrics';
2
+
3
+ interface MeasureInput {
4
+ name: string;
5
+ fn: () => void | Promise<void>;
6
+ iterations: number;
7
+ warmup?: number;
8
+ }
9
+ interface MeasureResult {
10
+ name: string;
11
+ iterations: number;
12
+ warmup: number;
13
+ samples: number[];
14
+ p50: number;
15
+ p95: number;
16
+ p99: number;
17
+ mean: number;
18
+ stdev: number;
19
+ minMs: number;
20
+ maxMs: number;
21
+ totalMs: number;
22
+ }
23
+ interface RegressionInput {
24
+ current: MeasureResult;
25
+ baseline: MeasureResult;
26
+ threshold?: number;
27
+ }
28
+ interface RegressionResult {
29
+ regressed: boolean;
30
+ deltaPct: number;
31
+ welchT: number;
32
+ significant: boolean;
33
+ verdict: 'improved' | 'stable' | 'regressed';
34
+ }
35
+ interface Thresholds {
36
+ p95Ms?: number;
37
+ costUsd?: number;
38
+ tokens?: number;
39
+ accuracy?: number;
40
+ }
41
+ interface PerfGateInput {
42
+ result: MeasureResult;
43
+ baseline?: MeasureResult | null;
44
+ thresholds?: Thresholds;
45
+ metrics?: {
46
+ costUsd?: number;
47
+ tokens?: number;
48
+ accuracy?: number;
49
+ };
50
+ }
51
+ interface PerfGateResult {
52
+ report: QualityReport;
53
+ verdict: ReleaseGateVerdict;
54
+ breaches: ReleaseGateBlocker[];
55
+ }
56
+
57
+ declare function measure(input: MeasureInput): Promise<MeasureResult>;
58
+ declare function buildMeasureResult(name: string, iterations: number, warmup: number, samples: number[]): MeasureResult;
59
+
60
+ declare function detectRegression(input: RegressionInput): RegressionResult;
61
+
62
+ declare function loadBaseline(path: string): Promise<MeasureResult | null>;
63
+ declare function saveBaseline(path: string, result: MeasureResult): Promise<void>;
64
+ declare function defaultBaselinePath(moduleName: string): string;
65
+
66
+ declare function evaluatePerfGate(input: PerfGateInput): PerfGateResult;
67
+
68
+ declare function emitPerfReport(result: MeasureResult, opts?: {
69
+ baseline?: MeasureResult;
70
+ includeSamples?: boolean;
71
+ }): string;
72
+
73
+ export { type MeasureInput, type MeasureResult, type PerfGateInput, type PerfGateResult, type RegressionInput, type RegressionResult, type Thresholds, buildMeasureResult, defaultBaselinePath, detectRegression, emitPerfReport, evaluatePerfGate, loadBaseline, measure, saveBaseline };
@@ -0,0 +1,73 @@
1
+ import { QualityReport, ReleaseGateVerdict, ReleaseGateBlocker } from '@kiwa-test/quality-metrics';
2
+
3
+ interface MeasureInput {
4
+ name: string;
5
+ fn: () => void | Promise<void>;
6
+ iterations: number;
7
+ warmup?: number;
8
+ }
9
+ interface MeasureResult {
10
+ name: string;
11
+ iterations: number;
12
+ warmup: number;
13
+ samples: number[];
14
+ p50: number;
15
+ p95: number;
16
+ p99: number;
17
+ mean: number;
18
+ stdev: number;
19
+ minMs: number;
20
+ maxMs: number;
21
+ totalMs: number;
22
+ }
23
+ interface RegressionInput {
24
+ current: MeasureResult;
25
+ baseline: MeasureResult;
26
+ threshold?: number;
27
+ }
28
+ interface RegressionResult {
29
+ regressed: boolean;
30
+ deltaPct: number;
31
+ welchT: number;
32
+ significant: boolean;
33
+ verdict: 'improved' | 'stable' | 'regressed';
34
+ }
35
+ interface Thresholds {
36
+ p95Ms?: number;
37
+ costUsd?: number;
38
+ tokens?: number;
39
+ accuracy?: number;
40
+ }
41
+ interface PerfGateInput {
42
+ result: MeasureResult;
43
+ baseline?: MeasureResult | null;
44
+ thresholds?: Thresholds;
45
+ metrics?: {
46
+ costUsd?: number;
47
+ tokens?: number;
48
+ accuracy?: number;
49
+ };
50
+ }
51
+ interface PerfGateResult {
52
+ report: QualityReport;
53
+ verdict: ReleaseGateVerdict;
54
+ breaches: ReleaseGateBlocker[];
55
+ }
56
+
57
+ declare function measure(input: MeasureInput): Promise<MeasureResult>;
58
+ declare function buildMeasureResult(name: string, iterations: number, warmup: number, samples: number[]): MeasureResult;
59
+
60
+ declare function detectRegression(input: RegressionInput): RegressionResult;
61
+
62
+ declare function loadBaseline(path: string): Promise<MeasureResult | null>;
63
+ declare function saveBaseline(path: string, result: MeasureResult): Promise<void>;
64
+ declare function defaultBaselinePath(moduleName: string): string;
65
+
66
+ declare function evaluatePerfGate(input: PerfGateInput): PerfGateResult;
67
+
68
+ declare function emitPerfReport(result: MeasureResult, opts?: {
69
+ baseline?: MeasureResult;
70
+ includeSamples?: boolean;
71
+ }): string;
72
+
73
+ export { type MeasureInput, type MeasureResult, type PerfGateInput, type PerfGateResult, type RegressionInput, type RegressionResult, type Thresholds, buildMeasureResult, defaultBaselinePath, detectRegression, emitPerfReport, evaluatePerfGate, loadBaseline, measure, saveBaseline };
package/dist/index.js ADDED
@@ -0,0 +1,355 @@
1
+ // src/measure.ts
2
+ async function measure(input) {
3
+ const warmup = input.warmup ?? 0;
4
+ if (input.iterations < 1) {
5
+ throw new Error(`measure: iterations must be >= 1, got ${input.iterations}`);
6
+ }
7
+ if (warmup < 0) {
8
+ throw new Error(`measure: warmup must be >= 0, got ${warmup}`);
9
+ }
10
+ for (let index = 0; index < warmup; index += 1) {
11
+ await input.fn();
12
+ }
13
+ const samples = [];
14
+ for (let index = 0; index < input.iterations; index += 1) {
15
+ const start = process.hrtime.bigint();
16
+ await input.fn();
17
+ const end = process.hrtime.bigint();
18
+ samples.push(Number(end - start) / 1e6);
19
+ }
20
+ return buildMeasureResult(input.name, input.iterations, warmup, samples);
21
+ }
22
+ function buildMeasureResult(name, iterations, warmup, samples) {
23
+ const sorted = [...samples].sort((left, right) => left - right);
24
+ const totalMs = samples.reduce((sum, sample) => sum + sample, 0);
25
+ const mean = totalMs / samples.length;
26
+ const variance = samples.length > 1 ? samples.reduce((sum, sample) => {
27
+ const delta = sample - mean;
28
+ return sum + delta * delta;
29
+ }, 0) / (samples.length - 1) : 0;
30
+ return {
31
+ name,
32
+ iterations,
33
+ warmup,
34
+ samples,
35
+ p50: percentile(sorted, 0.5),
36
+ p95: percentile(sorted, 0.95),
37
+ p99: percentile(sorted, 0.99),
38
+ mean,
39
+ stdev: Math.sqrt(variance),
40
+ minMs: sorted[0] ?? 0,
41
+ maxMs: sorted[sorted.length - 1] ?? 0,
42
+ totalMs
43
+ };
44
+ }
45
+ function percentile(sorted, ratio) {
46
+ if (sorted.length === 0) return 0;
47
+ const rank = Math.max(0, Math.ceil(sorted.length * ratio) - 1);
48
+ return sorted[rank] ?? sorted[sorted.length - 1] ?? 0;
49
+ }
50
+
51
+ // src/regression.ts
52
+ function detectRegression(input) {
53
+ const threshold = input.threshold ?? 0.2;
54
+ const current = normalize(input.current);
55
+ const baseline = normalize(input.baseline);
56
+ const deltaPct = baseline.p95 === 0 ? current.p95 === 0 ? 0 : Number.POSITIVE_INFINITY : (current.p95 - baseline.p95) / baseline.p95;
57
+ const welchT = welchTScore(current.samples, baseline.samples);
58
+ const significant = Math.abs(welchT) > 2;
59
+ let verdict = "stable";
60
+ if (significant && deltaPct >= threshold) {
61
+ verdict = "regressed";
62
+ } else if (significant && deltaPct <= -threshold) {
63
+ verdict = "improved";
64
+ }
65
+ return {
66
+ regressed: verdict === "regressed",
67
+ deltaPct,
68
+ welchT,
69
+ significant,
70
+ verdict
71
+ };
72
+ }
73
+ function normalize(result) {
74
+ if (result.samples.length === 0) {
75
+ return result;
76
+ }
77
+ return buildMeasureResult(result.name, result.iterations, result.warmup, result.samples);
78
+ }
79
+ function welchTScore(current, baseline) {
80
+ if (current.length < 2 || baseline.length < 2) {
81
+ return 0;
82
+ }
83
+ const currentStats = sampleStats(current);
84
+ const baselineStats = sampleStats(baseline);
85
+ const numerator = currentStats.mean - baselineStats.mean;
86
+ const denominator = Math.sqrt(
87
+ currentStats.variance / current.length + baselineStats.variance / baseline.length
88
+ );
89
+ if (!Number.isFinite(denominator) || denominator === 0) {
90
+ return 0;
91
+ }
92
+ return numerator / denominator;
93
+ }
94
+ function sampleStats(samples) {
95
+ const mean = samples.reduce((sum, sample) => sum + sample, 0) / samples.length;
96
+ const variance = samples.reduce((sum, sample) => {
97
+ const delta = sample - mean;
98
+ return sum + delta * delta;
99
+ }, 0) / (samples.length - 1);
100
+ return { mean, variance };
101
+ }
102
+
103
+ // src/baseline.ts
104
+ import { mkdir, readFile, writeFile } from "fs/promises";
105
+ import { dirname } from "path";
106
+ async function loadBaseline(path) {
107
+ try {
108
+ const body = await readFile(path, "utf8");
109
+ return JSON.parse(body);
110
+ } catch (error) {
111
+ if (isMissingFile(error)) {
112
+ return null;
113
+ }
114
+ throw error;
115
+ }
116
+ }
117
+ async function saveBaseline(path, result) {
118
+ await mkdir(dirname(path), { recursive: true });
119
+ await writeFile(path, `${JSON.stringify(result, null, 2)}
120
+ `, "utf8");
121
+ }
122
+ function defaultBaselinePath(moduleName) {
123
+ return `${process.cwd()}/.perf-baseline/${moduleName}.json`;
124
+ }
125
+ function isMissingFile(error) {
126
+ return typeof error === "object" && error !== null && "code" in error && error.code === "ENOENT";
127
+ }
128
+
129
+ // src/gate.ts
130
+ import {
131
+ evaluateReleaseGate
132
+ } from "@kiwa-test/quality-metrics";
133
+ function evaluatePerfGate(input) {
134
+ const thresholds = input.thresholds ?? {};
135
+ const enabledAxes = countThresholds(thresholds);
136
+ const report = buildReport(input, thresholds, enabledAxes === 0);
137
+ const relaxedVerdict = evaluateReleaseGate(report, {
138
+ coverageLine: 0,
139
+ coverageBranch: 0,
140
+ coverageFunction: 0,
141
+ fidelityRatio: 0,
142
+ perfP95Ms: thresholds.p95Ms ?? Number.POSITIVE_INFINITY,
143
+ mutationKillRate: 0,
144
+ behaviorTests: 0
145
+ });
146
+ if (enabledAxes === 0) {
147
+ return {
148
+ report,
149
+ verdict: { passed: true, blockers: [], axesEvaluated: 0 },
150
+ breaches: []
151
+ };
152
+ }
153
+ const breaches = [];
154
+ if (thresholds.p95Ms !== void 0 && relaxedVerdict.blockers.some((blocker) => blocker.axis === "perf.p95Ms")) {
155
+ breaches.push({
156
+ axis: "perf.p95Ms",
157
+ threshold: thresholds.p95Ms,
158
+ actual: input.result.p95,
159
+ op: "<="
160
+ });
161
+ }
162
+ appendOptionalBreach(
163
+ breaches,
164
+ "cost.perRequestUsd",
165
+ "<=",
166
+ thresholds.costUsd,
167
+ input.metrics?.costUsd,
168
+ Number.POSITIVE_INFINITY
169
+ );
170
+ appendOptionalBreach(
171
+ breaches,
172
+ "token.totalTokens",
173
+ "<=",
174
+ thresholds.tokens,
175
+ input.metrics?.tokens,
176
+ Number.POSITIVE_INFINITY
177
+ );
178
+ appendOptionalBreach(
179
+ breaches,
180
+ "accuracy.score",
181
+ ">=",
182
+ thresholds.accuracy,
183
+ input.metrics?.accuracy,
184
+ Number.NEGATIVE_INFINITY
185
+ );
186
+ const verdict = {
187
+ passed: breaches.length === 0,
188
+ blockers: breaches,
189
+ axesEvaluated: enabledAxes
190
+ };
191
+ return { report, verdict, breaches };
192
+ }
193
+ function buildReport(input, thresholds, empty) {
194
+ const perf = empty || thresholds.p95Ms === void 0 ? { p50Ms: 0, p95Ms: 0, p99Ms: 0, samples: 0 } : {
195
+ p50Ms: input.result.p50,
196
+ p95Ms: input.result.p95,
197
+ p99Ms: input.result.p99,
198
+ samples: input.result.samples.length
199
+ };
200
+ const report = {
201
+ provider: `@kiwa-test/perf-harness/${input.result.name}`,
202
+ version: "0.1.0",
203
+ reportedAt: (/* @__PURE__ */ new Date()).toISOString(),
204
+ coverage: { line: 0, branch: 0, function: 0 },
205
+ testCount: { behavior: 0, integration: 0, e2e: 0, total: 0 },
206
+ fidelity: { mockCoveredMethods: 0, realTotalMethods: 0, ratio: 0 },
207
+ perf,
208
+ mutation: { mutations: 0, killed: 0, survived: 0, killRate: 0 }
209
+ };
210
+ if (!empty && thresholds.costUsd !== void 0) {
211
+ const actual = input.metrics?.costUsd ?? 0;
212
+ report.cost = { perRequestUsd: actual, totalUsd: actual, requests: 1 };
213
+ }
214
+ if (!empty && thresholds.tokens !== void 0) {
215
+ const actual = input.metrics?.tokens ?? 0;
216
+ report.token = {
217
+ promptTokens: actual,
218
+ completionTokens: 0,
219
+ totalTokens: actual,
220
+ requests: 1
221
+ };
222
+ }
223
+ if (!empty && thresholds.accuracy !== void 0) {
224
+ report.accuracy = {
225
+ score: input.metrics?.accuracy ?? 0,
226
+ samples: 1,
227
+ method: "provided"
228
+ };
229
+ }
230
+ return report;
231
+ }
232
+ function appendOptionalBreach(breaches, axis, op, threshold, actual, missingFallback) {
233
+ if (threshold === void 0) {
234
+ return;
235
+ }
236
+ const resolvedActual = actual ?? missingFallback;
237
+ const passed = op === "<=" ? resolvedActual <= threshold : resolvedActual >= threshold;
238
+ if (!passed) {
239
+ breaches.push({
240
+ axis,
241
+ threshold,
242
+ actual: resolvedActual,
243
+ op
244
+ });
245
+ }
246
+ }
247
+ function countThresholds(thresholds) {
248
+ return [
249
+ thresholds.p95Ms,
250
+ thresholds.costUsd,
251
+ thresholds.tokens,
252
+ thresholds.accuracy
253
+ ].filter((value) => value !== void 0).length;
254
+ }
255
+
256
+ // src/report.ts
257
+ function emitPerfReport(result, opts = {}) {
258
+ const lines = [];
259
+ lines.push(`# Perf Report \u2014 ${result.name}`);
260
+ lines.push("");
261
+ lines.push("| metric | value |");
262
+ lines.push("|---|---|");
263
+ lines.push(`| iterations | ${result.iterations} |`);
264
+ lines.push(`| warmup | ${result.warmup} |`);
265
+ lines.push(`| p50 | ${formatMs(result.p50)} |`);
266
+ lines.push(`| p95 | ${formatMs(result.p95)} |`);
267
+ lines.push(`| p99 | ${formatMs(result.p99)} |`);
268
+ lines.push(`| mean | ${formatMs(result.mean)} |`);
269
+ lines.push(`| stdev | ${formatMs(result.stdev)} |`);
270
+ lines.push(`| min | ${formatMs(result.minMs)} |`);
271
+ lines.push(`| max | ${formatMs(result.maxMs)} |`);
272
+ lines.push(`| total | ${formatMs(result.totalMs)} |`);
273
+ lines.push("");
274
+ if (opts.baseline) {
275
+ const metrics = [
276
+ { label: "p50", current: result.p50, baseline: opts.baseline.p50 },
277
+ { label: "p95", current: result.p95, baseline: opts.baseline.p95 },
278
+ { label: "p99", current: result.p99, baseline: opts.baseline.p99 },
279
+ { label: "mean", current: result.mean, baseline: opts.baseline.mean },
280
+ { label: "min", current: result.minMs, baseline: opts.baseline.minMs },
281
+ { label: "max", current: result.maxMs, baseline: opts.baseline.maxMs },
282
+ { label: "total", current: result.totalMs, baseline: opts.baseline.totalMs }
283
+ ];
284
+ lines.push("## Baseline diff");
285
+ lines.push("");
286
+ lines.push("| metric | current | baseline | delta ms | delta % |");
287
+ lines.push("|---|---|---|---|---|");
288
+ for (const metric of metrics) {
289
+ const deltaMs = metric.current - metric.baseline;
290
+ const deltaPct = metric.baseline === 0 ? 0 : deltaMs / metric.baseline * 100;
291
+ lines.push(
292
+ `| ${metric.label} | ${formatMs(metric.current)} | ${formatMs(metric.baseline)} | ${formatSignedMs(deltaMs)} | ${formatSignedPct(deltaPct)} |`
293
+ );
294
+ }
295
+ lines.push("");
296
+ }
297
+ if (opts.includeSamples) {
298
+ lines.push("## Samples histogram");
299
+ lines.push("");
300
+ lines.push("| bin | range ms | count | bar |");
301
+ lines.push("|---|---|---|---|");
302
+ for (const row of histogramRows(result.samples, 10)) {
303
+ lines.push(`| ${row.index} | ${row.range} | ${row.count} | ${row.bar} |`);
304
+ }
305
+ lines.push("");
306
+ }
307
+ return lines.join("\n");
308
+ }
309
+ function histogramRows(samples, bins) {
310
+ if (samples.length === 0) {
311
+ return [];
312
+ }
313
+ const min = Math.min(...samples);
314
+ const max = Math.max(...samples);
315
+ const width = max === min ? 1 : (max - min) / bins;
316
+ const counts = new Array(bins).fill(0);
317
+ for (const sample of samples) {
318
+ const rawIndex = width === 0 ? 0 : Math.floor((sample - min) / width);
319
+ const index = Math.min(bins - 1, Math.max(0, rawIndex));
320
+ counts[index] = (counts[index] ?? 0) + 1;
321
+ }
322
+ const peak = Math.max(...counts, 1);
323
+ return counts.map((count, index) => {
324
+ const start = min + index * width;
325
+ const end = index === bins - 1 ? max : start + width;
326
+ return {
327
+ index: index + 1,
328
+ range: `${start.toFixed(2)}-${end.toFixed(2)}`,
329
+ count,
330
+ bar: "#".repeat(count === 0 ? 0 : Math.max(1, Math.round(count / peak * 10)))
331
+ };
332
+ });
333
+ }
334
+ function formatMs(value) {
335
+ return `${value.toFixed(2)}ms`;
336
+ }
337
+ function formatSignedMs(value) {
338
+ const sign = value > 0 ? "+" : "";
339
+ return `${sign}${value.toFixed(2)}ms`;
340
+ }
341
+ function formatSignedPct(value) {
342
+ const sign = value > 0 ? "+" : "";
343
+ return `${sign}${value.toFixed(2)}%`;
344
+ }
345
+ export {
346
+ buildMeasureResult,
347
+ defaultBaselinePath,
348
+ detectRegression,
349
+ emitPerfReport,
350
+ evaluatePerfGate,
351
+ loadBaseline,
352
+ measure,
353
+ saveBaseline
354
+ };
355
+ //# sourceMappingURL=index.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"sources":["../src/measure.ts","../src/regression.ts","../src/baseline.ts","../src/gate.ts","../src/report.ts"],"sourcesContent":["import type { MeasureInput, MeasureResult } from './types.js';\n\nexport async function measure(input: MeasureInput): Promise<MeasureResult> {\n const warmup = input.warmup ?? 0;\n if (input.iterations < 1) {\n throw new Error(`measure: iterations must be >= 1, got ${input.iterations}`);\n }\n if (warmup < 0) {\n throw new Error(`measure: warmup must be >= 0, got ${warmup}`);\n }\n\n for (let index = 0; index < warmup; index += 1) {\n await input.fn();\n }\n\n const samples: number[] = [];\n for (let index = 0; index < input.iterations; index += 1) {\n const start = process.hrtime.bigint();\n await input.fn();\n const end = process.hrtime.bigint();\n samples.push(Number(end - start) / 1_000_000);\n }\n\n return buildMeasureResult(input.name, input.iterations, warmup, samples);\n}\n\nexport function buildMeasureResult(\n name: string,\n iterations: number,\n warmup: number,\n samples: number[],\n): MeasureResult {\n const sorted = [...samples].sort((left, right) => left - right);\n const totalMs = samples.reduce((sum, sample) => sum + sample, 0);\n const mean = totalMs / samples.length;\n const variance = samples.length > 1\n ? samples.reduce((sum, sample) => {\n const delta = sample - mean;\n return sum + (delta * delta);\n }, 0) / (samples.length - 1)\n : 0;\n\n return {\n name,\n iterations,\n warmup,\n samples,\n p50: percentile(sorted, 0.5),\n p95: percentile(sorted, 0.95),\n p99: percentile(sorted, 0.99),\n mean,\n stdev: Math.sqrt(variance),\n minMs: sorted[0] ?? 0,\n maxMs: sorted[sorted.length - 1] ?? 0,\n totalMs,\n };\n}\n\nfunction percentile(sorted: number[], ratio: number): number {\n if (sorted.length === 0) return 0;\n const rank = Math.max(0, Math.ceil(sorted.length * ratio) - 1);\n return sorted[rank] ?? sorted[sorted.length - 1] ?? 0;\n}\n","import { buildMeasureResult } from './measure.js';\nimport type {\n MeasureResult,\n RegressionInput,\n RegressionResult,\n} from './types.js';\n\nexport function detectRegression(input: RegressionInput): RegressionResult {\n const threshold = input.threshold ?? 0.2;\n const current = normalize(input.current);\n const baseline = normalize(input.baseline);\n\n const deltaPct = baseline.p95 === 0\n ? current.p95 === 0\n ? 0\n : Number.POSITIVE_INFINITY\n : (current.p95 - baseline.p95) / baseline.p95;\n const welchT = welchTScore(current.samples, baseline.samples);\n const significant = Math.abs(welchT) > 2;\n\n let verdict: RegressionResult['verdict'] = 'stable';\n if (significant && deltaPct >= threshold) {\n verdict = 'regressed';\n } else if (significant && deltaPct <= -threshold) {\n verdict = 'improved';\n }\n\n return {\n regressed: verdict === 'regressed',\n deltaPct,\n welchT,\n significant,\n verdict,\n };\n}\n\nfunction normalize(result: MeasureResult): MeasureResult {\n if (result.samples.length === 0) {\n return result;\n }\n return buildMeasureResult(result.name, result.iterations, result.warmup, result.samples);\n}\n\nfunction welchTScore(current: number[], baseline: number[]): number {\n if (current.length < 2 || baseline.length < 2) {\n return 0;\n }\n\n const currentStats = sampleStats(current);\n const baselineStats = sampleStats(baseline);\n const numerator = currentStats.mean - baselineStats.mean;\n const denominator = Math.sqrt(\n (currentStats.variance / current.length) +\n (baselineStats.variance / baseline.length),\n );\n\n if (!Number.isFinite(denominator) || denominator === 0) {\n return 0;\n }\n return numerator / denominator;\n}\n\nfunction sampleStats(samples: number[]): { mean: number; variance: number } {\n const mean = samples.reduce((sum, sample) => sum + sample, 0) / samples.length;\n const variance = samples.reduce((sum, sample) => {\n const delta = sample - mean;\n return sum + (delta * delta);\n }, 0) / (samples.length - 1);\n return { mean, variance };\n}\n","import { mkdir, readFile, writeFile } from 'node:fs/promises';\nimport { dirname } from 'node:path';\nimport type { MeasureResult } from './types.js';\n\nexport async function loadBaseline(path: string): Promise<MeasureResult | null> {\n try {\n const body = await readFile(path, 'utf8');\n return JSON.parse(body) as MeasureResult;\n } catch (error) {\n if (isMissingFile(error)) {\n return null;\n }\n throw error;\n }\n}\n\nexport async function saveBaseline(path: string, result: MeasureResult): Promise<void> {\n await mkdir(dirname(path), { recursive: true });\n await writeFile(path, `${JSON.stringify(result, null, 2)}\\n`, 'utf8');\n}\n\nexport function defaultBaselinePath(moduleName: string): string {\n return `${process.cwd()}/.perf-baseline/${moduleName}.json`;\n}\n\nfunction isMissingFile(error: unknown): error is NodeJS.ErrnoException {\n return typeof error === 'object' && error !== null && 'code' in error && error.code === 'ENOENT';\n}\n","import {\n evaluateReleaseGate,\n type QualityReport,\n type ReleaseGateBlocker,\n type ReleaseGateVerdict,\n} from '@kiwa-test/quality-metrics';\nimport type {\n PerfGateInput,\n PerfGateResult,\n Thresholds,\n} from './types.js';\n\nexport function evaluatePerfGate(input: PerfGateInput): PerfGateResult {\n const thresholds = input.thresholds ?? {};\n const enabledAxes = countThresholds(thresholds);\n const report = buildReport(input, thresholds, enabledAxes === 0);\n const relaxedVerdict = evaluateReleaseGate(report, {\n coverageLine: 0,\n coverageBranch: 0,\n coverageFunction: 0,\n fidelityRatio: 0,\n perfP95Ms: thresholds.p95Ms ?? Number.POSITIVE_INFINITY,\n mutationKillRate: 0,\n behaviorTests: 0,\n });\n\n if (enabledAxes === 0) {\n return {\n report,\n verdict: { passed: true, blockers: [], axesEvaluated: 0 },\n breaches: [],\n };\n }\n\n const breaches: ReleaseGateBlocker[] = [];\n if (thresholds.p95Ms !== undefined && relaxedVerdict.blockers.some((blocker) => blocker.axis === 'perf.p95Ms')) {\n breaches.push({\n axis: 'perf.p95Ms',\n threshold: thresholds.p95Ms,\n actual: input.result.p95,\n op: '<=',\n });\n }\n\n appendOptionalBreach(\n breaches,\n 'cost.perRequestUsd',\n '<=',\n thresholds.costUsd,\n input.metrics?.costUsd,\n Number.POSITIVE_INFINITY,\n );\n appendOptionalBreach(\n breaches,\n 'token.totalTokens',\n '<=',\n thresholds.tokens,\n input.metrics?.tokens,\n Number.POSITIVE_INFINITY,\n );\n appendOptionalBreach(\n breaches,\n 'accuracy.score',\n '>=',\n thresholds.accuracy,\n input.metrics?.accuracy,\n Number.NEGATIVE_INFINITY,\n );\n\n const verdict: ReleaseGateVerdict = {\n passed: breaches.length === 0,\n blockers: breaches,\n axesEvaluated: enabledAxes,\n };\n\n return { report, verdict, breaches };\n}\n\nfunction buildReport(\n input: PerfGateInput,\n thresholds: Thresholds,\n empty: boolean,\n): QualityReport {\n const perf = empty || thresholds.p95Ms === undefined\n ? { p50Ms: 0, p95Ms: 0, p99Ms: 0, samples: 0 }\n : {\n p50Ms: input.result.p50,\n p95Ms: input.result.p95,\n p99Ms: input.result.p99,\n samples: input.result.samples.length,\n };\n const report: QualityReport = {\n provider: `@kiwa-test/perf-harness/${input.result.name}`,\n version: '0.1.0',\n reportedAt: new Date().toISOString(),\n coverage: { line: 0, branch: 0, function: 0 },\n testCount: { behavior: 0, integration: 0, e2e: 0, total: 0 },\n fidelity: { mockCoveredMethods: 0, realTotalMethods: 0, ratio: 0 },\n perf,\n mutation: { mutations: 0, killed: 0, survived: 0, killRate: 0 },\n };\n if (!empty && thresholds.costUsd !== undefined) {\n const actual = input.metrics?.costUsd ?? 0;\n report.cost = { perRequestUsd: actual, totalUsd: actual, requests: 1 };\n }\n if (!empty && thresholds.tokens !== undefined) {\n const actual = input.metrics?.tokens ?? 0;\n report.token = {\n promptTokens: actual,\n completionTokens: 0,\n totalTokens: actual,\n requests: 1,\n };\n }\n if (!empty && thresholds.accuracy !== undefined) {\n report.accuracy = {\n score: input.metrics?.accuracy ?? 0,\n samples: 1,\n method: 'provided',\n };\n }\n return report;\n}\n\nfunction appendOptionalBreach(\n breaches: ReleaseGateBlocker[],\n axis: string,\n op: '>=' | '<=',\n threshold: number | undefined,\n actual: number | undefined,\n missingFallback: number,\n): void {\n if (threshold === undefined) {\n return;\n }\n const resolvedActual = actual ?? missingFallback;\n const passed = op === '<=' ? resolvedActual <= threshold : resolvedActual >= threshold;\n if (!passed) {\n breaches.push({\n axis,\n threshold,\n actual: resolvedActual,\n op,\n });\n }\n}\n\nfunction countThresholds(thresholds: Thresholds): number {\n return [\n thresholds.p95Ms,\n thresholds.costUsd,\n thresholds.tokens,\n thresholds.accuracy,\n ].filter((value) => value !== undefined).length;\n}\n","import type { MeasureResult } from './types.js';\n\nexport function emitPerfReport(\n result: MeasureResult,\n opts: {\n baseline?: MeasureResult;\n includeSamples?: boolean;\n } = {},\n): string {\n const lines: string[] = [];\n lines.push(`# Perf Report — ${result.name}`);\n lines.push('');\n lines.push('| metric | value |');\n lines.push('|---|---|');\n lines.push(`| iterations | ${result.iterations} |`);\n lines.push(`| warmup | ${result.warmup} |`);\n lines.push(`| p50 | ${formatMs(result.p50)} |`);\n lines.push(`| p95 | ${formatMs(result.p95)} |`);\n lines.push(`| p99 | ${formatMs(result.p99)} |`);\n lines.push(`| mean | ${formatMs(result.mean)} |`);\n lines.push(`| stdev | ${formatMs(result.stdev)} |`);\n lines.push(`| min | ${formatMs(result.minMs)} |`);\n lines.push(`| max | ${formatMs(result.maxMs)} |`);\n lines.push(`| total | ${formatMs(result.totalMs)} |`);\n lines.push('');\n\n if (opts.baseline) {\n const metrics = [\n { label: 'p50', current: result.p50, baseline: opts.baseline.p50 },\n { label: 'p95', current: result.p95, baseline: opts.baseline.p95 },\n { label: 'p99', current: result.p99, baseline: opts.baseline.p99 },\n { label: 'mean', current: result.mean, baseline: opts.baseline.mean },\n { label: 'min', current: result.minMs, baseline: opts.baseline.minMs },\n { label: 'max', current: result.maxMs, baseline: opts.baseline.maxMs },\n { label: 'total', current: result.totalMs, baseline: opts.baseline.totalMs },\n ];\n lines.push('## Baseline diff');\n lines.push('');\n lines.push('| metric | current | baseline | delta ms | delta % |');\n lines.push('|---|---|---|---|---|');\n for (const metric of metrics) {\n const deltaMs = metric.current - metric.baseline;\n const deltaPct = metric.baseline === 0 ? 0 : (deltaMs / metric.baseline) * 100;\n lines.push(\n `| ${metric.label} | ${formatMs(metric.current)} | ${formatMs(metric.baseline)} | ${formatSignedMs(deltaMs)} | ${formatSignedPct(deltaPct)} |`,\n );\n }\n lines.push('');\n }\n\n if (opts.includeSamples) {\n lines.push('## Samples histogram');\n lines.push('');\n lines.push('| bin | range ms | count | bar |');\n lines.push('|---|---|---|---|');\n for (const row of histogramRows(result.samples, 10)) {\n lines.push(`| ${row.index} | ${row.range} | ${row.count} | ${row.bar} |`);\n }\n lines.push('');\n }\n\n return lines.join('\\n');\n}\n\nfunction histogramRows(samples: number[], bins: number): Array<{\n index: number;\n range: string;\n count: number;\n bar: string;\n}> {\n if (samples.length === 0) {\n return [];\n }\n\n const min = Math.min(...samples);\n const max = Math.max(...samples);\n const width = max === min ? 1 : (max - min) / bins;\n const counts = new Array<number>(bins).fill(0);\n\n for (const sample of samples) {\n const rawIndex = width === 0 ? 0 : Math.floor((sample - min) / width);\n const index = Math.min(bins - 1, Math.max(0, rawIndex));\n counts[index] = (counts[index] ?? 0) + 1;\n }\n\n const peak = Math.max(...counts, 1);\n return counts.map((count, index) => {\n const start = min + (index * width);\n const end = index === bins - 1 ? max : start + width;\n return {\n index: index + 1,\n range: `${start.toFixed(2)}-${end.toFixed(2)}`,\n count,\n bar: '#'.repeat(count === 0 ? 0 : Math.max(1, Math.round((count / peak) * 10))),\n };\n });\n}\n\nfunction formatMs(value: number): string {\n return `${value.toFixed(2)}ms`;\n}\n\nfunction formatSignedMs(value: number): string {\n const sign = value > 0 ? '+' : '';\n return `${sign}${value.toFixed(2)}ms`;\n}\n\nfunction formatSignedPct(value: number): string {\n const sign = value > 0 ? '+' : '';\n return `${sign}${value.toFixed(2)}%`;\n}\n"],"mappings":"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","names":[]}
package/package.json ADDED
@@ -0,0 +1,75 @@
1
+ {
2
+ "name": "@kiwa-test/perf-harness",
3
+ "version": "0.1.1",
4
+ "description": "Generic performance harness — p50/p95/p99 measurement + regression detection + baseline persistence + 11-axis release gate integration",
5
+ "license": "MIT",
6
+ "author": {
7
+ "name": "cardene",
8
+ "url": "https://github.com/cardene777"
9
+ },
10
+ "keywords": [
11
+ "kiwa",
12
+ "perf",
13
+ "harness",
14
+ "benchmark",
15
+ "regression",
16
+ "release-gate"
17
+ ],
18
+ "repository": {
19
+ "type": "git",
20
+ "url": "git+https://github.com/cardene777/kiwa.git",
21
+ "directory": "packages/perf-harness"
22
+ },
23
+ "homepage": "https://github.com/cardene777/kiwa#readme",
24
+ "bugs": {
25
+ "url": "https://github.com/cardene777/kiwa/issues"
26
+ },
27
+ "type": "module",
28
+ "main": "./dist/index.cjs",
29
+ "module": "./dist/index.js",
30
+ "types": "./dist/index.d.ts",
31
+ "exports": {
32
+ ".": {
33
+ "import": {
34
+ "types": "./dist/index.d.ts",
35
+ "default": "./dist/index.js"
36
+ },
37
+ "require": {
38
+ "types": "./dist/index.d.cts",
39
+ "default": "./dist/index.cjs"
40
+ }
41
+ }
42
+ },
43
+ "files": [
44
+ "dist",
45
+ "README.md"
46
+ ],
47
+ "publishConfig": {
48
+ "access": "public",
49
+ "provenance": true
50
+ },
51
+ "engines": {
52
+ "node": ">=20"
53
+ },
54
+ "scripts": {
55
+ "prebuild": "pnpm -F @kiwa-test/core -F @kiwa-test/quality-metrics build",
56
+ "build": "tsup",
57
+ "pretest": "pnpm -F @kiwa-test/core -F @kiwa-test/quality-metrics build",
58
+ "test": "node -e \"require('node:fs').rmSync('.vitest-dist',{recursive:true,force:true})\" && tsc -p tsconfig.vitest.json && vitest run .vitest-dist/tests --environment node",
59
+ "pretest:cov": "pnpm -F @kiwa-test/core -F @kiwa-test/quality-metrics build",
60
+ "test:cov": "node -e \"require('node:fs').rmSync('.vitest-dist',{recursive:true,force:true})\" && tsc -p tsconfig.vitest.json && vitest run .vitest-dist/tests --environment node --coverage --coverage.provider=v8 --coverage.reporter=json --coverage.reporter=json-summary --coverage.reporter=text --coverage.reportsDirectory=coverage --coverage.include='.vitest-dist/src/**/*.js' --coverage.exclude='.vitest-dist/tests/**'",
61
+ "pretypecheck": "pnpm -F @kiwa-test/core -F @kiwa-test/quality-metrics build",
62
+ "typecheck": "tsc --noEmit"
63
+ },
64
+ "dependencies": {
65
+ "@kiwa-test/core": "workspace:*",
66
+ "@kiwa-test/quality-metrics": "workspace:*"
67
+ },
68
+ "devDependencies": {
69
+ "@types/node": "^22.10.0",
70
+ "@vitest/coverage-v8": "^2.1.0",
71
+ "tsup": "^8.3.0",
72
+ "typescript": "^5.6.0",
73
+ "vitest": "^2.1.0"
74
+ }
75
+ }