@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/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,31 @@
|
|
|
1
1
|
# Changelog
|
|
2
2
|
|
|
3
|
+
## 0.1.2
|
|
4
|
+
|
|
5
|
+
### Patch Changes
|
|
6
|
+
|
|
7
|
+
- c0c095a: Large cases are measured instead of skipped. A case whose single call takes longer than a warmup
|
|
8
|
+
batch (50 ms) and has not settled by the scaling gate's rule after 500 ms moves to the large-case
|
|
9
|
+
plan: one call at a time until the median of the last 5 calls is within 5% of the median of the 5
|
|
10
|
+
before them, at most 30 s. Each timing process records the plan it ended on (`plan` in a reading's
|
|
11
|
+
`throughput.processes`, `basePlans` and `headPlans` in a comparison), and a reading marks such a
|
|
12
|
+
case. A case that settles by the standard rule warms up exactly as before.
|
|
13
|
+
|
|
14
|
+
Allocations per operation are counted on a case whose every window collects: the largest pair of
|
|
15
|
+
windows is counted with what its collections reclaimed (`method: "gc-accounted"`, with the number of
|
|
16
|
+
`collections`), where before the case was skipped as `allocation-window-gc`.
|
|
17
|
+
|
|
18
|
+
The peak reports external memory (Buffers, ArrayBuffers) beside the V8 heap, and the two together:
|
|
19
|
+
`peakExternalBytes`, `peakTotalBytes` and their medians. A memory budget reads `peakTotalBytes`.
|
|
20
|
+
Memory processes run with `--no-concurrent-array-buffer-sweeping`, so a collection frees dead
|
|
21
|
+
ArrayBuffers before it ends and the external memory it reports is exact.
|
|
22
|
+
|
|
23
|
+
The long run also runs on a benchmark's largest size whose 30 rounds fit in 60 s, besides its
|
|
24
|
+
long-run size. A reading now times every size of a benchmark before measuring its memory.
|
|
25
|
+
|
|
26
|
+
Every new field is added within results version 1 and none is renamed; documents written before
|
|
27
|
+
still validate.
|
|
28
|
+
|
|
3
29
|
## 0.1.1
|
|
4
30
|
|
|
5
31
|
### Patch Changes
|
package/README.md
CHANGED
|
@@ -131,21 +131,35 @@ the command line:
|
|
|
131
131
|
|
|
132
132
|
- the sizes, and which of them a pull request's comparison runs (`compare: false` leaves a slow size
|
|
133
133
|
to the readings);
|
|
134
|
-
- `weigh`, the number a result is reduced to, and `longRun`, the size the long run uses
|
|
134
|
+
- `weigh`, the number a result is reduced to, and `longRun`, the size the long run uses besides the
|
|
135
|
+
largest size whose long run fits in 60 s;
|
|
135
136
|
- `--only <benchmark or case>`, `--processes <n>`, `--no-memory` and `--no-import` on `run`.
|
|
136
137
|
|
|
137
138
|
## What it measures
|
|
138
139
|
|
|
139
|
-
| Measure | How
|
|
140
|
-
| -------------------------------------- |
|
|
141
|
-
| Throughput, small to very large inputs | Per size, fresh processes; a warmup by the scaling gate's rule; 5 samples of whole passes of at least 50 ms; the median of the processes' medians
|
|
142
|
-
| Latency p50, p95, p99 | One call at a time on a nanosecond clock, up to 2 s or 5,000 calls; a percentile is reported only with at least 10 calls above it
|
|
143
|
-
| Allocations per operation | Heap growth over windows of `n` and `2n` operations in which no collection ran, in a process with a 64 MiB young generation; the difference cancels the cost of reading the heap
|
|
144
|
-
| Peak
|
|
145
|
-
| Heap flat over a long run | 30 rounds, the heap settled after each; a Theil-Sen slope over the last 24; growing when the fitted growth reaches 64 KiB
|
|
146
|
-
| Cold import and loaded bytes | 20 fresh processes import the target, 20 import an empty module, interleaved; one more records every file the import loads, with its gzip size
|
|
147
|
-
|
|
148
|
-
Memory measurements need `gc()`; the runner starts those processes with `--expose-gc` itself
|
|
140
|
+
| Measure | How | What it cannot see |
|
|
141
|
+
| -------------------------------------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | ----------------------------------------------------------------------------------------------------------------------------------- |
|
|
142
|
+
| Throughput, small to very large inputs | Per size, fresh processes; a warmup by the scaling gate's rule, or by the large-case plan below when every warmup batch is one long pass; 5 samples of whole passes of at least 50 ms; the median of the processes' medians | Anything the suite's sizes do not cover; numbers from another machine |
|
|
143
|
+
| Latency p50, p95, p99 | One call at a time on a nanosecond clock, up to 2 s or 5,000 calls; a percentile is reported only with at least 10 calls above it | Calls shorter than 20 clock reads (skipped, typed); the tail of a size with few calls |
|
|
144
|
+
| Allocations per operation | Heap growth over windows of `n` and `2n` operations in which no collection ran, in a process with a 64 MiB young generation; the difference cancels the cost of reading the heap. When every window collects, the largest pair is counted with what its collections reclaimed (`method: "gc-accounted"`) | Native memory a library allocates outside V8's accounting |
|
|
145
|
+
| Peak memory | Per call, the higher of what was in use just before any collection during the call and at its end, above what was in use before it: the V8 heap (`peakHeapBytes`), external memory such as Buffers and ArrayBuffers (`peakExternalBytes`), and the two together (`peakTotalBytes`), which is what a memory budget reads | Native memory a library allocates outside V8's accounting |
|
|
146
|
+
| Heap flat over a long run | 30 rounds, the heap settled after each; a Theil-Sen slope over the last 24; growing when the fitted growth reaches 64 KiB. On the long-run size and on the largest size whose 30 rounds fit in 60 s | A leak too small to reach 64 KiB over the run (each result says the smallest it could see, `detectableBytesPerOp`); external memory |
|
|
147
|
+
| Cold import and loaded bytes | 20 fresh processes import the target, 20 import an empty module, interleaved; one more records every file the import loads, with its gzip size | What a bundler keeps after tree shaking |
|
|
148
|
+
|
|
149
|
+
Memory measurements need `gc()`; the runner starts those processes with `--expose-gc` itself, and
|
|
150
|
+
with `--no-concurrent-array-buffer-sweeping`, so a collection frees the ArrayBuffers it found dead
|
|
151
|
+
before it ends and the external memory it reports before and after is exact.
|
|
152
|
+
|
|
153
|
+
### Large cases
|
|
154
|
+
|
|
155
|
+
A case whose single call takes longer than a warmup batch (50 ms) cannot settle by the scaling
|
|
156
|
+
gate's warmup rule on a shared runner: every batch is one call, and one call's jitter is what the
|
|
157
|
+
batch was there to average away. When 500 ms of warmup have passed, the rule has not settled, and
|
|
158
|
+
the last three batches were each one call of at least 50 ms, the warmup moves to the large-case
|
|
159
|
+
plan: one call at a time until the median of the last 5 calls is within 5% of the median of the 5
|
|
160
|
+
before them, at most 30 s from the start. Its samples are the same 5, each one call at that size.
|
|
161
|
+
Each process records the plan it ended on (`plan`), and a reading marks such a case. A case that
|
|
162
|
+
settles by the standard rule never moves, so small cases are measured exactly as before.
|
|
149
163
|
|
|
150
164
|
## The comparison
|
|
151
165
|
|
package/dist/child-entry.mjs
CHANGED
|
@@ -29,6 +29,10 @@ var BENCH_CONTRACT = Object.freeze({
|
|
|
29
29
|
GC_SETTLE_ROUNDS: 2,
|
|
30
30
|
GC_MAX_ROUNDS: 7,
|
|
31
31
|
IMPORT_RUNS: 20,
|
|
32
|
+
LARGE_PASS_MS: 50,
|
|
33
|
+
LARGE_WARMUP_WINDOW: 5,
|
|
34
|
+
LARGE_WARMUP_MAX_MS: 3e4,
|
|
35
|
+
LONG_RUN_MAX_MS: 6e4,
|
|
32
36
|
REQUIRED_FALSE_ALARM_RATE: 0.01,
|
|
33
37
|
REQUIRED_CONFIDENCE: 0.95
|
|
34
38
|
});
|
|
@@ -78,9 +82,27 @@ function realProbe() {
|
|
|
78
82
|
profiler.start();
|
|
79
83
|
fn();
|
|
80
84
|
const stats = profiler.stop().statistics;
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
85
|
+
const report = {
|
|
86
|
+
gcs: stats.length,
|
|
87
|
+
peakBeforeGc: 0,
|
|
88
|
+
peakExternalBeforeGc: 0,
|
|
89
|
+
peakTotalBeforeGc: 0,
|
|
90
|
+
reclaimedHeap: 0,
|
|
91
|
+
reclaimedExternal: 0
|
|
92
|
+
};
|
|
93
|
+
for (const s of stats) {
|
|
94
|
+
const before = s.beforeGC.heapStatistics;
|
|
95
|
+
const after = s.afterGC.heapStatistics;
|
|
96
|
+
report.peakBeforeGc = Math.max(report.peakBeforeGc, before.usedHeapSize);
|
|
97
|
+
report.peakExternalBeforeGc = Math.max(report.peakExternalBeforeGc, before.externalMemory);
|
|
98
|
+
report.peakTotalBeforeGc = Math.max(
|
|
99
|
+
report.peakTotalBeforeGc,
|
|
100
|
+
before.usedHeapSize + before.externalMemory
|
|
101
|
+
);
|
|
102
|
+
report.reclaimedHeap += before.usedHeapSize - after.usedHeapSize;
|
|
103
|
+
report.reclaimedExternal += before.externalMemory - after.externalMemory;
|
|
104
|
+
}
|
|
105
|
+
return report;
|
|
84
106
|
}
|
|
85
107
|
};
|
|
86
108
|
}
|
|
@@ -102,43 +124,63 @@ function measureAllocations(call, probe, msPerOp2) {
|
|
|
102
124
|
const s = settle(probe);
|
|
103
125
|
const h0 = probe.usedHeap();
|
|
104
126
|
const e0 = probe.external();
|
|
105
|
-
const
|
|
127
|
+
const p = probe.profile(() => {
|
|
106
128
|
for (let i = 0; i < ops; i++) call(i);
|
|
107
129
|
});
|
|
108
|
-
return {
|
|
130
|
+
return {
|
|
131
|
+
settled: s.settled,
|
|
132
|
+
gcs: p.gcs,
|
|
133
|
+
heap: probe.usedHeap() - h0 + p.reclaimedHeap,
|
|
134
|
+
ext: probe.external() - e0 + p.reclaimedExternal
|
|
135
|
+
};
|
|
109
136
|
};
|
|
137
|
+
const perOp = (method, n, a2, b2) => ({
|
|
138
|
+
status: "measured",
|
|
139
|
+
method,
|
|
140
|
+
collections: a2.gcs + b2.gcs,
|
|
141
|
+
windowOps: n,
|
|
142
|
+
heapBytesPerOp: round((b2.heap - a2.heap) / n, 1),
|
|
143
|
+
externalBytesPerOp: round((b2.ext - a2.ext) / n, 1)
|
|
144
|
+
});
|
|
110
145
|
const paced = Math.floor(BENCH_CONTRACT.ALLOC_WINDOW_MS / msPerOp2);
|
|
111
146
|
const first = Math.max(1, Math.min(BENCH_CONTRACT.ALLOC_WINDOW_OPS, paced));
|
|
147
|
+
let largest = null;
|
|
112
148
|
for (let n = first; n >= 1; n = Math.floor(n / 2)) {
|
|
113
|
-
const
|
|
114
|
-
const
|
|
115
|
-
if (!
|
|
116
|
-
if (
|
|
117
|
-
|
|
118
|
-
status: "measured",
|
|
119
|
-
windowOps: n,
|
|
120
|
-
heapBytesPerOp: round((b.heap - a.heap) / n, 1),
|
|
121
|
-
externalBytesPerOp: round((b.ext - a.ext) / n, 1)
|
|
122
|
-
};
|
|
123
|
-
}
|
|
149
|
+
const a2 = window(n);
|
|
150
|
+
const b2 = window(2 * n);
|
|
151
|
+
if (!a2.settled || !b2.settled) return { status: "skipped", reason: "gc-unsettled" };
|
|
152
|
+
if (a2.gcs === 0 && b2.gcs === 0) return perOp("no-gc-window", n, a2, b2);
|
|
153
|
+
largest ??= [a2, b2];
|
|
124
154
|
}
|
|
125
|
-
|
|
155
|
+
const [a, b] = largest;
|
|
156
|
+
return perOp("gc-accounted", first, a, b);
|
|
126
157
|
}
|
|
127
158
|
function measurePeak(call, probe) {
|
|
128
|
-
const
|
|
159
|
+
const heap = [];
|
|
160
|
+
const external = [];
|
|
161
|
+
const total = [];
|
|
129
162
|
for (let i = 0; i < BENCH_CONTRACT.PEAK_CALLS; i++) {
|
|
130
163
|
const s = settle(probe);
|
|
131
164
|
if (!s.settled) return { status: "skipped", reason: "gc-unsettled" };
|
|
132
|
-
const
|
|
165
|
+
const e0 = probe.external();
|
|
166
|
+
const p = probe.profile(() => {
|
|
133
167
|
call(i);
|
|
134
168
|
});
|
|
135
|
-
|
|
169
|
+
const h1 = probe.usedHeap();
|
|
170
|
+
const e1 = probe.external();
|
|
171
|
+
heap.push(Math.max(p.peakBeforeGc, h1) - s.heap);
|
|
172
|
+
external.push(Math.max(p.peakExternalBeforeGc, e1) - e0);
|
|
173
|
+
total.push(Math.max(p.peakTotalBeforeGc, h1 + e1) - (s.heap + e0));
|
|
136
174
|
}
|
|
137
175
|
return {
|
|
138
176
|
status: "measured",
|
|
139
|
-
calls:
|
|
140
|
-
peakHeapBytes: Math.max(...
|
|
141
|
-
medianPeakHeapBytes: median(
|
|
177
|
+
calls: heap.length,
|
|
178
|
+
peakHeapBytes: Math.max(...heap),
|
|
179
|
+
medianPeakHeapBytes: median(heap),
|
|
180
|
+
peakExternalBytes: Math.max(...external),
|
|
181
|
+
medianPeakExternalBytes: median(external),
|
|
182
|
+
peakTotalBytes: Math.max(...total),
|
|
183
|
+
medianPeakTotalBytes: median(total)
|
|
142
184
|
};
|
|
143
185
|
}
|
|
144
186
|
function measureLongRun(pass, opsPerPass, probe, clock) {
|
|
@@ -208,6 +250,17 @@ function batchesAreStable(batches) {
|
|
|
208
250
|
const mid = median(window);
|
|
209
251
|
return window.every((b) => Math.abs(b - mid) / mid <= WARMUP.STABLE_TOL);
|
|
210
252
|
}
|
|
253
|
+
function isLargeCase(batches) {
|
|
254
|
+
if (batches.length < WARMUP.STABLE_BATCHES) return false;
|
|
255
|
+
return batches.slice(-WARMUP.STABLE_BATCHES).every((b) => b >= BENCH_CONTRACT.LARGE_PASS_MS);
|
|
256
|
+
}
|
|
257
|
+
function windowsAreStable(passes) {
|
|
258
|
+
const w = BENCH_CONTRACT.LARGE_WARMUP_WINDOW;
|
|
259
|
+
if (passes.length < 2 * w) return false;
|
|
260
|
+
const last = median(passes.slice(-w));
|
|
261
|
+
const before = median(passes.slice(-2 * w, -w));
|
|
262
|
+
return Math.abs(last - before) / before <= WARMUP.STABLE_TOL;
|
|
263
|
+
}
|
|
211
264
|
function warmUp(pass, clock) {
|
|
212
265
|
const start = clock();
|
|
213
266
|
const batches = [];
|
|
@@ -224,10 +277,31 @@ function warmUp(pass, clock) {
|
|
|
224
277
|
passes += inBatch;
|
|
225
278
|
batches.push(batchMs / inBatch);
|
|
226
279
|
const elapsedMs = clock() - start;
|
|
227
|
-
if (elapsedMs >= WARMUP.MIN_MS
|
|
228
|
-
|
|
280
|
+
if (elapsedMs >= WARMUP.MIN_MS) {
|
|
281
|
+
if (batchesAreStable(batches)) {
|
|
282
|
+
return { stable: true, elapsedMs, passes, batches, plan: "standard" };
|
|
283
|
+
}
|
|
284
|
+
if (isLargeCase(batches)) return warmUpLarge(pass, clock, start, batches, passes);
|
|
285
|
+
}
|
|
286
|
+
if (elapsedMs >= WARMUP.MAX_MS) {
|
|
287
|
+
return { stable: false, elapsedMs, passes, batches, plan: "standard" };
|
|
288
|
+
}
|
|
289
|
+
}
|
|
290
|
+
}
|
|
291
|
+
function warmUpLarge(pass, clock, start, batches, passesSoFar) {
|
|
292
|
+
let passes = passesSoFar;
|
|
293
|
+
for (; ; ) {
|
|
294
|
+
const p0 = clock();
|
|
295
|
+
pass();
|
|
296
|
+
const t = clock();
|
|
297
|
+
passes++;
|
|
298
|
+
batches.push(t - p0);
|
|
299
|
+
const elapsedMs = t - start;
|
|
300
|
+
if (windowsAreStable(batches))
|
|
301
|
+
return { stable: true, elapsedMs, passes, batches, plan: "large" };
|
|
302
|
+
if (elapsedMs >= BENCH_CONTRACT.LARGE_WARMUP_MAX_MS) {
|
|
303
|
+
return { stable: false, elapsedMs, passes, batches, plan: "large" };
|
|
229
304
|
}
|
|
230
|
-
if (elapsedMs >= WARMUP.MAX_MS) return { stable: false, elapsedMs, passes, batches };
|
|
231
305
|
}
|
|
232
306
|
}
|
|
233
307
|
function sampleThroughput(pass, opsPerPass, clock, inject = 0) {
|