@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/CHANGELOG.md +52 -0
- package/README.md +59 -17
- package/dist/child-entry.mjs +108 -26
- package/dist/child-entry.mjs.map +1 -1
- package/dist/cli.mjs +233 -63
- package/dist/cli.mjs.map +1 -1
- package/dist/index.d.ts +195 -9
- package/dist/index.mjs +63 -14
- package/dist/index.mjs.map +1 -1
- package/package.json +5 -5
- package/schema/results-v1.schema.json +122 -33
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,57 @@
|
|
|
1
1
|
# Changelog
|
|
2
2
|
|
|
3
|
+
## 0.1.3
|
|
4
|
+
|
|
5
|
+
### Patch Changes
|
|
6
|
+
|
|
7
|
+
- 027dbf5: A reading measures the empty-call floor once per run: the peak of a call that does nothing, read in
|
|
8
|
+
its own process through the same probe as the cases, which is what the probe itself costs. It is in
|
|
9
|
+
the reading as `memoryFloor`, the Markdown prints it, and every case's peak also gives each figure
|
|
10
|
+
above the floor's median, never below zero: `peakTotalAboveFloorBytes`,
|
|
11
|
+
`medianPeakTotalAboveFloorBytes`, and the same for the heap and external memory.
|
|
12
|
+
|
|
13
|
+
`budgetPeakBytes(caseReading)` is the peak a memory budget reads. For a case whose inputs average
|
|
14
|
+
under `BENCH_CONTRACT.FLOOR_INPUT_BYTES` (64 KiB) it is the median call's total above the floor's
|
|
15
|
+
median; for any other, or for a suite that gave no byte count, the highest raw `peakTotalBytes`, as
|
|
16
|
+
before. On a GitHub-hosted runner the floor's median call is about 3.3 KiB and its highest about
|
|
17
|
+
28 KiB, and that highest lands in a small case's highest peak, so a small input reads its median
|
|
18
|
+
call. A budget script that read `peakTotalBytes` should call `budgetPeakBytes` instead, and a suite
|
|
19
|
+
that added its own empty "floor" case can drop it.
|
|
20
|
+
|
|
21
|
+
One case that cannot be measured no longer stops the run. `cosyte-bench run` writes every other case,
|
|
22
|
+
lists what failed in the reading's `failures` (the phase, `timing`, `memory` or `floor`, the case,
|
|
23
|
+
and the first line of the error) and in the Markdown, prints the whole error to stderr, and exits 2.
|
|
24
|
+
A case whose timing failed is left out of `cases`; one whose memory failed keeps its timing. A script
|
|
25
|
+
that ran each case alone to get past one failure can run them together and read `failures`.
|
|
26
|
+
|
|
27
|
+
Every new field is added within results version 1, and documents written before still validate.
|
|
28
|
+
|
|
29
|
+
## 0.1.2
|
|
30
|
+
|
|
31
|
+
### Patch Changes
|
|
32
|
+
|
|
33
|
+
- c0c095a: Large cases are measured instead of skipped. A case whose single call takes longer than a warmup
|
|
34
|
+
batch (50 ms) and has not settled by the scaling gate's rule after 500 ms moves to the large-case
|
|
35
|
+
plan: one call at a time until the median of the last 5 calls is within 5% of the median of the 5
|
|
36
|
+
before them, at most 30 s. Each timing process records the plan it ended on (`plan` in a reading's
|
|
37
|
+
`throughput.processes`, `basePlans` and `headPlans` in a comparison), and a reading marks such a
|
|
38
|
+
case. A case that settles by the standard rule warms up exactly as before.
|
|
39
|
+
|
|
40
|
+
Allocations per operation are counted on a case whose every window collects: the largest pair of
|
|
41
|
+
windows is counted with what its collections reclaimed (`method: "gc-accounted"`, with the number of
|
|
42
|
+
`collections`), where before the case was skipped as `allocation-window-gc`.
|
|
43
|
+
|
|
44
|
+
The peak reports external memory (Buffers, ArrayBuffers) beside the V8 heap, and the two together:
|
|
45
|
+
`peakExternalBytes`, `peakTotalBytes` and their medians. A memory budget reads `peakTotalBytes`.
|
|
46
|
+
Memory processes run with `--no-concurrent-array-buffer-sweeping`, so a collection frees dead
|
|
47
|
+
ArrayBuffers before it ends and the external memory it reports is exact.
|
|
48
|
+
|
|
49
|
+
The long run also runs on a benchmark's largest size whose 30 rounds fit in 60 s, besides its
|
|
50
|
+
long-run size. A reading now times every size of a benchmark before measuring its memory.
|
|
51
|
+
|
|
52
|
+
Every new field is added within results version 1 and none is renamed; documents written before
|
|
53
|
+
still validate.
|
|
54
|
+
|
|
3
55
|
## 0.1.1
|
|
4
56
|
|
|
5
57
|
### Patch Changes
|
package/README.md
CHANGED
|
@@ -98,14 +98,21 @@ cosyte-bench check bench.json
|
|
|
98
98
|
Exit codes: 0 clean, 1 a finding (a regression, or a results file that does not match the schema),
|
|
99
99
|
2 could not measure (a skip, a process that died, an input it could not read). A skip is not a pass.
|
|
100
100
|
|
|
101
|
+
A `run` whose measuring process fails for one case still measures and writes every other case. The
|
|
102
|
+
reading lists what failed in `failures` (the phase, `timing`, `memory` or `floor`, the case, and the
|
|
103
|
+
first line of the error; the whole error goes to stderr) and the command exits 2. A case whose timing
|
|
104
|
+
failed is left out of `cases`; one whose memory failed keeps its timing with its memory `null`. A
|
|
105
|
+
case whose warmup never settled is not a failure: it is in `cases`, its throughput `skipped` with
|
|
106
|
+
the reason `warmup-unstable`.
|
|
107
|
+
|
|
101
108
|
## Entry points
|
|
102
109
|
|
|
103
|
-
| entry point | what it is
|
|
104
|
-
| --------------------------------------------- |
|
|
105
|
-
| `cosyte-bench` | the bin: `run`, `compare`, `calibrate` and `check`
|
|
106
|
-
| `@cosyte/bench` | writing a suite (`defineSuite`, `defineBenchmark`), judging ratios (`judge`), reading results (`readResults`, `RESULTS_SCHEMA`), and `BENCH_CONTRACT` |
|
|
107
|
-
| `@cosyte/bench/schema/results-v1.schema.json` | the results schema as JSON Schema, for a tool in another language
|
|
108
|
-
| `@cosyte/bench/package.json` | the manifest
|
|
110
|
+
| entry point | what it is |
|
|
111
|
+
| --------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------ |
|
|
112
|
+
| `cosyte-bench` | the bin: `run`, `compare`, `calibrate` and `check` |
|
|
113
|
+
| `@cosyte/bench` | writing a suite (`defineSuite`, `defineBenchmark`), judging ratios (`judge`), reading results (`readResults`, `RESULTS_SCHEMA`, `budgetPeakBytes`), and `BENCH_CONTRACT` |
|
|
114
|
+
| `@cosyte/bench/schema/results-v1.schema.json` | the results schema as JSON Schema, for a tool in another language |
|
|
115
|
+
| `@cosyte/bench/package.json` | the manifest |
|
|
109
116
|
|
|
110
117
|
The module entry point has no side effects: importing it starts no process. The schema is plain JSON:
|
|
111
118
|
|
|
@@ -131,21 +138,54 @@ the command line:
|
|
|
131
138
|
|
|
132
139
|
- the sizes, and which of them a pull request's comparison runs (`compare: false` leaves a slow size
|
|
133
140
|
to the readings);
|
|
134
|
-
- `weigh`, the number a result is reduced to, and `longRun`, the size the long run uses
|
|
141
|
+
- `weigh`, the number a result is reduced to, and `longRun`, the size the long run uses besides the
|
|
142
|
+
largest size whose long run fits in 60 s;
|
|
135
143
|
- `--only <benchmark or case>`, `--processes <n>`, `--no-memory` and `--no-import` on `run`.
|
|
136
144
|
|
|
137
145
|
## What it measures
|
|
138
146
|
|
|
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
|
|
147
|
+
| Measure | How | What it cannot see |
|
|
148
|
+
| -------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | ----------------------------------------------------------------------------------------------------------------------------------- |
|
|
149
|
+
| 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 |
|
|
150
|
+
| 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 |
|
|
151
|
+
| 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 |
|
|
152
|
+
| 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`); each also above the run's empty-call floor (`peakTotalAboveFloorBytes` and the like), below | Native memory a library allocates outside V8's accounting |
|
|
153
|
+
| 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 |
|
|
154
|
+
| 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 |
|
|
155
|
+
|
|
156
|
+
Memory measurements need `gc()`; the runner starts those processes with `--expose-gc` itself, and
|
|
157
|
+
with `--no-concurrent-array-buffer-sweeping`, so a collection frees the ArrayBuffers it found dead
|
|
158
|
+
before it ends and the external memory it reports before and after is exact.
|
|
159
|
+
|
|
160
|
+
### The empty-call floor and small inputs
|
|
161
|
+
|
|
162
|
+
Reading the peak costs memory of its own: settling the heap, the profiler that records each
|
|
163
|
+
collection, the record itself. A reading therefore measures an empty call once, in a fresh process
|
|
164
|
+
through the same probe (`memoryFloor`), and gives each peak figure both raw and above the floor's
|
|
165
|
+
median, never below zero (`peakTotalAboveFloorBytes`, `medianPeakTotalAboveFloorBytes`, and the same
|
|
166
|
+
for the heap and external memory).
|
|
167
|
+
|
|
168
|
+
On a GitHub-hosted runner the floor's median call is about 3.3 KiB and its highest about 28 KiB (one
|
|
169
|
+
call, the first). That highest call is the probe's, and it lands in a case's highest peak too, which
|
|
170
|
+
is why a few hundred bytes of input could not read under 20 times its size whatever the code did.
|
|
171
|
+
|
|
172
|
+
A memory budget reads `budgetPeakBytes(caseReading)`: for a case whose inputs average under
|
|
173
|
+
`BENCH_CONTRACT.FLOOR_INPUT_BYTES` (64 KiB) it is the median call's total above the floor's median,
|
|
174
|
+
the code's typical call without the probe; for any other case, or one whose suite gave no byte
|
|
175
|
+
count, it is the highest raw `peakTotalBytes`. At 64 KiB even the probe's highest call is about 2%
|
|
176
|
+
of a 20 times budget, so a large input keeps the raw highest figure, which needs nothing from
|
|
177
|
+
another process. A suite that added its own empty "floor" case can drop it.
|
|
178
|
+
|
|
179
|
+
### Large cases
|
|
180
|
+
|
|
181
|
+
A case whose single call takes longer than a warmup batch (50 ms) cannot settle by the scaling
|
|
182
|
+
gate's warmup rule on a shared runner: every batch is one call, and one call's jitter is what the
|
|
183
|
+
batch was there to average away. When 500 ms of warmup have passed, the rule has not settled, and
|
|
184
|
+
the last three batches were each one call of at least 50 ms, the warmup moves to the large-case
|
|
185
|
+
plan: one call at a time until the median of the last 5 calls is within 5% of the median of the 5
|
|
186
|
+
before them, at most 30 s from the start. Its samples are the same 5, each one call at that size.
|
|
187
|
+
Each process records the plan it ended on (`plan`), and a reading marks such a case. A case that
|
|
188
|
+
settles by the standard rule never moves, so small cases are measured exactly as before.
|
|
149
189
|
|
|
150
190
|
## The comparison
|
|
151
191
|
|
|
@@ -189,6 +229,8 @@ CI runner image, never by host name, user or absolute path.
|
|
|
189
229
|
- `judge(ratios, threshold?, alpha?)`: the comparison's verdict for one case from its per-round ratios.
|
|
190
230
|
- `readResults(text)`: parse and check a results document; throws a `TypeError` listing every
|
|
191
231
|
problem. `RESULTS_SCHEMA` is the schema, also shipped as `schema/results-v1.schema.json`.
|
|
232
|
+
- `budgetPeakBytes(caseReading)`: the peak a memory budget reads for a case: the median call above
|
|
233
|
+
the empty-call floor for an input under 64 KiB, the highest raw total for any other.
|
|
192
234
|
- `BENCH_CONTRACT`: the frozen constants.
|
|
193
235
|
- `BenchError`: what a run that could not measure throws.
|
|
194
236
|
|
package/dist/child-entry.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
|
});
|
|
@@ -78,9 +83,27 @@ function realProbe() {
|
|
|
78
83
|
profiler.start();
|
|
79
84
|
fn();
|
|
80
85
|
const stats = profiler.stop().statistics;
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
86
|
+
const report = {
|
|
87
|
+
gcs: stats.length,
|
|
88
|
+
peakBeforeGc: 0,
|
|
89
|
+
peakExternalBeforeGc: 0,
|
|
90
|
+
peakTotalBeforeGc: 0,
|
|
91
|
+
reclaimedHeap: 0,
|
|
92
|
+
reclaimedExternal: 0
|
|
93
|
+
};
|
|
94
|
+
for (const s of stats) {
|
|
95
|
+
const before = s.beforeGC.heapStatistics;
|
|
96
|
+
const after = s.afterGC.heapStatistics;
|
|
97
|
+
report.peakBeforeGc = Math.max(report.peakBeforeGc, before.usedHeapSize);
|
|
98
|
+
report.peakExternalBeforeGc = Math.max(report.peakExternalBeforeGc, before.externalMemory);
|
|
99
|
+
report.peakTotalBeforeGc = Math.max(
|
|
100
|
+
report.peakTotalBeforeGc,
|
|
101
|
+
before.usedHeapSize + before.externalMemory
|
|
102
|
+
);
|
|
103
|
+
report.reclaimedHeap += before.usedHeapSize - after.usedHeapSize;
|
|
104
|
+
report.reclaimedExternal += before.externalMemory - after.externalMemory;
|
|
105
|
+
}
|
|
106
|
+
return report;
|
|
84
107
|
}
|
|
85
108
|
};
|
|
86
109
|
}
|
|
@@ -102,43 +125,63 @@ function measureAllocations(call, probe, msPerOp2) {
|
|
|
102
125
|
const s = settle(probe);
|
|
103
126
|
const h0 = probe.usedHeap();
|
|
104
127
|
const e0 = probe.external();
|
|
105
|
-
const
|
|
128
|
+
const p = probe.profile(() => {
|
|
106
129
|
for (let i = 0; i < ops; i++) call(i);
|
|
107
130
|
});
|
|
108
|
-
return {
|
|
131
|
+
return {
|
|
132
|
+
settled: s.settled,
|
|
133
|
+
gcs: p.gcs,
|
|
134
|
+
heap: probe.usedHeap() - h0 + p.reclaimedHeap,
|
|
135
|
+
ext: probe.external() - e0 + p.reclaimedExternal
|
|
136
|
+
};
|
|
109
137
|
};
|
|
138
|
+
const perOp = (method, n, a2, b2) => ({
|
|
139
|
+
status: "measured",
|
|
140
|
+
method,
|
|
141
|
+
collections: a2.gcs + b2.gcs,
|
|
142
|
+
windowOps: n,
|
|
143
|
+
heapBytesPerOp: round((b2.heap - a2.heap) / n, 1),
|
|
144
|
+
externalBytesPerOp: round((b2.ext - a2.ext) / n, 1)
|
|
145
|
+
});
|
|
110
146
|
const paced = Math.floor(BENCH_CONTRACT.ALLOC_WINDOW_MS / msPerOp2);
|
|
111
147
|
const first = Math.max(1, Math.min(BENCH_CONTRACT.ALLOC_WINDOW_OPS, paced));
|
|
148
|
+
let largest = null;
|
|
112
149
|
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
|
-
}
|
|
150
|
+
const a2 = window(n);
|
|
151
|
+
const b2 = window(2 * n);
|
|
152
|
+
if (!a2.settled || !b2.settled) return { status: "skipped", reason: "gc-unsettled" };
|
|
153
|
+
if (a2.gcs === 0 && b2.gcs === 0) return perOp("no-gc-window", n, a2, b2);
|
|
154
|
+
largest ??= [a2, b2];
|
|
124
155
|
}
|
|
125
|
-
|
|
156
|
+
const [a, b] = largest;
|
|
157
|
+
return perOp("gc-accounted", first, a, b);
|
|
126
158
|
}
|
|
127
159
|
function measurePeak(call, probe) {
|
|
128
|
-
const
|
|
160
|
+
const heap = [];
|
|
161
|
+
const external = [];
|
|
162
|
+
const total = [];
|
|
129
163
|
for (let i = 0; i < BENCH_CONTRACT.PEAK_CALLS; i++) {
|
|
130
164
|
const s = settle(probe);
|
|
131
165
|
if (!s.settled) return { status: "skipped", reason: "gc-unsettled" };
|
|
132
|
-
const
|
|
166
|
+
const e0 = probe.external();
|
|
167
|
+
const p = probe.profile(() => {
|
|
133
168
|
call(i);
|
|
134
169
|
});
|
|
135
|
-
|
|
170
|
+
const h1 = probe.usedHeap();
|
|
171
|
+
const e1 = probe.external();
|
|
172
|
+
heap.push(Math.max(p.peakBeforeGc, h1) - s.heap);
|
|
173
|
+
external.push(Math.max(p.peakExternalBeforeGc, e1) - e0);
|
|
174
|
+
total.push(Math.max(p.peakTotalBeforeGc, h1 + e1) - (s.heap + e0));
|
|
136
175
|
}
|
|
137
176
|
return {
|
|
138
177
|
status: "measured",
|
|
139
|
-
calls:
|
|
140
|
-
peakHeapBytes: Math.max(...
|
|
141
|
-
medianPeakHeapBytes: median(
|
|
178
|
+
calls: heap.length,
|
|
179
|
+
peakHeapBytes: Math.max(...heap),
|
|
180
|
+
medianPeakHeapBytes: median(heap),
|
|
181
|
+
peakExternalBytes: Math.max(...external),
|
|
182
|
+
medianPeakExternalBytes: median(external),
|
|
183
|
+
peakTotalBytes: Math.max(...total),
|
|
184
|
+
medianPeakTotalBytes: median(total)
|
|
142
185
|
};
|
|
143
186
|
}
|
|
144
187
|
function measureLongRun(pass, opsPerPass, probe, clock) {
|
|
@@ -208,6 +251,17 @@ function batchesAreStable(batches) {
|
|
|
208
251
|
const mid = median(window);
|
|
209
252
|
return window.every((b) => Math.abs(b - mid) / mid <= WARMUP.STABLE_TOL);
|
|
210
253
|
}
|
|
254
|
+
function isLargeCase(batches) {
|
|
255
|
+
if (batches.length < WARMUP.STABLE_BATCHES) return false;
|
|
256
|
+
return batches.slice(-WARMUP.STABLE_BATCHES).every((b) => b >= BENCH_CONTRACT.LARGE_PASS_MS);
|
|
257
|
+
}
|
|
258
|
+
function windowsAreStable(passes) {
|
|
259
|
+
const w = BENCH_CONTRACT.LARGE_WARMUP_WINDOW;
|
|
260
|
+
if (passes.length < 2 * w) return false;
|
|
261
|
+
const last = median(passes.slice(-w));
|
|
262
|
+
const before = median(passes.slice(-2 * w, -w));
|
|
263
|
+
return Math.abs(last - before) / before <= WARMUP.STABLE_TOL;
|
|
264
|
+
}
|
|
211
265
|
function warmUp(pass, clock) {
|
|
212
266
|
const start = clock();
|
|
213
267
|
const batches = [];
|
|
@@ -224,10 +278,31 @@ function warmUp(pass, clock) {
|
|
|
224
278
|
passes += inBatch;
|
|
225
279
|
batches.push(batchMs / inBatch);
|
|
226
280
|
const elapsedMs = clock() - start;
|
|
227
|
-
if (elapsedMs >= WARMUP.MIN_MS
|
|
228
|
-
|
|
281
|
+
if (elapsedMs >= WARMUP.MIN_MS) {
|
|
282
|
+
if (batchesAreStable(batches)) {
|
|
283
|
+
return { stable: true, elapsedMs, passes, batches, plan: "standard" };
|
|
284
|
+
}
|
|
285
|
+
if (isLargeCase(batches)) return warmUpLarge(pass, clock, start, batches, passes);
|
|
286
|
+
}
|
|
287
|
+
if (elapsedMs >= WARMUP.MAX_MS) {
|
|
288
|
+
return { stable: false, elapsedMs, passes, batches, plan: "standard" };
|
|
289
|
+
}
|
|
290
|
+
}
|
|
291
|
+
}
|
|
292
|
+
function warmUpLarge(pass, clock, start, batches, passesSoFar) {
|
|
293
|
+
let passes = passesSoFar;
|
|
294
|
+
for (; ; ) {
|
|
295
|
+
const p0 = clock();
|
|
296
|
+
pass();
|
|
297
|
+
const t = clock();
|
|
298
|
+
passes++;
|
|
299
|
+
batches.push(t - p0);
|
|
300
|
+
const elapsedMs = t - start;
|
|
301
|
+
if (windowsAreStable(batches))
|
|
302
|
+
return { stable: true, elapsedMs, passes, batches, plan: "large" };
|
|
303
|
+
if (elapsedMs >= BENCH_CONTRACT.LARGE_WARMUP_MAX_MS) {
|
|
304
|
+
return { stable: false, elapsedMs, passes, batches, plan: "large" };
|
|
229
305
|
}
|
|
230
|
-
if (elapsedMs >= WARMUP.MAX_MS) return { stable: false, elapsedMs, passes, batches };
|
|
231
306
|
}
|
|
232
307
|
}
|
|
233
308
|
function sampleThroughput(pass, opsPerPass, clock, inject = 0) {
|
|
@@ -413,10 +488,17 @@ function measureJob(job, shape, warmup, pass, call, deps) {
|
|
|
413
488
|
};
|
|
414
489
|
}
|
|
415
490
|
function measuredSomething(result) {
|
|
491
|
+
if (result.mode === "floor") return result.peak.status === "measured";
|
|
416
492
|
if (result.mode === "timing") return result.status === "measured";
|
|
417
493
|
return [result.allocations, result.peak, result.longRun].some((r) => r?.status === "measured");
|
|
418
494
|
}
|
|
419
495
|
async function runJob(job, deps = realDeps()) {
|
|
496
|
+
if (job.mode === "floor") {
|
|
497
|
+
return {
|
|
498
|
+
mode: "floor",
|
|
499
|
+
peak: deps.probe === null ? SKIPPED_NO_GC : measurePeak(() => void 0, deps.probe)
|
|
500
|
+
};
|
|
501
|
+
}
|
|
420
502
|
const suiteModule = await import(pathToFileURL(job.suite).href);
|
|
421
503
|
const suite = checkSuite(suiteModule.default);
|
|
422
504
|
const target = await import(pathToFileURL(job.target).href);
|