@cosyte/bench 0.1.2 → 0.1.4

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/index.d.ts CHANGED
@@ -43,6 +43,15 @@ interface BenchContractShape {
43
43
  readonly ALLOC_SEMI_SPACE_MIB: number;
44
44
  /** Calls measured one by one for the peak heap. */
45
45
  readonly PEAK_CALLS: number;
46
+ /**
47
+ * An input under this many bytes, on average, is budgeted on its median call's total peak above
48
+ * the empty-call floor's median, rather than its highest raw peak. On a GitHub-hosted runner the
49
+ * peak probe costs about 3.3 KiB on a typical empty call and 28 KiB on its highest (one call, the
50
+ * first), so the highest of a small input's calls reads the probe more than the code: 20 times a
51
+ * 1 KiB input is spent before the code runs. At 64 KiB even the probe's highest call is about 2%
52
+ * of a 20 times budget, so above it the raw highest peak is read as before.
53
+ */
54
+ readonly FLOOR_INPUT_BYTES: number;
46
55
  /** Rounds in the long run that checks the heap stays flat. */
47
56
  readonly LONG_RUN_ROUNDS: number;
48
57
  /** Rounds at the start of the long run left out of the slope, while caches and compiled code settle. */
@@ -319,6 +328,17 @@ type PeakReport = {
319
328
  readonly peakTotalBytes?: number;
320
329
  /** The median call's peak of the two together. */
321
330
  readonly medianPeakTotalBytes?: number;
331
+ /**
332
+ * Each figure above, less the same figure of the run's empty-call floor (its median), and
333
+ * never below zero. Absent when the run measured no floor, and in a document written before it.
334
+ * See {@link aboveFloor}.
335
+ */
336
+ readonly peakHeapAboveFloorBytes?: number;
337
+ readonly medianPeakHeapAboveFloorBytes?: number;
338
+ readonly peakExternalAboveFloorBytes?: number;
339
+ readonly medianPeakExternalAboveFloorBytes?: number;
340
+ readonly peakTotalAboveFloorBytes?: number;
341
+ readonly medianPeakTotalAboveFloorBytes?: number;
322
342
  } | {
323
343
  readonly status: "skipped";
324
344
  readonly reason: MemorySkipReason;
@@ -549,6 +569,19 @@ type Throughput = {
549
569
  readonly status: "skipped";
550
570
  readonly reason: string;
551
571
  };
572
+ /**
573
+ * A part of a run that could not be measured: a case's timing or memory, or the empty-call floor.
574
+ * The message is the first line of what went wrong. Only the first line is kept, because the lines
575
+ * after it may be a measuring process's stderr, which may quote an input; the command line prints
576
+ * the whole of it to its own stderr.
577
+ */
578
+ interface RunFailure {
579
+ readonly phase: "timing" | "memory" | "floor";
580
+ /** The case, or `null` for the floor. */
581
+ readonly benchmark: string | null;
582
+ readonly size: string | null;
583
+ readonly message: string;
584
+ }
552
585
  /** Everything a reading measured for one case. */
553
586
  interface CaseReading extends CaseRef {
554
587
  readonly inputsPerPass: number;
@@ -564,9 +597,59 @@ interface ReadingBody {
564
597
  /** Fresh processes timed per case. */
565
598
  readonly processes: number;
566
599
  readonly cases: readonly CaseReading[];
600
+ /**
601
+ * The peak of an empty call, measured once per run through the same probe as the cases: what the
602
+ * probe itself costs. Each case's peak also gives its figures above this one. `null` when memory
603
+ * was not measured or the floor could not be. Absent in a document written before it.
604
+ */
605
+ readonly memoryFloor?: PeakReport | null;
606
+ /**
607
+ * What could not be measured. A case whose timing failed is here and not in `cases`; one whose
608
+ * memory failed is in both, with the memory it could not read as `null`. Every other case is
609
+ * written as usual. Empty when everything measured; absent in a document written before it.
610
+ */
611
+ readonly failures?: readonly RunFailure[];
567
612
  /** `null` when import cost was not asked for. */
568
613
  readonly importCost: ImportCostReport | null;
569
614
  }
615
+ /**
616
+ * The peak, in bytes, a memory budget reads for a case, or `null` when the case has none. A case
617
+ * whose inputs average under `FLOOR_INPUT_BYTES` (its bytes a pass over its inputs a pass) reads its
618
+ * median call's total peak above the run's empty-call floor (`medianPeakTotalAboveFloorBytes`); any
619
+ * other case, and one whose suite gave no byte count, reads the highest raw total peak (the V8 heap
620
+ * and external memory together). The probe costs a few KiB on a typical call and tens of KiB on its
621
+ * highest, which is most of the budget of a small input and nothing the code under test did, and a
622
+ * rounding error on a large one. A document without a floor reads the raw highest.
623
+ *
624
+ * @example
625
+ * ```ts
626
+ * import { budgetPeakBytes } from "@cosyte/bench";
627
+ *
628
+ * // 300 bytes an input, and a floor whose median call is 3,400 bytes: the highest call read
629
+ * // 30,000 bytes, most of it the probe, and the median call 6,000 above the floor.
630
+ * budgetPeakBytes({
631
+ * benchmark: "parse",
632
+ * size: "300 B",
633
+ * inputsPerPass: 1,
634
+ * bytesPerPass: 300,
635
+ * throughput: { status: "skipped", reason: "warmup-unstable" },
636
+ * latency: null,
637
+ * allocations: null,
638
+ * peak: {
639
+ * status: "measured",
640
+ * calls: 20,
641
+ * peakHeapBytes: 30_000,
642
+ * medianPeakHeapBytes: 9_400,
643
+ * peakTotalBytes: 30_000,
644
+ * medianPeakTotalBytes: 9_400,
645
+ * peakTotalAboveFloorBytes: 26_600,
646
+ * medianPeakTotalAboveFloorBytes: 6_000,
647
+ * },
648
+ * longRun: null,
649
+ * }); // => 6000
650
+ * ```
651
+ */
652
+ declare function budgetPeakBytes(c: CaseReading): number | null;
570
653
 
571
654
  /**
572
655
  * The machine a measurement was taken on. Every results document carries it, because a number
@@ -1036,6 +1119,25 @@ declare const RESULTS_SCHEMA: {
1036
1119
  };
1037
1120
  };
1038
1121
  };
1122
+ readonly peak: Schema;
1123
+ readonly failure: {
1124
+ readonly type: "object";
1125
+ readonly required: readonly ["phase", "benchmark", "size", "message"];
1126
+ readonly properties: {
1127
+ readonly phase: {
1128
+ readonly enum: readonly ["timing", "memory", "floor"];
1129
+ };
1130
+ readonly benchmark: {
1131
+ readonly type: readonly ["string", "null"];
1132
+ };
1133
+ readonly size: {
1134
+ readonly type: readonly ["string", "null"];
1135
+ };
1136
+ readonly message: {
1137
+ readonly type: "string";
1138
+ };
1139
+ };
1140
+ };
1039
1141
  readonly reading: {
1040
1142
  readonly type: "object";
1041
1143
  readonly required: readonly ["processes", "cases", "importCost"];
@@ -1050,6 +1152,13 @@ declare const RESULTS_SCHEMA: {
1050
1152
  readonly $ref: "#/$defs/caseReading";
1051
1153
  };
1052
1154
  };
1155
+ readonly memoryFloor: Schema;
1156
+ readonly failures: {
1157
+ readonly type: "array";
1158
+ readonly items: {
1159
+ readonly $ref: "#/$defs/failure";
1160
+ };
1161
+ };
1053
1162
  readonly importCost: Schema;
1054
1163
  };
1055
1164
  };
@@ -1116,5 +1225,5 @@ declare class BenchError extends Error {
1116
1225
  readonly name = "BenchError";
1117
1226
  }
1118
1227
 
1119
- export { BENCH_CONTRACT, BenchError, RESULTS_SCHEMA, SCHEMA_VERSION, defineBenchmark, defineSuite, judge, readResults };
1120
- export type { AllocationReport, AnyBenchmark, BenchContractShape, BenchCorpus, BenchSize, BenchSuite, Benchmark, BuildRef, CalibrationBody, CaseComparison, CaseReading, CaseRef, ComparisonBody, CountedRate, Environment, ImportCostReport, LatencyReport, LoadedFile, LongRunReport, MemorySkipReason, PeakReport, ReadingBody, ResultsDocument, Runner, Throughput, Verdict, WarmupReport };
1228
+ export { BENCH_CONTRACT, BenchError, RESULTS_SCHEMA, SCHEMA_VERSION, budgetPeakBytes, defineBenchmark, defineSuite, judge, readResults };
1229
+ export type { AllocationReport, AnyBenchmark, BenchContractShape, BenchCorpus, BenchSize, BenchSuite, Benchmark, BuildRef, CalibrationBody, CaseComparison, CaseReading, CaseRef, ComparisonBody, CountedRate, Environment, ImportCostReport, LatencyReport, LoadedFile, LongRunReport, MemorySkipReason, PeakReport, ReadingBody, ResultsDocument, RunFailure, Runner, Throughput, Verdict, WarmupReport };
package/dist/index.mjs CHANGED
@@ -21,6 +21,7 @@ var BENCH_CONTRACT = Object.freeze({
21
21
  ALLOC_WINDOW_MS: 100,
22
22
  ALLOC_SEMI_SPACE_MIB: 64,
23
23
  PEAK_CALLS: 20,
24
+ FLOOR_INPUT_BYTES: 64 * 1024,
24
25
  LONG_RUN_ROUNDS: 30,
25
26
  LONG_RUN_SETTLE_ROUNDS: 5,
26
27
  LONG_RUN_ROUND_MS: 100,
@@ -142,6 +143,19 @@ function judge(ratios, threshold = BENCH_CONTRACT.THRESHOLD, alpha = BENCH_CONTR
142
143
  else verdict = "inconclusive";
143
144
  return { verdict, interval, medianRatio };
144
145
  }
146
+
147
+ // src/reading.ts
148
+ function budgetPeakBytes(c) {
149
+ const p = c.peak;
150
+ return p === null || p.status !== "measured" ? null : budgetOf(c, p);
151
+ }
152
+ function budgetOf(c, p) {
153
+ const raw = p.peakTotalBytes ?? p.peakHeapBytes;
154
+ if (c.bytesPerPass === null) return raw;
155
+ const perInput = c.bytesPerPass / c.inputsPerPass;
156
+ if (perInput >= BENCH_CONTRACT.FLOOR_INPUT_BYTES) return raw;
157
+ return p.medianPeakTotalAboveFloorBytes ?? raw;
158
+ }
145
159
  var SCHEMA_VERSION = 1;
146
160
  var num = { type: "number" };
147
161
  var int = { type: "integer", minimum: 0 };
@@ -336,12 +350,46 @@ var RESULTS_SCHEMA = {
336
350
  }
337
351
  }
338
352
  },
353
+ peak: measuredOrSkipped({
354
+ type: "object",
355
+ required: ["status", "calls", "peakHeapBytes", "medianPeakHeapBytes"],
356
+ properties: {
357
+ status: { const: "measured" },
358
+ calls: int,
359
+ peakHeapBytes: num,
360
+ medianPeakHeapBytes: num,
361
+ // Added within version 1, so not required.
362
+ peakExternalBytes: num,
363
+ medianPeakExternalBytes: num,
364
+ peakTotalBytes: num,
365
+ medianPeakTotalBytes: num,
366
+ peakHeapAboveFloorBytes: num,
367
+ medianPeakHeapAboveFloorBytes: num,
368
+ peakExternalAboveFloorBytes: num,
369
+ medianPeakExternalAboveFloorBytes: num,
370
+ peakTotalAboveFloorBytes: num,
371
+ medianPeakTotalAboveFloorBytes: num
372
+ }
373
+ }),
374
+ failure: {
375
+ type: "object",
376
+ required: ["phase", "benchmark", "size", "message"],
377
+ properties: {
378
+ phase: { enum: ["timing", "memory", "floor"] },
379
+ benchmark: strOrNull,
380
+ size: strOrNull,
381
+ message: str
382
+ }
383
+ },
339
384
  reading: {
340
385
  type: "object",
341
386
  required: ["processes", "cases", "importCost"],
342
387
  properties: {
343
388
  processes: int,
344
389
  cases: { type: "array", items: { $ref: "#/$defs/caseReading" } },
390
+ // Added within version 1, so not required.
391
+ memoryFloor: orNull({ $ref: "#/$defs/peak" }),
392
+ failures: { type: "array", items: { $ref: "#/$defs/failure" } },
345
393
  importCost: orNull({
346
394
  type: "object",
347
395
  required: [
@@ -441,23 +489,7 @@ var RESULTS_SCHEMA = {
441
489
  }
442
490
  })
443
491
  ),
444
- peak: orNull(
445
- measuredOrSkipped({
446
- type: "object",
447
- required: ["status", "calls", "peakHeapBytes", "medianPeakHeapBytes"],
448
- properties: {
449
- status: { const: "measured" },
450
- calls: int,
451
- peakHeapBytes: num,
452
- medianPeakHeapBytes: num,
453
- // Added within version 1, so not required.
454
- peakExternalBytes: num,
455
- medianPeakExternalBytes: num,
456
- peakTotalBytes: num,
457
- medianPeakTotalBytes: num
458
- }
459
- })
460
- ),
492
+ peak: orNull({ $ref: "#/$defs/peak" }),
461
493
  longRun: orNull(
462
494
  measuredOrSkipped({
463
495
  type: "object",
@@ -567,6 +599,6 @@ Object.freeze({
567
599
  importer: fileURLToPath(new URL("./import-entry.mjs", import.meta.url))
568
600
  });
569
601
 
570
- export { BENCH_CONTRACT, BenchError, RESULTS_SCHEMA, SCHEMA_VERSION, defineBenchmark, defineSuite, judge, readResults };
602
+ export { BENCH_CONTRACT, BenchError, RESULTS_SCHEMA, SCHEMA_VERSION, budgetPeakBytes, defineBenchmark, defineSuite, judge, readResults };
571
603
  //# sourceMappingURL=index.mjs.map
572
604
  //# sourceMappingURL=index.mjs.map