@cosyte/bench 0.0.0-stage → 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/CHANGELOG.md +7 -0
- package/LICENSE +21 -0
- package/README.md +209 -2
- package/dist/child-entry.d.ts +2 -0
- package/dist/child-entry.mjs +480 -0
- package/dist/child-entry.mjs.map +1 -0
- package/dist/cli.d.ts +1 -0
- package/dist/cli.mjs +1441 -0
- package/dist/cli.mjs.map +1 -0
- package/dist/import-entry.d.ts +2 -0
- package/dist/import-entry.mjs +42 -0
- package/dist/import-entry.mjs.map +1 -0
- package/dist/index.d.ts +1043 -0
- package/dist/index.mjs +555 -0
- package/dist/index.mjs.map +1 -0
- package/package.json +74 -3
- package/schema/results-v1.schema.json +747 -0
package/dist/index.mjs
ADDED
|
@@ -0,0 +1,555 @@
|
|
|
1
|
+
import 'module';
|
|
2
|
+
import 'child_process';
|
|
3
|
+
import 'fs';
|
|
4
|
+
import 'os';
|
|
5
|
+
import 'path';
|
|
6
|
+
import { fileURLToPath } from 'url';
|
|
7
|
+
|
|
8
|
+
// src/contract.ts
|
|
9
|
+
var BENCH_CONTRACT = Object.freeze({
|
|
10
|
+
SAMPLES: 5,
|
|
11
|
+
SAMPLE_MIN_MS: 50,
|
|
12
|
+
ROUNDS: 12,
|
|
13
|
+
ALPHA: 0.05,
|
|
14
|
+
THRESHOLD: 0.03,
|
|
15
|
+
LATENCY_BUDGET_MS: 2e3,
|
|
16
|
+
LATENCY_MAX_CALLS: 5e3,
|
|
17
|
+
LATENCY_MIN_CALLS: 20,
|
|
18
|
+
LATENCY_TAIL_CALLS: 10,
|
|
19
|
+
LATENCY_TIMER_MULTIPLE: 20,
|
|
20
|
+
ALLOC_WINDOW_OPS: 256,
|
|
21
|
+
ALLOC_WINDOW_MS: 100,
|
|
22
|
+
ALLOC_SEMI_SPACE_MIB: 64,
|
|
23
|
+
PEAK_CALLS: 20,
|
|
24
|
+
LONG_RUN_ROUNDS: 30,
|
|
25
|
+
LONG_RUN_SETTLE_ROUNDS: 5,
|
|
26
|
+
LONG_RUN_ROUND_MS: 100,
|
|
27
|
+
LONG_RUN_GROWTH_BYTES: 64 * 1024,
|
|
28
|
+
GC_SETTLE_BYTES: 64 * 1024,
|
|
29
|
+
GC_SETTLE_ROUNDS: 2,
|
|
30
|
+
GC_MAX_ROUNDS: 7,
|
|
31
|
+
IMPORT_RUNS: 20,
|
|
32
|
+
REQUIRED_FALSE_ALARM_RATE: 0.01,
|
|
33
|
+
REQUIRED_CONFIDENCE: 0.95
|
|
34
|
+
});
|
|
35
|
+
|
|
36
|
+
// src/suite.ts
|
|
37
|
+
function defineBenchmark(benchmark) {
|
|
38
|
+
return benchmark;
|
|
39
|
+
}
|
|
40
|
+
function defineSuite(suite) {
|
|
41
|
+
return checkSuite(suite);
|
|
42
|
+
}
|
|
43
|
+
function fail(message) {
|
|
44
|
+
throw new TypeError(`@cosyte/bench: ${message}`);
|
|
45
|
+
}
|
|
46
|
+
function isRecord(value) {
|
|
47
|
+
return typeof value === "object" && value !== null;
|
|
48
|
+
}
|
|
49
|
+
function checkName(value, what) {
|
|
50
|
+
if (typeof value !== "string" || value.trim() === "") fail(`${what} must be a non-empty string`);
|
|
51
|
+
return value;
|
|
52
|
+
}
|
|
53
|
+
function checkSuite(value) {
|
|
54
|
+
if (!isRecord(value)) fail("a suite must be an object made by defineSuite");
|
|
55
|
+
const name = checkName(value["name"], "the suite name");
|
|
56
|
+
const benchmarks = value["benchmarks"];
|
|
57
|
+
if (!Array.isArray(benchmarks) || benchmarks.length === 0) {
|
|
58
|
+
fail(`suite "${name}" must have at least one benchmark`);
|
|
59
|
+
}
|
|
60
|
+
const seen = /* @__PURE__ */ new Set();
|
|
61
|
+
for (const b of benchmarks) {
|
|
62
|
+
if (!isRecord(b)) fail(`suite "${name}" holds a benchmark that is not an object`);
|
|
63
|
+
const bName = checkName(b["name"], `a benchmark name in suite "${name}"`);
|
|
64
|
+
if (seen.has(bName)) fail(`suite "${name}" has two benchmarks named "${bName}"`);
|
|
65
|
+
seen.add(bName);
|
|
66
|
+
if (typeof b["bind"] !== "function") fail(`benchmark "${bName}" has no bind function`);
|
|
67
|
+
if (b["weigh"] !== void 0 && typeof b["weigh"] !== "function") {
|
|
68
|
+
fail(`benchmark "${bName}" has a weigh that is not a function`);
|
|
69
|
+
}
|
|
70
|
+
const sizes = b["sizes"];
|
|
71
|
+
if (!Array.isArray(sizes) || sizes.length === 0) fail(`benchmark "${bName}" has no sizes`);
|
|
72
|
+
const labels = /* @__PURE__ */ new Set();
|
|
73
|
+
for (const s of sizes) {
|
|
74
|
+
if (!isRecord(s)) fail(`benchmark "${bName}" holds a size that is not an object`);
|
|
75
|
+
const label = checkName(s["label"], `a size label in benchmark "${bName}"`);
|
|
76
|
+
if (labels.has(label)) fail(`benchmark "${bName}" has two sizes labelled "${label}"`);
|
|
77
|
+
labels.add(label);
|
|
78
|
+
if (typeof s["build"] !== "function")
|
|
79
|
+
fail(`size "${label}" of "${bName}" has no build function`);
|
|
80
|
+
if (s["compare"] !== void 0 && typeof s["compare"] !== "boolean") {
|
|
81
|
+
fail(`size "${label}" of "${bName}" has a compare that is not true or false`);
|
|
82
|
+
}
|
|
83
|
+
}
|
|
84
|
+
if (b["longRun"] !== void 0 && !labels.has(b["longRun"])) {
|
|
85
|
+
fail(`benchmark "${bName}" names a longRun size it does not have`);
|
|
86
|
+
}
|
|
87
|
+
}
|
|
88
|
+
return Object.freeze(value);
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
// src/stats.ts
|
|
92
|
+
function sortedCopy(xs) {
|
|
93
|
+
return [...xs].sort((a, b) => a - b);
|
|
94
|
+
}
|
|
95
|
+
function median(xs) {
|
|
96
|
+
const s = sortedCopy(xs);
|
|
97
|
+
const mid = Math.floor(s.length / 2);
|
|
98
|
+
return s.length % 2 === 1 ? s[mid] : (s[mid - 1] + s[mid]) / 2;
|
|
99
|
+
}
|
|
100
|
+
function binomialCdf(k, n, p) {
|
|
101
|
+
const logP = Math.log(p);
|
|
102
|
+
const logQ = Math.log1p(-p);
|
|
103
|
+
let logCoef = 0;
|
|
104
|
+
let total = 0;
|
|
105
|
+
for (let i = 0; i <= Math.min(k, n); i++) {
|
|
106
|
+
if (i > 0) logCoef += Math.log(n - i + 1) - Math.log(i);
|
|
107
|
+
total += Math.exp(logCoef + i * logP + (n - i) * logQ);
|
|
108
|
+
}
|
|
109
|
+
return Math.min(1, total);
|
|
110
|
+
}
|
|
111
|
+
function signTestRank(n, alpha) {
|
|
112
|
+
let k = 0;
|
|
113
|
+
while (k < n && binomialCdf(k, n, 0.5) <= alpha) k++;
|
|
114
|
+
return k;
|
|
115
|
+
}
|
|
116
|
+
function signTestInterval(xs, alpha) {
|
|
117
|
+
const rank = signTestRank(xs.length, alpha);
|
|
118
|
+
if (rank === 0) return null;
|
|
119
|
+
const s = sortedCopy(xs);
|
|
120
|
+
return {
|
|
121
|
+
low: s[rank - 1],
|
|
122
|
+
high: s[s.length - rank],
|
|
123
|
+
rank,
|
|
124
|
+
tail: binomialCdf(rank - 1, xs.length, 0.5)
|
|
125
|
+
};
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
// src/compare.ts
|
|
129
|
+
function judge(ratios, threshold = BENCH_CONTRACT.THRESHOLD, alpha = BENCH_CONTRACT.ALPHA) {
|
|
130
|
+
const interval = signTestInterval(ratios, alpha);
|
|
131
|
+
const medianRatio = median(ratios);
|
|
132
|
+
if (interval === null) return { verdict: "inconclusive", interval, medianRatio };
|
|
133
|
+
const up = 1 + threshold;
|
|
134
|
+
let verdict;
|
|
135
|
+
if (interval.low > up) verdict = "regression";
|
|
136
|
+
else if (interval.high < 1 / up) verdict = "improvement";
|
|
137
|
+
else if (interval.high <= up) verdict = "no-regression";
|
|
138
|
+
else verdict = "inconclusive";
|
|
139
|
+
return { verdict, interval, medianRatio };
|
|
140
|
+
}
|
|
141
|
+
var SCHEMA_VERSION = 1;
|
|
142
|
+
var num = { type: "number" };
|
|
143
|
+
var int = { type: "integer", minimum: 0 };
|
|
144
|
+
var str = { type: "string" };
|
|
145
|
+
var strOrNull = { type: ["string", "null"] };
|
|
146
|
+
var numOrNull = { type: ["number", "null"] };
|
|
147
|
+
var nums = { type: "array", items: num };
|
|
148
|
+
var skipped = {
|
|
149
|
+
type: "object",
|
|
150
|
+
required: ["status", "reason"],
|
|
151
|
+
properties: { status: { const: "skipped" }, reason: str }
|
|
152
|
+
};
|
|
153
|
+
var measuredOrSkipped = (measured) => ({ oneOf: [measured, skipped] });
|
|
154
|
+
var orNull = (s) => ({ oneOf: [{ type: "null" }, s] });
|
|
155
|
+
var RESULTS_SCHEMA = {
|
|
156
|
+
$schema: "https://json-schema.org/draft/2020-12/schema",
|
|
157
|
+
$id: "https://github.com/cosyte/config/blob/main/packages/bench/schema/results-v1.schema.json",
|
|
158
|
+
title: "@cosyte/bench results, version 1",
|
|
159
|
+
type: "object",
|
|
160
|
+
required: ["schemaVersion", "kind", "tool", "createdAt", "suite", "environment", "contract"],
|
|
161
|
+
properties: {
|
|
162
|
+
schemaVersion: { const: 1 },
|
|
163
|
+
kind: { enum: ["reading", "comparison", "calibration"] },
|
|
164
|
+
tool: {
|
|
165
|
+
type: "object",
|
|
166
|
+
required: ["name", "version"],
|
|
167
|
+
properties: { name: { const: "@cosyte/bench" }, version: str }
|
|
168
|
+
},
|
|
169
|
+
createdAt: str,
|
|
170
|
+
suite: { type: "object", required: ["name", "file"], properties: { name: str, file: str } },
|
|
171
|
+
environment: { $ref: "#/$defs/environment" },
|
|
172
|
+
contract: {
|
|
173
|
+
type: "object",
|
|
174
|
+
required: ["bench", "warmup"],
|
|
175
|
+
properties: { bench: { type: "object" }, warmup: { type: "object" } }
|
|
176
|
+
}
|
|
177
|
+
},
|
|
178
|
+
oneOf: [
|
|
179
|
+
{
|
|
180
|
+
required: ["subject", "reading"],
|
|
181
|
+
properties: {
|
|
182
|
+
kind: { const: "reading" },
|
|
183
|
+
subject: { $ref: "#/$defs/build" },
|
|
184
|
+
reading: { $ref: "#/$defs/reading" }
|
|
185
|
+
}
|
|
186
|
+
},
|
|
187
|
+
{
|
|
188
|
+
required: ["base", "head", "comparison"],
|
|
189
|
+
properties: {
|
|
190
|
+
kind: { const: "comparison" },
|
|
191
|
+
base: { $ref: "#/$defs/build" },
|
|
192
|
+
head: { $ref: "#/$defs/build" },
|
|
193
|
+
comparison: { $ref: "#/$defs/comparison" }
|
|
194
|
+
}
|
|
195
|
+
},
|
|
196
|
+
{
|
|
197
|
+
required: ["base", "head", "calibration"],
|
|
198
|
+
properties: {
|
|
199
|
+
kind: { const: "calibration" },
|
|
200
|
+
base: { $ref: "#/$defs/build" },
|
|
201
|
+
head: { $ref: "#/$defs/build" },
|
|
202
|
+
calibration: { $ref: "#/$defs/calibration" }
|
|
203
|
+
}
|
|
204
|
+
}
|
|
205
|
+
],
|
|
206
|
+
$defs: {
|
|
207
|
+
build: {
|
|
208
|
+
type: "object",
|
|
209
|
+
required: ["target", "commit"],
|
|
210
|
+
properties: { target: str, commit: strOrNull }
|
|
211
|
+
},
|
|
212
|
+
environment: {
|
|
213
|
+
type: "object",
|
|
214
|
+
required: [
|
|
215
|
+
"node",
|
|
216
|
+
"v8",
|
|
217
|
+
"platform",
|
|
218
|
+
"arch",
|
|
219
|
+
"osRelease",
|
|
220
|
+
"cpuModel",
|
|
221
|
+
"logicalCpus",
|
|
222
|
+
"totalMemoryBytes",
|
|
223
|
+
"cgroupCpuMax",
|
|
224
|
+
"runner"
|
|
225
|
+
],
|
|
226
|
+
properties: {
|
|
227
|
+
node: str,
|
|
228
|
+
v8: str,
|
|
229
|
+
platform: str,
|
|
230
|
+
arch: str,
|
|
231
|
+
osRelease: str,
|
|
232
|
+
cpuModel: strOrNull,
|
|
233
|
+
logicalCpus: int,
|
|
234
|
+
totalMemoryBytes: int,
|
|
235
|
+
cgroupCpuMax: strOrNull,
|
|
236
|
+
// Added within version 1, so not required: a document written before it is still valid.
|
|
237
|
+
loadAverage: nums,
|
|
238
|
+
runner: {
|
|
239
|
+
type: "object",
|
|
240
|
+
required: ["ci", "provider", "class"],
|
|
241
|
+
properties: { ci: { type: "boolean" }, provider: strOrNull, class: str }
|
|
242
|
+
}
|
|
243
|
+
}
|
|
244
|
+
},
|
|
245
|
+
interval: orNull({
|
|
246
|
+
type: "object",
|
|
247
|
+
required: ["low", "high", "rank", "tail"],
|
|
248
|
+
properties: { low: num, high: num, rank: int, tail: num }
|
|
249
|
+
}),
|
|
250
|
+
caseComparison: {
|
|
251
|
+
type: "object",
|
|
252
|
+
required: [
|
|
253
|
+
"benchmark",
|
|
254
|
+
"size",
|
|
255
|
+
"baseMsPerOp",
|
|
256
|
+
"headMsPerOp",
|
|
257
|
+
"ratios",
|
|
258
|
+
"medianRatio",
|
|
259
|
+
"interval",
|
|
260
|
+
"verdict",
|
|
261
|
+
"skipReason"
|
|
262
|
+
],
|
|
263
|
+
properties: {
|
|
264
|
+
benchmark: str,
|
|
265
|
+
size: str,
|
|
266
|
+
baseMsPerOp: nums,
|
|
267
|
+
headMsPerOp: nums,
|
|
268
|
+
ratios: nums,
|
|
269
|
+
medianRatio: numOrNull,
|
|
270
|
+
interval: { $ref: "#/$defs/interval" },
|
|
271
|
+
verdict: {
|
|
272
|
+
enum: ["regression", "improvement", "no-regression", "inconclusive", "skipped"]
|
|
273
|
+
},
|
|
274
|
+
skipReason: strOrNull
|
|
275
|
+
}
|
|
276
|
+
},
|
|
277
|
+
comparison: {
|
|
278
|
+
type: "object",
|
|
279
|
+
required: ["rounds", "threshold", "alpha", "inject", "cases"],
|
|
280
|
+
properties: {
|
|
281
|
+
rounds: int,
|
|
282
|
+
threshold: num,
|
|
283
|
+
alpha: num,
|
|
284
|
+
inject: num,
|
|
285
|
+
cases: { type: "array", items: { $ref: "#/$defs/caseComparison" } }
|
|
286
|
+
}
|
|
287
|
+
},
|
|
288
|
+
rate: {
|
|
289
|
+
type: "object",
|
|
290
|
+
required: ["events", "trials", "rate", "upperBound"],
|
|
291
|
+
properties: { events: int, trials: int, rate: num, upperBound: num }
|
|
292
|
+
},
|
|
293
|
+
calibration: {
|
|
294
|
+
type: "object",
|
|
295
|
+
required: [
|
|
296
|
+
"inject",
|
|
297
|
+
"comparisons",
|
|
298
|
+
"runs",
|
|
299
|
+
"perCase",
|
|
300
|
+
"perComparison",
|
|
301
|
+
"verdicts",
|
|
302
|
+
"requiredRule"
|
|
303
|
+
],
|
|
304
|
+
properties: {
|
|
305
|
+
inject: num,
|
|
306
|
+
comparisons: int,
|
|
307
|
+
runs: {
|
|
308
|
+
type: "array",
|
|
309
|
+
items: { type: "array", items: { $ref: "#/$defs/caseComparison" } }
|
|
310
|
+
},
|
|
311
|
+
perCase: { $ref: "#/$defs/rate" },
|
|
312
|
+
perComparison: { $ref: "#/$defs/rate" },
|
|
313
|
+
// Added within version 1, so optional: documents written before it validate unchanged.
|
|
314
|
+
skippedComparisons: { $ref: "#/$defs/rate" },
|
|
315
|
+
verdicts: { type: "object" },
|
|
316
|
+
observed: {
|
|
317
|
+
type: "array",
|
|
318
|
+
items: {
|
|
319
|
+
type: "object",
|
|
320
|
+
required: ["benchmark", "size", "rounds", "medianRatio"],
|
|
321
|
+
properties: { benchmark: str, size: str, rounds: int, medianRatio: numOrNull }
|
|
322
|
+
}
|
|
323
|
+
},
|
|
324
|
+
requiredRule: {
|
|
325
|
+
type: "object",
|
|
326
|
+
required: ["applies", "met", "reason"],
|
|
327
|
+
properties: { applies: { type: "boolean" }, met: { type: "boolean" }, reason: str }
|
|
328
|
+
}
|
|
329
|
+
}
|
|
330
|
+
},
|
|
331
|
+
reading: {
|
|
332
|
+
type: "object",
|
|
333
|
+
required: ["processes", "cases", "importCost"],
|
|
334
|
+
properties: {
|
|
335
|
+
processes: int,
|
|
336
|
+
cases: { type: "array", items: { $ref: "#/$defs/caseReading" } },
|
|
337
|
+
importCost: orNull({
|
|
338
|
+
type: "object",
|
|
339
|
+
required: [
|
|
340
|
+
"runs",
|
|
341
|
+
"coldImportMs",
|
|
342
|
+
"medianMs",
|
|
343
|
+
"p95Ms",
|
|
344
|
+
"emptyImportMs",
|
|
345
|
+
"emptyMedianMs",
|
|
346
|
+
"files",
|
|
347
|
+
"totalBytes",
|
|
348
|
+
"totalGzipBytes"
|
|
349
|
+
],
|
|
350
|
+
properties: {
|
|
351
|
+
runs: int,
|
|
352
|
+
coldImportMs: nums,
|
|
353
|
+
medianMs: num,
|
|
354
|
+
p95Ms: num,
|
|
355
|
+
emptyImportMs: nums,
|
|
356
|
+
emptyMedianMs: num,
|
|
357
|
+
files: {
|
|
358
|
+
type: "array",
|
|
359
|
+
items: {
|
|
360
|
+
type: "object",
|
|
361
|
+
required: ["path", "bytes", "gzipBytes"],
|
|
362
|
+
properties: { path: str, bytes: int, gzipBytes: int }
|
|
363
|
+
}
|
|
364
|
+
},
|
|
365
|
+
totalBytes: int,
|
|
366
|
+
totalGzipBytes: int
|
|
367
|
+
}
|
|
368
|
+
})
|
|
369
|
+
}
|
|
370
|
+
},
|
|
371
|
+
caseReading: {
|
|
372
|
+
type: "object",
|
|
373
|
+
required: [
|
|
374
|
+
"benchmark",
|
|
375
|
+
"size",
|
|
376
|
+
"inputsPerPass",
|
|
377
|
+
"bytesPerPass",
|
|
378
|
+
"throughput",
|
|
379
|
+
"latency",
|
|
380
|
+
"allocations",
|
|
381
|
+
"peak",
|
|
382
|
+
"longRun"
|
|
383
|
+
],
|
|
384
|
+
properties: {
|
|
385
|
+
benchmark: str,
|
|
386
|
+
size: str,
|
|
387
|
+
inputsPerPass: int,
|
|
388
|
+
bytesPerPass: numOrNull,
|
|
389
|
+
throughput: measuredOrSkipped({
|
|
390
|
+
type: "object",
|
|
391
|
+
required: ["status", "processes", "medianMsPerOp", "opsPerSecond", "bytesPerSecond"],
|
|
392
|
+
properties: {
|
|
393
|
+
status: { const: "measured" },
|
|
394
|
+
processes: {
|
|
395
|
+
type: "array",
|
|
396
|
+
items: {
|
|
397
|
+
type: "object",
|
|
398
|
+
required: ["samplesMsPerOp", "medianMsPerOp"],
|
|
399
|
+
properties: { samplesMsPerOp: nums, medianMsPerOp: num }
|
|
400
|
+
}
|
|
401
|
+
},
|
|
402
|
+
medianMsPerOp: num,
|
|
403
|
+
opsPerSecond: num,
|
|
404
|
+
bytesPerSecond: numOrNull
|
|
405
|
+
}
|
|
406
|
+
}),
|
|
407
|
+
latency: orNull({
|
|
408
|
+
type: "object",
|
|
409
|
+
required: ["status", "calls", "timerOverheadNs", "p50Ns"],
|
|
410
|
+
properties: {
|
|
411
|
+
status: { enum: ["measured", "skipped"] },
|
|
412
|
+
calls: int,
|
|
413
|
+
timerOverheadNs: num,
|
|
414
|
+
p50Ns: numOrNull,
|
|
415
|
+
p95Ns: numOrNull,
|
|
416
|
+
p99Ns: numOrNull,
|
|
417
|
+
maxNs: num
|
|
418
|
+
}
|
|
419
|
+
}),
|
|
420
|
+
allocations: orNull(
|
|
421
|
+
measuredOrSkipped({
|
|
422
|
+
type: "object",
|
|
423
|
+
required: ["status", "windowOps", "heapBytesPerOp", "externalBytesPerOp"],
|
|
424
|
+
properties: {
|
|
425
|
+
status: { const: "measured" },
|
|
426
|
+
windowOps: int,
|
|
427
|
+
heapBytesPerOp: num,
|
|
428
|
+
externalBytesPerOp: num
|
|
429
|
+
}
|
|
430
|
+
})
|
|
431
|
+
),
|
|
432
|
+
peak: orNull(
|
|
433
|
+
measuredOrSkipped({
|
|
434
|
+
type: "object",
|
|
435
|
+
required: ["status", "calls", "peakHeapBytes", "medianPeakHeapBytes"],
|
|
436
|
+
properties: {
|
|
437
|
+
status: { const: "measured" },
|
|
438
|
+
calls: int,
|
|
439
|
+
peakHeapBytes: num,
|
|
440
|
+
medianPeakHeapBytes: num
|
|
441
|
+
}
|
|
442
|
+
})
|
|
443
|
+
),
|
|
444
|
+
longRun: orNull(
|
|
445
|
+
measuredOrSkipped({
|
|
446
|
+
type: "object",
|
|
447
|
+
required: ["status", "verdict", "ops", "opsPerRound", "settledHeap", "slopeBytesPerOp"],
|
|
448
|
+
properties: {
|
|
449
|
+
status: { const: "measured" },
|
|
450
|
+
verdict: { enum: ["flat", "growing"] },
|
|
451
|
+
ops: int,
|
|
452
|
+
opsPerRound: int,
|
|
453
|
+
settledHeap: nums,
|
|
454
|
+
slopeBytesPerOp: num
|
|
455
|
+
}
|
|
456
|
+
})
|
|
457
|
+
)
|
|
458
|
+
}
|
|
459
|
+
}
|
|
460
|
+
}
|
|
461
|
+
};
|
|
462
|
+
function typeOf(value) {
|
|
463
|
+
if (value === null) return "null";
|
|
464
|
+
if (Array.isArray(value)) return "array";
|
|
465
|
+
if (Number.isInteger(value)) return "integer";
|
|
466
|
+
return typeof value;
|
|
467
|
+
}
|
|
468
|
+
function typeMatches(value, type) {
|
|
469
|
+
const actual = typeOf(value);
|
|
470
|
+
return actual === type || type === "number" && actual === "integer";
|
|
471
|
+
}
|
|
472
|
+
function resolveRef(ref, root) {
|
|
473
|
+
const target = root.$defs?.[ref.replace("#/$defs/", "")];
|
|
474
|
+
if (target === void 0) throw new Error(`schema has no definition ${ref}`);
|
|
475
|
+
return target;
|
|
476
|
+
}
|
|
477
|
+
function validate(value, schema, root = schema, at = "") {
|
|
478
|
+
const problems = [];
|
|
479
|
+
const here = at === "" ? "/" : at;
|
|
480
|
+
if (typeof schema.$ref === "string")
|
|
481
|
+
return validate(value, resolveRef(schema.$ref, root), root, at);
|
|
482
|
+
if ("const" in schema && value !== schema.const)
|
|
483
|
+
problems.push(`${here}: must be ${JSON.stringify(schema.const)}`);
|
|
484
|
+
if (schema.enum !== void 0 && !schema.enum.includes(value)) {
|
|
485
|
+
problems.push(
|
|
486
|
+
`${here}: must be one of ${schema.enum.map((e) => JSON.stringify(e)).join(", ")}`
|
|
487
|
+
);
|
|
488
|
+
}
|
|
489
|
+
if (schema.type !== void 0) {
|
|
490
|
+
const types = typeof schema.type === "string" ? [schema.type] : schema.type;
|
|
491
|
+
if (!types.some((t) => typeMatches(value, t))) {
|
|
492
|
+
problems.push(`${here}: must be ${types.join(" or ")}, is ${typeOf(value)}`);
|
|
493
|
+
return problems;
|
|
494
|
+
}
|
|
495
|
+
}
|
|
496
|
+
if (typeof value === "number" && value < (schema.minimum ?? -Infinity)) {
|
|
497
|
+
problems.push(`${here}: must be at least ${String(schema.minimum)}`);
|
|
498
|
+
}
|
|
499
|
+
if (typeof value === "object" && value !== null && !Array.isArray(value)) {
|
|
500
|
+
const record = value;
|
|
501
|
+
for (const key of schema.required ?? []) {
|
|
502
|
+
if (!(key in record)) problems.push(`${here}: is missing "${key}"`);
|
|
503
|
+
}
|
|
504
|
+
for (const [key, sub] of Object.entries(schema.properties ?? {})) {
|
|
505
|
+
if (key in record) problems.push(...validate(record[key], sub, root, `${at}/${key}`));
|
|
506
|
+
}
|
|
507
|
+
}
|
|
508
|
+
if (Array.isArray(value) && schema.items !== void 0) {
|
|
509
|
+
const items = schema.items;
|
|
510
|
+
value.forEach((item, i) => problems.push(...validate(item, items, root, `${at}/${String(i)}`)));
|
|
511
|
+
}
|
|
512
|
+
if (schema.oneOf !== void 0) {
|
|
513
|
+
const results = schema.oneOf.map((s) => validate(value, s, root, at));
|
|
514
|
+
const passing = results.filter((r) => r.length === 0).length;
|
|
515
|
+
if (passing !== 1) {
|
|
516
|
+
problems.push(
|
|
517
|
+
`${here}: must match exactly one of ${String(results.length)} shapes, matches ${String(passing)}`
|
|
518
|
+
);
|
|
519
|
+
problems.push(...results.reduce((a, b) => b.length < a.length ? b : a));
|
|
520
|
+
}
|
|
521
|
+
}
|
|
522
|
+
return problems;
|
|
523
|
+
}
|
|
524
|
+
function readResults(text) {
|
|
525
|
+
let value;
|
|
526
|
+
try {
|
|
527
|
+
value = JSON.parse(text);
|
|
528
|
+
} catch {
|
|
529
|
+
throw new TypeError("@cosyte/bench: the results are not JSON");
|
|
530
|
+
}
|
|
531
|
+
const version = value?.schemaVersion;
|
|
532
|
+
if (version !== SCHEMA_VERSION) {
|
|
533
|
+
throw new TypeError(
|
|
534
|
+
`@cosyte/bench: results version ${String(version)} is not one this package reads (${String(SCHEMA_VERSION)})`
|
|
535
|
+
);
|
|
536
|
+
}
|
|
537
|
+
const problems = validate(value, RESULTS_SCHEMA);
|
|
538
|
+
if (problems.length > 0)
|
|
539
|
+
throw new TypeError(
|
|
540
|
+
`@cosyte/bench: the results do not match the schema:
|
|
541
|
+
${problems.join("\n")}`
|
|
542
|
+
);
|
|
543
|
+
return value;
|
|
544
|
+
}
|
|
545
|
+
var BenchError = class extends Error {
|
|
546
|
+
name = "BenchError";
|
|
547
|
+
};
|
|
548
|
+
Object.freeze({
|
|
549
|
+
child: fileURLToPath(new URL("./child-entry.mjs", import.meta.url)),
|
|
550
|
+
importer: fileURLToPath(new URL("./import-entry.mjs", import.meta.url))
|
|
551
|
+
});
|
|
552
|
+
|
|
553
|
+
export { BENCH_CONTRACT, BenchError, RESULTS_SCHEMA, SCHEMA_VERSION, defineBenchmark, defineSuite, judge, readResults };
|
|
554
|
+
//# sourceMappingURL=index.mjs.map
|
|
555
|
+
//# sourceMappingURL=index.mjs.map
|