@cosyte/bench 0.1.1 → 0.1.2
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 +26 -0
- package/README.md +25 -11
- package/dist/child-entry.mjs +100 -26
- package/dist/child-entry.mjs.map +1 -1
- package/dist/cli.mjs +94 -45
- package/dist/cli.mjs.map +1 -1
- package/dist/index.d.ts +84 -7
- package/dist/index.mjs +19 -2
- package/dist/index.mjs.map +1 -1
- package/package.json +1 -1
- package/schema/results-v1.schema.json +34 -0
package/dist/cli.mjs
CHANGED
|
@@ -67,6 +67,10 @@ var BENCH_CONTRACT = Object.freeze({
|
|
|
67
67
|
GC_SETTLE_ROUNDS: 2,
|
|
68
68
|
GC_MAX_ROUNDS: 7,
|
|
69
69
|
IMPORT_RUNS: 20,
|
|
70
|
+
LARGE_PASS_MS: 50,
|
|
71
|
+
LARGE_WARMUP_WINDOW: 5,
|
|
72
|
+
LARGE_WARMUP_MAX_MS: 3e4,
|
|
73
|
+
LONG_RUN_MAX_MS: 6e4,
|
|
70
74
|
REQUIRED_FALSE_ALARM_RATE: 0.01,
|
|
71
75
|
REQUIRED_CONFIDENCE: 0.95
|
|
72
76
|
});
|
|
@@ -110,9 +114,10 @@ function runNode(entry, args, execArgv, timeoutMs, execPath = process.execPath)
|
|
|
110
114
|
}
|
|
111
115
|
function flagsFor(job) {
|
|
112
116
|
if (job.mode === "timing") return [];
|
|
113
|
-
|
|
117
|
+
const memory = ["--expose-gc", "--no-concurrent-array-buffer-sweeping"];
|
|
118
|
+
if (job.mode === "heap") return memory;
|
|
114
119
|
const mib = String(BENCH_CONTRACT.ALLOC_SEMI_SPACE_MIB);
|
|
115
|
-
return [
|
|
120
|
+
return [...memory, `--min-semi-space-size=${mib}`, `--max-semi-space-size=${mib}`];
|
|
116
121
|
}
|
|
117
122
|
function describe(job) {
|
|
118
123
|
return `${job.mode} job for "${job.case.benchmark} @ ${job.case.size}"`;
|
|
@@ -380,11 +385,18 @@ async function compareBuilds(options) {
|
|
|
380
385
|
s.skip = `${side}: ${result.reason}`;
|
|
381
386
|
break;
|
|
382
387
|
}
|
|
383
|
-
got.set(side, result.samplesMsPerOp);
|
|
388
|
+
got.set(side, { samples: result.samplesMsPerOp, plan: result.warmup.plan });
|
|
384
389
|
}
|
|
385
390
|
const base = got.get("base");
|
|
386
391
|
const head = got.get("head");
|
|
387
|
-
if (base !== void 0 && head !== void 0)
|
|
392
|
+
if (base !== void 0 && head !== void 0) {
|
|
393
|
+
s.rounds.push({
|
|
394
|
+
base: base.samples,
|
|
395
|
+
head: head.samples,
|
|
396
|
+
basePlan: base.plan,
|
|
397
|
+
headPlan: head.plan
|
|
398
|
+
});
|
|
399
|
+
}
|
|
388
400
|
}
|
|
389
401
|
options.onProgress?.(`round ${String(r + 1)} of ${String(BENCH_CONTRACT.ROUNDS)} done`);
|
|
390
402
|
}
|
|
@@ -398,7 +410,9 @@ async function compareBuilds(options) {
|
|
|
398
410
|
baseMsPerOp: rounds.map((p) => Math.min(...p.base)),
|
|
399
411
|
headMsPerOp: rounds.map((p) => Math.min(...p.head)),
|
|
400
412
|
baseSamples: rounds.map((p) => p.base),
|
|
401
|
-
headSamples: rounds.map((p) => p.head)
|
|
413
|
+
headSamples: rounds.map((p) => p.head),
|
|
414
|
+
basePlans: rounds.map((p) => p.basePlan),
|
|
415
|
+
headPlans: rounds.map((p) => p.headPlan)
|
|
402
416
|
};
|
|
403
417
|
if (skip !== null) {
|
|
404
418
|
return {
|
|
@@ -560,7 +574,8 @@ function summarizeThroughput(first, rest) {
|
|
|
560
574
|
}
|
|
561
575
|
const per = measured.map((t) => ({
|
|
562
576
|
samplesMsPerOp: t.samplesMsPerOp,
|
|
563
|
-
medianMsPerOp: t.medianMsPerOp
|
|
577
|
+
medianMsPerOp: t.medianMsPerOp,
|
|
578
|
+
plan: t.warmup.plan
|
|
564
579
|
}));
|
|
565
580
|
const mid = median(per.map((x) => x.medianMsPerOp));
|
|
566
581
|
const perOpBytes = first.bytesPerPass === null ? null : first.bytesPerPass / first.inputsPerPass;
|
|
@@ -572,6 +587,14 @@ function summarizeThroughput(first, rest) {
|
|
|
572
587
|
bytesPerSecond: perOpBytes === null ? null : round(perOpBytes * (1e3 / mid), 0)
|
|
573
588
|
};
|
|
574
589
|
}
|
|
590
|
+
function longRunFits(throughput, inputsPerPass) {
|
|
591
|
+
if (throughput.status !== "measured") return false;
|
|
592
|
+
const roundMs = Math.max(
|
|
593
|
+
BENCH_CONTRACT.LONG_RUN_ROUND_MS,
|
|
594
|
+
throughput.medianMsPerOp * inputsPerPass
|
|
595
|
+
);
|
|
596
|
+
return BENCH_CONTRACT.LONG_RUN_ROUNDS * roundMs <= BENCH_CONTRACT.LONG_RUN_MAX_MS;
|
|
597
|
+
}
|
|
575
598
|
async function readBuild(options) {
|
|
576
599
|
const runJob = options.runJob;
|
|
577
600
|
const processes = options.processes ?? 3;
|
|
@@ -579,38 +602,49 @@ async function readBuild(options) {
|
|
|
579
602
|
throw new TypeError("@cosyte/bench: processes must be a positive integer");
|
|
580
603
|
}
|
|
581
604
|
const base = { suite: options.suiteFile, target: options.target };
|
|
605
|
+
const listed = listCases(options.suite, options.only);
|
|
582
606
|
const cases = [];
|
|
583
|
-
for (const
|
|
584
|
-
const
|
|
585
|
-
const
|
|
586
|
-
|
|
587
|
-
|
|
588
|
-
|
|
589
|
-
|
|
590
|
-
|
|
591
|
-
|
|
592
|
-
|
|
593
|
-
|
|
594
|
-
allocations =
|
|
595
|
-
|
|
596
|
-
|
|
597
|
-
|
|
598
|
-
await runJob({ mode: "
|
|
599
|
-
|
|
600
|
-
|
|
601
|
-
|
|
607
|
+
for (const name of new Set(listed.map((c) => c.benchmark))) {
|
|
608
|
+
const timed = [];
|
|
609
|
+
for (const c of listed.filter((l) => l.benchmark === name)) {
|
|
610
|
+
const timing = async (latency) => expectTiming(await runJob({ mode: "timing", ...base, case: c, latency, inject: 0 }));
|
|
611
|
+
const first = await timing(true);
|
|
612
|
+
const rest = [];
|
|
613
|
+
for (let p = 1; p < processes; p++) rest.push(await timing(false));
|
|
614
|
+
timed.push({ c, first, throughput: summarizeThroughput(first, rest) });
|
|
615
|
+
}
|
|
616
|
+
const largest = timed.filter((t) => longRunFits(t.throughput, t.first.inputsPerPass)).pop();
|
|
617
|
+
for (const { c, first, throughput } of timed) {
|
|
618
|
+
let allocations = null;
|
|
619
|
+
let peak = null;
|
|
620
|
+
let longRun = null;
|
|
621
|
+
if (options.memory ?? true) {
|
|
622
|
+
allocations = expectMemory(await runJob({ mode: "alloc", ...base, case: c })).allocations;
|
|
623
|
+
const { benchmark } = findCase(options.suite, c);
|
|
624
|
+
const longRunSize = benchmark.longRun ?? benchmark.sizes[0].label;
|
|
625
|
+
const heap = expectMemory(
|
|
626
|
+
await runJob({
|
|
627
|
+
mode: "heap",
|
|
628
|
+
...base,
|
|
629
|
+
case: c,
|
|
630
|
+
longRun: c.size === longRunSize || c.size === largest?.c.size
|
|
631
|
+
})
|
|
632
|
+
);
|
|
633
|
+
peak = heap.peak;
|
|
634
|
+
longRun = heap.longRun;
|
|
635
|
+
}
|
|
636
|
+
cases.push({
|
|
637
|
+
...c,
|
|
638
|
+
inputsPerPass: first.inputsPerPass,
|
|
639
|
+
bytesPerPass: first.bytesPerPass,
|
|
640
|
+
throughput,
|
|
641
|
+
latency: first.status === "measured" ? first.latency : null,
|
|
642
|
+
allocations,
|
|
643
|
+
peak,
|
|
644
|
+
longRun
|
|
645
|
+
});
|
|
646
|
+
options.onProgress?.(`read ${c.benchmark} @ ${c.size}`);
|
|
602
647
|
}
|
|
603
|
-
cases.push({
|
|
604
|
-
...c,
|
|
605
|
-
inputsPerPass: first.inputsPerPass,
|
|
606
|
-
bytesPerPass: first.bytesPerPass,
|
|
607
|
-
throughput,
|
|
608
|
-
latency,
|
|
609
|
-
allocations,
|
|
610
|
-
peak,
|
|
611
|
-
longRun
|
|
612
|
-
});
|
|
613
|
-
options.onProgress?.(`read ${c.benchmark} @ ${c.size}`);
|
|
614
648
|
}
|
|
615
649
|
const importCost = options.importCost ?? true ? await options.measureImport(options.target) : null;
|
|
616
650
|
return { processes, cases, importCost };
|
|
@@ -698,26 +732,28 @@ function renderMarkdown(doc) {
|
|
|
698
732
|
const r = doc.reading;
|
|
699
733
|
lines.push(...header(doc, "Reading"));
|
|
700
734
|
lines.push(
|
|
701
|
-
`Throughput is the median of ${String(r.processes)} fresh processes. Latency is per call, nearest rank. Memory: allocations per operation, the peak
|
|
735
|
+
`Throughput is the median of ${String(r.processes)} fresh processes. "Large-case plan" marks a case whose calls each outlast a warmup batch. Latency is per call, nearest rank. Memory: allocations per operation ("collections counted" when every window collected), the peak of one call above what was in use before it (the V8 heap, external memory such as Buffers, and the two together, which a memory budget reads), and whether the settled heap stays flat over a long run.`,
|
|
702
736
|
"",
|
|
703
|
-
"| Benchmark | Size | Per op | Ops/s | Bytes/s | p50 | p95 | p99 | Alloc/op | Peak heap | Long run |",
|
|
704
|
-
"| --- | --- | --- | --- | --- | --- | --- | --- | --- | --- | --- |"
|
|
737
|
+
"| Benchmark | Size | Per op | Ops/s | Bytes/s | p50 | p95 | p99 | Alloc/op | Peak heap | Peak external | Peak total | Long run |",
|
|
738
|
+
"| --- | --- | --- | --- | --- | --- | --- | --- | --- | --- | --- | --- | --- |"
|
|
705
739
|
);
|
|
706
740
|
for (const c of r.cases) {
|
|
707
741
|
const t = c.throughput;
|
|
708
|
-
const
|
|
742
|
+
const large = t.status === "measured" && t.processes.some((p2) => p2.plan === "large") ? " (large-case plan)" : "";
|
|
743
|
+
const perOp = t.status === "measured" ? `${time(t.medianMsPerOp)}${large}` : `skipped (${t.reason})`;
|
|
709
744
|
const ops = t.status === "measured" ? String(t.opsPerSecond) : "n/a";
|
|
710
745
|
const bps = t.status === "measured" && t.bytesPerSecond !== null ? `${bytes(t.bytesPerSecond)}/s` : "n/a";
|
|
711
746
|
const l = c.latency;
|
|
712
747
|
const lat = l === null ? ["n/a", "n/a", "n/a"] : l.status === "measured" ? [ns(l.p50Ns), ns(l.p95Ns), ns(l.p99Ns)] : [`skipped (${l.reason})`, "n/a", "n/a"];
|
|
713
748
|
const a = c.allocations;
|
|
714
|
-
const alloc = a === null ? "n/a" : a.status === "measured" ? bytes(a.heapBytesPerOp + a.externalBytesPerOp) : `skipped (${a.reason})`;
|
|
749
|
+
const alloc = a === null ? "n/a" : a.status === "measured" ? `${bytes(a.heapBytesPerOp + a.externalBytesPerOp)}${a.method === "gc-accounted" ? " (collections counted)" : ""}` : `skipped (${a.reason})`;
|
|
715
750
|
const p = c.peak;
|
|
716
|
-
const
|
|
751
|
+
const optional = (n) => n === void 0 ? "n/a" : bytes(n);
|
|
752
|
+
const peak = p === null ? ["n/a", "n/a", "n/a"] : p.status === "measured" ? [bytes(p.peakHeapBytes), optional(p.peakExternalBytes), optional(p.peakTotalBytes)] : Array.from({ length: 3 }, () => `skipped (${p.reason})`);
|
|
717
753
|
const lr = c.longRun;
|
|
718
754
|
const long = lr === null ? "" : lr.status === "measured" ? `${lr.verdict} (${bytes(lr.growthBytes)} over ${String(lr.ops)} ops, ${String(lr.slopeBytesPerOp)} B/op)` : `skipped (${lr.reason})`;
|
|
719
755
|
lines.push(
|
|
720
|
-
`| ${c.benchmark} | ${c.size} | ${perOp} | ${ops} | ${bps} | ${lat.join(" | ")} | ${alloc} | ${peak} | ${long} |`
|
|
756
|
+
`| ${c.benchmark} | ${c.size} | ${perOp} | ${ops} | ${bps} | ${lat.join(" | ")} | ${alloc} | ${peak.join(" | ")} | ${long} |`
|
|
721
757
|
);
|
|
722
758
|
}
|
|
723
759
|
if (r.importCost !== null) {
|
|
@@ -783,6 +819,7 @@ var skipped = {
|
|
|
783
819
|
properties: { status: { const: "skipped" }, reason: str }
|
|
784
820
|
};
|
|
785
821
|
var measuredOrSkipped = (measured) => ({ oneOf: [measured, skipped] });
|
|
822
|
+
var plan = { enum: ["standard", "large"] };
|
|
786
823
|
var orNull = (s) => ({ oneOf: [{ type: "null" }, s] });
|
|
787
824
|
var RESULTS_SCHEMA = {
|
|
788
825
|
$schema: "https://json-schema.org/draft/2020-12/schema",
|
|
@@ -899,6 +936,9 @@ var RESULTS_SCHEMA = {
|
|
|
899
936
|
headMsPerOp: nums,
|
|
900
937
|
ratios: nums,
|
|
901
938
|
medianRatio: numOrNull,
|
|
939
|
+
// Added within version 1, so not required: a document written before them is still valid.
|
|
940
|
+
basePlans: { type: "array", items: plan },
|
|
941
|
+
headPlans: { type: "array", items: plan },
|
|
902
942
|
interval: { $ref: "#/$defs/interval" },
|
|
903
943
|
verdict: {
|
|
904
944
|
enum: ["regression", "improvement", "no-regression", "inconclusive", "skipped"]
|
|
@@ -1028,7 +1068,8 @@ var RESULTS_SCHEMA = {
|
|
|
1028
1068
|
items: {
|
|
1029
1069
|
type: "object",
|
|
1030
1070
|
required: ["samplesMsPerOp", "medianMsPerOp"],
|
|
1031
|
-
|
|
1071
|
+
// `plan` was added within version 1, so it is not required.
|
|
1072
|
+
properties: { samplesMsPerOp: nums, medianMsPerOp: num, plan }
|
|
1032
1073
|
}
|
|
1033
1074
|
},
|
|
1034
1075
|
medianMsPerOp: num,
|
|
@@ -1055,6 +1096,9 @@ var RESULTS_SCHEMA = {
|
|
|
1055
1096
|
required: ["status", "windowOps", "heapBytesPerOp", "externalBytesPerOp"],
|
|
1056
1097
|
properties: {
|
|
1057
1098
|
status: { const: "measured" },
|
|
1099
|
+
// Added within version 1, so not required.
|
|
1100
|
+
method: { enum: ["no-gc-window", "gc-accounted"] },
|
|
1101
|
+
collections: int,
|
|
1058
1102
|
windowOps: int,
|
|
1059
1103
|
heapBytesPerOp: num,
|
|
1060
1104
|
externalBytesPerOp: num
|
|
@@ -1069,7 +1113,12 @@ var RESULTS_SCHEMA = {
|
|
|
1069
1113
|
status: { const: "measured" },
|
|
1070
1114
|
calls: int,
|
|
1071
1115
|
peakHeapBytes: num,
|
|
1072
|
-
medianPeakHeapBytes: num
|
|
1116
|
+
medianPeakHeapBytes: num,
|
|
1117
|
+
// Added within version 1, so not required.
|
|
1118
|
+
peakExternalBytes: num,
|
|
1119
|
+
medianPeakExternalBytes: num,
|
|
1120
|
+
peakTotalBytes: num,
|
|
1121
|
+
medianPeakTotalBytes: num
|
|
1073
1122
|
}
|
|
1074
1123
|
})
|
|
1075
1124
|
),
|