@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/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
- if (job.mode === "heap") return ["--expose-gc"];
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 ["--expose-gc", `--min-semi-space-size=${mib}`, `--max-semi-space-size=${mib}`];
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) s.rounds.push({ base, head });
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 c of listCases(options.suite, options.only)) {
584
- const timing = async (latency2) => expectTiming(await runJob({ mode: "timing", ...base, case: c, latency: latency2, inject: 0 }));
585
- const first = await timing(true);
586
- const rest = [];
587
- for (let p = 1; p < processes; p++) rest.push(await timing(false));
588
- const throughput = summarizeThroughput(first, rest);
589
- const latency = first.status === "measured" ? first.latency : null;
590
- let allocations = null;
591
- let peak = null;
592
- let longRun = null;
593
- if (options.memory ?? true) {
594
- allocations = expectMemory(await runJob({ mode: "alloc", ...base, case: c })).allocations;
595
- const { benchmark } = findCase(options.suite, c);
596
- const longRunSize = benchmark.longRun ?? benchmark.sizes[0].label;
597
- const heap = expectMemory(
598
- await runJob({ mode: "heap", ...base, case: c, longRun: c.size === longRunSize })
599
- );
600
- peak = heap.peak;
601
- longRun = heap.longRun;
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 heap of one call above the settled heap, and whether the settled heap stays flat over a long run.`,
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 perOp = t.status === "measured" ? time(t.medianMsPerOp) : `skipped (${t.reason})`;
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 peak = p === null ? "n/a" : p.status === "measured" ? bytes(p.peakHeapBytes) : `skipped (${p.reason})`;
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
- properties: { samplesMsPerOp: nums, medianMsPerOp: num }
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
  ),