@vercel/devlow-bench 0.3.4 → 16.4.0-canary.24
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 +50 -33
- package/dist/browser.d.ts +1 -1
- package/dist/browser.js +52 -44
- package/dist/cli.js +114 -69
- package/dist/compare.d.ts +14 -0
- package/dist/compare.js +209 -0
- package/dist/describe.d.ts +1 -1
- package/dist/describe.js +18 -18
- package/dist/file.js +5 -5
- package/dist/index.d.ts +16 -3
- package/dist/index.js +3 -2
- package/dist/interfaces/compare.d.ts +4 -0
- package/dist/interfaces/compare.js +27 -0
- package/dist/interfaces/compose.d.ts +1 -1
- package/dist/interfaces/compose.js +1 -1
- package/dist/interfaces/console.d.ts +4 -2
- package/dist/interfaces/console.js +30 -8
- package/dist/interfaces/constants.js +7 -7
- package/dist/interfaces/datadog.d.ts +1 -1
- package/dist/interfaces/datadog.js +13 -13
- package/dist/interfaces/interactive.d.ts +1 -1
- package/dist/interfaces/interactive.js +8 -8
- package/dist/interfaces/json.d.ts +4 -2
- package/dist/interfaces/json.js +36 -17
- package/dist/interfaces/shell-test.d.ts +1 -0
- package/dist/interfaces/shell-test.js +56 -0
- package/dist/interfaces/snapshot.d.ts +6 -0
- package/dist/interfaces/snapshot.js +60 -0
- package/dist/interfaces/snowflake-test.js +3 -3
- package/dist/interfaces/snowflake.d.ts +1 -1
- package/dist/interfaces/snowflake.js +15 -13
- package/dist/runner.d.ts +5 -2
- package/dist/runner.js +132 -24
- package/dist/shell.d.ts +5 -3
- package/dist/shell.js +56 -28
- package/dist/snapshot.d.ts +18 -0
- package/dist/snapshot.js +162 -0
- package/dist/statistics-test.d.ts +1 -0
- package/dist/statistics-test.js +102 -0
- package/dist/statistics.d.ts +19 -0
- package/dist/statistics.js +123 -0
- package/dist/table.js +51 -51
- package/dist/units.js +6 -6
- package/dist/utils.d.ts +1 -0
- package/dist/utils.js +8 -6
- package/package.json +6 -5
package/dist/interfaces/json.js
CHANGED
|
@@ -1,5 +1,6 @@
|
|
|
1
|
-
import {
|
|
2
|
-
import {
|
|
1
|
+
import { quantile, mean as statsMean } from '../statistics.js';
|
|
2
|
+
import { formatUnit } from '../units.js';
|
|
3
|
+
import { writeFile } from 'fs/promises';
|
|
3
4
|
function filterProp(prop) {
|
|
4
5
|
const filteredProp = {};
|
|
5
6
|
for (const [key, value] of Object.entries(prop)) {
|
|
@@ -10,12 +11,14 @@ function filterProp(prop) {
|
|
|
10
11
|
return filteredProp;
|
|
11
12
|
}
|
|
12
13
|
export default function createInterface(file = (() => {
|
|
13
|
-
const
|
|
14
|
-
if (!
|
|
15
|
-
throw new Error(
|
|
14
|
+
const fromEnv = process.env.JSON_OUTPUT_FILE;
|
|
15
|
+
if (!fromEnv) {
|
|
16
|
+
throw new Error('env var JSON_OUTPUT_FILE is not set');
|
|
16
17
|
}
|
|
17
|
-
return
|
|
18
|
-
})()) {
|
|
18
|
+
return fromEnv;
|
|
19
|
+
})(), options = {}) {
|
|
20
|
+
const n = options.n ?? 1;
|
|
21
|
+
// Per-(scenario, props, name) sample accumulator.
|
|
19
22
|
const metrics = new Map();
|
|
20
23
|
const iface = {
|
|
21
24
|
measurement: async (scenario, props, name, value, unit, relativeTo) => {
|
|
@@ -27,30 +30,46 @@ export default function createInterface(file = (() => {
|
|
|
27
30
|
const key = JSON.stringify(keyObject);
|
|
28
31
|
const current = metrics.get(key);
|
|
29
32
|
if (current) {
|
|
30
|
-
current.value
|
|
31
|
-
current.count++;
|
|
33
|
+
current.samples.push(value);
|
|
32
34
|
}
|
|
33
35
|
else {
|
|
34
36
|
metrics.set(key, {
|
|
35
37
|
key: keyObject,
|
|
36
|
-
value,
|
|
37
|
-
unit
|
|
38
|
-
count: 1,
|
|
38
|
+
samples: [value],
|
|
39
|
+
unit,
|
|
39
40
|
relativeTo,
|
|
40
41
|
});
|
|
41
42
|
}
|
|
42
43
|
},
|
|
43
44
|
finish: async () => {
|
|
44
|
-
|
|
45
|
+
const results = [...metrics.values()].map(({ key, samples, unit, relativeTo }) => {
|
|
46
|
+
if (samples.length === 1) {
|
|
47
|
+
// Preserve the original single-run shape exactly for back-compat.
|
|
48
|
+
return {
|
|
49
|
+
key,
|
|
50
|
+
value: samples[0],
|
|
51
|
+
unit,
|
|
52
|
+
text: formatUnit(samples[0], unit),
|
|
53
|
+
datapoints: 1,
|
|
54
|
+
relativeTo,
|
|
55
|
+
};
|
|
56
|
+
}
|
|
57
|
+
const m = statsMean(samples);
|
|
45
58
|
return {
|
|
46
59
|
key,
|
|
47
|
-
value:
|
|
60
|
+
value: m,
|
|
48
61
|
unit,
|
|
49
|
-
text: formatUnit(
|
|
50
|
-
datapoints:
|
|
62
|
+
text: formatUnit(m, unit),
|
|
63
|
+
datapoints: samples.length,
|
|
64
|
+
mean: m,
|
|
65
|
+
p50: quantile(samples, 0.5),
|
|
66
|
+
p90: quantile(samples, 0.9),
|
|
67
|
+
samples,
|
|
51
68
|
relativeTo,
|
|
52
69
|
};
|
|
53
|
-
})
|
|
70
|
+
});
|
|
71
|
+
const payload = n > 1 ? { results } : results;
|
|
72
|
+
await writeFile(file, JSON.stringify(payload));
|
|
54
73
|
},
|
|
55
74
|
};
|
|
56
75
|
return iface;
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|
|
@@ -0,0 +1,56 @@
|
|
|
1
|
+
import assert from 'assert';
|
|
2
|
+
import { mkdtemp, rm, writeFile } from 'node:fs/promises';
|
|
3
|
+
import { tmpdir } from 'node:os';
|
|
4
|
+
import { join } from 'node:path';
|
|
5
|
+
import { test } from 'node:test';
|
|
6
|
+
import { command } from '../shell.js';
|
|
7
|
+
async function withTempScript(scriptContents, fn) {
|
|
8
|
+
const dir = await mkdtemp(join(tmpdir(), 'devlow-shell-test-'));
|
|
9
|
+
const scriptPath = join(dir, 'script.js');
|
|
10
|
+
try {
|
|
11
|
+
await writeFile(scriptPath, scriptContents, 'utf8');
|
|
12
|
+
await fn(scriptPath);
|
|
13
|
+
}
|
|
14
|
+
finally {
|
|
15
|
+
await rm(dir, { recursive: true, force: true });
|
|
16
|
+
}
|
|
17
|
+
}
|
|
18
|
+
test('waitForOutput handles sequential waits after buffered output', async () => {
|
|
19
|
+
await withTempScript(`
|
|
20
|
+
console.log('FIRST_MARKER')
|
|
21
|
+
console.log('SECOND_MARKER')
|
|
22
|
+
setTimeout(() => {}, 1000)
|
|
23
|
+
`, async (scriptPath) => {
|
|
24
|
+
const shell = command('node', [scriptPath]);
|
|
25
|
+
try {
|
|
26
|
+
const first = await shell.waitForOutput(/FIRST_MARKER\n/, {
|
|
27
|
+
timeoutMs: 2_000,
|
|
28
|
+
});
|
|
29
|
+
assert.equal(first[0], 'FIRST_MARKER\n');
|
|
30
|
+
const second = await shell.waitForOutput(/SECOND_MARKER\n/, {
|
|
31
|
+
timeoutMs: 2_000,
|
|
32
|
+
});
|
|
33
|
+
assert.equal(second[0], 'SECOND_MARKER\n');
|
|
34
|
+
}
|
|
35
|
+
finally {
|
|
36
|
+
await shell.kill();
|
|
37
|
+
}
|
|
38
|
+
});
|
|
39
|
+
});
|
|
40
|
+
test('waitForOutput times out when output never appears', async () => {
|
|
41
|
+
await withTempScript(`
|
|
42
|
+
console.log('FIRST_MARKER')
|
|
43
|
+
setTimeout(() => {}, 1000)
|
|
44
|
+
`, async (scriptPath) => {
|
|
45
|
+
const shell = command('node', [scriptPath]);
|
|
46
|
+
try {
|
|
47
|
+
await shell.waitForOutput(/FIRST_MARKER\n/, {
|
|
48
|
+
timeoutMs: 2_000,
|
|
49
|
+
});
|
|
50
|
+
await assert.rejects(shell.waitForOutput(/NEVER_SEEN\n/, { timeoutMs: 100 }), /Timed out waiting for output matching/);
|
|
51
|
+
}
|
|
52
|
+
finally {
|
|
53
|
+
await shell.kill();
|
|
54
|
+
}
|
|
55
|
+
});
|
|
56
|
+
});
|
|
@@ -0,0 +1,60 @@
|
|
|
1
|
+
import { execFile } from 'node:child_process';
|
|
2
|
+
import { promisify } from 'node:util';
|
|
3
|
+
import picocolors from 'picocolors';
|
|
4
|
+
import { defaultSnapshotPath, writeSnapshot, } from '../snapshot.js';
|
|
5
|
+
import { formatVariantProps } from '../utils.js';
|
|
6
|
+
const execFileAsync = promisify(execFile);
|
|
7
|
+
async function readGitInfo() {
|
|
8
|
+
const sha = process.env.GITHUB_SHA || (await tryGit(['rev-parse', 'HEAD']));
|
|
9
|
+
const branch = process.env.GITHUB_REF_NAME ||
|
|
10
|
+
(await tryGit(['rev-parse', '--abbrev-ref', 'HEAD']));
|
|
11
|
+
return { sha, branch };
|
|
12
|
+
}
|
|
13
|
+
async function tryGit(args) {
|
|
14
|
+
try {
|
|
15
|
+
const { stdout } = await execFileAsync('git', args);
|
|
16
|
+
return stdout.trim();
|
|
17
|
+
}
|
|
18
|
+
catch {
|
|
19
|
+
return '';
|
|
20
|
+
}
|
|
21
|
+
}
|
|
22
|
+
export default function createInterface(options = {}) {
|
|
23
|
+
const path = options.path ?? defaultSnapshotPath();
|
|
24
|
+
const rows = [];
|
|
25
|
+
const timestamp = new Date().toISOString();
|
|
26
|
+
const iface = {
|
|
27
|
+
resolvedPath: path,
|
|
28
|
+
variantStatistics: async (scenario, props, stats) => {
|
|
29
|
+
const variant = formatVariantProps(props);
|
|
30
|
+
for (const [metric, s] of Object.entries(stats)) {
|
|
31
|
+
for (let i = 0; i < s.samples.length; i++) {
|
|
32
|
+
rows.push({
|
|
33
|
+
timestamp,
|
|
34
|
+
scenario,
|
|
35
|
+
variant,
|
|
36
|
+
metric,
|
|
37
|
+
sample_idx: i + 1,
|
|
38
|
+
value: s.samples[i],
|
|
39
|
+
unit: s.unit,
|
|
40
|
+
relative_to: s.relativeTo ?? '',
|
|
41
|
+
git_sha: '',
|
|
42
|
+
git_branch: '',
|
|
43
|
+
});
|
|
44
|
+
}
|
|
45
|
+
}
|
|
46
|
+
},
|
|
47
|
+
finish: async () => {
|
|
48
|
+
if (rows.length === 0)
|
|
49
|
+
return;
|
|
50
|
+
const { sha, branch } = await readGitInfo();
|
|
51
|
+
for (const r of rows) {
|
|
52
|
+
r.git_sha = sha;
|
|
53
|
+
r.git_branch = branch;
|
|
54
|
+
}
|
|
55
|
+
await writeSnapshot(path, rows);
|
|
56
|
+
console.log(picocolors.dim(`Wrote ${rows.length} rows to ${path}`));
|
|
57
|
+
},
|
|
58
|
+
};
|
|
59
|
+
return iface;
|
|
60
|
+
}
|
|
@@ -1,6 +1,6 @@
|
|
|
1
|
-
import assert from
|
|
2
|
-
import createInterface from
|
|
3
|
-
import { mock, test } from
|
|
1
|
+
import assert from 'assert';
|
|
2
|
+
import createInterface from './snowflake.js';
|
|
3
|
+
import { mock, test } from 'node:test';
|
|
4
4
|
test('sending measurements to the batch endpoint', async () => {
|
|
5
5
|
const requests = [];
|
|
6
6
|
const fetchMock = mock.method(global, 'fetch', (req, options) => {
|
|
@@ -1,13 +1,15 @@
|
|
|
1
|
-
import os from
|
|
2
|
-
import { CPU_ARCH, CPU_MODEL, GIT_BRANCH, GIT_SHA, IS_CI, NODE_VERSION, NUM_CPUS, OS, OS_RELEASE, USERNAME } from
|
|
3
|
-
import { randomUUID } from
|
|
4
|
-
export default function createInterface({ gatewayUri = process.env.SNOWFLAKE_BATCH_URI, topicName = process.env.SNOWFLAKE_TOPIC_NAME, schemaId = process.env.SNOWFLAKE_SCHEMA_ID
|
|
1
|
+
import os from 'os';
|
|
2
|
+
import { CPU_ARCH, CPU_MODEL, GIT_BRANCH, GIT_SHA, IS_CI, NODE_VERSION, NUM_CPUS, OS, OS_RELEASE, USERNAME, } from './constants.js';
|
|
3
|
+
import { randomUUID } from 'crypto';
|
|
4
|
+
export default function createInterface({ gatewayUri = process.env.SNOWFLAKE_BATCH_URI, topicName = process.env.SNOWFLAKE_TOPIC_NAME, schemaId = process.env.SNOWFLAKE_SCHEMA_ID
|
|
5
|
+
? parseInt(process.env.SNOWFLAKE_SCHEMA_ID, 10)
|
|
6
|
+
: undefined, } = {}) {
|
|
5
7
|
if (!gatewayUri)
|
|
6
|
-
throw new Error(
|
|
8
|
+
throw new Error('Snowflake gateway URI is required (set SNOWFLAKE_GATEWAY_URI)');
|
|
7
9
|
if (!topicName)
|
|
8
|
-
throw new Error(
|
|
10
|
+
throw new Error('Snowflake topic name is required (set SNOWFLAKE_TOPIC_NAME)');
|
|
9
11
|
if (!schemaId)
|
|
10
|
-
throw new Error(
|
|
12
|
+
throw new Error('Snowflake schema ID is required (set SNOWFLAKE_SCHEMA_ID to a valid integer)');
|
|
11
13
|
const records = [];
|
|
12
14
|
const iface = {
|
|
13
15
|
measurement: async (scenario, props, name, value, unit, relativeTo) => {
|
|
@@ -32,7 +34,7 @@ export default function createInterface({ gatewayUri = process.env.SNOWFLAKE_BAT
|
|
|
32
34
|
git_branch: GIT_BRANCH,
|
|
33
35
|
});
|
|
34
36
|
},
|
|
35
|
-
end: async (
|
|
37
|
+
end: async (_scenario, _props) => {
|
|
36
38
|
await trackAnalytics(gatewayUri, topicName, schemaId, records);
|
|
37
39
|
},
|
|
38
40
|
};
|
|
@@ -41,10 +43,10 @@ export default function createInterface({ gatewayUri = process.env.SNOWFLAKE_BAT
|
|
|
41
43
|
async function trackAnalytics(batchUri, topic, schemaId, records) {
|
|
42
44
|
try {
|
|
43
45
|
const res = await fetch(batchUri, {
|
|
44
|
-
method:
|
|
46
|
+
method: 'POST',
|
|
45
47
|
headers: {
|
|
46
|
-
|
|
47
|
-
|
|
48
|
+
'Client-Id': 'nextjs',
|
|
49
|
+
'Content-Type': 'application/json',
|
|
48
50
|
},
|
|
49
51
|
body: JSON.stringify({
|
|
50
52
|
schema_id: schemaId,
|
|
@@ -53,7 +55,7 @@ async function trackAnalytics(batchUri, topic, schemaId, records) {
|
|
|
53
55
|
...record,
|
|
54
56
|
id: randomUUID(),
|
|
55
57
|
props_json: JSON.stringify(record.props),
|
|
56
|
-
}, [
|
|
58
|
+
}, ['props'])),
|
|
57
59
|
}),
|
|
58
60
|
});
|
|
59
61
|
if (!res.ok) {
|
|
@@ -61,7 +63,7 @@ async function trackAnalytics(batchUri, topic, schemaId, records) {
|
|
|
61
63
|
}
|
|
62
64
|
}
|
|
63
65
|
catch (e) {
|
|
64
|
-
const wrappedError = new Error(
|
|
66
|
+
const wrappedError = new Error('Unexpected error tracking analytics event');
|
|
65
67
|
wrappedError.cause = e;
|
|
66
68
|
console.error(wrappedError);
|
|
67
69
|
}
|
package/dist/runner.d.ts
CHANGED
|
@@ -1,2 +1,5 @@
|
|
|
1
|
-
import { Interface, Scenario } from
|
|
2
|
-
export declare function runScenarios(scenarios: Scenario[], iface: Interface
|
|
1
|
+
import type { Interface, Scenario } from './index.js';
|
|
2
|
+
export declare function runScenarios(scenarios: Scenario[], iface: Interface, options?: {
|
|
3
|
+
n?: number;
|
|
4
|
+
warmup?: number;
|
|
5
|
+
}): Promise<void>;
|
package/dist/runner.js
CHANGED
|
@@ -1,20 +1,31 @@
|
|
|
1
|
-
import { withCurrent } from
|
|
2
|
-
import { intoFullInterface } from
|
|
3
|
-
|
|
1
|
+
import { withCurrent } from './describe.js';
|
|
2
|
+
import { intoFullInterface } from './index.js';
|
|
3
|
+
import { summary } from './statistics.js';
|
|
4
|
+
import { formatVariant } from './utils.js';
|
|
5
|
+
// Total attempts per variant are capped at this multiple of the requested
|
|
6
|
+
// `warmup + n`. Allows retrying flaky runs without burning unbounded time when
|
|
7
|
+
// every attempt fails.
|
|
8
|
+
const MAX_ATTEMPT_MULTIPLIER = 2;
|
|
9
|
+
export async function runScenarios(scenarios, iface, options = {}) {
|
|
10
|
+
const n = Math.max(1, Math.floor(options.n ?? 1));
|
|
11
|
+
const warmup = Math.max(0, Math.floor(options.warmup ?? 0));
|
|
4
12
|
const fullIface = intoFullInterface(iface);
|
|
5
13
|
if (scenarios.some((scenario) => scenario.only)) {
|
|
6
14
|
scenarios = scenarios.filter((scenario) => scenario.only);
|
|
7
15
|
}
|
|
8
16
|
scenarios = await fullIface.filterScenarios(scenarios);
|
|
17
|
+
let totalFailedAttempts = 0;
|
|
18
|
+
let variantsWithRetries = 0;
|
|
19
|
+
let variantsShortOfTarget = 0;
|
|
9
20
|
let variants = [];
|
|
10
21
|
for (const scenario of scenarios) {
|
|
11
22
|
let props = [{}];
|
|
12
|
-
for (const [key,
|
|
23
|
+
for (const [key, values] of Object.entries(scenario.config)) {
|
|
13
24
|
const newProps = [];
|
|
14
25
|
for (const prop of props) {
|
|
15
|
-
if (prop ===
|
|
26
|
+
if (prop === 'scenario' || prop === 'name')
|
|
16
27
|
throw new Error("Cannot use 'scenario' or 'name' as a property name");
|
|
17
|
-
for (const value of
|
|
28
|
+
for (const value of values) {
|
|
18
29
|
newProps.push({
|
|
19
30
|
...prop,
|
|
20
31
|
[key]: value,
|
|
@@ -23,32 +34,129 @@ export async function runScenarios(scenarios, iface) {
|
|
|
23
34
|
}
|
|
24
35
|
props = newProps;
|
|
25
36
|
}
|
|
26
|
-
variants.push(...props.map((
|
|
37
|
+
variants.push(...props.map((variantProps) => ({
|
|
27
38
|
scenario,
|
|
28
|
-
props,
|
|
39
|
+
props: variantProps,
|
|
29
40
|
})));
|
|
30
41
|
}
|
|
31
42
|
variants = await fullIface.filterScenarioVariants(variants);
|
|
32
43
|
for (const variant of variants) {
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
+
const samplesByMetric = new Map();
|
|
45
|
+
// Each attempt buffers its measurements here. On success we merge into
|
|
46
|
+
// `samplesByMetric`; on failure we discard, so a partial run can't
|
|
47
|
+
// pollute the aggregated stats. Downstream per-sample reporters (Datadog,
|
|
48
|
+
// console, …) still receive every measurement inline — only the
|
|
49
|
+
// aggregated per-variant samples are gated on success.
|
|
50
|
+
let collecting = false;
|
|
51
|
+
let perRun = new Map();
|
|
52
|
+
const wrappedIface = {
|
|
53
|
+
...fullIface,
|
|
54
|
+
measurement: async (scenario, props, name, value, unit, relativeTo) => {
|
|
55
|
+
if (collecting) {
|
|
56
|
+
let list = perRun.get(name);
|
|
57
|
+
if (!list) {
|
|
58
|
+
list = [];
|
|
59
|
+
perRun.set(name, list);
|
|
60
|
+
}
|
|
61
|
+
list.push({ value, unit, relativeTo });
|
|
62
|
+
}
|
|
63
|
+
await fullIface.measurement(scenario, props, name, value, unit, relativeTo);
|
|
64
|
+
},
|
|
65
|
+
};
|
|
66
|
+
const maxAttempts = (warmup + n) * MAX_ATTEMPT_MULTIPLIER;
|
|
67
|
+
const showRunInfo = warmup + n > 1;
|
|
68
|
+
let warmupDone = 0;
|
|
69
|
+
let collectedRuns = 0;
|
|
70
|
+
let totalAttempts = 0;
|
|
71
|
+
while ((warmupDone < warmup || collectedRuns < n) &&
|
|
72
|
+
totalAttempts < maxAttempts) {
|
|
73
|
+
totalAttempts++;
|
|
74
|
+
const isWarmup = warmupDone < warmup;
|
|
75
|
+
collecting = !isWarmup;
|
|
76
|
+
perRun = new Map();
|
|
77
|
+
const runInfo = showRunInfo
|
|
78
|
+
? isWarmup
|
|
79
|
+
? { run: warmupDone + 1, total: warmup, warmup: true }
|
|
80
|
+
: { run: collectedRuns + 1, total: n, warmup: false }
|
|
81
|
+
: undefined;
|
|
82
|
+
try {
|
|
83
|
+
const measurements = new Map();
|
|
84
|
+
await withCurrent({
|
|
85
|
+
iface: wrappedIface,
|
|
86
|
+
measurements,
|
|
87
|
+
scenario: variant,
|
|
88
|
+
}, async () => {
|
|
89
|
+
await wrappedIface.start(variant.scenario.name, variant.props, runInfo);
|
|
90
|
+
measurements.set('start', {
|
|
91
|
+
value: Date.now(),
|
|
92
|
+
unit: 'ms',
|
|
93
|
+
});
|
|
94
|
+
await variant.scenario.fn(variant.props);
|
|
95
|
+
await wrappedIface.end(variant.scenario.name, variant.props);
|
|
44
96
|
});
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
97
|
+
if (collecting) {
|
|
98
|
+
// Commit the buffered samples for this run into the aggregate.
|
|
99
|
+
for (const [name, entries] of perRun) {
|
|
100
|
+
let set = samplesByMetric.get(name);
|
|
101
|
+
if (!set) {
|
|
102
|
+
const first = entries[0];
|
|
103
|
+
set = {
|
|
104
|
+
samples: [],
|
|
105
|
+
unit: first.unit,
|
|
106
|
+
relativeTo: first.relativeTo,
|
|
107
|
+
};
|
|
108
|
+
samplesByMetric.set(name, set);
|
|
109
|
+
}
|
|
110
|
+
for (const e of entries) {
|
|
111
|
+
set.samples.push(e.value);
|
|
112
|
+
}
|
|
113
|
+
}
|
|
114
|
+
collectedRuns++;
|
|
115
|
+
}
|
|
116
|
+
else {
|
|
117
|
+
warmupDone++;
|
|
118
|
+
}
|
|
119
|
+
}
|
|
120
|
+
catch (e) {
|
|
121
|
+
await wrappedIface.error(variant.scenario.name, variant.props, e);
|
|
122
|
+
// Loop continues; the buffered samples for this attempt are dropped
|
|
123
|
+
// when `perRun` is replaced on the next iteration.
|
|
124
|
+
}
|
|
125
|
+
}
|
|
126
|
+
const failedAttempts = totalAttempts - warmupDone - collectedRuns;
|
|
127
|
+
if (failedAttempts > 0) {
|
|
128
|
+
const label = formatVariant(variant.scenario.name, variant.props);
|
|
129
|
+
console.log(`${label}: collected ${collectedRuns}/${n} samples after ${totalAttempts} attempts (${failedAttempts} failed)`);
|
|
130
|
+
totalFailedAttempts += failedAttempts;
|
|
131
|
+
variantsWithRetries++;
|
|
132
|
+
}
|
|
133
|
+
if (collectedRuns < n) {
|
|
134
|
+
process.exitCode = 1;
|
|
135
|
+
variantsShortOfTarget++;
|
|
136
|
+
}
|
|
137
|
+
if (samplesByMetric.size > 0) {
|
|
138
|
+
const stats = {};
|
|
139
|
+
for (const [name, set] of samplesByMetric) {
|
|
140
|
+
stats[name] = {
|
|
141
|
+
samples: set.samples,
|
|
142
|
+
unit: set.unit,
|
|
143
|
+
relativeTo: set.relativeTo,
|
|
144
|
+
...summary(set.samples),
|
|
145
|
+
};
|
|
146
|
+
}
|
|
147
|
+
await fullIface.variantStatistics(variant.scenario.name, variant.props, stats);
|
|
148
|
+
}
|
|
149
|
+
}
|
|
150
|
+
if (totalFailedAttempts > 0) {
|
|
151
|
+
const recovered = variantsWithRetries - variantsShortOfTarget;
|
|
152
|
+
const parts = [];
|
|
153
|
+
if (recovered > 0) {
|
|
154
|
+
parts.push(`${recovered} variant${recovered === 1 ? '' : 's'} hit n=${n} after retries`);
|
|
48
155
|
}
|
|
49
|
-
|
|
50
|
-
|
|
156
|
+
if (variantsShortOfTarget > 0) {
|
|
157
|
+
parts.push(`${variantsShortOfTarget} variant${variantsShortOfTarget === 1 ? '' : 's'} fell short of n=${n}`);
|
|
51
158
|
}
|
|
159
|
+
console.log(`\n${totalFailedAttempts} failed attempt${totalFailedAttempts === 1 ? '' : 's'} across ${variantsWithRetries} variant${variantsWithRetries === 1 ? '' : 's'}: ${parts.join('; ')}.`);
|
|
52
160
|
}
|
|
53
161
|
await fullIface.finish();
|
|
54
162
|
}
|
package/dist/shell.d.ts
CHANGED
|
@@ -1,9 +1,11 @@
|
|
|
1
|
-
import { PREVIOUS } from
|
|
1
|
+
import type { PREVIOUS } from './describe.js';
|
|
2
2
|
export interface Command {
|
|
3
3
|
ok(): Promise<void>;
|
|
4
4
|
kill(): Promise<void>;
|
|
5
5
|
end(): Promise<number>;
|
|
6
|
-
waitForOutput(regex: RegExp
|
|
6
|
+
waitForOutput(regex: RegExp, options?: {
|
|
7
|
+
timeoutMs?: number;
|
|
8
|
+
}): Promise<RegExpMatchArray>;
|
|
7
9
|
reportMemUsage(metricName: string, options: {
|
|
8
10
|
relativeTo?: string | typeof PREVIOUS;
|
|
9
11
|
scenario?: string;
|
|
@@ -13,7 +15,7 @@ export interface Command {
|
|
|
13
15
|
stderr: string;
|
|
14
16
|
output: string;
|
|
15
17
|
}
|
|
16
|
-
export declare function command(
|
|
18
|
+
export declare function command(executable: string, args: string[], options?: {
|
|
17
19
|
env?: Record<string, string>;
|
|
18
20
|
cwd?: string;
|
|
19
21
|
}): Command;
|
package/dist/shell.js
CHANGED
|
@@ -1,22 +1,23 @@
|
|
|
1
|
-
import { spawn } from
|
|
2
|
-
import split2 from
|
|
3
|
-
import treeKill from
|
|
4
|
-
import pidusage from
|
|
5
|
-
import { reportMeasurement } from
|
|
1
|
+
import { spawn } from 'child_process';
|
|
2
|
+
import split2 from 'split2';
|
|
3
|
+
import treeKill from 'tree-kill';
|
|
4
|
+
import pidusage from 'pidusage-tree';
|
|
5
|
+
import { reportMeasurement } from './describe.js';
|
|
6
6
|
const shellOutput = !!process.env.SHELL_OUTPUT;
|
|
7
7
|
class CommandImpl {
|
|
8
8
|
process;
|
|
9
|
-
stdout =
|
|
10
|
-
stderr =
|
|
11
|
-
output =
|
|
9
|
+
stdout = '';
|
|
10
|
+
stderr = '';
|
|
11
|
+
output = '';
|
|
12
|
+
outputCursor = 0;
|
|
12
13
|
exitPromise;
|
|
13
14
|
waitingForOutput = [];
|
|
14
15
|
constructor(process) {
|
|
15
16
|
this.process = process;
|
|
16
|
-
process.stdout?.pipe(split2()).on(
|
|
17
|
+
process.stdout?.pipe(split2()).on('data', (data) => {
|
|
17
18
|
const str = data.toString();
|
|
18
|
-
this.stdout += str +
|
|
19
|
-
this.output += str +
|
|
19
|
+
this.stdout += str + '\n';
|
|
20
|
+
this.output += str + '\n';
|
|
20
21
|
if (shellOutput) {
|
|
21
22
|
console.log(`[STDOUT] ${str}`);
|
|
22
23
|
}
|
|
@@ -28,10 +29,10 @@ class CommandImpl {
|
|
|
28
29
|
}
|
|
29
30
|
}
|
|
30
31
|
});
|
|
31
|
-
process.stderr?.pipe(split2()).on(
|
|
32
|
+
process.stderr?.pipe(split2()).on('data', (data) => {
|
|
32
33
|
const str = data.toString();
|
|
33
|
-
this.stderr += str +
|
|
34
|
-
this.output += str +
|
|
34
|
+
this.stderr += str + '\n';
|
|
35
|
+
this.output += str + '\n';
|
|
35
36
|
if (shellOutput) {
|
|
36
37
|
console.log(`[STDERR] ${str}`);
|
|
37
38
|
}
|
|
@@ -44,8 +45,8 @@ class CommandImpl {
|
|
|
44
45
|
}
|
|
45
46
|
});
|
|
46
47
|
this.exitPromise = new Promise((resolve, reject) => {
|
|
47
|
-
process.on(
|
|
48
|
-
process.on(
|
|
48
|
+
process.on('error', reject);
|
|
49
|
+
process.on('exit', resolve);
|
|
49
50
|
});
|
|
50
51
|
}
|
|
51
52
|
async ok() {
|
|
@@ -67,20 +68,47 @@ class CommandImpl {
|
|
|
67
68
|
}));
|
|
68
69
|
await this.exitPromise;
|
|
69
70
|
}
|
|
70
|
-
async waitForOutput(regex) {
|
|
71
|
-
let start = this.
|
|
71
|
+
async waitForOutput(regex, options = {}) {
|
|
72
|
+
let start = this.outputCursor;
|
|
73
|
+
const deadline = options.timeoutMs === undefined
|
|
74
|
+
? undefined
|
|
75
|
+
: Date.now() + options.timeoutMs;
|
|
72
76
|
while (true) {
|
|
73
|
-
const
|
|
77
|
+
const outputToSearch = this.output.slice(start);
|
|
78
|
+
const match = outputToSearch.match(regex);
|
|
74
79
|
if (match) {
|
|
80
|
+
const matchIndex = match.index ?? outputToSearch.search(regex);
|
|
81
|
+
if (matchIndex >= 0) {
|
|
82
|
+
this.outputCursor = start + matchIndex + match[0].length;
|
|
83
|
+
}
|
|
84
|
+
else {
|
|
85
|
+
this.outputCursor = this.output.length;
|
|
86
|
+
}
|
|
75
87
|
return match;
|
|
76
88
|
}
|
|
77
|
-
const
|
|
89
|
+
const promises = [
|
|
78
90
|
this.exitPromise,
|
|
79
91
|
new Promise((resolve) => {
|
|
80
92
|
this.waitingForOutput.push(resolve);
|
|
81
|
-
}).then(() =>
|
|
82
|
-
]
|
|
83
|
-
|
|
93
|
+
}).then(() => 'output'),
|
|
94
|
+
];
|
|
95
|
+
let timeoutId;
|
|
96
|
+
if (deadline !== undefined) {
|
|
97
|
+
const timeoutMs = deadline - Date.now();
|
|
98
|
+
if (timeoutMs <= 0) {
|
|
99
|
+
throw new Error(`Timed out waiting for output matching ${regex}\n\nOutput:\n${this.output}`);
|
|
100
|
+
}
|
|
101
|
+
promises.push(new Promise((resolve) => {
|
|
102
|
+
timeoutId = setTimeout(() => resolve('timeout'), timeoutMs);
|
|
103
|
+
}));
|
|
104
|
+
}
|
|
105
|
+
const waitResult = await Promise.race(promises);
|
|
106
|
+
if (timeoutId)
|
|
107
|
+
clearTimeout(timeoutId);
|
|
108
|
+
if (waitResult !== 'output') {
|
|
109
|
+
if (waitResult === 'timeout') {
|
|
110
|
+
throw new Error(`Timed out waiting for output matching ${regex}\n\nOutput:\n${this.output}`);
|
|
111
|
+
}
|
|
84
112
|
throw new Error(`Command exited with code ${waitResult}\n\nOutput:\n${this.output}`);
|
|
85
113
|
}
|
|
86
114
|
}
|
|
@@ -93,21 +121,21 @@ class CommandImpl {
|
|
|
93
121
|
.filter((x) => x)
|
|
94
122
|
.map((x) => x.memory)
|
|
95
123
|
.reduce((a, b) => a + b, 0);
|
|
96
|
-
await reportMeasurement(metricName, memUsage,
|
|
124
|
+
await reportMeasurement(metricName, memUsage, 'bytes', options);
|
|
97
125
|
}
|
|
98
126
|
catch (e) {
|
|
99
127
|
// ignore
|
|
100
128
|
}
|
|
101
129
|
}
|
|
102
130
|
}
|
|
103
|
-
export function command(
|
|
104
|
-
const process = spawn(
|
|
131
|
+
export function command(executable, args, options = {}) {
|
|
132
|
+
const process = spawn(executable, args, {
|
|
105
133
|
shell: true,
|
|
106
134
|
...options,
|
|
107
|
-
stdio: [
|
|
135
|
+
stdio: ['ignore', 'pipe', 'pipe'],
|
|
108
136
|
});
|
|
109
137
|
if (shellOutput) {
|
|
110
|
-
console.log(`[SHELL] ${
|
|
138
|
+
console.log(`[SHELL] ${executable} ${args.join(' ')} ${JSON.stringify(options)}`);
|
|
111
139
|
}
|
|
112
140
|
return new CommandImpl(process);
|
|
113
141
|
}
|