@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 +55 -0
- package/dist/index.cjs +387 -0
- package/dist/index.cjs.map +1 -0
- package/dist/index.d.cts +73 -0
- package/dist/index.d.ts +73 -0
- package/dist/index.js +355 -0
- package/dist/index.js.map +1 -0
- package/package.json +75 -0
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":[]}
|
package/dist/index.d.cts
ADDED
|
@@ -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.d.ts
ADDED
|
@@ -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
|
+
}
|