zbench-js 0.1.0 → 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/dist/config.d.ts
CHANGED
|
@@ -1,70 +1,92 @@
|
|
|
1
|
+
import * as z from 'zod';
|
|
1
2
|
import { type Timespan } from './units.js';
|
|
3
|
+
declare const Timespan: z.ZodLiteral<"ns" | "us" | "ms" | "s">;
|
|
2
4
|
/** A measurement is either derived at an inferred timespan (`true`) or at an explicit one. */
|
|
3
|
-
export
|
|
5
|
+
export declare const MeasureValue: z.ZodUnion<readonly [z.ZodBoolean, z.ZodLiteral<"ns" | "us" | "ms" | "s">]>;
|
|
6
|
+
export type MeasureValue = z.infer<typeof MeasureValue>;
|
|
4
7
|
/**
|
|
5
8
|
* How to turn one of a test's quantities into a reported measurement.
|
|
6
9
|
* The quantity's amount per iteration comes from what `test` returns, falling back to the configuration value.
|
|
7
10
|
*/
|
|
8
|
-
export
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
unit?: string;
|
|
19
|
-
/**
|
|
20
|
-
* Multiplier from the raw amount to `unit`.
|
|
21
|
-
* Defaults to 1, except for byte units (`MB`, `MiB`, ...), where amounts are taken to be bytes.
|
|
22
|
-
*/
|
|
23
|
-
scale?: number;
|
|
11
|
+
export declare const MeasureConfig: z.ZodObject<{
|
|
12
|
+
throughput: z.ZodOptional<z.ZodUnion<readonly [z.ZodBoolean, z.ZodLiteral<"ns" | "us" | "ms" | "s">]>>;
|
|
13
|
+
aggregate_throughput: z.ZodOptional<z.ZodUnion<readonly [z.ZodBoolean, z.ZodLiteral<"ns" | "us" | "ms" | "s">]>>;
|
|
14
|
+
cost: z.ZodOptional<z.ZodUnion<readonly [z.ZodBoolean, z.ZodLiteral<"ns" | "us" | "ms" | "s">]>>;
|
|
15
|
+
aggregate_cost: z.ZodOptional<z.ZodUnion<readonly [z.ZodBoolean, z.ZodLiteral<"ns" | "us" | "ms" | "s">]>>;
|
|
16
|
+
unit: z.ZodOptional<z.ZodString>;
|
|
17
|
+
scale: z.ZodOptional<z.ZodNumber>;
|
|
18
|
+
compare: z.ZodOptional<z.ZodBoolean>;
|
|
19
|
+
}, z.core.$strip>;
|
|
20
|
+
export interface MeasureConfig extends z.infer<typeof MeasureConfig> {
|
|
24
21
|
}
|
|
25
22
|
/** One point in a test's matrix. */
|
|
26
|
-
export
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
cpu?: number;
|
|
34
|
-
/** Minimum memory level needed to run this configuration. Also logarithmic. */
|
|
35
|
-
mem?: number;
|
|
36
|
-
/** Passed to the test, and the source of measurement amounts. */
|
|
37
|
-
value: Record<string, number>;
|
|
23
|
+
export declare const CaseConfig: z.ZodObject<{
|
|
24
|
+
name: z.ZodOptional<z.ZodString>;
|
|
25
|
+
cpu: z.ZodOptional<z.ZodNumber>;
|
|
26
|
+
mem: z.ZodOptional<z.ZodNumber>;
|
|
27
|
+
value: z.ZodRecord<z.ZodString, z.ZodNumber>;
|
|
28
|
+
}, z.core.$strip>;
|
|
29
|
+
export interface CaseConfig extends z.infer<typeof CaseConfig> {
|
|
38
30
|
}
|
|
39
|
-
export
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
31
|
+
export declare const TestConfig: z.ZodObject<{
|
|
32
|
+
path: z.ZodString;
|
|
33
|
+
name: z.ZodOptional<z.ZodString>;
|
|
34
|
+
measure: z.ZodOptional<z.ZodRecord<z.ZodString, z.ZodObject<{
|
|
35
|
+
throughput: z.ZodOptional<z.ZodUnion<readonly [z.ZodBoolean, z.ZodLiteral<"ns" | "us" | "ms" | "s">]>>;
|
|
36
|
+
aggregate_throughput: z.ZodOptional<z.ZodUnion<readonly [z.ZodBoolean, z.ZodLiteral<"ns" | "us" | "ms" | "s">]>>;
|
|
37
|
+
cost: z.ZodOptional<z.ZodUnion<readonly [z.ZodBoolean, z.ZodLiteral<"ns" | "us" | "ms" | "s">]>>;
|
|
38
|
+
aggregate_cost: z.ZodOptional<z.ZodUnion<readonly [z.ZodBoolean, z.ZodLiteral<"ns" | "us" | "ms" | "s">]>>;
|
|
39
|
+
unit: z.ZodOptional<z.ZodString>;
|
|
40
|
+
scale: z.ZodOptional<z.ZodNumber>;
|
|
41
|
+
compare: z.ZodOptional<z.ZodBoolean>;
|
|
42
|
+
}, z.core.$strip>>>;
|
|
43
|
+
configurations: z.ZodArray<z.ZodObject<{
|
|
44
|
+
name: z.ZodOptional<z.ZodString>;
|
|
45
|
+
cpu: z.ZodOptional<z.ZodNumber>;
|
|
46
|
+
mem: z.ZodOptional<z.ZodNumber>;
|
|
47
|
+
value: z.ZodRecord<z.ZodString, z.ZodNumber>;
|
|
48
|
+
}, z.core.$strip>>;
|
|
49
|
+
flags: z.ZodOptional<z.ZodUnion<readonly [z.ZodArray<z.ZodString>, z.ZodRecord<z.ZodString, z.ZodArray<z.ZodUnknown>>]>>;
|
|
50
|
+
iterations: z.ZodOptional<z.ZodNumber>;
|
|
51
|
+
warmup: z.ZodOptional<z.ZodNumber>;
|
|
52
|
+
}, z.core.$strip>;
|
|
53
|
+
export interface TestConfig extends z.infer<typeof TestConfig> {
|
|
50
54
|
}
|
|
51
|
-
export
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
55
|
+
export declare const SuiteConfig: z.ZodObject<{
|
|
56
|
+
iterations: z.ZodOptional<z.ZodNumber>;
|
|
57
|
+
warmup: z.ZodOptional<z.ZodNumber>;
|
|
58
|
+
flags: z.ZodOptional<z.ZodRecord<z.ZodString, z.ZodArray<z.ZodUnknown>>>;
|
|
59
|
+
build: z.ZodOptional<z.ZodString>;
|
|
60
|
+
root: z.ZodOptional<z.ZodString>;
|
|
61
|
+
tests: z.ZodArray<z.ZodObject<{
|
|
62
|
+
path: z.ZodString;
|
|
63
|
+
name: z.ZodOptional<z.ZodString>;
|
|
64
|
+
measure: z.ZodOptional<z.ZodRecord<z.ZodString, z.ZodObject<{
|
|
65
|
+
throughput: z.ZodOptional<z.ZodUnion<readonly [z.ZodBoolean, z.ZodLiteral<"ns" | "us" | "ms" | "s">]>>;
|
|
66
|
+
aggregate_throughput: z.ZodOptional<z.ZodUnion<readonly [z.ZodBoolean, z.ZodLiteral<"ns" | "us" | "ms" | "s">]>>;
|
|
67
|
+
cost: z.ZodOptional<z.ZodUnion<readonly [z.ZodBoolean, z.ZodLiteral<"ns" | "us" | "ms" | "s">]>>;
|
|
68
|
+
aggregate_cost: z.ZodOptional<z.ZodUnion<readonly [z.ZodBoolean, z.ZodLiteral<"ns" | "us" | "ms" | "s">]>>;
|
|
69
|
+
unit: z.ZodOptional<z.ZodString>;
|
|
70
|
+
scale: z.ZodOptional<z.ZodNumber>;
|
|
71
|
+
compare: z.ZodOptional<z.ZodBoolean>;
|
|
72
|
+
}, z.core.$strip>>>;
|
|
73
|
+
configurations: z.ZodArray<z.ZodObject<{
|
|
74
|
+
name: z.ZodOptional<z.ZodString>;
|
|
75
|
+
cpu: z.ZodOptional<z.ZodNumber>;
|
|
76
|
+
mem: z.ZodOptional<z.ZodNumber>;
|
|
77
|
+
value: z.ZodRecord<z.ZodString, z.ZodNumber>;
|
|
78
|
+
}, z.core.$strip>>;
|
|
79
|
+
flags: z.ZodOptional<z.ZodUnion<readonly [z.ZodArray<z.ZodString>, z.ZodRecord<z.ZodString, z.ZodArray<z.ZodUnknown>>]>>;
|
|
80
|
+
iterations: z.ZodOptional<z.ZodNumber>;
|
|
81
|
+
warmup: z.ZodOptional<z.ZodNumber>;
|
|
82
|
+
}, z.core.$strip>>;
|
|
83
|
+
}, z.core.$strip>;
|
|
84
|
+
export interface SuiteConfig extends z.infer<typeof SuiteConfig> {
|
|
63
85
|
}
|
|
64
86
|
/** Config files are looked for at these paths, in order, relative to the working directory. */
|
|
65
87
|
export declare const configPaths: string[];
|
|
66
88
|
export declare const defaultBuild = "npm install --no-audit --no-fund && npm run build";
|
|
67
|
-
/** A single reported column, derived from one `
|
|
89
|
+
/** A single reported column, derived from one `MeasureConfig` entry. */
|
|
68
90
|
export interface Metric {
|
|
69
91
|
/** The quantity this is derived from */
|
|
70
92
|
key: string;
|
|
@@ -76,6 +98,8 @@ export interface Metric {
|
|
|
76
98
|
scale: number;
|
|
77
99
|
unit: string;
|
|
78
100
|
higherIsBetter: boolean;
|
|
101
|
+
/** Whether the measurement asked to be the one comparisons report */
|
|
102
|
+
compare: boolean;
|
|
79
103
|
}
|
|
80
104
|
/** A test with its defaults filled in and its measurements flattened into metrics. */
|
|
81
105
|
export interface Test {
|
|
@@ -86,7 +110,7 @@ export interface Test {
|
|
|
86
110
|
/** Path as written, for display */
|
|
87
111
|
path: string;
|
|
88
112
|
metrics: Metric[];
|
|
89
|
-
configurations: Required<Pick<
|
|
113
|
+
configurations: Required<Pick<CaseConfig, 'name' | 'cpu' | 'mem' | 'value'>>[];
|
|
90
114
|
flags: Record<string, unknown[]>;
|
|
91
115
|
iterations: number;
|
|
92
116
|
warmup: number;
|
|
@@ -101,10 +125,11 @@ export interface Suite {
|
|
|
101
125
|
}
|
|
102
126
|
/** Find the config file, starting from `from` and walking up to the filesystem root. */
|
|
103
127
|
export declare function findConfig(from?: string): string | null;
|
|
104
|
-
/**
|
|
128
|
+
/** Validate already-loaded config data. `file` is what test paths resolve against. */
|
|
105
129
|
export declare function parseSuite(data: unknown, file: string): Suite;
|
|
106
130
|
export declare function loadSuite(file: string): Suite;
|
|
107
131
|
/** Every combination of the given flags, in declaration order. Always at least one (possibly empty) combination. */
|
|
108
132
|
export declare function flagCombinations(flags: Record<string, unknown[]>): Record<string, unknown>[];
|
|
109
133
|
/** A stable, human-readable label for a flag combination. Empty when there are no flags. */
|
|
110
134
|
export declare function flagLabel(flags: Record<string, unknown>): string;
|
|
135
|
+
export {};
|
package/dist/config.js
CHANGED
|
@@ -1,23 +1,114 @@
|
|
|
1
1
|
// SPDX-License-Identifier: LGPL-3.0-or-later
|
|
2
|
-
import
|
|
2
|
+
import * as io from 'ioium/node';
|
|
3
|
+
import { existsSync } from 'node:fs';
|
|
3
4
|
import { dirname, isAbsolute, join, resolve } from 'node:path';
|
|
4
|
-
import
|
|
5
|
+
import * as z from 'zod';
|
|
6
|
+
import { timespanOrder, unitScale } from './units.js';
|
|
7
|
+
const Timespan = z.literal(timespanOrder);
|
|
8
|
+
/** A measurement is either derived at an inferred timespan (`true`) or at an explicit one. */
|
|
9
|
+
export const MeasureValue = z.union([z.boolean(), Timespan], {
|
|
10
|
+
error: 'expected true, false, or a timespan ("ns", "us", "ms", "s")',
|
|
11
|
+
});
|
|
12
|
+
/** The four ways a quantity can be turned into a reported measurement. */
|
|
13
|
+
const measurements = ['throughput', 'aggregate_throughput', 'cost', 'aggregate_cost'];
|
|
14
|
+
/**
|
|
15
|
+
* How to turn one of a test's quantities into a reported measurement.
|
|
16
|
+
* The quantity's amount per iteration comes from what `test` returns, falling back to the configuration value.
|
|
17
|
+
*/
|
|
18
|
+
export const MeasureConfig = z
|
|
19
|
+
.object({
|
|
20
|
+
// `<unit>`s per `<timespan>` for each configuration, e.g. `MB/s`
|
|
21
|
+
throughput: MeasureValue.optional(),
|
|
22
|
+
// `<unit>`s per `<timespan>` across every configuration in the matrix
|
|
23
|
+
aggregate_throughput: MeasureValue.optional(),
|
|
24
|
+
// `<timespan>` per `<unit>` for each configuration, e.g. `ms/entry`
|
|
25
|
+
cost: MeasureValue.optional(),
|
|
26
|
+
// `<timespan>` per `<unit>` across every configuration in the matrix
|
|
27
|
+
aggregate_cost: MeasureValue.optional(),
|
|
28
|
+
// What the amount is counted in. Defaults to the quantity's key.
|
|
29
|
+
unit: z.string().optional(),
|
|
30
|
+
// Multiplier from the raw amount to `unit`. Defaults to 1, except for byte units, where amounts are bytes.
|
|
31
|
+
scale: z.number().optional(),
|
|
32
|
+
// If any of a test's measurements set this, comparisons report only those, and the first is
|
|
33
|
+
// the one speedup factors are computed from
|
|
34
|
+
compare: z.boolean().optional(),
|
|
35
|
+
})
|
|
36
|
+
.refine(cfg => measurements.some(field => cfg[field] !== undefined && cfg[field] !== false), 'declares no measurements; set at least one of throughput, cost, aggregate_throughput, aggregate_cost');
|
|
37
|
+
/** One point in a test's matrix. */
|
|
38
|
+
export const CaseConfig = z.object({
|
|
39
|
+
// Defaults to the configuration's values, e.g. `size=128, entries=1000`
|
|
40
|
+
name: z.string().optional(),
|
|
41
|
+
// Minimum CPU level needed to run this configuration. Levels are logarithmic: `n + 1` costs about twice `n`.
|
|
42
|
+
cpu: z.number().optional(),
|
|
43
|
+
// Minimum memory level needed to run this configuration. Also logarithmic.
|
|
44
|
+
mem: z.number().optional(),
|
|
45
|
+
// Passed to the test, and the source of measurement amounts
|
|
46
|
+
value: z.record(z.string(), z.number()),
|
|
47
|
+
});
|
|
48
|
+
/** A non-empty list of values a flag can take. */
|
|
49
|
+
const flagValues = z.array(z.unknown()).min(1, 'expected a non-empty array of values');
|
|
50
|
+
export const TestConfig = z.object({
|
|
51
|
+
// Resolved relative to the config file
|
|
52
|
+
path: z.string(),
|
|
53
|
+
// Defaults to `path`
|
|
54
|
+
name: z.string().optional(),
|
|
55
|
+
measure: z.record(z.string(), MeasureConfig).optional(),
|
|
56
|
+
configurations: z.array(CaseConfig).min(1, 'expected at least one configuration'),
|
|
57
|
+
// Flags from the suite's pool to apply, or flags declared inline. The matrix runs once per combination.
|
|
58
|
+
flags: z
|
|
59
|
+
.union([z.array(z.string()), z.record(z.string(), flagValues)], {
|
|
60
|
+
error: 'expected an array of flag names or an object of flag values',
|
|
61
|
+
})
|
|
62
|
+
.optional(),
|
|
63
|
+
iterations: z.number().optional(),
|
|
64
|
+
warmup: z.number().optional(),
|
|
65
|
+
});
|
|
66
|
+
export const SuiteConfig = z
|
|
67
|
+
.object({
|
|
68
|
+
// Timed runs per configuration. @default 5
|
|
69
|
+
iterations: z.number().optional(),
|
|
70
|
+
// Untimed runs before the timed ones. @default 1
|
|
71
|
+
warmup: z.number().optional(),
|
|
72
|
+
// Flag pool that tests can draw from by name
|
|
73
|
+
flags: z.record(z.string(), flagValues).optional(),
|
|
74
|
+
// Shell command that makes a git reference's worktree runnable
|
|
75
|
+
build: z.string().optional(),
|
|
76
|
+
// Directory copied into a reference's worktree so every reference runs today's tests.
|
|
77
|
+
// Defaults to the config file's directory.
|
|
78
|
+
root: z.string().optional(),
|
|
79
|
+
tests: z.array(TestConfig),
|
|
80
|
+
})
|
|
81
|
+
// Neither of these is about one test's shape: a flag name is only meaningful against the pool,
|
|
82
|
+
// and a duplicate id only shows up once the other tests are known
|
|
83
|
+
.superRefine((spec, ctx) => {
|
|
84
|
+
const pool = spec.flags ?? {};
|
|
85
|
+
const seen = new Set();
|
|
86
|
+
for (const [i, test] of spec.tests.entries()) {
|
|
87
|
+
if (Array.isArray(test.flags)) {
|
|
88
|
+
for (const [j, name] of test.flags.entries()) {
|
|
89
|
+
if (name in pool)
|
|
90
|
+
continue;
|
|
91
|
+
ctx.addIssue({
|
|
92
|
+
code: 'custom',
|
|
93
|
+
path: ['tests', i, 'flags', j],
|
|
94
|
+
message: `no flag named ${JSON.stringify(name)} is declared by the suite`,
|
|
95
|
+
});
|
|
96
|
+
}
|
|
97
|
+
}
|
|
98
|
+
// Results are keyed by id, so a duplicate would silently merge two tests' numbers
|
|
99
|
+
const id = test.name ?? test.path;
|
|
100
|
+
if (seen.has(id))
|
|
101
|
+
ctx.addIssue({
|
|
102
|
+
code: 'custom',
|
|
103
|
+
path: ['tests', i, test.name === undefined ? 'path' : 'name'],
|
|
104
|
+
message: `more than one test is named ${JSON.stringify(id)}`,
|
|
105
|
+
});
|
|
106
|
+
seen.add(id);
|
|
107
|
+
}
|
|
108
|
+
});
|
|
5
109
|
/** Config files are looked for at these paths, in order, relative to the working directory. */
|
|
6
110
|
export const configPaths = ['tests/perf/config.json', 'tests/perf.json', 'tests/perf.config.json', 'zbench.json'];
|
|
7
111
|
export const defaultBuild = 'npm install --no-audit --no-fund && npm run build';
|
|
8
|
-
class ConfigError extends Error {
|
|
9
|
-
constructor(path, message) {
|
|
10
|
-
super(`${path}: ${message}`);
|
|
11
|
-
this.name = 'ConfigError';
|
|
12
|
-
}
|
|
13
|
-
}
|
|
14
|
-
function check(condition, path, message) {
|
|
15
|
-
if (!condition)
|
|
16
|
-
throw new ConfigError(path, message);
|
|
17
|
-
}
|
|
18
|
-
function isRecord(value) {
|
|
19
|
-
return typeof value == 'object' && value !== null && !Array.isArray(value);
|
|
20
|
-
}
|
|
21
112
|
/** Find the config file, starting from `from` and walking up to the filesystem root. */
|
|
22
113
|
export function findConfig(from = process.cwd()) {
|
|
23
114
|
for (let dir = resolve(from);; dir = dirname(dir)) {
|
|
@@ -30,37 +121,29 @@ export function findConfig(from = process.cwd()) {
|
|
|
30
121
|
return null;
|
|
31
122
|
}
|
|
32
123
|
}
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
return undefined;
|
|
36
|
-
if (value === true)
|
|
37
|
-
return null;
|
|
38
|
-
check(isTimespan(value), path, `expected true, false, or a timespan ("ns", "us", "ms", "s")`);
|
|
39
|
-
return value;
|
|
40
|
-
}
|
|
41
|
-
function parseMetrics(measure, path) {
|
|
42
|
-
if (measure === undefined)
|
|
43
|
-
return [];
|
|
44
|
-
check(isRecord(measure), path, 'expected an object');
|
|
124
|
+
/** Flatten a test's measurements into one metric per declared timespan. */
|
|
125
|
+
function parseMetrics(measure) {
|
|
45
126
|
const metrics = [];
|
|
46
|
-
for (const [key,
|
|
47
|
-
const at = `${path}.${key}`;
|
|
48
|
-
check(isRecord(raw), at, 'expected an object');
|
|
49
|
-
const spec = raw;
|
|
127
|
+
for (const [key, spec] of Object.entries(measure ?? {})) {
|
|
50
128
|
const unit = spec.unit ?? key;
|
|
51
|
-
check(typeof unit == 'string', `${at}.unit`, 'expected a string');
|
|
52
|
-
check(spec.scale === undefined || typeof spec.scale == 'number', `${at}.scale`, 'expected a number');
|
|
53
129
|
const scale = spec.scale ?? unitScale(unit);
|
|
54
130
|
for (const kind of ['throughput', 'cost']) {
|
|
55
131
|
for (const aggregate of [false, true]) {
|
|
56
|
-
const
|
|
57
|
-
|
|
58
|
-
if (span === undefined)
|
|
132
|
+
const value = spec[aggregate ? `aggregate_${kind}` : kind];
|
|
133
|
+
if (value === undefined || value === false)
|
|
59
134
|
continue;
|
|
60
|
-
metrics.push({
|
|
135
|
+
metrics.push({
|
|
136
|
+
key,
|
|
137
|
+
kind,
|
|
138
|
+
aggregate,
|
|
139
|
+
span: value === true ? null : value,
|
|
140
|
+
scale,
|
|
141
|
+
unit,
|
|
142
|
+
higherIsBetter: kind == 'throughput',
|
|
143
|
+
compare: spec.compare ?? false,
|
|
144
|
+
});
|
|
61
145
|
}
|
|
62
146
|
}
|
|
63
|
-
check(metrics.some(m => m.key == key), at, 'declares no measurements; set at least one of throughput, cost, aggregate_throughput, aggregate_cost');
|
|
64
147
|
}
|
|
65
148
|
return metrics;
|
|
66
149
|
}
|
|
@@ -69,96 +152,57 @@ function defaultName(value) {
|
|
|
69
152
|
.map(([k, v]) => `${k}=${v}`)
|
|
70
153
|
.join(', ');
|
|
71
154
|
}
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
selected[name] = pool[name];
|
|
81
|
-
}
|
|
82
|
-
return selected;
|
|
83
|
-
}
|
|
84
|
-
check(isRecord(flags), path, 'expected an array of flag names or an object of flag values');
|
|
85
|
-
const inline = {};
|
|
86
|
-
for (const [name, values] of Object.entries(flags)) {
|
|
87
|
-
check(Array.isArray(values) && values.length > 0, `${path}.${name}`, 'expected a non-empty array of values');
|
|
88
|
-
inline[name] = values;
|
|
89
|
-
}
|
|
90
|
-
return inline;
|
|
155
|
+
/** A test's flags: the pool entries it named, the ones it declared inline, or none. */
|
|
156
|
+
function parseFlags(flags, pool) {
|
|
157
|
+
if (!flags)
|
|
158
|
+
return {};
|
|
159
|
+
// Every name was checked against the pool while the suite was validated
|
|
160
|
+
if (Array.isArray(flags))
|
|
161
|
+
return Object.fromEntries(flags.map(name => [name, pool[name]]));
|
|
162
|
+
return flags;
|
|
91
163
|
}
|
|
92
|
-
|
|
93
|
-
|
|
164
|
+
const ConfigFile = z.preprocess(data => (Array.isArray(data) ? { tests: data } : data), SuiteConfig);
|
|
165
|
+
/** Turn validated config data into a suite. `file` is what test paths resolve against. */
|
|
166
|
+
function toSuite(cfg, file) {
|
|
94
167
|
const dir = dirname(resolve(file));
|
|
95
|
-
const
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
const
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
const cat = `${at}.configurations[${j}]`;
|
|
115
|
-
check(isRecord(config), cat, 'expected an object');
|
|
116
|
-
check(isRecord(config.value), `${cat}.value`, 'expected an object');
|
|
117
|
-
for (const [k, v] of Object.entries(config.value))
|
|
118
|
-
check(typeof v == 'number', `${cat}.value.${k}`, 'expected a number');
|
|
119
|
-
return {
|
|
120
|
-
name: config.name ?? defaultName(config.value),
|
|
121
|
-
cpu: config.cpu ?? 0,
|
|
122
|
-
mem: config.mem ?? 0,
|
|
123
|
-
value: config.value,
|
|
124
|
-
};
|
|
125
|
-
});
|
|
126
|
-
check(configurations.length > 0, `${at}.configurations`, 'expected at least one configuration');
|
|
127
|
-
return {
|
|
128
|
-
// The name, so one file can back several tests that differ only in their matrix
|
|
129
|
-
id: test.name ?? test.path,
|
|
130
|
-
name: test.name ?? test.path,
|
|
131
|
-
file: isAbsolute(test.path) ? test.path : resolve(dir, test.path),
|
|
132
|
-
path: test.path,
|
|
133
|
-
metrics: parseMetrics(test.measure, `${at}.measure`),
|
|
134
|
-
configurations,
|
|
135
|
-
flags: parseFlags(test.flags, pool, `${at}.flags`) ?? {},
|
|
136
|
-
iterations: test.iterations ?? iterations,
|
|
137
|
-
warmup: test.warmup ?? warmup,
|
|
138
|
-
};
|
|
139
|
-
});
|
|
140
|
-
// Results are keyed by id, so a duplicate would silently merge two tests' numbers
|
|
141
|
-
const seen = new Set();
|
|
142
|
-
for (const test of tests) {
|
|
143
|
-
check(!seen.has(test.id), `config.tests`, `more than one test is named ${JSON.stringify(test.id)}`);
|
|
144
|
-
seen.add(test.id);
|
|
145
|
-
}
|
|
168
|
+
const iterations = cfg.iterations ?? 5;
|
|
169
|
+
const warmup = cfg.warmup ?? 1;
|
|
170
|
+
const pool = cfg.flags ?? {};
|
|
171
|
+
const tests = cfg.tests.map((test) => ({
|
|
172
|
+
id: test.name ?? test.path,
|
|
173
|
+
name: test.name ?? test.path,
|
|
174
|
+
file: isAbsolute(test.path) ? test.path : resolve(dir, test.path),
|
|
175
|
+
path: test.path,
|
|
176
|
+
metrics: parseMetrics(test.measure),
|
|
177
|
+
configurations: test.configurations.map(config => ({
|
|
178
|
+
name: config.name ?? defaultName(config.value),
|
|
179
|
+
cpu: config.cpu ?? 0,
|
|
180
|
+
mem: config.mem ?? 0,
|
|
181
|
+
value: config.value,
|
|
182
|
+
})),
|
|
183
|
+
flags: parseFlags(test.flags, pool),
|
|
184
|
+
iterations: test.iterations ?? iterations,
|
|
185
|
+
warmup: test.warmup ?? warmup,
|
|
186
|
+
}));
|
|
146
187
|
return {
|
|
147
188
|
file: resolve(file),
|
|
148
|
-
root:
|
|
149
|
-
build:
|
|
189
|
+
root: cfg.root ? resolve(dir, cfg.root) : dir,
|
|
190
|
+
build: cfg.build ?? defaultBuild,
|
|
150
191
|
tests,
|
|
151
192
|
};
|
|
152
193
|
}
|
|
153
|
-
|
|
154
|
-
|
|
194
|
+
/** Validate already-loaded config data. `file` is what test paths resolve against. */
|
|
195
|
+
export function parseSuite(data, file) {
|
|
155
196
|
try {
|
|
156
|
-
|
|
197
|
+
return toSuite(ConfigFile.parse(data), file);
|
|
157
198
|
}
|
|
158
199
|
catch (e) {
|
|
159
|
-
|
|
200
|
+
// eslint-disable-next-line @typescript-eslint/only-throw-error
|
|
201
|
+
throw io.errorText(e);
|
|
160
202
|
}
|
|
161
|
-
|
|
203
|
+
}
|
|
204
|
+
export function loadSuite(file) {
|
|
205
|
+
return toSuite(io.readJSON(file, ConfigFile), file);
|
|
162
206
|
}
|
|
163
207
|
/** Every combination of the given flags, in declaration order. Always at least one (possibly empty) combination. */
|
|
164
208
|
export function flagCombinations(flags) {
|
package/dist/index.d.ts
CHANGED
package/dist/index.js
CHANGED
package/dist/isolate.d.ts
CHANGED
|
@@ -1,7 +1,8 @@
|
|
|
1
|
-
import {
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
|
|
1
|
+
import type { CaseResult } from './types.js';
|
|
2
|
+
import type { Test } from './config.js';
|
|
3
|
+
/** Everything a worker needs to run exactly one matrix. */
|
|
4
|
+
export interface WorkerConfig {
|
|
5
|
+
/** Absolute path to the config file, inside the reference's worktree when there is one */
|
|
5
6
|
config: string;
|
|
6
7
|
/** The test's id */
|
|
7
8
|
test: string;
|
|
@@ -12,16 +13,11 @@ export interface WorkerSpec {
|
|
|
12
13
|
cpu?: number;
|
|
13
14
|
mem?: number;
|
|
14
15
|
all?: boolean;
|
|
15
|
-
|
|
16
|
-
out: string;
|
|
16
|
+
onCase?(test: Test, flags: Record<string, unknown>, configuration: string, index: number, total: number): void;
|
|
17
17
|
}
|
|
18
|
-
/** Run one matrix and write the results where the parent expects them. */
|
|
19
|
-
export declare function runWorker(specPath: string): Promise<void>;
|
|
20
18
|
/**
|
|
21
|
-
* Run one matrix in a fresh
|
|
22
|
-
* Every matrix gets its own heap and its own module graph, which is what keeps one configuration
|
|
23
|
-
* from warming up (or poisoning) the next, and what lets a git reference's own build be loaded.
|
|
19
|
+
* Run one matrix in a fresh worker thread.
|
|
24
20
|
*/
|
|
25
|
-
export declare function runIsolated(spec:
|
|
21
|
+
export declare function runIsolated(spec: WorkerConfig,
|
|
26
22
|
/** Seconds before the worker is killed. 0 waits forever. */
|
|
27
23
|
timeout?: number): Promise<CaseResult[]>;
|