@effect-bench/cli 0.0.0-stage → 0.0.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/dist/src/Cli.d.ts +5 -0
- package/dist/src/Cli.d.ts.map +1 -0
- package/dist/src/Cli.js +81 -0
- package/dist/src/Cli.js.map +1 -0
- package/dist/src/Config.d.ts +26 -0
- package/dist/src/Config.d.ts.map +1 -0
- package/dist/src/Config.js +13 -0
- package/dist/src/Config.js.map +1 -0
- package/dist/src/Discover.d.ts +15 -0
- package/dist/src/Discover.d.ts.map +1 -0
- package/dist/src/Discover.js +52 -0
- package/dist/src/Discover.js.map +1 -0
- package/dist/src/Fork.d.ts +42 -0
- package/dist/src/Fork.d.ts.map +1 -0
- package/dist/src/Fork.js +41 -0
- package/dist/src/Fork.js.map +1 -0
- package/dist/src/bin.d.ts +3 -0
- package/dist/src/bin.d.ts.map +1 -0
- package/dist/src/bin.js +9 -0
- package/dist/src/bin.js.map +1 -0
- package/dist/src/index.d.ts +3 -0
- package/dist/src/index.d.ts.map +1 -0
- package/dist/src/index.js +3 -0
- package/dist/src/index.js.map +1 -0
- package/package.json +30 -3
- package/src/Cli.ts +139 -0
- package/src/Config.ts +33 -0
- package/src/Discover.ts +54 -0
- package/src/Fork.ts +60 -0
- package/src/bin.ts +9 -0
- package/src/index.ts +2 -0
- package/README.md +0 -3
|
@@ -0,0 +1,5 @@
|
|
|
1
|
+
import { Effect } from 'effect';
|
|
2
|
+
import { Command } from 'effect/cli';
|
|
3
|
+
export declare const command: Command.Command<"effect-bench", {}, {}, unknown, never>;
|
|
4
|
+
export declare const main: Effect.Effect<void, unknown, Command.Environment>;
|
|
5
|
+
//# sourceMappingURL=Cli.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"Cli.d.ts","sourceRoot":"","sources":["../../src/Cli.ts"],"names":[],"mappings":"AAEA,OAAO,EAAW,MAAM,EAAiB,MAAM,QAAQ,CAAA;AACvD,OAAO,EAAY,OAAO,EAAQ,MAAM,YAAY,CAAA;AAkIpD,eAAO,MAAM,OAAO,yDAGnB,CAAA;AAED,eAAO,MAAM,IAAI,mDAA6C,CAAA"}
|
package/dist/src/Cli.js
ADDED
|
@@ -0,0 +1,81 @@
|
|
|
1
|
+
import { Reporter, Runner } from '@effect-bench/core';
|
|
2
|
+
import { NodeBench } from '@effect-bench/node';
|
|
3
|
+
import { Console, Effect, Layer, Option } from 'effect';
|
|
4
|
+
import { Argument, Command, Flag } from 'effect/cli';
|
|
5
|
+
import { defaultModes, defaults } from './Config.js';
|
|
6
|
+
import { discover, loadConfig } from './Discover.js';
|
|
7
|
+
import { measureInChild, recordPrefix } from './Fork.js';
|
|
8
|
+
const filter = Argument.String('filter').pipe(Argument.withDescription('Only benches whose id contains this text, or that carry this tag'), Argument.optional);
|
|
9
|
+
const json = Flag.Boolean('json').pipe(Flag.withDescription('Print one JSON record per line on stdout'), Flag.withDefault(false));
|
|
10
|
+
const save = Flag.String('save').pipe(Flag.withDescription('Save results as a named baseline'), Flag.optional);
|
|
11
|
+
const compare = Flag.String('compare').pipe(Flag.withDescription('Compare against a saved baseline'), Flag.optional);
|
|
12
|
+
const mode = Flag.String('mode').pipe(Flag.withDescription('Sampling preset from the config, e.g. quick or rigorous'), Flag.optional);
|
|
13
|
+
const quick = Flag.Boolean('quick').pipe(Flag.withDescription('Same as --mode quick'), Flag.withDefault(false));
|
|
14
|
+
const rigorous = Flag.Boolean('rigorous').pipe(Flag.withDescription('Same as --mode rigorous'), Flag.withDefault(false));
|
|
15
|
+
const forks = Flag.Int('forks').pipe(Flag.withDescription('Fresh processes per suite (default 3); 0 measures in this process'), Flag.optional);
|
|
16
|
+
const threshold = Flag.Finite('threshold').pipe(Flag.withDescription('Relative change treated as noise (default 0.03)'), Flag.optional);
|
|
17
|
+
const load = Effect.gen(function* () {
|
|
18
|
+
const cwd = process.cwd();
|
|
19
|
+
const config = yield* loadConfig(cwd);
|
|
20
|
+
return { config, suites: yield* discover(cwd, config) };
|
|
21
|
+
});
|
|
22
|
+
const platformLayer = (config) => {
|
|
23
|
+
const node = NodeBench.layer({ baselineDirectory: config.baselineDirectory ?? defaults.baselineDirectory });
|
|
24
|
+
return config.layer === undefined ? node : Layer.merge(node, config.layer);
|
|
25
|
+
};
|
|
26
|
+
const resolveMode = (config, name) => Effect.gen(function* () {
|
|
27
|
+
const modes = { ...defaultModes, ...config.modes };
|
|
28
|
+
if (name === undefined)
|
|
29
|
+
return undefined;
|
|
30
|
+
const mode = modes[name];
|
|
31
|
+
if (mode === undefined) {
|
|
32
|
+
return yield* Effect.fail(`Unknown mode "${name}". Known modes: ${Object.keys(modes).join(', ')}`);
|
|
33
|
+
}
|
|
34
|
+
return mode;
|
|
35
|
+
});
|
|
36
|
+
const runBenches = (options) => Effect.gen(function* () {
|
|
37
|
+
const { config, suites } = yield* load;
|
|
38
|
+
const modeName = options.quick ? 'quick' : options.rigorous ? 'rigorous' : Option.getOrUndefined(options.mode);
|
|
39
|
+
const mode = yield* resolveMode(config, modeName);
|
|
40
|
+
const filter = Option.getOrUndefined(options.filter);
|
|
41
|
+
const forkCount = Option.getOrElse(options.forks, () => mode?.forks ?? defaults.forks);
|
|
42
|
+
const plan = Runner.plan(suites, { filter, mode });
|
|
43
|
+
const forksRaw = [];
|
|
44
|
+
if (forkCount === 0) {
|
|
45
|
+
forksRaw.push(yield* Runner.measureSuites(suites, { filter, mode }).pipe(Effect.provide(platformLayer(config))));
|
|
46
|
+
}
|
|
47
|
+
// Fork-major order spreads slow drift (thermal, background load) across every suite instead of one.
|
|
48
|
+
for (let fork = 1; fork <= forkCount; fork++) {
|
|
49
|
+
const raws = [];
|
|
50
|
+
for (const { suite, benches } of plan) {
|
|
51
|
+
yield* Console.error(`fork ${fork}/${forkCount} ${suite.name}`);
|
|
52
|
+
raws.push(...(yield* measureInChild(benches.map(({ planned }) => planned), { suite: suite.name, filter, mode: modeName })));
|
|
53
|
+
}
|
|
54
|
+
forksRaw.push(raws);
|
|
55
|
+
}
|
|
56
|
+
const { summary } = yield* Runner.finish(forksRaw, {
|
|
57
|
+
save: Option.getOrUndefined(options.save),
|
|
58
|
+
compare: Option.getOrUndefined(options.compare),
|
|
59
|
+
noiseThreshold: Option.getOrElse(options.threshold, () => config.noiseThreshold ?? defaults.noiseThreshold),
|
|
60
|
+
}).pipe(Effect.provide(Layer.merge(platformLayer(config), options.json ? Reporter.json : Reporter.table)));
|
|
61
|
+
process.exitCode = summary.exitCode;
|
|
62
|
+
});
|
|
63
|
+
const measureCommand = Command.make('measure', { filter, mode, suite: Flag.String('suite') }, options => Effect.gen(function* () {
|
|
64
|
+
const { config, suites } = yield* load;
|
|
65
|
+
const mode = yield* resolveMode(config, Option.getOrUndefined(options.mode));
|
|
66
|
+
const raws = yield* Runner.measureSuites(suites.filter(suite => suite.name === options.suite), { filter: Option.getOrUndefined(options.filter), mode }).pipe(Effect.provide(platformLayer(config)));
|
|
67
|
+
for (const raw of raws)
|
|
68
|
+
yield* Console.log(`${recordPrefix}${JSON.stringify(raw)}`);
|
|
69
|
+
})).pipe(Command.withDescription('Internal: measure one suite in this process and print raw records'));
|
|
70
|
+
const runConfig = { filter, json, save, compare, mode, quick, rigorous, forks, threshold };
|
|
71
|
+
const run = Command.make('run', runConfig, runBenches).pipe(Command.withDescription('Run benches, optionally saving or comparing a baseline'));
|
|
72
|
+
const check = Command.make('check', { ...runConfig, compare: Flag.String('compare').pipe(Flag.withDescription('Baseline to compare against')) }, options => runBenches({ ...options, compare: Option.some(options.compare) })).pipe(Command.withDescription('Run benches against a baseline; exit 1 on a regression or blown budget'));
|
|
73
|
+
const list = Command.make('list', { filter, json }, options => Effect.gen(function* () {
|
|
74
|
+
const { suites } = yield* load;
|
|
75
|
+
const planned = Runner.list(suites, Option.getOrUndefined(options.filter));
|
|
76
|
+
for (const bench of planned)
|
|
77
|
+
yield* Console.log(options.json ? JSON.stringify(bench) : bench.id);
|
|
78
|
+
})).pipe(Command.withDescription('List bench ids without running anything'));
|
|
79
|
+
export const command = Command.make('effect-bench').pipe(Command.withDescription('Benchmarks for Effect code'), Command.withSubcommands([run, check, list, measureCommand]));
|
|
80
|
+
export const main = Command.run(command, { version: '0.0.0' });
|
|
81
|
+
//# sourceMappingURL=Cli.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"Cli.js","sourceRoot":"","sources":["../../src/Cli.ts"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
import type { Bench } from '@effect-bench/core';
|
|
2
|
+
import type { Layer } from 'effect';
|
|
3
|
+
export interface Config {
|
|
4
|
+
/** Globs relative to the working directory. */
|
|
5
|
+
readonly include?: ReadonlyArray<string>;
|
|
6
|
+
readonly exclude?: ReadonlyArray<string>;
|
|
7
|
+
readonly modes?: Readonly<Record<string, Mode>>;
|
|
8
|
+
readonly noiseThreshold?: number;
|
|
9
|
+
readonly baselineDirectory?: string;
|
|
10
|
+
/** Replaces services from the Node layer, e.g. a different baseline store. */
|
|
11
|
+
readonly layer?: Layer.Layer<never, unknown>;
|
|
12
|
+
}
|
|
13
|
+
export interface Mode extends Bench.Options {
|
|
14
|
+
/** Fresh processes per suite; 0 measures in the CLI process. */
|
|
15
|
+
readonly forks?: number;
|
|
16
|
+
}
|
|
17
|
+
export declare const make: (config: Config) => Config;
|
|
18
|
+
export declare const defaultModes: Readonly<Record<string, Mode>>;
|
|
19
|
+
export declare const defaults: {
|
|
20
|
+
readonly include: readonly ['**/*.bench.ts'];
|
|
21
|
+
readonly exclude: readonly ['**/node_modules/**', '**/dist/**'];
|
|
22
|
+
readonly noiseThreshold: 0.03;
|
|
23
|
+
readonly forks: 3;
|
|
24
|
+
readonly baselineDirectory: '.bench/baselines';
|
|
25
|
+
};
|
|
26
|
+
//# sourceMappingURL=Config.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"Config.d.ts","sourceRoot":"","sources":["../../src/Config.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,KAAK,EAAE,MAAM,oBAAoB,CAAA;AAC/C,OAAO,KAAK,EAAE,KAAK,EAAE,MAAM,QAAQ,CAAA;AAEnC,MAAM,WAAW,MAAM;IACrB,+CAA+C;IAC/C,QAAQ,CAAC,OAAO,CAAC,EAAE,aAAa,CAAC,MAAM,CAAC,CAAA;IACxC,QAAQ,CAAC,OAAO,CAAC,EAAE,aAAa,CAAC,MAAM,CAAC,CAAA;IACxC,QAAQ,CAAC,KAAK,CAAC,EAAE,QAAQ,CAAC,MAAM,CAAC,MAAM,EAAE,IAAI,CAAC,CAAC,CAAA;IAC/C,QAAQ,CAAC,cAAc,CAAC,EAAE,MAAM,CAAA;IAChC,QAAQ,CAAC,iBAAiB,CAAC,EAAE,MAAM,CAAA;IACnC,8EAA8E;IAC9E,QAAQ,CAAC,KAAK,CAAC,EAAE,KAAK,CAAC,KAAK,CAAC,KAAK,EAAE,OAAO,CAAC,CAAA;CAC7C;AAED,MAAM,WAAW,IAAK,SAAQ,KAAK,CAAC,OAAO;IACzC,gEAAgE;IAChE,QAAQ,CAAC,KAAK,CAAC,EAAE,MAAM,CAAA;CACxB;AAED,eAAO,MAAM,IAAI,WAAY,MAAM,KAAG,MAAgB,CAAA;AAEtD,eAAO,MAAM,YAAY,EAAE,QAAQ,CAAC,MAAM,CAAC,MAAM,EAAE,IAAI,CAAC,CAGvD,CAAA;AAED,eAAO,MAAM,QAAQ;aACnB,OAAO,YAAG,eAAe;aACzB,OAAO,YAAG,oBAAoB,EAAE,YAAY;aAC5C,cAAc,EAAE,IAAI;aACpB,KAAK,EAAE,CAAC;aACR,iBAAiB,EAAE,kBAAkB;CAC7B,CAAA"}
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
export const make = (config) => config;
|
|
2
|
+
export const defaultModes = {
|
|
3
|
+
quick: { samples: 10, minTime: '100 millis', warmup: '50 millis', forks: 3 },
|
|
4
|
+
rigorous: { samples: 100, minTime: '2 seconds', warmup: '1 second', forks: 6 },
|
|
5
|
+
};
|
|
6
|
+
export const defaults = {
|
|
7
|
+
include: ['**/*.bench.ts'],
|
|
8
|
+
exclude: ['**/node_modules/**', '**/dist/**'],
|
|
9
|
+
noiseThreshold: 0.03,
|
|
10
|
+
forks: 3,
|
|
11
|
+
baselineDirectory: '.bench/baselines',
|
|
12
|
+
};
|
|
13
|
+
//# sourceMappingURL=Config.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"Config.js","sourceRoot":"","sources":["../../src/Config.ts"],"names":[],"mappings":"AAmBA,MAAM,CAAC,MAAM,IAAI,GAAG,CAAC,MAAc,EAAU,EAAE,CAAC,MAAM,CAAA;AAEtD,MAAM,CAAC,MAAM,YAAY,GAAmC;IAC1D,KAAK,EAAE,EAAE,OAAO,EAAE,EAAE,EAAE,OAAO,EAAE,YAAY,EAAE,MAAM,EAAE,WAAW,EAAE,KAAK,EAAE,CAAC,EAAE;IAC5E,QAAQ,EAAE,EAAE,OAAO,EAAE,GAAG,EAAE,OAAO,EAAE,WAAW,EAAE,MAAM,EAAE,UAAU,EAAE,KAAK,EAAE,CAAC,EAAE;CAC/E,CAAA;AAED,MAAM,CAAC,MAAM,QAAQ,GAAG;IACtB,OAAO,EAAE,CAAC,eAAe,CAAC;IAC1B,OAAO,EAAE,CAAC,oBAAoB,EAAE,YAAY,CAAC;IAC7C,cAAc,EAAE,IAAI;IACpB,KAAK,EAAE,CAAC;IACR,iBAAiB,EAAE,kBAAkB;CAC7B,CAAA"}
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
import type { Bench } from '@effect-bench/core';
|
|
2
|
+
import { Effect } from 'effect';
|
|
3
|
+
import { type Config } from './Config.ts';
|
|
4
|
+
declare const DiscoveryError_base: new <A extends Record<string, any> = {}>(args: import("effect/Types").VoidIfEmpty<{ readonly [P in keyof A as P extends "_tag" ? never : P]: A[P]; }>) => import("effect/Cause").YieldableError & {
|
|
5
|
+
readonly _tag: "DiscoveryError";
|
|
6
|
+
} & Readonly<A>;
|
|
7
|
+
export declare class DiscoveryError extends DiscoveryError_base<{
|
|
8
|
+
readonly message: string;
|
|
9
|
+
readonly cause?: unknown;
|
|
10
|
+
}> {
|
|
11
|
+
}
|
|
12
|
+
export declare const loadConfig: (cwd: string) => Effect.Effect<Config, DiscoveryError, never>;
|
|
13
|
+
export declare const discover: (cwd: string, config: Config) => Effect.Effect<Bench.Suite[], DiscoveryError, never>;
|
|
14
|
+
export {};
|
|
15
|
+
//# sourceMappingURL=Discover.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"Discover.d.ts","sourceRoot":"","sources":["../../src/Discover.ts"],"names":[],"mappings":"AAIA,OAAO,KAAK,EAAE,KAAK,EAAE,MAAM,oBAAoB,CAAA;AAC/C,OAAO,EAAQ,MAAM,EAAE,MAAM,QAAQ,CAAA;AACrC,OAAO,EAAE,KAAK,MAAM,EAAY,MAAM,aAAa,CAAA;;;;AAEnD,qBAAa,cAAe,SAAQ,oBAAmC;IACrE,QAAQ,CAAC,OAAO,EAAE,MAAM,CAAA;IACxB,QAAQ,CAAC,KAAK,CAAC,EAAE,OAAO,CAAA;CACzB,CAAC;CAAG;AAUL,eAAO,MAAM,UAAU,+DAIrB,CAAA;AAKF,eAAO,MAAM,QAAQ,sFAuBnB,CAAA"}
|
|
@@ -0,0 +1,52 @@
|
|
|
1
|
+
var __rewriteRelativeImportExtension = (this && this.__rewriteRelativeImportExtension) || function (path, preserveJsx) {
|
|
2
|
+
if (typeof path === "string" && /^\.\.?\//.test(path)) {
|
|
3
|
+
return path.replace(/\.(tsx)$|((?:\.d)?)((?:\.[^./]+?)?)\.([cm]?)ts$/i, function (m, tsx, d, ext, cm) {
|
|
4
|
+
return tsx ? preserveJsx ? ".jsx" : ".js" : d && (!ext || !cm) ? m : (d + ext + "." + cm.toLowerCase() + "js");
|
|
5
|
+
});
|
|
6
|
+
}
|
|
7
|
+
return path;
|
|
8
|
+
};
|
|
9
|
+
import { existsSync } from 'node:fs';
|
|
10
|
+
import { glob } from 'node:fs/promises';
|
|
11
|
+
import { resolve } from 'node:path';
|
|
12
|
+
import { pathToFileURL } from 'node:url';
|
|
13
|
+
import { Data, Effect } from 'effect';
|
|
14
|
+
import { defaults } from './Config.js';
|
|
15
|
+
export class DiscoveryError extends Data.TaggedError('DiscoveryError') {
|
|
16
|
+
}
|
|
17
|
+
const configFile = 'effect-bench.config.ts';
|
|
18
|
+
const importModule = (file) => Effect.tryPromise({
|
|
19
|
+
try: () => import(__rewriteRelativeImportExtension(pathToFileURL(resolve(file)).href)),
|
|
20
|
+
catch: cause => new DiscoveryError({ message: `Could not import ${file}.`, cause }),
|
|
21
|
+
});
|
|
22
|
+
export const loadConfig = Effect.fn('effectBench/loadConfig')(function* (cwd) {
|
|
23
|
+
const file = resolve(cwd, configFile);
|
|
24
|
+
if (!existsSync(file))
|
|
25
|
+
return {};
|
|
26
|
+
return ((yield* importModule(file)).default ?? {});
|
|
27
|
+
});
|
|
28
|
+
const isSuite = (value) => typeof value === 'object' && value !== null && '_tag' in value && value._tag === 'Suite';
|
|
29
|
+
export const discover = Effect.fn('effectBench/discover')(function* (cwd, config) {
|
|
30
|
+
const exclude = config.exclude ?? defaults.exclude;
|
|
31
|
+
const files = yield* Effect.tryPromise({
|
|
32
|
+
try: async () => {
|
|
33
|
+
const found = [];
|
|
34
|
+
for await (const file of glob([...(config.include ?? defaults.include)], { cwd, exclude: [...exclude] })) {
|
|
35
|
+
found.push(file);
|
|
36
|
+
}
|
|
37
|
+
return found;
|
|
38
|
+
},
|
|
39
|
+
catch: cause => new DiscoveryError({ message: 'Could not search for bench files.', cause }),
|
|
40
|
+
});
|
|
41
|
+
const suites = [];
|
|
42
|
+
for (const file of files.sort()) {
|
|
43
|
+
const exported = (yield* importModule(resolve(cwd, file))).default;
|
|
44
|
+
const candidates = Array.isArray(exported) ? exported : [exported];
|
|
45
|
+
if (!candidates.every(isSuite)) {
|
|
46
|
+
return yield* new DiscoveryError({ message: `${file} must default-export a suite or an array of suites.` });
|
|
47
|
+
}
|
|
48
|
+
suites.push(...candidates);
|
|
49
|
+
}
|
|
50
|
+
return suites;
|
|
51
|
+
});
|
|
52
|
+
//# sourceMappingURL=Discover.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"Discover.js","sourceRoot":"","sources":["../../src/Discover.ts"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
import { Runner } from '@effect-bench/core';
|
|
2
|
+
import { Effect } from 'effect';
|
|
3
|
+
/** Marks result lines so stray console output from bench code is ignored. */
|
|
4
|
+
export declare const recordPrefix = "@@effect-bench ";
|
|
5
|
+
/** Runs one suite in a fresh process and returns its raw records; a crash fails every planned bench. */
|
|
6
|
+
export declare const measureInChild: (planned: readonly Runner.PlannedBench[], options: {
|
|
7
|
+
readonly suite: string;
|
|
8
|
+
readonly filter: string | undefined;
|
|
9
|
+
readonly mode: string | undefined;
|
|
10
|
+
}) => Effect.Effect<({
|
|
11
|
+
readonly id: string;
|
|
12
|
+
readonly suite: string;
|
|
13
|
+
readonly name: string;
|
|
14
|
+
readonly group: string | null;
|
|
15
|
+
readonly params: {
|
|
16
|
+
readonly [x: string]: string | number | boolean;
|
|
17
|
+
};
|
|
18
|
+
readonly tags: readonly string[];
|
|
19
|
+
readonly status: "ok";
|
|
20
|
+
readonly relativeTo: string | null;
|
|
21
|
+
readonly samplesNs: readonly number[];
|
|
22
|
+
readonly batchSize: number;
|
|
23
|
+
readonly overheadNs: number;
|
|
24
|
+
readonly heapDeltaBytes: number | null;
|
|
25
|
+
readonly budgetNs: {
|
|
26
|
+
readonly p50: number | null;
|
|
27
|
+
readonly p99: number | null;
|
|
28
|
+
} | null;
|
|
29
|
+
} | {
|
|
30
|
+
readonly id: string;
|
|
31
|
+
readonly suite: string;
|
|
32
|
+
readonly name: string;
|
|
33
|
+
readonly group: string | null;
|
|
34
|
+
readonly params: {
|
|
35
|
+
readonly [x: string]: string | number | boolean;
|
|
36
|
+
};
|
|
37
|
+
readonly tags: readonly string[];
|
|
38
|
+
readonly status: "failed";
|
|
39
|
+
readonly phase: "check" | "prepare" | "run" | "setup";
|
|
40
|
+
readonly message: string;
|
|
41
|
+
})[], never, never>;
|
|
42
|
+
//# sourceMappingURL=Fork.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"Fork.d.ts","sourceRoot":"","sources":["../../src/Fork.ts"],"names":[],"mappings":"AAEA,OAAO,EAAa,MAAM,EAAE,MAAM,oBAAoB,CAAA;AACtD,OAAO,EAAW,MAAM,EAAU,MAAM,QAAQ,CAAA;AAEhD,6EAA6E;AAC7E,eAAO,MAAM,YAAY,oBAAoB,CAAA;AAyB7C,wGAAwG;AACxG,eAAO,MAAM,cAAc;oBAEE,MAAM;qBAAmB,MAAM,GAAG,SAAS;mBAAiB,MAAM,GAAG,SAAS;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;mBAyBzG,CAAA"}
|
package/dist/src/Fork.js
ADDED
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
import { execFile } from 'node:child_process';
|
|
2
|
+
import { fileURLToPath } from 'node:url';
|
|
3
|
+
import { RawResult, Runner } from '@effect-bench/core';
|
|
4
|
+
import { Console, Effect, Schema } from 'effect';
|
|
5
|
+
/** Marks result lines so stray console output from bench code is ignored. */
|
|
6
|
+
export const recordPrefix = '@@effect-bench ';
|
|
7
|
+
// Published builds run from dist/*.js, the workspace from src/*.ts.
|
|
8
|
+
const bin = fileURLToPath(new URL(import.meta.url.endsWith('.ts') ? './bin.ts' : './bin.js', import.meta.url));
|
|
9
|
+
const decodeRaw = Schema.decodeUnknownSync(Schema.fromJsonString(RawResult));
|
|
10
|
+
const spawn = (args) => Effect.callback(resume => {
|
|
11
|
+
const execArgv = [...new Set([...process.execArgv, '--expose-gc'])];
|
|
12
|
+
const child = execFile(process.execPath, [...execArgv, bin, ...args], { maxBuffer: 256 * 1024 * 1024, env: process.env }, (error, stdout, stderr) => resume(Effect.succeed({ stdout, stderr, exitCode: error === null ? null : (error.code ?? null) })));
|
|
13
|
+
return Effect.sync(() => child.kill());
|
|
14
|
+
});
|
|
15
|
+
/** Runs one suite in a fresh process and returns its raw records; a crash fails every planned bench. */
|
|
16
|
+
export const measureInChild = Effect.fn('effectBench/measureInChild')(function* (planned, options) {
|
|
17
|
+
const output = yield* spawn([
|
|
18
|
+
'measure',
|
|
19
|
+
'--suite',
|
|
20
|
+
options.suite,
|
|
21
|
+
...(options.mode === undefined ? [] : ['--mode', options.mode]),
|
|
22
|
+
...(options.filter === undefined ? [] : [options.filter]),
|
|
23
|
+
]);
|
|
24
|
+
const records = output.stdout
|
|
25
|
+
.split('\n')
|
|
26
|
+
.filter(line => line.startsWith(recordPrefix))
|
|
27
|
+
.map(line => decodeRaw(line.slice(recordPrefix.length)));
|
|
28
|
+
const missing = planned.filter(bench => !records.some(record => record.id === bench.id));
|
|
29
|
+
if (missing.length > 0) {
|
|
30
|
+
const stderr = output.stderr.trim().split('\n').slice(-20).join('\n');
|
|
31
|
+
const exited = output.exitCode === null
|
|
32
|
+
? 'the child process produced no result'
|
|
33
|
+
: `the child process exited with ${output.exitCode}`;
|
|
34
|
+
const reason = stderr === '' ? exited : `${exited}:\n${stderr}`;
|
|
35
|
+
if (output.exitCode !== null)
|
|
36
|
+
yield* Console.error(reason);
|
|
37
|
+
return [...records, ...missing.map(bench => Runner.failed(bench, 'run', reason))];
|
|
38
|
+
}
|
|
39
|
+
return records;
|
|
40
|
+
});
|
|
41
|
+
//# sourceMappingURL=Fork.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"Fork.js","sourceRoot":"","sources":["../../src/Fork.ts"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"bin.d.ts","sourceRoot":"","sources":["../../src/bin.ts"],"names":[],"mappings":""}
|
package/dist/src/bin.js
ADDED
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
import { NodeRuntime, NodeServices } from '@effect/platform-node';
|
|
3
|
+
import { Effect, Exit } from 'effect';
|
|
4
|
+
import { main } from './Cli.js';
|
|
5
|
+
NodeRuntime.runMain(main.pipe(Effect.provide(NodeServices.layer)), {
|
|
6
|
+
// Failures (bad flags, missing baseline, import errors) exit 2; otherwise keep the code the run decided.
|
|
7
|
+
teardown: (exit, onExit) => onExit(Exit.isSuccess(exit) ? Number(process.exitCode ?? 0) : 2),
|
|
8
|
+
});
|
|
9
|
+
//# sourceMappingURL=bin.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"bin.js","sourceRoot":"","sources":["../../src/bin.ts"],"names":[],"mappings":";AACA,OAAO,EAAE,WAAW,EAAE,YAAY,EAAE,MAAM,uBAAuB,CAAA;AACjE,OAAO,EAAE,MAAM,EAAE,IAAI,EAAE,MAAM,QAAQ,CAAA;AACrC,OAAO,EAAE,IAAI,EAAE,MAAM,UAAU,CAAA;AAE/B,WAAW,CAAC,OAAO,CAAC,IAAI,CAAC,IAAI,CAAC,MAAM,CAAC,OAAO,CAAC,YAAY,CAAC,KAAK,CAAC,CAAC,EAAE;IACjE,yGAAyG;IACzG,QAAQ,EAAE,CAAC,IAAI,EAAE,MAAM,EAAE,EAAE,CAAC,MAAM,CAAC,IAAI,CAAC,SAAS,CAAC,IAAI,CAAC,CAAC,CAAC,CAAC,MAAM,CAAC,OAAO,CAAC,QAAQ,IAAI,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC;CAC7F,CAAC,CAAA"}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../src/index.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,WAAW,MAAM,aAAa,CAAA;AAC1C,OAAO,EAAE,OAAO,EAAE,IAAI,EAAE,MAAM,UAAU,CAAA"}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"index.js","sourceRoot":"","sources":["../../src/index.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,WAAW,MAAM,aAAa,CAAA;AAC1C,OAAO,EAAE,OAAO,EAAE,IAAI,EAAE,MAAM,UAAU,CAAA"}
|
package/package.json
CHANGED
|
@@ -1,6 +1,33 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@effect-bench/cli",
|
|
3
|
-
"version": "0.0.0
|
|
4
|
-
"
|
|
5
|
-
"
|
|
3
|
+
"version": "0.0.0",
|
|
4
|
+
"type": "module",
|
|
5
|
+
"bin": {
|
|
6
|
+
"effect-bench": "./dist/src/bin.js"
|
|
7
|
+
},
|
|
8
|
+
"exports": {
|
|
9
|
+
".": {
|
|
10
|
+
"types": "./dist/src/index.d.ts",
|
|
11
|
+
"default": "./dist/src/index.js"
|
|
12
|
+
}
|
|
13
|
+
},
|
|
14
|
+
"dependencies": {
|
|
15
|
+
"@effect-bench/core": "0.0.0",
|
|
16
|
+
"@effect-bench/node": "0.0.0"
|
|
17
|
+
},
|
|
18
|
+
"peerDependencies": {
|
|
19
|
+
"effect": "^4.1.2",
|
|
20
|
+
"@effect/platform-node": "^4.1.2"
|
|
21
|
+
},
|
|
22
|
+
"devDependencies": {
|
|
23
|
+
"effect": "4.1.2",
|
|
24
|
+
"@effect/platform-node": "4.1.2"
|
|
25
|
+
},
|
|
26
|
+
"files": [
|
|
27
|
+
"dist/src",
|
|
28
|
+
"src"
|
|
29
|
+
],
|
|
30
|
+
"publishConfig": {
|
|
31
|
+
"access": "public"
|
|
32
|
+
}
|
|
6
33
|
}
|
package/src/Cli.ts
ADDED
|
@@ -0,0 +1,139 @@
|
|
|
1
|
+
import { type RawResult, Reporter, Runner } from '@effect-bench/core'
|
|
2
|
+
import { NodeBench } from '@effect-bench/node'
|
|
3
|
+
import { Console, Effect, Layer, Option } from 'effect'
|
|
4
|
+
import { Argument, Command, Flag } from 'effect/cli'
|
|
5
|
+
import { type Config, defaultModes, defaults } from './Config.ts'
|
|
6
|
+
import { discover, loadConfig } from './Discover.ts'
|
|
7
|
+
import { measureInChild, recordPrefix } from './Fork.ts'
|
|
8
|
+
|
|
9
|
+
const filter = Argument.String('filter').pipe(
|
|
10
|
+
Argument.withDescription('Only benches whose id contains this text, or that carry this tag'),
|
|
11
|
+
Argument.optional
|
|
12
|
+
)
|
|
13
|
+
const json = Flag.Boolean('json').pipe(
|
|
14
|
+
Flag.withDescription('Print one JSON record per line on stdout'),
|
|
15
|
+
Flag.withDefault(false)
|
|
16
|
+
)
|
|
17
|
+
const save = Flag.String('save').pipe(Flag.withDescription('Save results as a named baseline'), Flag.optional)
|
|
18
|
+
const compare = Flag.String('compare').pipe(Flag.withDescription('Compare against a saved baseline'), Flag.optional)
|
|
19
|
+
const mode = Flag.String('mode').pipe(
|
|
20
|
+
Flag.withDescription('Sampling preset from the config, e.g. quick or rigorous'),
|
|
21
|
+
Flag.optional
|
|
22
|
+
)
|
|
23
|
+
const quick = Flag.Boolean('quick').pipe(Flag.withDescription('Same as --mode quick'), Flag.withDefault(false))
|
|
24
|
+
const rigorous = Flag.Boolean('rigorous').pipe(Flag.withDescription('Same as --mode rigorous'), Flag.withDefault(false))
|
|
25
|
+
const forks = Flag.Int('forks').pipe(
|
|
26
|
+
Flag.withDescription('Fresh processes per suite (default 3); 0 measures in this process'),
|
|
27
|
+
Flag.optional
|
|
28
|
+
)
|
|
29
|
+
const threshold = Flag.Finite('threshold').pipe(
|
|
30
|
+
Flag.withDescription('Relative change treated as noise (default 0.03)'),
|
|
31
|
+
Flag.optional
|
|
32
|
+
)
|
|
33
|
+
|
|
34
|
+
const load = Effect.gen(function* () {
|
|
35
|
+
const cwd = process.cwd()
|
|
36
|
+
const config = yield* loadConfig(cwd)
|
|
37
|
+
return { config, suites: yield* discover(cwd, config) }
|
|
38
|
+
})
|
|
39
|
+
|
|
40
|
+
const platformLayer = (config: Config) => {
|
|
41
|
+
const node = NodeBench.layer({ baselineDirectory: config.baselineDirectory ?? defaults.baselineDirectory })
|
|
42
|
+
return config.layer === undefined ? node : Layer.merge(node, config.layer)
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
const resolveMode = (config: Config, name: string | undefined) =>
|
|
46
|
+
Effect.gen(function* () {
|
|
47
|
+
const modes = { ...defaultModes, ...config.modes }
|
|
48
|
+
if (name === undefined) return undefined
|
|
49
|
+
const mode = modes[name]
|
|
50
|
+
if (mode === undefined) {
|
|
51
|
+
return yield* Effect.fail(`Unknown mode "${name}". Known modes: ${Object.keys(modes).join(', ')}`)
|
|
52
|
+
}
|
|
53
|
+
return mode
|
|
54
|
+
})
|
|
55
|
+
|
|
56
|
+
const runBenches = (options: {
|
|
57
|
+
readonly filter: Option.Option<string>
|
|
58
|
+
readonly json: boolean
|
|
59
|
+
readonly save: Option.Option<string>
|
|
60
|
+
readonly compare: Option.Option<string>
|
|
61
|
+
readonly mode: Option.Option<string>
|
|
62
|
+
readonly quick: boolean
|
|
63
|
+
readonly rigorous: boolean
|
|
64
|
+
readonly forks: Option.Option<number>
|
|
65
|
+
readonly threshold: Option.Option<number>
|
|
66
|
+
}) =>
|
|
67
|
+
Effect.gen(function* () {
|
|
68
|
+
const { config, suites } = yield* load
|
|
69
|
+
const modeName = options.quick ? 'quick' : options.rigorous ? 'rigorous' : Option.getOrUndefined(options.mode)
|
|
70
|
+
const mode = yield* resolveMode(config, modeName)
|
|
71
|
+
const filter = Option.getOrUndefined(options.filter)
|
|
72
|
+
const forkCount = Option.getOrElse(options.forks, () => mode?.forks ?? defaults.forks)
|
|
73
|
+
|
|
74
|
+
const plan = Runner.plan(suites, { filter, mode })
|
|
75
|
+
const forksRaw: Array<ReadonlyArray<RawResult>> = []
|
|
76
|
+
if (forkCount === 0) {
|
|
77
|
+
forksRaw.push(yield* Runner.measureSuites(suites, { filter, mode }).pipe(Effect.provide(platformLayer(config))))
|
|
78
|
+
}
|
|
79
|
+
// Fork-major order spreads slow drift (thermal, background load) across every suite instead of one.
|
|
80
|
+
for (let fork = 1; fork <= forkCount; fork++) {
|
|
81
|
+
const raws: Array<RawResult> = []
|
|
82
|
+
for (const { suite, benches } of plan) {
|
|
83
|
+
yield* Console.error(`fork ${fork}/${forkCount} ${suite.name}`)
|
|
84
|
+
raws.push(
|
|
85
|
+
...(yield* measureInChild(
|
|
86
|
+
benches.map(({ planned }) => planned),
|
|
87
|
+
{ suite: suite.name, filter, mode: modeName }
|
|
88
|
+
))
|
|
89
|
+
)
|
|
90
|
+
}
|
|
91
|
+
forksRaw.push(raws)
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
const { summary } = yield* Runner.finish(forksRaw, {
|
|
95
|
+
save: Option.getOrUndefined(options.save),
|
|
96
|
+
compare: Option.getOrUndefined(options.compare),
|
|
97
|
+
noiseThreshold: Option.getOrElse(options.threshold, () => config.noiseThreshold ?? defaults.noiseThreshold),
|
|
98
|
+
}).pipe(Effect.provide(Layer.merge(platformLayer(config), options.json ? Reporter.json : Reporter.table)))
|
|
99
|
+
process.exitCode = summary.exitCode
|
|
100
|
+
})
|
|
101
|
+
|
|
102
|
+
const measureCommand = Command.make('measure', { filter, mode, suite: Flag.String('suite') }, options =>
|
|
103
|
+
Effect.gen(function* () {
|
|
104
|
+
const { config, suites } = yield* load
|
|
105
|
+
const mode = yield* resolveMode(config, Option.getOrUndefined(options.mode))
|
|
106
|
+
const raws = yield* Runner.measureSuites(
|
|
107
|
+
suites.filter(suite => suite.name === options.suite),
|
|
108
|
+
{ filter: Option.getOrUndefined(options.filter), mode }
|
|
109
|
+
).pipe(Effect.provide(platformLayer(config)))
|
|
110
|
+
for (const raw of raws) yield* Console.log(`${recordPrefix}${JSON.stringify(raw)}`)
|
|
111
|
+
})
|
|
112
|
+
).pipe(Command.withDescription('Internal: measure one suite in this process and print raw records'))
|
|
113
|
+
|
|
114
|
+
const runConfig = { filter, json, save, compare, mode, quick, rigorous, forks, threshold }
|
|
115
|
+
|
|
116
|
+
const run = Command.make('run', runConfig, runBenches).pipe(
|
|
117
|
+
Command.withDescription('Run benches, optionally saving or comparing a baseline')
|
|
118
|
+
)
|
|
119
|
+
|
|
120
|
+
const check = Command.make(
|
|
121
|
+
'check',
|
|
122
|
+
{ ...runConfig, compare: Flag.String('compare').pipe(Flag.withDescription('Baseline to compare against')) },
|
|
123
|
+
options => runBenches({ ...options, compare: Option.some(options.compare) })
|
|
124
|
+
).pipe(Command.withDescription('Run benches against a baseline; exit 1 on a regression or blown budget'))
|
|
125
|
+
|
|
126
|
+
const list = Command.make('list', { filter, json }, options =>
|
|
127
|
+
Effect.gen(function* () {
|
|
128
|
+
const { suites } = yield* load
|
|
129
|
+
const planned = Runner.list(suites, Option.getOrUndefined(options.filter))
|
|
130
|
+
for (const bench of planned) yield* Console.log(options.json ? JSON.stringify(bench) : bench.id)
|
|
131
|
+
})
|
|
132
|
+
).pipe(Command.withDescription('List bench ids without running anything'))
|
|
133
|
+
|
|
134
|
+
export const command = Command.make('effect-bench').pipe(
|
|
135
|
+
Command.withDescription('Benchmarks for Effect code'),
|
|
136
|
+
Command.withSubcommands([run, check, list, measureCommand])
|
|
137
|
+
)
|
|
138
|
+
|
|
139
|
+
export const main = Command.run(command, { version: '0.0.0' })
|
package/src/Config.ts
ADDED
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
import type { Bench } from '@effect-bench/core'
|
|
2
|
+
import type { Layer } from 'effect'
|
|
3
|
+
|
|
4
|
+
export interface Config {
|
|
5
|
+
/** Globs relative to the working directory. */
|
|
6
|
+
readonly include?: ReadonlyArray<string>
|
|
7
|
+
readonly exclude?: ReadonlyArray<string>
|
|
8
|
+
readonly modes?: Readonly<Record<string, Mode>>
|
|
9
|
+
readonly noiseThreshold?: number
|
|
10
|
+
readonly baselineDirectory?: string
|
|
11
|
+
/** Replaces services from the Node layer, e.g. a different baseline store. */
|
|
12
|
+
readonly layer?: Layer.Layer<never, unknown>
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
export interface Mode extends Bench.Options {
|
|
16
|
+
/** Fresh processes per suite; 0 measures in the CLI process. */
|
|
17
|
+
readonly forks?: number
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
export const make = (config: Config): Config => config
|
|
21
|
+
|
|
22
|
+
export const defaultModes: Readonly<Record<string, Mode>> = {
|
|
23
|
+
quick: { samples: 10, minTime: '100 millis', warmup: '50 millis', forks: 3 },
|
|
24
|
+
rigorous: { samples: 100, minTime: '2 seconds', warmup: '1 second', forks: 6 },
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
export const defaults = {
|
|
28
|
+
include: ['**/*.bench.ts'],
|
|
29
|
+
exclude: ['**/node_modules/**', '**/dist/**'],
|
|
30
|
+
noiseThreshold: 0.03,
|
|
31
|
+
forks: 3,
|
|
32
|
+
baselineDirectory: '.bench/baselines',
|
|
33
|
+
} as const
|
package/src/Discover.ts
ADDED
|
@@ -0,0 +1,54 @@
|
|
|
1
|
+
import { existsSync } from 'node:fs'
|
|
2
|
+
import { glob } from 'node:fs/promises'
|
|
3
|
+
import { resolve } from 'node:path'
|
|
4
|
+
import { pathToFileURL } from 'node:url'
|
|
5
|
+
import type { Bench } from '@effect-bench/core'
|
|
6
|
+
import { Data, Effect } from 'effect'
|
|
7
|
+
import { type Config, defaults } from './Config.ts'
|
|
8
|
+
|
|
9
|
+
export class DiscoveryError extends Data.TaggedError('DiscoveryError')<{
|
|
10
|
+
readonly message: string
|
|
11
|
+
readonly cause?: unknown
|
|
12
|
+
}> {}
|
|
13
|
+
|
|
14
|
+
const configFile = 'effect-bench.config.ts'
|
|
15
|
+
|
|
16
|
+
const importModule = (file: string) =>
|
|
17
|
+
Effect.tryPromise({
|
|
18
|
+
try: () => import(pathToFileURL(resolve(file)).href) as Promise<{ readonly default?: unknown }>,
|
|
19
|
+
catch: cause => new DiscoveryError({ message: `Could not import ${file}.`, cause }),
|
|
20
|
+
})
|
|
21
|
+
|
|
22
|
+
export const loadConfig = Effect.fn('effectBench/loadConfig')(function* (cwd: string) {
|
|
23
|
+
const file = resolve(cwd, configFile)
|
|
24
|
+
if (!existsSync(file)) return {} satisfies Config
|
|
25
|
+
return ((yield* importModule(file)).default ?? {}) as Config
|
|
26
|
+
})
|
|
27
|
+
|
|
28
|
+
const isSuite = (value: unknown): value is Bench.Suite =>
|
|
29
|
+
typeof value === 'object' && value !== null && '_tag' in value && value._tag === 'Suite'
|
|
30
|
+
|
|
31
|
+
export const discover = Effect.fn('effectBench/discover')(function* (cwd: string, config: Config) {
|
|
32
|
+
const exclude = config.exclude ?? defaults.exclude
|
|
33
|
+
const files = yield* Effect.tryPromise({
|
|
34
|
+
try: async () => {
|
|
35
|
+
const found: Array<string> = []
|
|
36
|
+
for await (const file of glob([...(config.include ?? defaults.include)], { cwd, exclude: [...exclude] })) {
|
|
37
|
+
found.push(file)
|
|
38
|
+
}
|
|
39
|
+
return found
|
|
40
|
+
},
|
|
41
|
+
catch: cause => new DiscoveryError({ message: 'Could not search for bench files.', cause }),
|
|
42
|
+
})
|
|
43
|
+
|
|
44
|
+
const suites: Array<Bench.Suite> = []
|
|
45
|
+
for (const file of files.sort()) {
|
|
46
|
+
const exported = (yield* importModule(resolve(cwd, file))).default
|
|
47
|
+
const candidates = Array.isArray(exported) ? exported : [exported]
|
|
48
|
+
if (!candidates.every(isSuite)) {
|
|
49
|
+
return yield* new DiscoveryError({ message: `${file} must default-export a suite or an array of suites.` })
|
|
50
|
+
}
|
|
51
|
+
suites.push(...candidates)
|
|
52
|
+
}
|
|
53
|
+
return suites
|
|
54
|
+
})
|
package/src/Fork.ts
ADDED
|
@@ -0,0 +1,60 @@
|
|
|
1
|
+
import { execFile } from 'node:child_process'
|
|
2
|
+
import { fileURLToPath } from 'node:url'
|
|
3
|
+
import { RawResult, Runner } from '@effect-bench/core'
|
|
4
|
+
import { Console, Effect, Schema } from 'effect'
|
|
5
|
+
|
|
6
|
+
/** Marks result lines so stray console output from bench code is ignored. */
|
|
7
|
+
export const recordPrefix = '@@effect-bench '
|
|
8
|
+
|
|
9
|
+
// Published builds run from dist/*.js, the workspace from src/*.ts.
|
|
10
|
+
const bin = fileURLToPath(new URL(import.meta.url.endsWith('.ts') ? './bin.ts' : './bin.js', import.meta.url))
|
|
11
|
+
const decodeRaw = Schema.decodeUnknownSync(Schema.fromJsonString(RawResult))
|
|
12
|
+
|
|
13
|
+
interface ChildOutput {
|
|
14
|
+
readonly stdout: string
|
|
15
|
+
readonly stderr: string
|
|
16
|
+
readonly exitCode: number | string | null
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
const spawn = (args: ReadonlyArray<string>) =>
|
|
20
|
+
Effect.callback<ChildOutput>(resume => {
|
|
21
|
+
const execArgv = [...new Set([...process.execArgv, '--expose-gc'])]
|
|
22
|
+
const child = execFile(
|
|
23
|
+
process.execPath,
|
|
24
|
+
[...execArgv, bin, ...args],
|
|
25
|
+
{ maxBuffer: 256 * 1024 * 1024, env: process.env },
|
|
26
|
+
(error, stdout, stderr) =>
|
|
27
|
+
resume(Effect.succeed({ stdout, stderr, exitCode: error === null ? null : (error.code ?? null) }))
|
|
28
|
+
)
|
|
29
|
+
return Effect.sync(() => child.kill())
|
|
30
|
+
})
|
|
31
|
+
|
|
32
|
+
/** Runs one suite in a fresh process and returns its raw records; a crash fails every planned bench. */
|
|
33
|
+
export const measureInChild = Effect.fn('effectBench/measureInChild')(function* (
|
|
34
|
+
planned: ReadonlyArray<Runner.PlannedBench>,
|
|
35
|
+
options: { readonly suite: string; readonly filter: string | undefined; readonly mode: string | undefined }
|
|
36
|
+
) {
|
|
37
|
+
const output = yield* spawn([
|
|
38
|
+
'measure',
|
|
39
|
+
'--suite',
|
|
40
|
+
options.suite,
|
|
41
|
+
...(options.mode === undefined ? [] : ['--mode', options.mode]),
|
|
42
|
+
...(options.filter === undefined ? [] : [options.filter]),
|
|
43
|
+
])
|
|
44
|
+
const records = output.stdout
|
|
45
|
+
.split('\n')
|
|
46
|
+
.filter(line => line.startsWith(recordPrefix))
|
|
47
|
+
.map(line => decodeRaw(line.slice(recordPrefix.length)))
|
|
48
|
+
const missing = planned.filter(bench => !records.some(record => record.id === bench.id))
|
|
49
|
+
if (missing.length > 0) {
|
|
50
|
+
const stderr = output.stderr.trim().split('\n').slice(-20).join('\n')
|
|
51
|
+
const exited =
|
|
52
|
+
output.exitCode === null
|
|
53
|
+
? 'the child process produced no result'
|
|
54
|
+
: `the child process exited with ${output.exitCode}`
|
|
55
|
+
const reason = stderr === '' ? exited : `${exited}:\n${stderr}`
|
|
56
|
+
if (output.exitCode !== null) yield* Console.error(reason)
|
|
57
|
+
return [...records, ...missing.map(bench => Runner.failed(bench, 'run', reason))]
|
|
58
|
+
}
|
|
59
|
+
return records
|
|
60
|
+
})
|
package/src/bin.ts
ADDED
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
import { NodeRuntime, NodeServices } from '@effect/platform-node'
|
|
3
|
+
import { Effect, Exit } from 'effect'
|
|
4
|
+
import { main } from './Cli.ts'
|
|
5
|
+
|
|
6
|
+
NodeRuntime.runMain(main.pipe(Effect.provide(NodeServices.layer)), {
|
|
7
|
+
// Failures (bad flags, missing baseline, import errors) exit 2; otherwise keep the code the run decided.
|
|
8
|
+
teardown: (exit, onExit) => onExit(Exit.isSuccess(exit) ? Number(process.exitCode ?? 0) : 2),
|
|
9
|
+
})
|
package/src/index.ts
ADDED
package/README.md
DELETED