@cosyte/bench 0.1.1 → 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/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. */
@@ -59,6 +68,20 @@ interface BenchContractShape {
59
68
  readonly GC_MAX_ROUNDS: number;
60
69
  /** Fresh processes timed for the cold import, for the target and for an empty module each. */
61
70
  readonly IMPORT_RUNS: number;
71
+ /**
72
+ * A case whose last three warmup batches each took at least this many milliseconds a pass runs on
73
+ * the large-case plan: every batch is one pass, so the warmup rule's aggregation cannot happen.
74
+ */
75
+ readonly LARGE_PASS_MS: number;
76
+ /** Passes in each of the two windows whose medians the large-case warmup compares. */
77
+ readonly LARGE_WARMUP_WINDOW: number;
78
+ /** Hard cap on the large-case warmup, in milliseconds, from the start of the warmup. */
79
+ readonly LARGE_WARMUP_MAX_MS: number;
80
+ /**
81
+ * The long run also runs on a benchmark's largest size whose rounds fit in this many milliseconds,
82
+ * a round being at least one pass.
83
+ */
84
+ readonly LONG_RUN_MAX_MS: number;
62
85
  /** The timing comparison may become a required check when A/A false alarms are under this rate. */
63
86
  readonly REQUIRED_FALSE_ALARM_RATE: number;
64
87
  /** The confidence that the false alarm rate is under the line, for the rule above. */
@@ -199,16 +222,27 @@ declare const WARMUP: Readonly<{
199
222
  STABLE_TOL: number;
200
223
  STABLE_BATCHES: number;
201
224
  }>;
225
+ /**
226
+ * Which warmup rule a process ended on. `standard` is ADR 0001 section 2's. `large` is the
227
+ * large-case plan, for a case whose every batch is a single pass: see {@link warmUp}.
228
+ */
229
+ type WarmupPlan = "standard" | "large";
202
230
  /** What the warmup did. */
203
231
  interface WarmupReport {
204
- /** Whether three batches in a row came within 5% of their median before 5 s ran out. */
232
+ /**
233
+ * Whether the warmup settled: on the standard plan, three batches in a row within 5% of their
234
+ * median before 5 s ran out; on the large-case plan, two windows of passes with medians within 5%
235
+ * of each other before 30 s ran out.
236
+ */
205
237
  readonly stable: boolean;
206
238
  /** Wall time spent warming, in milliseconds. */
207
239
  readonly elapsedMs: number;
208
240
  /** Passes over the corpus. */
209
241
  readonly passes: number;
210
- /** Each batch's milliseconds per pass. */
242
+ /** Each batch's milliseconds per pass. On the large-case plan every batch after the switch is one pass. */
211
243
  readonly batches: readonly number[];
244
+ /** The rule the warmup ended on. */
245
+ readonly plan: WarmupPlan;
212
246
  }
213
247
  /** Per-call latency, or why there is none. */
214
248
  type LatencyReport = {
@@ -235,16 +269,32 @@ type LatencyReport = {
235
269
  *
236
270
  * They read the heap through a {@link HeapProbe}, so their tests can drive them with a scripted heap.
237
271
  * The real probe follows ADR 0001 section 8: a zero-argument `gc()` only, settled by rule rather than
238
- * by a count.
272
+ * by a count. The measuring process also runs with `--no-concurrent-array-buffer-sweeping`, so a
273
+ * collection frees the ArrayBuffers it found dead before it ends, and the external memory a
274
+ * collection reports before and after is what it held and what it kept.
239
275
  *
240
276
  * @packageDocumentation
241
277
  */
242
278
 
243
- /** Why a memory measurement has no number. */
279
+ /**
280
+ * Why a memory measurement has no number. `allocation-window-gc` is no longer written: a case whose
281
+ * every window collects is now counted with its collections. It stays here because documents
282
+ * written before carry it.
283
+ */
244
284
  type MemorySkipReason = "gc-unavailable" | "gc-unsettled" | "allocation-window-gc";
285
+ /**
286
+ * How allocations were counted. `no-gc-window`: the heap's growth over windows in which no
287
+ * collection ran. `gc-accounted`: every window collected, so the growth over each window plus what
288
+ * its collections reclaimed.
289
+ */
290
+ type AllocationMethod = "no-gc-window" | "gc-accounted";
245
291
  /** Allocations per operation. */
246
292
  type AllocationReport = {
247
293
  readonly status: "measured";
294
+ /** How they were counted. Absent in a document written before it was recorded: `no-gc-window`. */
295
+ readonly method?: AllocationMethod;
296
+ /** Collections that ran in the two windows counted: 0 for `no-gc-window`. */
297
+ readonly collections?: number;
248
298
  /** The smaller of the two windows, in operations. */
249
299
  readonly windowOps: number;
250
300
  /** Bytes allocated on the V8 heap per operation. */
@@ -255,14 +305,40 @@ type AllocationReport = {
255
305
  readonly status: "skipped";
256
306
  readonly reason: MemorySkipReason;
257
307
  };
258
- /** Peak heap per call. */
308
+ /** Peak memory per call: the V8 heap, external memory, and the two together. */
259
309
  type PeakReport = {
260
310
  readonly status: "measured";
261
311
  readonly calls: number;
262
- /** The highest peak of any call, in bytes above the settled heap before it. */
312
+ /** The highest V8 heap peak of any call, in bytes above the settled heap before it. */
263
313
  readonly peakHeapBytes: number;
264
- /** The median call's peak. */
314
+ /** The median call's V8 heap peak. */
265
315
  readonly medianPeakHeapBytes: number;
316
+ /**
317
+ * The highest external peak of any call (Buffers, ArrayBuffers), above the external memory
318
+ * held before it. Absent in a document written before external memory was read.
319
+ */
320
+ readonly peakExternalBytes?: number;
321
+ /** The median call's external peak. */
322
+ readonly medianPeakExternalBytes?: number;
323
+ /**
324
+ * The highest peak of the V8 heap and external memory together, above the two before the call:
325
+ * the figure a memory budget reads. Not the sum of the two peaks above, which need not come at
326
+ * one moment.
327
+ */
328
+ readonly peakTotalBytes?: number;
329
+ /** The median call's peak of the two together. */
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;
266
342
  } | {
267
343
  readonly status: "skipped";
268
344
  readonly reason: MemorySkipReason;
@@ -356,6 +432,13 @@ interface CaseComparison extends CaseRef {
356
432
  readonly baseSamples: readonly (readonly number[])[];
357
433
  /** Each round's head samples, whole. */
358
434
  readonly headSamples: readonly (readonly number[])[];
435
+ /**
436
+ * Each round's base process: the warmup rule it ended on. Absent in a document written before it
437
+ * was recorded.
438
+ */
439
+ readonly basePlans?: readonly WarmupPlan[];
440
+ /** Each round's head process: the warmup rule it ended on. */
441
+ readonly headPlans?: readonly WarmupPlan[];
359
442
  /** Each round's `head / base`. */
360
443
  readonly ratios: readonly number[];
361
444
  readonly medianRatio: number | null;
@@ -474,6 +557,8 @@ type Throughput = {
474
557
  readonly processes: readonly {
475
558
  readonly samplesMsPerOp: readonly number[];
476
559
  readonly medianMsPerOp: number;
560
+ /** The warmup rule the process ended on. Absent in a document written before it was recorded. */
561
+ readonly plan?: WarmupPlan;
477
562
  }[];
478
563
  /** The median of the process medians. */
479
564
  readonly medianMsPerOp: number;
@@ -484,6 +569,19 @@ type Throughput = {
484
569
  readonly status: "skipped";
485
570
  readonly reason: string;
486
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
+ }
487
585
  /** Everything a reading measured for one case. */
488
586
  interface CaseReading extends CaseRef {
489
587
  readonly inputsPerPass: number;
@@ -499,9 +597,59 @@ interface ReadingBody {
499
597
  /** Fresh processes timed per case. */
500
598
  readonly processes: number;
501
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[];
502
612
  /** `null` when import cost was not asked for. */
503
613
  readonly importCost: ImportCostReport | null;
504
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;
505
653
 
506
654
  /**
507
655
  * The machine a measurement was taken on. Every results document carries it, because a number
@@ -832,6 +980,18 @@ declare const RESULTS_SCHEMA: {
832
980
  readonly medianRatio: {
833
981
  readonly type: readonly ["number", "null"];
834
982
  };
983
+ readonly basePlans: {
984
+ readonly type: "array";
985
+ readonly items: {
986
+ readonly enum: readonly ["standard", "large"];
987
+ };
988
+ };
989
+ readonly headPlans: {
990
+ readonly type: "array";
991
+ readonly items: {
992
+ readonly enum: readonly ["standard", "large"];
993
+ };
994
+ };
835
995
  readonly interval: {
836
996
  readonly $ref: "#/$defs/interval";
837
997
  };
@@ -959,6 +1119,25 @@ declare const RESULTS_SCHEMA: {
959
1119
  };
960
1120
  };
961
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
+ };
962
1141
  readonly reading: {
963
1142
  readonly type: "object";
964
1143
  readonly required: readonly ["processes", "cases", "importCost"];
@@ -973,6 +1152,13 @@ declare const RESULTS_SCHEMA: {
973
1152
  readonly $ref: "#/$defs/caseReading";
974
1153
  };
975
1154
  };
1155
+ readonly memoryFloor: Schema;
1156
+ readonly failures: {
1157
+ readonly type: "array";
1158
+ readonly items: {
1159
+ readonly $ref: "#/$defs/failure";
1160
+ };
1161
+ };
976
1162
  readonly importCost: Schema;
977
1163
  };
978
1164
  };
@@ -1039,5 +1225,5 @@ declare class BenchError extends Error {
1039
1225
  readonly name = "BenchError";
1040
1226
  }
1041
1227
 
1042
- export { BENCH_CONTRACT, BenchError, RESULTS_SCHEMA, SCHEMA_VERSION, defineBenchmark, defineSuite, judge, readResults };
1043
- 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,
@@ -29,6 +30,10 @@ var BENCH_CONTRACT = Object.freeze({
29
30
  GC_SETTLE_ROUNDS: 2,
30
31
  GC_MAX_ROUNDS: 7,
31
32
  IMPORT_RUNS: 20,
33
+ LARGE_PASS_MS: 50,
34
+ LARGE_WARMUP_WINDOW: 5,
35
+ LARGE_WARMUP_MAX_MS: 3e4,
36
+ LONG_RUN_MAX_MS: 6e4,
32
37
  REQUIRED_FALSE_ALARM_RATE: 0.01,
33
38
  REQUIRED_CONFIDENCE: 0.95
34
39
  });
@@ -138,6 +143,19 @@ function judge(ratios, threshold = BENCH_CONTRACT.THRESHOLD, alpha = BENCH_CONTR
138
143
  else verdict = "inconclusive";
139
144
  return { verdict, interval, medianRatio };
140
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
+ }
141
159
  var SCHEMA_VERSION = 1;
142
160
  var num = { type: "number" };
143
161
  var int = { type: "integer", minimum: 0 };
@@ -151,6 +169,7 @@ var skipped = {
151
169
  properties: { status: { const: "skipped" }, reason: str }
152
170
  };
153
171
  var measuredOrSkipped = (measured) => ({ oneOf: [measured, skipped] });
172
+ var plan = { enum: ["standard", "large"] };
154
173
  var orNull = (s) => ({ oneOf: [{ type: "null" }, s] });
155
174
  var RESULTS_SCHEMA = {
156
175
  $schema: "https://json-schema.org/draft/2020-12/schema",
@@ -267,6 +286,9 @@ var RESULTS_SCHEMA = {
267
286
  headMsPerOp: nums,
268
287
  ratios: nums,
269
288
  medianRatio: numOrNull,
289
+ // Added within version 1, so not required: a document written before them is still valid.
290
+ basePlans: { type: "array", items: plan },
291
+ headPlans: { type: "array", items: plan },
270
292
  interval: { $ref: "#/$defs/interval" },
271
293
  verdict: {
272
294
  enum: ["regression", "improvement", "no-regression", "inconclusive", "skipped"]
@@ -328,12 +350,46 @@ var RESULTS_SCHEMA = {
328
350
  }
329
351
  }
330
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
+ },
331
384
  reading: {
332
385
  type: "object",
333
386
  required: ["processes", "cases", "importCost"],
334
387
  properties: {
335
388
  processes: int,
336
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" } },
337
393
  importCost: orNull({
338
394
  type: "object",
339
395
  required: [
@@ -396,7 +452,8 @@ var RESULTS_SCHEMA = {
396
452
  items: {
397
453
  type: "object",
398
454
  required: ["samplesMsPerOp", "medianMsPerOp"],
399
- properties: { samplesMsPerOp: nums, medianMsPerOp: num }
455
+ // `plan` was added within version 1, so it is not required.
456
+ properties: { samplesMsPerOp: nums, medianMsPerOp: num, plan }
400
457
  }
401
458
  },
402
459
  medianMsPerOp: num,
@@ -423,24 +480,16 @@ var RESULTS_SCHEMA = {
423
480
  required: ["status", "windowOps", "heapBytesPerOp", "externalBytesPerOp"],
424
481
  properties: {
425
482
  status: { const: "measured" },
483
+ // Added within version 1, so not required.
484
+ method: { enum: ["no-gc-window", "gc-accounted"] },
485
+ collections: int,
426
486
  windowOps: int,
427
487
  heapBytesPerOp: num,
428
488
  externalBytesPerOp: num
429
489
  }
430
490
  })
431
491
  ),
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
- ),
492
+ peak: orNull({ $ref: "#/$defs/peak" }),
444
493
  longRun: orNull(
445
494
  measuredOrSkipped({
446
495
  type: "object",
@@ -550,6 +599,6 @@ Object.freeze({
550
599
  importer: fileURLToPath(new URL("./import-entry.mjs", import.meta.url))
551
600
  });
552
601
 
553
- 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 };
554
603
  //# sourceMappingURL=index.mjs.map
555
604
  //# sourceMappingURL=index.mjs.map