@cosyte/bench 0.1.2 → 0.1.3
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 +42 -14
- package/dist/child-entry.mjs +8 -0
- package/dist/child-entry.mjs.map +1 -1
- package/dist/cli.mjs +168 -47
- package/dist/cli.mjs.map +1 -1
- package/dist/index.d.ts +111 -2
- package/dist/index.mjs +50 -18
- package/dist/index.mjs.map +1 -1
- package/package.json +5 -5
- package/schema/results-v1.schema.json +100 -45
package/dist/cli.mjs
CHANGED
|
@@ -7,6 +7,7 @@ import 'perf_hooks';
|
|
|
7
7
|
import { fileURLToPath, pathToFileURL } from 'url';
|
|
8
8
|
import { gzipSync } from 'zlib';
|
|
9
9
|
import { spawn } from 'child_process';
|
|
10
|
+
import 'v8';
|
|
10
11
|
|
|
11
12
|
function readCgroupCpuMax(path = "/sys/fs/cgroup/cpu.max") {
|
|
12
13
|
try {
|
|
@@ -59,6 +60,7 @@ var BENCH_CONTRACT = Object.freeze({
|
|
|
59
60
|
ALLOC_WINDOW_MS: 100,
|
|
60
61
|
ALLOC_SEMI_SPACE_MIB: 64,
|
|
61
62
|
PEAK_CALLS: 20,
|
|
63
|
+
FLOOR_INPUT_BYTES: 64 * 1024,
|
|
62
64
|
LONG_RUN_ROUNDS: 30,
|
|
63
65
|
LONG_RUN_SETTLE_ROUNDS: 5,
|
|
64
66
|
LONG_RUN_ROUND_MS: 100,
|
|
@@ -115,11 +117,12 @@ function runNode(entry, args, execArgv, timeoutMs, execPath = process.execPath)
|
|
|
115
117
|
function flagsFor(job) {
|
|
116
118
|
if (job.mode === "timing") return [];
|
|
117
119
|
const memory = ["--expose-gc", "--no-concurrent-array-buffer-sweeping"];
|
|
118
|
-
if (job.mode === "heap") return memory;
|
|
120
|
+
if (job.mode === "heap" || job.mode === "floor") return memory;
|
|
119
121
|
const mib = String(BENCH_CONTRACT.ALLOC_SEMI_SPACE_MIB);
|
|
120
122
|
return [...memory, `--min-semi-space-size=${mib}`, `--max-semi-space-size=${mib}`];
|
|
121
123
|
}
|
|
122
124
|
function describe(job) {
|
|
125
|
+
if (job.mode === "floor") return "floor job";
|
|
123
126
|
return `${job.mode} job for "${job.case.benchmark} @ ${job.case.size}"`;
|
|
124
127
|
}
|
|
125
128
|
async function runInChild(job, options = {}) {
|
|
@@ -554,18 +557,55 @@ async function calibrateComparison(options) {
|
|
|
554
557
|
}
|
|
555
558
|
return summarizeCalibration(options.inject ?? 0, runs);
|
|
556
559
|
}
|
|
560
|
+
function aboveFloor(peak, floor) {
|
|
561
|
+
if (peak.status !== "measured" || floor === null || floor.status !== "measured") return peak;
|
|
562
|
+
const less = (value, base) => value === void 0 || base === void 0 ? void 0 : Math.max(0, value - base);
|
|
563
|
+
const out = {
|
|
564
|
+
...peak,
|
|
565
|
+
peakHeapAboveFloorBytes: less(peak.peakHeapBytes, floor.medianPeakHeapBytes),
|
|
566
|
+
medianPeakHeapAboveFloorBytes: less(peak.medianPeakHeapBytes, floor.medianPeakHeapBytes),
|
|
567
|
+
peakExternalAboveFloorBytes: less(peak.peakExternalBytes, floor.medianPeakExternalBytes),
|
|
568
|
+
medianPeakExternalAboveFloorBytes: less(
|
|
569
|
+
peak.medianPeakExternalBytes,
|
|
570
|
+
floor.medianPeakExternalBytes
|
|
571
|
+
),
|
|
572
|
+
peakTotalAboveFloorBytes: less(peak.peakTotalBytes, floor.medianPeakTotalBytes),
|
|
573
|
+
medianPeakTotalAboveFloorBytes: less(peak.medianPeakTotalBytes, floor.medianPeakTotalBytes)
|
|
574
|
+
};
|
|
575
|
+
return Object.fromEntries(
|
|
576
|
+
Object.entries(out).filter(([, v]) => v !== void 0)
|
|
577
|
+
);
|
|
578
|
+
}
|
|
557
579
|
|
|
558
580
|
// src/reading.ts
|
|
581
|
+
var WrongKind = class extends Error {
|
|
582
|
+
};
|
|
559
583
|
function expectTiming(result) {
|
|
560
584
|
if (result.mode !== "timing")
|
|
561
|
-
throw new
|
|
585
|
+
throw new WrongKind("@cosyte/bench: a timing job returned another kind");
|
|
562
586
|
return result;
|
|
563
587
|
}
|
|
564
588
|
function expectMemory(result) {
|
|
565
|
-
if (result.mode === "timing")
|
|
566
|
-
throw new
|
|
589
|
+
if (result.mode === "timing" || result.mode === "floor")
|
|
590
|
+
throw new WrongKind("@cosyte/bench: a memory job returned another kind");
|
|
567
591
|
return result;
|
|
568
592
|
}
|
|
593
|
+
function expectFloor(result) {
|
|
594
|
+
if (result.mode !== "floor")
|
|
595
|
+
throw new WrongKind("@cosyte/bench: a floor job returned another kind");
|
|
596
|
+
return result.peak;
|
|
597
|
+
}
|
|
598
|
+
function firstLine(error) {
|
|
599
|
+
const text = error instanceof Error ? error.message : String(error);
|
|
600
|
+
return text.split("\n")[0];
|
|
601
|
+
}
|
|
602
|
+
function budgetOf(c, p) {
|
|
603
|
+
const raw = p.peakTotalBytes ?? p.peakHeapBytes;
|
|
604
|
+
if (c.bytesPerPass === null) return raw;
|
|
605
|
+
const perInput = c.bytesPerPass / c.inputsPerPass;
|
|
606
|
+
if (perInput >= BENCH_CONTRACT.FLOOR_INPUT_BYTES) return raw;
|
|
607
|
+
return p.medianPeakTotalAboveFloorBytes ?? raw;
|
|
608
|
+
}
|
|
569
609
|
function summarizeThroughput(first, rest) {
|
|
570
610
|
const measured = [];
|
|
571
611
|
for (const t of [first, ...rest]) {
|
|
@@ -601,37 +641,56 @@ async function readBuild(options) {
|
|
|
601
641
|
if (!Number.isInteger(processes) || processes < 1) {
|
|
602
642
|
throw new TypeError("@cosyte/bench: processes must be a positive integer");
|
|
603
643
|
}
|
|
644
|
+
const memory = options.memory ?? true;
|
|
604
645
|
const base = { suite: options.suiteFile, target: options.target };
|
|
605
646
|
const listed = listCases(options.suite, options.only);
|
|
606
647
|
const cases = [];
|
|
648
|
+
const failures = [];
|
|
649
|
+
const fail2 = (phase, c, error) => {
|
|
650
|
+
if (error instanceof WrongKind) throw error;
|
|
651
|
+
const message = firstLine(error);
|
|
652
|
+
failures.push({ phase, benchmark: c?.benchmark ?? null, size: c?.size ?? null, message });
|
|
653
|
+
const what = c === null ? "the memory floor" : `${phase} of ${c.benchmark} @ ${c.size}`;
|
|
654
|
+
options.onProgress?.(
|
|
655
|
+
`could not measure ${what}: ${error instanceof Error ? error.message : String(error)}`
|
|
656
|
+
);
|
|
657
|
+
};
|
|
607
658
|
for (const name of new Set(listed.map((c) => c.benchmark))) {
|
|
608
659
|
const timed = [];
|
|
609
660
|
for (const c of listed.filter((l) => l.benchmark === name)) {
|
|
610
661
|
const timing = async (latency) => expectTiming(await runJob({ mode: "timing", ...base, case: c, latency, inject: 0 }));
|
|
611
|
-
|
|
612
|
-
|
|
613
|
-
|
|
614
|
-
|
|
662
|
+
try {
|
|
663
|
+
const first = await timing(true);
|
|
664
|
+
const rest = [];
|
|
665
|
+
for (let p = 1; p < processes; p++) rest.push(await timing(false));
|
|
666
|
+
timed.push({ c, first, throughput: summarizeThroughput(first, rest) });
|
|
667
|
+
} catch (error) {
|
|
668
|
+
fail2("timing", c, error);
|
|
669
|
+
}
|
|
615
670
|
}
|
|
616
671
|
const largest = timed.filter((t) => longRunFits(t.throughput, t.first.inputsPerPass)).pop();
|
|
617
672
|
for (const { c, first, throughput } of timed) {
|
|
618
673
|
let allocations = null;
|
|
619
674
|
let peak = null;
|
|
620
675
|
let longRun = null;
|
|
621
|
-
if (
|
|
622
|
-
|
|
623
|
-
|
|
624
|
-
|
|
625
|
-
|
|
626
|
-
|
|
627
|
-
|
|
628
|
-
|
|
629
|
-
|
|
630
|
-
|
|
631
|
-
|
|
632
|
-
|
|
633
|
-
|
|
634
|
-
|
|
676
|
+
if (memory) {
|
|
677
|
+
try {
|
|
678
|
+
allocations = expectMemory(await runJob({ mode: "alloc", ...base, case: c })).allocations;
|
|
679
|
+
const { benchmark } = findCase(options.suite, c);
|
|
680
|
+
const longRunSize = benchmark.longRun ?? benchmark.sizes[0].label;
|
|
681
|
+
const heap = expectMemory(
|
|
682
|
+
await runJob({
|
|
683
|
+
mode: "heap",
|
|
684
|
+
...base,
|
|
685
|
+
case: c,
|
|
686
|
+
longRun: c.size === longRunSize || c.size === largest?.c.size
|
|
687
|
+
})
|
|
688
|
+
);
|
|
689
|
+
peak = heap.peak;
|
|
690
|
+
longRun = heap.longRun;
|
|
691
|
+
} catch (error) {
|
|
692
|
+
fail2("memory", c, error);
|
|
693
|
+
}
|
|
635
694
|
}
|
|
636
695
|
cases.push({
|
|
637
696
|
...c,
|
|
@@ -646,8 +705,24 @@ async function readBuild(options) {
|
|
|
646
705
|
options.onProgress?.(`read ${c.benchmark} @ ${c.size}`);
|
|
647
706
|
}
|
|
648
707
|
}
|
|
708
|
+
let memoryFloor = null;
|
|
709
|
+
if (memory) {
|
|
710
|
+
try {
|
|
711
|
+
memoryFloor = expectFloor(await runJob({ mode: "floor" }));
|
|
712
|
+
} catch (error) {
|
|
713
|
+
fail2("floor", null, error);
|
|
714
|
+
}
|
|
715
|
+
}
|
|
649
716
|
const importCost = options.importCost ?? true ? await options.measureImport(options.target) : null;
|
|
650
|
-
return {
|
|
717
|
+
return {
|
|
718
|
+
processes,
|
|
719
|
+
cases: cases.map(
|
|
720
|
+
(c) => c.peak === null ? c : { ...c, peak: aboveFloor(c.peak, memoryFloor) }
|
|
721
|
+
),
|
|
722
|
+
memoryFloor,
|
|
723
|
+
failures,
|
|
724
|
+
importCost
|
|
725
|
+
};
|
|
651
726
|
}
|
|
652
727
|
|
|
653
728
|
// src/report.ts
|
|
@@ -732,10 +807,19 @@ function renderMarkdown(doc) {
|
|
|
732
807
|
const r = doc.reading;
|
|
733
808
|
lines.push(...header(doc, "Reading"));
|
|
734
809
|
lines.push(
|
|
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,
|
|
736
|
-
""
|
|
737
|
-
|
|
738
|
-
|
|
810
|
+
`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), the total above the empty-call floor, the peak a memory budget reads (the median call's total above the floor for an input under ${bytes(BENCH_CONTRACT.FLOOR_INPUT_BYTES)}, the highest raw total for any other), and whether the settled heap stays flat over a long run.`,
|
|
811
|
+
""
|
|
812
|
+
);
|
|
813
|
+
const f = r.memoryFloor;
|
|
814
|
+
if (f !== void 0 && f !== null) {
|
|
815
|
+
lines.push(
|
|
816
|
+
f.status === "measured" ? `Empty-call floor, what the peak probe itself costs: ${bytes(f.medianPeakHeapBytes)} heap, ${f.medianPeakTotalBytes === void 0 ? "n/a" : bytes(f.medianPeakTotalBytes)} total (the median of ${String(f.calls)} calls).` : `Empty-call floor: skipped (${f.reason}).`,
|
|
817
|
+
""
|
|
818
|
+
);
|
|
819
|
+
}
|
|
820
|
+
lines.push(
|
|
821
|
+
"| Benchmark | Size | Per op | Ops/s | Bytes/s | p50 | p95 | p99 | Alloc/op | Peak heap | Peak external | Peak total | Above floor | Budget reads | Long run |",
|
|
822
|
+
"| --- | --- | --- | --- | --- | --- | --- | --- | --- | --- | --- | --- | --- | --- | --- |"
|
|
739
823
|
);
|
|
740
824
|
for (const c of r.cases) {
|
|
741
825
|
const t = c.throughput;
|
|
@@ -749,13 +833,30 @@ function renderMarkdown(doc) {
|
|
|
749
833
|
const alloc = a === null ? "n/a" : a.status === "measured" ? `${bytes(a.heapBytesPerOp + a.externalBytesPerOp)}${a.method === "gc-accounted" ? " (collections counted)" : ""}` : `skipped (${a.reason})`;
|
|
750
834
|
const p = c.peak;
|
|
751
835
|
const optional = (n) => n === void 0 ? "n/a" : bytes(n);
|
|
752
|
-
const peak = p === null ? ["n/a", "n/a", "n/a"] : p.status === "measured" ? [
|
|
836
|
+
const peak = p === null ? ["n/a", "n/a", "n/a", "n/a", "n/a"] : p.status === "measured" ? [
|
|
837
|
+
bytes(p.peakHeapBytes),
|
|
838
|
+
optional(p.peakExternalBytes),
|
|
839
|
+
optional(p.peakTotalBytes),
|
|
840
|
+
optional(p.peakTotalAboveFloorBytes),
|
|
841
|
+
bytes(budgetOf(c, p))
|
|
842
|
+
] : Array.from({ length: 5 }, () => `skipped (${p.reason})`);
|
|
753
843
|
const lr = c.longRun;
|
|
754
844
|
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})`;
|
|
755
845
|
lines.push(
|
|
756
846
|
`| ${c.benchmark} | ${c.size} | ${perOp} | ${ops} | ${bps} | ${lat.join(" | ")} | ${alloc} | ${peak.join(" | ")} | ${long} |`
|
|
757
847
|
);
|
|
758
848
|
}
|
|
849
|
+
const failures = r.failures ?? [];
|
|
850
|
+
if (failures.length > 0) {
|
|
851
|
+
lines.push(
|
|
852
|
+
"",
|
|
853
|
+
`Could not measure ${String(failures.length)} part(s) of the run; every other case above was measured and written:`,
|
|
854
|
+
"",
|
|
855
|
+
...failures.map(
|
|
856
|
+
(x) => x.benchmark === null ? `- the empty-call floor: ${x.message}` : `- ${x.phase} of ${x.benchmark} @ ${String(x.size)}: ${x.message}`
|
|
857
|
+
)
|
|
858
|
+
);
|
|
859
|
+
}
|
|
759
860
|
if (r.importCost !== null) {
|
|
760
861
|
const i = r.importCost;
|
|
761
862
|
lines.push(
|
|
@@ -1000,12 +1101,46 @@ var RESULTS_SCHEMA = {
|
|
|
1000
1101
|
}
|
|
1001
1102
|
}
|
|
1002
1103
|
},
|
|
1104
|
+
peak: measuredOrSkipped({
|
|
1105
|
+
type: "object",
|
|
1106
|
+
required: ["status", "calls", "peakHeapBytes", "medianPeakHeapBytes"],
|
|
1107
|
+
properties: {
|
|
1108
|
+
status: { const: "measured" },
|
|
1109
|
+
calls: int,
|
|
1110
|
+
peakHeapBytes: num,
|
|
1111
|
+
medianPeakHeapBytes: num,
|
|
1112
|
+
// Added within version 1, so not required.
|
|
1113
|
+
peakExternalBytes: num,
|
|
1114
|
+
medianPeakExternalBytes: num,
|
|
1115
|
+
peakTotalBytes: num,
|
|
1116
|
+
medianPeakTotalBytes: num,
|
|
1117
|
+
peakHeapAboveFloorBytes: num,
|
|
1118
|
+
medianPeakHeapAboveFloorBytes: num,
|
|
1119
|
+
peakExternalAboveFloorBytes: num,
|
|
1120
|
+
medianPeakExternalAboveFloorBytes: num,
|
|
1121
|
+
peakTotalAboveFloorBytes: num,
|
|
1122
|
+
medianPeakTotalAboveFloorBytes: num
|
|
1123
|
+
}
|
|
1124
|
+
}),
|
|
1125
|
+
failure: {
|
|
1126
|
+
type: "object",
|
|
1127
|
+
required: ["phase", "benchmark", "size", "message"],
|
|
1128
|
+
properties: {
|
|
1129
|
+
phase: { enum: ["timing", "memory", "floor"] },
|
|
1130
|
+
benchmark: strOrNull,
|
|
1131
|
+
size: strOrNull,
|
|
1132
|
+
message: str
|
|
1133
|
+
}
|
|
1134
|
+
},
|
|
1003
1135
|
reading: {
|
|
1004
1136
|
type: "object",
|
|
1005
1137
|
required: ["processes", "cases", "importCost"],
|
|
1006
1138
|
properties: {
|
|
1007
1139
|
processes: int,
|
|
1008
1140
|
cases: { type: "array", items: { $ref: "#/$defs/caseReading" } },
|
|
1141
|
+
// Added within version 1, so not required.
|
|
1142
|
+
memoryFloor: orNull({ $ref: "#/$defs/peak" }),
|
|
1143
|
+
failures: { type: "array", items: { $ref: "#/$defs/failure" } },
|
|
1009
1144
|
importCost: orNull({
|
|
1010
1145
|
type: "object",
|
|
1011
1146
|
required: [
|
|
@@ -1105,23 +1240,7 @@ var RESULTS_SCHEMA = {
|
|
|
1105
1240
|
}
|
|
1106
1241
|
})
|
|
1107
1242
|
),
|
|
1108
|
-
peak: orNull(
|
|
1109
|
-
measuredOrSkipped({
|
|
1110
|
-
type: "object",
|
|
1111
|
-
required: ["status", "calls", "peakHeapBytes", "medianPeakHeapBytes"],
|
|
1112
|
-
properties: {
|
|
1113
|
-
status: { const: "measured" },
|
|
1114
|
-
calls: int,
|
|
1115
|
-
peakHeapBytes: num,
|
|
1116
|
-
medianPeakHeapBytes: num,
|
|
1117
|
-
// Added within version 1, so not required.
|
|
1118
|
-
peakExternalBytes: num,
|
|
1119
|
-
medianPeakExternalBytes: num,
|
|
1120
|
-
peakTotalBytes: num,
|
|
1121
|
-
medianPeakTotalBytes: num
|
|
1122
|
-
}
|
|
1123
|
-
})
|
|
1124
|
-
),
|
|
1243
|
+
peak: orNull({ $ref: "#/$defs/peak" }),
|
|
1125
1244
|
longRun: orNull(
|
|
1126
1245
|
measuredOrSkipped({
|
|
1127
1246
|
type: "object",
|
|
@@ -1235,7 +1354,8 @@ var USAGE = `Usage:
|
|
|
1235
1354
|
cosyte-bench check <results.json>
|
|
1236
1355
|
|
|
1237
1356
|
Exit codes: 0 clean, 1 a finding (a regression, or results that do not match the schema),
|
|
1238
|
-
2 could not measure or could not read its input.
|
|
1357
|
+
2 could not measure or could not read its input. A run that could not measure a case still
|
|
1358
|
+
writes every other case, lists what failed, and exits 2.
|
|
1239
1359
|
`;
|
|
1240
1360
|
var UsageError = class extends Error {
|
|
1241
1361
|
};
|
|
@@ -1423,7 +1543,8 @@ async function main(argv, io) {
|
|
|
1423
1543
|
subject: { target: rel(target), commit },
|
|
1424
1544
|
reading
|
|
1425
1545
|
},
|
|
1426
|
-
|
|
1546
|
+
// Whatever measured is written; a part that could not be is still exit 2.
|
|
1547
|
+
reading.failures.length > 0 ? 2 : 0
|
|
1427
1548
|
);
|
|
1428
1549
|
}
|
|
1429
1550
|
{
|