zbench-js 0.1.1 → 0.2.0
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/README.md +31 -9
- package/dist/cli.js +110 -101
- package/dist/compare.d.ts +29 -9
- package/dist/compare.js +59 -39
- package/dist/config.d.ts +80 -55
- package/dist/config.js +161 -117
- package/dist/index.d.ts +0 -1
- package/dist/index.js +0 -1
- package/dist/isolate.d.ts +8 -12
- package/dist/isolate.js +31 -62
- package/dist/measure.d.ts +3 -1
- package/dist/measure.js +3 -0
- package/dist/refs.d.ts +1 -8
- package/dist/refs.js +37 -44
- package/dist/report.d.ts +8 -2
- package/dist/report.js +62 -29
- package/dist/types.d.ts +18 -0
- package/dist/units.d.ts +3 -3
- package/dist/units.js +2 -2
- package/dist/worker.d.ts +3 -0
- package/dist/worker.js +114 -0
- package/package.json +4 -3
- package/dist/pool.d.ts +0 -5
- package/dist/pool.js +0 -15
- package/dist/runner.d.ts +0 -40
- package/dist/runner.js +0 -121
package/README.md
CHANGED
|
@@ -105,6 +105,11 @@ readable. The timespan is settled once per matrix, so a column has one heading.
|
|
|
105
105
|
`"unit": "MB"` reports MB/s with no further arithmetic. `scale` sets the multiplier explicitly and
|
|
106
106
|
overrides that.
|
|
107
107
|
|
|
108
|
+
`compare` narrows what a comparison reports. A test's measurements are all proportional to its
|
|
109
|
+
timings, so every one of them would show the same speedup; marking a single measurement with
|
|
110
|
+
`"compare": true` keeps the comparison table down to the numbers worth reading, and makes that
|
|
111
|
+
measurement the one the factor is computed from.
|
|
112
|
+
|
|
108
113
|
## Flags
|
|
109
114
|
|
|
110
115
|
Flags are alternative code paths rather than points in the matrix: the whole matrix runs once per
|
|
@@ -141,11 +146,21 @@ zbench -R v3.2.1 -R main -R HEAD
|
|
|
141
146
|
```
|
|
142
147
|
|
|
143
148
|
Each reference is checked out into a cached worktree under `.zbench/`, built, and run. The first is
|
|
144
|
-
the baseline; every other
|
|
149
|
+
the baseline; every other reference gets its columns and a speedup factor that is `> 1` when better,
|
|
145
150
|
whichever direction the column improves in. A change is only colored when it is larger than both
|
|
146
151
|
`--threshold` (default 1%) and the two runs' combined noise — so a green number means the change
|
|
147
152
|
outran the variance, and a gray one means it did not.
|
|
148
153
|
|
|
154
|
+
A matrix is one table, grouped by reference, with a column per measurement and a single factor for
|
|
155
|
+
each reference. The measurements of a matrix are proportional, so the factor is taken from the first
|
|
156
|
+
column rather than repeated across all of them; `"compare": true` on a measurement picks which one
|
|
157
|
+
that is and drops the rest.
|
|
158
|
+
|
|
159
|
+
Only the matrices worth reading are printed. A matrix where nothing beat the threshold is left out
|
|
160
|
+
unless you pass `--unchanged`, and one that fewer than two references could report — the rest being
|
|
161
|
+
`N/A` — is left out unless you pass `--partial`; either way the count of what was hidden is printed.
|
|
162
|
+
`--no-runs` drops each reference's own tables and reports nothing but the changes.
|
|
163
|
+
|
|
149
164
|
Use `.` for the working tree as it is, without a checkout or a build.
|
|
150
165
|
|
|
151
166
|
The tests themselves always come from the working tree and are copied into each worktree, so an old
|
|
@@ -155,14 +170,18 @@ runnable (default `npm install --no-audit --no-fund && npm run build`), `--rebui
|
|
|
155
170
|
|
|
156
171
|
## Isolation and concurrency
|
|
157
172
|
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
173
|
+
Every matrix runs in a fresh worker thread, so one configuration cannot warm up or poison the next,
|
|
174
|
+
and a reference's own build is what gets loaded.
|
|
175
|
+
|
|
176
|
+
`-J` sets how many matrices are timed at once, defaulting to half the number of hardware threads.
|
|
177
|
+
Concurrent matrices contend for the machine, which inflates absolute numbers and widens `±`;
|
|
178
|
+
the comparison factors hold up because every reference runs under the same contention,
|
|
179
|
+
but pass `-J 1` when the absolute numbers matter more.
|
|
161
180
|
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
181
|
+
References do not overlap: one is finished before the next one starts, so a reference's matrices all
|
|
182
|
+
meet the same contention rather than some of them sharing the machine with the reference they are
|
|
183
|
+
being compared against. `--overlap` lifts that and lets `-J` fill from every reference at once,
|
|
184
|
+
which finishes sooner at the cost of a noisier comparison.
|
|
166
185
|
|
|
167
186
|
A matrix that crashes or outruns `--timeout` (default 300s) is reported as `N/A` rather than
|
|
168
187
|
bringing down the run, so the references that did finish are still compared. This matters when the
|
|
@@ -183,9 +202,12 @@ minutes on the commit before it.
|
|
|
183
202
|
-J, --jobs <n> Matrices to time at once.
|
|
184
203
|
-l, --list List what would run, then exit.
|
|
185
204
|
-j, --json <path> Write the raw results as JSON.
|
|
205
|
+
--overlap Let matrices from different references run at the same time.
|
|
206
|
+
--no-runs When comparing, skip each reference's own tables and only report changes.
|
|
207
|
+
--unchanged Also report matrices where nothing beat the threshold.
|
|
208
|
+
--partial Also report matrices where fewer than two references produced numbers.
|
|
186
209
|
--cpu <n> Override the detected CPU level.
|
|
187
210
|
--mem <n> Override the detected memory level.
|
|
188
|
-
--no-isolate Run in this process instead of one child per matrix.
|
|
189
211
|
--build <cmd> Command that makes a reference's worktree runnable.
|
|
190
212
|
--rebuild Rebuild reference worktrees even when they are up to date.
|
|
191
213
|
--clean Remove cached reference worktrees, then exit.
|
package/dist/cli.js
CHANGED
|
@@ -1,17 +1,16 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
|
-
// SPDX-License-Identifier: LGPL-3.0-or-later
|
|
3
2
|
import * as io from 'ioium/node';
|
|
4
3
|
import { writeFile } from 'node:fs/promises';
|
|
5
4
|
import { join, relative, resolve } from 'node:path';
|
|
5
|
+
import { performance } from 'node:perf_hooks';
|
|
6
6
|
import { parseArgs, styleText } from 'node:util';
|
|
7
7
|
import { resolveCapabilities } from './capabilities.js';
|
|
8
8
|
import { compare } from './compare.js';
|
|
9
9
|
import { findConfig, flagCombinations, flagLabel, loadSuite } from './config.js';
|
|
10
|
-
import { runIsolated
|
|
11
|
-
import { cleanRefs,
|
|
10
|
+
import { runIsolated } from './isolate.js';
|
|
11
|
+
import { cleanRefs, prepareRef, repoRoot, workingRefs } from './refs.js';
|
|
12
12
|
import { reportComparison, reportEnvironment, reportList, reportRun } from './report.js';
|
|
13
|
-
import {
|
|
14
|
-
import { runTest } from './runner.js';
|
|
13
|
+
import { duration } from './units.js';
|
|
15
14
|
const usage = `Usage: zbench [options] [filter...]
|
|
16
15
|
|
|
17
16
|
Run the performance tests named by the config file. Filters match a test's name or path.
|
|
@@ -28,14 +27,19 @@ Options:
|
|
|
28
27
|
0 waits forever. A reference with a pathological regression can
|
|
29
28
|
otherwise hold the whole comparison open.
|
|
30
29
|
-a, --all Run every configuration, ignoring cpu/mem requirements.
|
|
31
|
-
-J, --jobs <n> Matrices to time at once. [one per hardware
|
|
30
|
+
-J, --jobs <n> Matrices to time at once. [one per two hardware threads]
|
|
32
31
|
Concurrent matrices contend for the machine, which widens the ± column;
|
|
33
32
|
pass -J 1 for the quietest numbers.
|
|
34
33
|
-l, --list List what would run, then exit.
|
|
35
34
|
-j, --json <path> Write the raw results as JSON.
|
|
35
|
+
--overlap Let matrices from different references run at the same time. By default
|
|
36
|
+
a reference is finished before the next one starts, so every matrix of a
|
|
37
|
+
reference meets the same contention.
|
|
38
|
+
--no-runs When comparing, skip each reference's own tables and only report changes.
|
|
39
|
+
--unchanged Also report matrices where nothing beat the threshold.
|
|
40
|
+
--partial Also report matrices where fewer than two references produced numbers.
|
|
36
41
|
--cpu <n> Override the detected CPU level.
|
|
37
42
|
--mem <n> Override the detected memory level.
|
|
38
|
-
--no-isolate Run in this process instead of one child per matrix.
|
|
39
43
|
--build <cmd> Command that makes a reference's worktree runnable.
|
|
40
44
|
--rebuild Rebuild reference worktrees even when they are up to date.
|
|
41
45
|
--clean Remove cached reference worktrees, then exit.
|
|
@@ -55,36 +59,40 @@ const { values: opts, positionals: filters } = parseArgs({
|
|
|
55
59
|
jobs: { short: 'J', type: 'string' },
|
|
56
60
|
list: { short: 'l', type: 'boolean' },
|
|
57
61
|
json: { short: 'j', type: 'string' },
|
|
62
|
+
overlap: { type: 'boolean' },
|
|
63
|
+
runs: { type: 'boolean', default: true },
|
|
64
|
+
unchanged: { type: 'boolean' },
|
|
65
|
+
partial: { type: 'boolean' },
|
|
58
66
|
cpu: { type: 'string' },
|
|
59
67
|
mem: { type: 'string' },
|
|
60
|
-
isolate: { type: 'boolean', default: true },
|
|
61
68
|
build: { type: 'string' },
|
|
62
69
|
rebuild: { type: 'boolean' },
|
|
63
70
|
clean: { type: 'boolean' },
|
|
64
71
|
quiet: { short: 'q', type: 'boolean' },
|
|
65
72
|
help: { short: 'h', type: 'boolean' },
|
|
66
|
-
worker: { type: 'string' },
|
|
67
73
|
},
|
|
68
74
|
allowPositionals: true,
|
|
75
|
+
allowNegative: true,
|
|
69
76
|
});
|
|
70
77
|
if (opts.help) {
|
|
71
78
|
console.log(usage);
|
|
72
79
|
process.exit(0);
|
|
73
80
|
}
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
io.error
|
|
80
|
-
|
|
81
|
+
const dim = (text) => styleText('gray', text);
|
|
82
|
+
const ignore = () => { };
|
|
83
|
+
// Results go to the console directly, so silencing ioium leaves them in place while the job lines,
|
|
84
|
+
// the progress line and the incidental notices go away
|
|
85
|
+
if (opts.quiet) {
|
|
86
|
+
io.useOutput({ debug: ignore, log: ignore, info: ignore, warn: console.warn, error: console.error });
|
|
87
|
+
io.jobs.useDraw(ignore);
|
|
88
|
+
io.jobs.useClear(ignore);
|
|
81
89
|
}
|
|
82
90
|
function number(value, name) {
|
|
83
91
|
if (value === undefined)
|
|
84
92
|
return undefined;
|
|
85
93
|
const parsed = Number(value);
|
|
86
94
|
if (!Number.isFinite(parsed))
|
|
87
|
-
|
|
95
|
+
io.exit(`--${name} expects a number, got ${JSON.stringify(value)}`);
|
|
88
96
|
return parsed;
|
|
89
97
|
}
|
|
90
98
|
/** Parse `-f name=json`, falling back to the raw string when it isn't valid JSON. */
|
|
@@ -93,7 +101,7 @@ function parseFlagOverrides(entries) {
|
|
|
93
101
|
for (const entry of entries) {
|
|
94
102
|
const at = entry.indexOf('=');
|
|
95
103
|
if (at < 0)
|
|
96
|
-
|
|
104
|
+
io.exit(`--flag expects name=value, got ${JSON.stringify(entry)}`);
|
|
97
105
|
const name = entry.slice(0, at);
|
|
98
106
|
const raw = entry.slice(at + 1);
|
|
99
107
|
let value;
|
|
@@ -109,19 +117,19 @@ function parseFlagOverrides(entries) {
|
|
|
109
117
|
}
|
|
110
118
|
const configPath = opts.config ? resolve(opts.config) : findConfig();
|
|
111
119
|
if (!configPath)
|
|
112
|
-
|
|
120
|
+
io.exit('no config file found; pass --config or add one at tests/perf/config.json');
|
|
113
121
|
let suite;
|
|
114
122
|
try {
|
|
115
123
|
suite = loadSuite(configPath);
|
|
116
124
|
}
|
|
117
125
|
catch (e) {
|
|
118
|
-
|
|
126
|
+
io.exit(e);
|
|
119
127
|
}
|
|
120
128
|
if (filters.length) {
|
|
121
129
|
const matches = (test) => filters.some(f => test.name.toLowerCase().includes(f.toLowerCase()) || test.path.includes(f));
|
|
122
130
|
suite.tests = suite.tests.filter(matches);
|
|
123
131
|
if (!suite.tests.length)
|
|
124
|
-
|
|
132
|
+
io.exit(`no test matches ${filters.map(f => JSON.stringify(f)).join(', ')}`);
|
|
125
133
|
}
|
|
126
134
|
if (opts.list) {
|
|
127
135
|
reportList(suite);
|
|
@@ -139,30 +147,8 @@ const threshold = number(opts.threshold, 'threshold') ?? 1;
|
|
|
139
147
|
const timeout = number(opts.timeout, 'timeout') ?? 300;
|
|
140
148
|
const iterations = number(opts.iterations, 'iterations');
|
|
141
149
|
const warmup = number(opts.warmup, 'warmup');
|
|
142
|
-
const cli = resolve(import.meta.dirname, 'cli.js');
|
|
143
150
|
const refNames = opts.ref.length ? opts.ref : ['.'];
|
|
144
|
-
|
|
145
|
-
const jobs = Math.max(1, number(opts.jobs, 'jobs') ?? (opts.isolate ? Math.round((navigator.hardwareConcurrency || 0) / 2) || 1 : 1));
|
|
146
|
-
if (jobs > 1 && !opts.isolate)
|
|
147
|
-
fail('--jobs above 1 needs process isolation, so it cannot be used with --no-isolate');
|
|
148
|
-
/** Overwrite the progress line, if there is one to overwrite. */
|
|
149
|
-
const progress = {
|
|
150
|
-
active: false,
|
|
151
|
-
show(message) {
|
|
152
|
-
if (opts.quiet || !process.stdout.isTTY)
|
|
153
|
-
return;
|
|
154
|
-
this.clear();
|
|
155
|
-
process.stdout.write(styleText('gray', message));
|
|
156
|
-
this.active = true;
|
|
157
|
-
},
|
|
158
|
-
clear() {
|
|
159
|
-
if (!this.active)
|
|
160
|
-
return;
|
|
161
|
-
process.stdout.clearLine(0);
|
|
162
|
-
process.stdout.cursorTo(0);
|
|
163
|
-
this.active = false;
|
|
164
|
-
},
|
|
165
|
-
};
|
|
151
|
+
const jobs = Math.max(1, number(opts.jobs, 'jobs') ?? (Math.round((navigator.hardwareConcurrency || 0) / 2) || 1));
|
|
166
152
|
/** Every (test, flag combination) pair that will run, i.e. one table each. */
|
|
167
153
|
function* planned(suite) {
|
|
168
154
|
for (const test of suite.tests) {
|
|
@@ -174,78 +160,101 @@ function* planned(suite) {
|
|
|
174
160
|
yield [test, flags];
|
|
175
161
|
}
|
|
176
162
|
}
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
163
|
+
if (!opts.quiet)
|
|
164
|
+
reportEnvironment(cpu, mem, iterations ?? 5, warmup ?? 1, jobs);
|
|
165
|
+
const repo = await repoRoot().catch(() => process.cwd());
|
|
166
|
+
const refOptions = { cache, build: opts.build ?? suite.build, root: suite.root, rebuild: opts.rebuild };
|
|
167
|
+
const prepared = new Array(refNames.length);
|
|
168
|
+
// The working tree is used where it is, so the usual case has nothing to report and no job to run
|
|
169
|
+
if (refNames.every(name => workingRefs.includes(name))) {
|
|
170
|
+
prepared.splice(0, refNames.length, ...(await Promise.all(refNames.map(name => prepareRef(name, refOptions)))));
|
|
171
|
+
}
|
|
172
|
+
else {
|
|
173
|
+
const failures = [];
|
|
174
|
+
await io.jobs.runWithData({
|
|
175
|
+
concurrency: refNames.length,
|
|
176
|
+
jobStartText: dim('preparing'),
|
|
177
|
+
name: ({ name }) => name,
|
|
178
|
+
async run({ name, index }, progress) {
|
|
179
|
+
try {
|
|
180
|
+
prepared[index] = await prepareRef(name, { ...refOptions, onStep: m => progress(dim(m)) });
|
|
181
|
+
return { status: 'succeeded', text: dim('ready') };
|
|
182
|
+
}
|
|
183
|
+
catch (e) {
|
|
184
|
+
const text = String(e?.stderr || e?.message || e).trim();
|
|
185
|
+
const reason = text.split('\n').filter(Boolean).at(-1) ?? 'failed';
|
|
186
|
+
failures.push(`${name}: ${reason}`);
|
|
187
|
+
return { status: 'failed', text: reason };
|
|
188
|
+
}
|
|
189
|
+
},
|
|
190
|
+
}, refNames.map((name, index) => ({ name, index })));
|
|
191
|
+
// A reference that will not build has no numbers at all, so there is nothing left to compare
|
|
192
|
+
if (failures.length)
|
|
193
|
+
io.exit(`could not prepare references:\n ${failures.join('\n ')}`);
|
|
194
|
+
}
|
|
195
|
+
const plan = [...planned(suite)];
|
|
196
|
+
const queue = [];
|
|
197
|
+
// Every reference's matrices go into one queue, so a reference that finishes early does not leave
|
|
198
|
+
// the machine idle while a slow one is still running
|
|
199
|
+
for (const ref of prepared)
|
|
200
|
+
for (const [test, flags] of plan)
|
|
201
|
+
queue.push({ index: queue.length, ref, test, flags });
|
|
202
|
+
const results = new Array(queue.length);
|
|
203
|
+
const comparing = prepared.length > 1;
|
|
204
|
+
await io.jobs.runWithData({
|
|
205
|
+
concurrency: jobs,
|
|
206
|
+
jobStartText: 'running',
|
|
207
|
+
name: job => [job.test.name, flagLabel(job.flags)].filter(Boolean).join(' ')
|
|
208
|
+
+ (comparing ? dim(' @ ' + job.ref.name) : ''),
|
|
209
|
+
group: opts.overlap ? undefined : job => job.ref,
|
|
210
|
+
async run(job) {
|
|
211
|
+
const config = job.ref.sha ? join(job.ref.dir, relative(repo, suite.file)) : suite.file;
|
|
212
|
+
const started = performance.now();
|
|
184
213
|
try {
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
: await runTest(test, {
|
|
188
|
-
iterations,
|
|
189
|
-
warmup,
|
|
190
|
-
cpu,
|
|
191
|
-
mem,
|
|
192
|
-
all: opts.all,
|
|
193
|
-
flags: Object.fromEntries(Object.entries(flags).map(([k, v]) => [k, [v]])),
|
|
194
|
-
});
|
|
214
|
+
results[job.index] = await runIsolated({ config, test: job.test.id, flags: job.flags, iterations, warmup, cpu, mem, all: opts.all }, timeout);
|
|
215
|
+
return { status: 'succeeded', text: styleText('blue', duration(performance.now() - started)) };
|
|
195
216
|
}
|
|
196
217
|
catch (e) {
|
|
218
|
+
const error = String(e?.message ?? e);
|
|
197
219
|
// A matrix that cannot run is one reference's problem, not the whole comparison's:
|
|
198
220
|
// report it as N/A and let the references that did run still be compared
|
|
199
|
-
|
|
200
|
-
test: test.id,
|
|
201
|
-
flags,
|
|
221
|
+
results[job.index] = job.test.configurations.map((configuration) => ({
|
|
222
|
+
test: job.test.id,
|
|
223
|
+
flags: job.flags,
|
|
202
224
|
configuration: configuration.name,
|
|
203
225
|
value: configuration.value,
|
|
204
226
|
setup: 0,
|
|
205
227
|
samples: [],
|
|
206
228
|
amounts: {},
|
|
207
|
-
error
|
|
229
|
+
error,
|
|
208
230
|
}));
|
|
231
|
+
return { status: 'failed', text: error.split('\n')[0] };
|
|
209
232
|
}
|
|
210
|
-
|
|
211
|
-
|
|
233
|
+
},
|
|
234
|
+
}, queue);
|
|
235
|
+
// The queue is reference-major, so each reference owns one contiguous run of the results
|
|
236
|
+
const refs = prepared.map((ref, i) => ({
|
|
237
|
+
ref: ref.name,
|
|
238
|
+
cases: results.slice(i * plan.length, (i + 1) * plan.length).flat(),
|
|
239
|
+
}));
|
|
240
|
+
// With one reference there is no comparison to fall back on, so its own tables are the whole report
|
|
241
|
+
if (opts.runs || !comparing) {
|
|
242
|
+
for (const [i, ref] of refs.entries()) {
|
|
243
|
+
if (comparing && !opts.quiet) {
|
|
244
|
+
const { sha } = prepared[i];
|
|
245
|
+
console.log();
|
|
246
|
+
console.log(styleText(['bold', 'underline'], ref.ref) + (sha ? dim(' ' + sha.slice(0, 8)) : ''));
|
|
212
247
|
}
|
|
213
|
-
|
|
214
|
-
progress.clear();
|
|
215
|
-
return results.flat();
|
|
216
|
-
}
|
|
217
|
-
if (!opts.quiet)
|
|
218
|
-
reportEnvironment(cpu, mem, iterations ?? 5, warmup ?? 1, jobs);
|
|
219
|
-
const repo = await repoRoot().catch(() => process.cwd());
|
|
220
|
-
let prepared;
|
|
221
|
-
try {
|
|
222
|
-
prepared = await prepareRefs(refNames, {
|
|
223
|
-
cache,
|
|
224
|
-
build: opts.build ?? suite.build,
|
|
225
|
-
root: suite.root,
|
|
226
|
-
rebuild: opts.rebuild,
|
|
227
|
-
onStep: message => progress.show(message + '...'),
|
|
228
|
-
});
|
|
229
|
-
}
|
|
230
|
-
catch (e) {
|
|
231
|
-
progress.clear();
|
|
232
|
-
fail(`could not prepare references: ${e.stderr || e.message}`);
|
|
233
|
-
}
|
|
234
|
-
progress.clear();
|
|
235
|
-
const refs = [];
|
|
236
|
-
for (const ref of prepared) {
|
|
237
|
-
if (prepared.length > 1 && !opts.quiet) {
|
|
238
|
-
console.log();
|
|
239
|
-
console.log(styleText(['bold', 'underline'], ref.name) + (ref.sha ? styleText('gray', ' ' + ref.sha.slice(0, 8)) : ''));
|
|
248
|
+
reportRun(suite, ref.cases);
|
|
240
249
|
}
|
|
241
|
-
const cases = await runRef(ref, repo);
|
|
242
|
-
reportRun(suite, cases);
|
|
243
|
-
refs.push({ ref: ref.name, cases });
|
|
244
250
|
}
|
|
245
|
-
if (
|
|
251
|
+
if (comparing) {
|
|
246
252
|
console.log();
|
|
247
253
|
console.log(styleText(['bold', 'underline'], `Change vs ${refs[0].ref}`));
|
|
248
|
-
reportComparison(compare(suite, refs, threshold), refs
|
|
254
|
+
reportComparison(compare(suite, refs, threshold), refs, {
|
|
255
|
+
unchanged: opts.unchanged,
|
|
256
|
+
partial: opts.partial,
|
|
257
|
+
});
|
|
249
258
|
}
|
|
250
259
|
if (opts.json) {
|
|
251
260
|
await writeFile(opts.json, JSON.stringify({
|
|
@@ -270,9 +279,9 @@ if (failed.length) {
|
|
|
270
279
|
for (const [message, where] of byMessage) {
|
|
271
280
|
io.error(`${message} (${where.length} configuration${where.length == 1 ? '' : 's'})`);
|
|
272
281
|
for (const at of where.slice(0, 4))
|
|
273
|
-
console.error(
|
|
282
|
+
console.error(dim(' ' + at));
|
|
274
283
|
if (where.length > 4)
|
|
275
|
-
console.error(
|
|
284
|
+
console.error(dim(` ...and ${where.length - 4} more`));
|
|
276
285
|
}
|
|
277
286
|
process.exit(1);
|
|
278
287
|
}
|
package/dist/compare.d.ts
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
|
-
import { type Column } from './measure.js';
|
|
2
|
-
import type { CaseResult } from './
|
|
1
|
+
import { type Column, type Matrix } from './measure.js';
|
|
2
|
+
import type { CaseResult } from './types.js';
|
|
3
3
|
import type { Suite, Test } from './config.js';
|
|
4
4
|
/** Results for one of the states being compared. */
|
|
5
5
|
export interface RefResults {
|
|
@@ -26,18 +26,31 @@ export interface Delta {
|
|
|
26
26
|
}
|
|
27
27
|
export interface ComparisonRow {
|
|
28
28
|
configuration: string;
|
|
29
|
-
/** Indexed
|
|
30
|
-
cells: Cell[];
|
|
29
|
+
/** Indexed by state first (baseline first), then by column */
|
|
30
|
+
cells: Cell[][];
|
|
31
|
+
/**
|
|
32
|
+
* One per state, from the primary column.
|
|
33
|
+
* The metrics of a matrix are proportional to each other, so every column would report the
|
|
34
|
+
* same factor; taking it from one keeps the table free of repeated numbers.
|
|
35
|
+
*/
|
|
31
36
|
deltas: Delta[];
|
|
32
37
|
}
|
|
38
|
+
/** One matrix, with every column it reports lined up across the states being compared. */
|
|
33
39
|
export interface Comparison {
|
|
34
40
|
test: Test;
|
|
35
41
|
/** Flag combination, empty when the test has no flags */
|
|
36
42
|
label: string;
|
|
37
|
-
|
|
43
|
+
/** Columns reported once per configuration. The first is the one `deltas` come from. */
|
|
44
|
+
columns: Column[];
|
|
38
45
|
rows: ComparisonRow[];
|
|
39
|
-
/**
|
|
40
|
-
|
|
46
|
+
/** Columns reported once for the whole matrix rather than once per row */
|
|
47
|
+
aggregateColumns: Column[];
|
|
48
|
+
/** The single row those columns fill, when there are any */
|
|
49
|
+
aggregate: ComparisonRow | null;
|
|
50
|
+
/** Whether any row's factor beat both the threshold and the noise */
|
|
51
|
+
changed: boolean;
|
|
52
|
+
/** How many states produced at least one number here. Below 2 there is nothing to compare. */
|
|
53
|
+
reported: number;
|
|
41
54
|
}
|
|
42
55
|
/**
|
|
43
56
|
* Compare a cell against the baseline.
|
|
@@ -45,7 +58,14 @@ export interface Comparison {
|
|
|
45
58
|
*/
|
|
46
59
|
export declare function delta(baseline: Cell, other: Cell, higherIsBetter: boolean, threshold: number): Delta;
|
|
47
60
|
/**
|
|
48
|
-
*
|
|
49
|
-
*
|
|
61
|
+
* The columns a comparison reports for a matrix.
|
|
62
|
+
* A test that marks any measurement with `compare` gets only those; otherwise everything but
|
|
63
|
+
* `ops/s`, which is just the reciprocal of `avg`.
|
|
64
|
+
*/
|
|
65
|
+
export declare function comparedColumns(matrix: Matrix): Column[];
|
|
66
|
+
/**
|
|
67
|
+
* Line up every state's results against the baseline, one comparison per matrix.
|
|
68
|
+
* Every column of a matrix lands in the same table, so a speedup factor is reported once rather
|
|
69
|
+
* than once per measurement.
|
|
50
70
|
*/
|
|
51
71
|
export declare function compare(suite: Suite, refs: RefResults[], threshold: number): Comparison[];
|
package/dist/compare.js
CHANGED
|
@@ -9,13 +9,14 @@ function cell(column, result) {
|
|
|
9
9
|
return { value: null, noise: 0, missing: true };
|
|
10
10
|
return { value: column.value(result), noise: timing(result).rsd, missing: false };
|
|
11
11
|
}
|
|
12
|
+
const noDelta = { factor: null, change: null, significant: false, better: false };
|
|
12
13
|
/**
|
|
13
14
|
* Compare a cell against the baseline.
|
|
14
15
|
* A change is only called out when it is bigger than both `threshold` and the two runs' combined noise.
|
|
15
16
|
*/
|
|
16
17
|
export function delta(baseline, other, higherIsBetter, threshold) {
|
|
17
18
|
if (baseline.value === null || other.value === null || !baseline.value)
|
|
18
|
-
return
|
|
19
|
+
return noDelta;
|
|
19
20
|
const factor = higherIsBetter ? other.value / baseline.value : baseline.value / other.value;
|
|
20
21
|
const change = ((other.value - baseline.value) / baseline.value) * 100;
|
|
21
22
|
const noise = combinedNoise(baseline.noise, other.noise);
|
|
@@ -27,8 +28,44 @@ export function delta(baseline, other, higherIsBetter, threshold) {
|
|
|
27
28
|
};
|
|
28
29
|
}
|
|
29
30
|
/**
|
|
30
|
-
*
|
|
31
|
-
*
|
|
31
|
+
* The columns a comparison reports for a matrix.
|
|
32
|
+
* A test that marks any measurement with `compare` gets only those; otherwise everything but
|
|
33
|
+
* `ops/s`, which is just the reciprocal of `avg`.
|
|
34
|
+
*/
|
|
35
|
+
export function comparedColumns(matrix) {
|
|
36
|
+
const all = columns(matrix);
|
|
37
|
+
const selected = all.filter(column => column.compare);
|
|
38
|
+
return selected.length ? selected : all.filter(column => column.label != 'ops/s');
|
|
39
|
+
}
|
|
40
|
+
/** Line up one configuration's cells across every state, with the primary column's delta. */
|
|
41
|
+
function row(configuration, cells, primary, threshold) {
|
|
42
|
+
return {
|
|
43
|
+
configuration,
|
|
44
|
+
cells,
|
|
45
|
+
deltas: primary
|
|
46
|
+
? cells.map(c => delta(cells[0][0], c[0], primary.higherIsBetter, threshold))
|
|
47
|
+
: cells.map(() => noDelta),
|
|
48
|
+
};
|
|
49
|
+
}
|
|
50
|
+
/** The matrix-wide value for each state, compared the same way a row is. */
|
|
51
|
+
function aggregateRow(matrix, cols, refs, threshold) {
|
|
52
|
+
const flags = JSON.stringify(matrix.flags);
|
|
53
|
+
const cells = refs.map((ref) => {
|
|
54
|
+
const cases = ref.cases.filter(r => r.test == matrix.test.id && JSON.stringify(r.flags) == flags);
|
|
55
|
+
const noise = cases.filter(c => c.samples.length).map(c => timing(c).rsd);
|
|
56
|
+
// An aggregate spans the whole matrix, so its noise is the matrix's, not any one row's
|
|
57
|
+
const spread = noise.length ? noise.reduce((sum, v) => sum + v, 0) / noise.length : 0;
|
|
58
|
+
return cols.map((column) => {
|
|
59
|
+
const total = column.total?.(cases) ?? null;
|
|
60
|
+
return { value: total, noise: spread, missing: total === null };
|
|
61
|
+
});
|
|
62
|
+
});
|
|
63
|
+
return row('aggregate', cells, cols[0], threshold);
|
|
64
|
+
}
|
|
65
|
+
/**
|
|
66
|
+
* Line up every state's results against the baseline, one comparison per matrix.
|
|
67
|
+
* Every column of a matrix lands in the same table, so a speedup factor is reported once rather
|
|
68
|
+
* than once per measurement.
|
|
32
69
|
*/
|
|
33
70
|
export function compare(suite, refs, threshold) {
|
|
34
71
|
const [baseline, ...others] = refs;
|
|
@@ -38,43 +75,26 @@ export function compare(suite, refs, threshold) {
|
|
|
38
75
|
const comparisons = [];
|
|
39
76
|
for (const test of suite.tests) {
|
|
40
77
|
for (const matrix of matrices(test, baseline.cases)) {
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
78
|
+
const selected = comparedColumns(matrix);
|
|
79
|
+
const perRow = selected.filter(column => !column.aggregate);
|
|
80
|
+
const aggregateColumns = selected.filter(column => column.aggregate);
|
|
81
|
+
const rows = perRow.length
|
|
82
|
+
? matrix.cases.map(result => row(result.configuration, indexes.map(index => perRow.map(column => cell(column, index.get(key(result))))), perRow[0], threshold))
|
|
83
|
+
: [];
|
|
84
|
+
const aggregate = aggregateColumns.length ? aggregateRow(matrix, aggregateColumns, refs, threshold) : null;
|
|
85
|
+
const all = aggregate ? [...rows, aggregate] : rows;
|
|
86
|
+
comparisons.push({
|
|
87
|
+
test,
|
|
88
|
+
label: matrix.label,
|
|
89
|
+
columns: perRow,
|
|
90
|
+
rows,
|
|
91
|
+
aggregateColumns,
|
|
92
|
+
aggregate,
|
|
93
|
+
// The baseline's delta against itself is never significant, so this only counts real changes
|
|
94
|
+
changed: all.some(r => r.deltas.some(d => d.significant)),
|
|
95
|
+
reported: refs.filter((_, r) => all.some(row => row.cells[r].some(c => c.value !== null))).length,
|
|
96
|
+
});
|
|
58
97
|
}
|
|
59
98
|
}
|
|
60
99
|
return comparisons;
|
|
61
100
|
}
|
|
62
|
-
/** The matrix-wide value for each state, compared the same way a row is. */
|
|
63
|
-
function aggregateRow(matrix, column, refs, threshold) {
|
|
64
|
-
const flags = JSON.stringify(matrix.flags);
|
|
65
|
-
const cells = refs.map((ref) => {
|
|
66
|
-
const cases = ref.cases.filter(r => r.test == matrix.test.id && JSON.stringify(r.flags) == flags);
|
|
67
|
-
const total = column.total?.(cases) ?? null;
|
|
68
|
-
const noise = cases.filter(c => c.samples.length).map(c => timing(c).rsd);
|
|
69
|
-
return {
|
|
70
|
-
value: total,
|
|
71
|
-
noise: noise.length ? noise.reduce((sum, v) => sum + v, 0) / noise.length : 0,
|
|
72
|
-
missing: total === null,
|
|
73
|
-
};
|
|
74
|
-
});
|
|
75
|
-
return {
|
|
76
|
-
configuration: 'aggregate',
|
|
77
|
-
cells,
|
|
78
|
-
deltas: cells.map(c => delta(cells[0], c, column.higherIsBetter, threshold)),
|
|
79
|
-
};
|
|
80
|
-
}
|