@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.
@@ -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"}
@@ -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":"AAAA,OAAO,EAAkB,QAAQ,EAAE,MAAM,EAAE,MAAM,oBAAoB,CAAA;AACrE,OAAO,EAAE,SAAS,EAAE,MAAM,oBAAoB,CAAA;AAC9C,OAAO,EAAE,OAAO,EAAE,MAAM,EAAE,KAAK,EAAE,MAAM,EAAE,MAAM,QAAQ,CAAA;AACvD,OAAO,EAAE,QAAQ,EAAE,OAAO,EAAE,IAAI,EAAE,MAAM,YAAY,CAAA;AACpD,OAAO,EAAe,YAAY,EAAE,QAAQ,EAAE,MAAM,aAAa,CAAA;AACjE,OAAO,EAAE,QAAQ,EAAE,UAAU,EAAE,MAAM,eAAe,CAAA;AACpD,OAAO,EAAE,cAAc,EAAE,YAAY,EAAE,MAAM,WAAW,CAAA;AAExD,MAAM,MAAM,GAAG,QAAQ,CAAC,MAAM,CAAC,QAAQ,CAAC,CAAC,IAAI,CAC3C,QAAQ,CAAC,eAAe,CAAC,kEAAkE,CAAC,EAC5F,QAAQ,CAAC,QAAQ,CAClB,CAAA;AACD,MAAM,IAAI,GAAG,IAAI,CAAC,OAAO,CAAC,MAAM,CAAC,CAAC,IAAI,CACpC,IAAI,CAAC,eAAe,CAAC,0CAA0C,CAAC,EAChE,IAAI,CAAC,WAAW,CAAC,KAAK,CAAC,CACxB,CAAA;AACD,MAAM,IAAI,GAAG,IAAI,CAAC,MAAM,CAAC,MAAM,CAAC,CAAC,IAAI,CAAC,IAAI,CAAC,eAAe,CAAC,kCAAkC,CAAC,EAAE,IAAI,CAAC,QAAQ,CAAC,CAAA;AAC9G,MAAM,OAAO,GAAG,IAAI,CAAC,MAAM,CAAC,SAAS,CAAC,CAAC,IAAI,CAAC,IAAI,CAAC,eAAe,CAAC,kCAAkC,CAAC,EAAE,IAAI,CAAC,QAAQ,CAAC,CAAA;AACpH,MAAM,IAAI,GAAG,IAAI,CAAC,MAAM,CAAC,MAAM,CAAC,CAAC,IAAI,CACnC,IAAI,CAAC,eAAe,CAAC,yDAAyD,CAAC,EAC/E,IAAI,CAAC,QAAQ,CACd,CAAA;AACD,MAAM,KAAK,GAAG,IAAI,CAAC,OAAO,CAAC,OAAO,CAAC,CAAC,IAAI,CAAC,IAAI,CAAC,eAAe,CAAC,sBAAsB,CAAC,EAAE,IAAI,CAAC,WAAW,CAAC,KAAK,CAAC,CAAC,CAAA;AAC/G,MAAM,QAAQ,GAAG,IAAI,CAAC,OAAO,CAAC,UAAU,CAAC,CAAC,IAAI,CAAC,IAAI,CAAC,eAAe,CAAC,yBAAyB,CAAC,EAAE,IAAI,CAAC,WAAW,CAAC,KAAK,CAAC,CAAC,CAAA;AACxH,MAAM,KAAK,GAAG,IAAI,CAAC,GAAG,CAAC,OAAO,CAAC,CAAC,IAAI,CAClC,IAAI,CAAC,eAAe,CAAC,mEAAmE,CAAC,EACzF,IAAI,CAAC,QAAQ,CACd,CAAA;AACD,MAAM,SAAS,GAAG,IAAI,CAAC,MAAM,CAAC,WAAW,CAAC,CAAC,IAAI,CAC7C,IAAI,CAAC,eAAe,CAAC,iDAAiD,CAAC,EACvE,IAAI,CAAC,QAAQ,CACd,CAAA;AAED,MAAM,IAAI,GAAG,MAAM,CAAC,GAAG,CAAC,QAAQ,CAAC;IAC/B,MAAM,GAAG,GAAG,OAAO,CAAC,GAAG,EAAE,CAAA;IACzB,MAAM,MAAM,GAAG,KAAK,CAAC,CAAC,UAAU,CAAC,GAAG,CAAC,CAAA;IACrC,OAAO,EAAE,MAAM,EAAE,MAAM,EAAE,KAAK,CAAC,CAAC,QAAQ,CAAC,GAAG,EAAE,MAAM,CAAC,EAAE,CAAA;AACzD,CAAC,CAAC,CAAA;AAEF,MAAM,aAAa,GAAG,CAAC,MAAc,EAAE,EAAE;IACvC,MAAM,IAAI,GAAG,SAAS,CAAC,KAAK,CAAC,EAAE,iBAAiB,EAAE,MAAM,CAAC,iBAAiB,IAAI,QAAQ,CAAC,iBAAiB,EAAE,CAAC,CAAA;IAC3G,OAAO,MAAM,CAAC,KAAK,KAAK,SAAS,CAAC,CAAC,CAAC,IAAI,CAAC,CAAC,CAAC,KAAK,CAAC,KAAK,CAAC,IAAI,EAAE,MAAM,CAAC,KAAK,CAAC,CAAA;AAC5E,CAAC,CAAA;AAED,MAAM,WAAW,GAAG,CAAC,MAAc,EAAE,IAAwB,EAAE,EAAE,CAC/D,MAAM,CAAC,GAAG,CAAC,QAAQ,CAAC;IAClB,MAAM,KAAK,GAAG,EAAE,GAAG,YAAY,EAAE,GAAG,MAAM,CAAC,KAAK,EAAE,CAAA;IAClD,IAAI,IAAI,KAAK,SAAS;QAAE,OAAO,SAAS,CAAA;IACxC,MAAM,IAAI,GAAG,KAAK,CAAC,IAAI,CAAC,CAAA;IACxB,IAAI,IAAI,KAAK,SAAS,EAAE,CAAC;QACvB,OAAO,KAAK,CAAC,CAAC,MAAM,CAAC,IAAI,CAAC,iBAAiB,IAAI,mBAAmB,MAAM,CAAC,IAAI,CAAC,KAAK,CAAC,CAAC,IAAI,CAAC,IAAI,CAAC,EAAE,CAAC,CAAA;IACpG,CAAC;IACD,OAAO,IAAI,CAAA;AACb,CAAC,CAAC,CAAA;AAEJ,MAAM,UAAU,GAAG,CAAC,OAUnB,EAAE,EAAE,CACH,MAAM,CAAC,GAAG,CAAC,QAAQ,CAAC;IAClB,MAAM,EAAE,MAAM,EAAE,MAAM,EAAE,GAAG,KAAK,CAAC,CAAC,IAAI,CAAA;IACtC,MAAM,QAAQ,GAAG,OAAO,CAAC,KAAK,CAAC,CAAC,CAAC,OAAO,CAAC,CAAC,CAAC,OAAO,CAAC,QAAQ,CAAC,CAAC,CAAC,UAAU,CAAC,CAAC,CAAC,MAAM,CAAC,cAAc,CAAC,OAAO,CAAC,IAAI,CAAC,CAAA;IAC9G,MAAM,IAAI,GAAG,KAAK,CAAC,CAAC,WAAW,CAAC,MAAM,EAAE,QAAQ,CAAC,CAAA;IACjD,MAAM,MAAM,GAAG,MAAM,CAAC,cAAc,CAAC,OAAO,CAAC,MAAM,CAAC,CAAA;IACpD,MAAM,SAAS,GAAG,MAAM,CAAC,SAAS,CAAC,OAAO,CAAC,KAAK,EAAE,GAAG,EAAE,CAAC,IAAI,EAAE,KAAK,IAAI,QAAQ,CAAC,KAAK,CAAC,CAAA;IAEtF,MAAM,IAAI,GAAG,MAAM,CAAC,IAAI,CAAC,MAAM,EAAE,EAAE,MAAM,EAAE,IAAI,EAAE,CAAC,CAAA;IAClD,MAAM,QAAQ,GAAoC,EAAE,CAAA;IACpD,IAAI,SAAS,KAAK,CAAC,EAAE,CAAC;QACpB,QAAQ,CAAC,IAAI,CAAC,KAAK,CAAC,CAAC,MAAM,CAAC,aAAa,CAAC,MAAM,EAAE,EAAE,MAAM,EAAE,IAAI,EAAE,CAAC,CAAC,IAAI,CAAC,MAAM,CAAC,OAAO,CAAC,aAAa,CAAC,MAAM,CAAC,CAAC,CAAC,CAAC,CAAA;IAClH,CAAC;IACD,oGAAoG;IACpG,KAAK,IAAI,IAAI,GAAG,CAAC,EAAE,IAAI,IAAI,SAAS,EAAE,IAAI,EAAE,EAAE,CAAC;QAC7C,MAAM,IAAI,GAAqB,EAAE,CAAA;QACjC,KAAK,MAAM,EAAE,KAAK,EAAE,OAAO,EAAE,IAAI,IAAI,EAAE,CAAC;YACtC,KAAK,CAAC,CAAC,OAAO,CAAC,KAAK,CAAC,QAAQ,IAAI,IAAI,SAAS,KAAK,KAAK,CAAC,IAAI,EAAE,CAAC,CAAA;YAChE,IAAI,CAAC,IAAI,CACP,GAAG,CAAC,KAAK,CAAC,CAAC,cAAc,CACvB,OAAO,CAAC,GAAG,CAAC,CAAC,EAAE,OAAO,EAAE,EAAE,EAAE,CAAC,OAAO,CAAC,EACrC,EAAE,KAAK,EAAE,KAAK,CAAC,IAAI,EAAE,MAAM,EAAE,IAAI,EAAE,QAAQ,EAAE,CAC9C,CAAC,CACH,CAAA;QACH,CAAC;QACD,QAAQ,CAAC,IAAI,CAAC,IAAI,CAAC,CAAA;IACrB,CAAC;IAED,MAAM,EAAE,OAAO,EAAE,GAAG,KAAK,CAAC,CAAC,MAAM,CAAC,MAAM,CAAC,QAAQ,EAAE;QACjD,IAAI,EAAE,MAAM,CAAC,cAAc,CAAC,OAAO,CAAC,IAAI,CAAC;QACzC,OAAO,EAAE,MAAM,CAAC,cAAc,CAAC,OAAO,CAAC,OAAO,CAAC;QAC/C,cAAc,EAAE,MAAM,CAAC,SAAS,CAAC,OAAO,CAAC,SAAS,EAAE,GAAG,EAAE,CAAC,MAAM,CAAC,cAAc,IAAI,QAAQ,CAAC,cAAc,CAAC;KAC5G,CAAC,CAAC,IAAI,CAAC,MAAM,CAAC,OAAO,CAAC,KAAK,CAAC,KAAK,CAAC,aAAa,CAAC,MAAM,CAAC,EAAE,OAAO,CAAC,IAAI,CAAC,CAAC,CAAC,QAAQ,CAAC,IAAI,CAAC,CAAC,CAAC,QAAQ,CAAC,KAAK,CAAC,CAAC,CAAC,CAAA;IAC1G,OAAO,CAAC,QAAQ,GAAG,OAAO,CAAC,QAAQ,CAAA;AACrC,CAAC,CAAC,CAAA;AAEJ,MAAM,cAAc,GAAG,OAAO,CAAC,IAAI,CAAC,SAAS,EAAE,EAAE,MAAM,EAAE,IAAI,EAAE,KAAK,EAAE,IAAI,CAAC,MAAM,CAAC,OAAO,CAAC,EAAE,EAAE,OAAO,CAAC,EAAE,CACtG,MAAM,CAAC,GAAG,CAAC,QAAQ,CAAC;IAClB,MAAM,EAAE,MAAM,EAAE,MAAM,EAAE,GAAG,KAAK,CAAC,CAAC,IAAI,CAAA;IACtC,MAAM,IAAI,GAAG,KAAK,CAAC,CAAC,WAAW,CAAC,MAAM,EAAE,MAAM,CAAC,cAAc,CAAC,OAAO,CAAC,IAAI,CAAC,CAAC,CAAA;IAC5E,MAAM,IAAI,GAAG,KAAK,CAAC,CAAC,MAAM,CAAC,aAAa,CACtC,MAAM,CAAC,MAAM,CAAC,KAAK,CAAC,EAAE,CAAC,KAAK,CAAC,IAAI,KAAK,OAAO,CAAC,KAAK,CAAC,EACpD,EAAE,MAAM,EAAE,MAAM,CAAC,cAAc,CAAC,OAAO,CAAC,MAAM,CAAC,EAAE,IAAI,EAAE,CACxD,CAAC,IAAI,CAAC,MAAM,CAAC,OAAO,CAAC,aAAa,CAAC,MAAM,CAAC,CAAC,CAAC,CAAA;IAC7C,KAAK,MAAM,GAAG,IAAI,IAAI;QAAE,KAAK,CAAC,CAAC,OAAO,CAAC,GAAG,CAAC,GAAG,YAAY,GAAG,IAAI,CAAC,SAAS,CAAC,GAAG,CAAC,EAAE,CAAC,CAAA;AACrF,CAAC,CAAC,CACH,CAAC,IAAI,CAAC,OAAO,CAAC,eAAe,CAAC,mEAAmE,CAAC,CAAC,CAAA;AAEpG,MAAM,SAAS,GAAG,EAAE,MAAM,EAAE,IAAI,EAAE,IAAI,EAAE,OAAO,EAAE,IAAI,EAAE,KAAK,EAAE,QAAQ,EAAE,KAAK,EAAE,SAAS,EAAE,CAAA;AAE1F,MAAM,GAAG,GAAG,OAAO,CAAC,IAAI,CAAC,KAAK,EAAE,SAAS,EAAE,UAAU,CAAC,CAAC,IAAI,CACzD,OAAO,CAAC,eAAe,CAAC,wDAAwD,CAAC,CAClF,CAAA;AAED,MAAM,KAAK,GAAG,OAAO,CAAC,IAAI,CACxB,OAAO,EACP,EAAE,GAAG,SAAS,EAAE,OAAO,EAAE,IAAI,CAAC,MAAM,CAAC,SAAS,CAAC,CAAC,IAAI,CAAC,IAAI,CAAC,eAAe,CAAC,6BAA6B,CAAC,CAAC,EAAE,EAC3G,OAAO,CAAC,EAAE,CAAC,UAAU,CAAC,EAAE,GAAG,OAAO,EAAE,OAAO,EAAE,MAAM,CAAC,IAAI,CAAC,OAAO,CAAC,OAAO,CAAC,EAAE,CAAC,CAC7E,CAAC,IAAI,CAAC,OAAO,CAAC,eAAe,CAAC,wEAAwE,CAAC,CAAC,CAAA;AAEzG,MAAM,IAAI,GAAG,OAAO,CAAC,IAAI,CAAC,MAAM,EAAE,EAAE,MAAM,EAAE,IAAI,EAAE,EAAE,OAAO,CAAC,EAAE,CAC5D,MAAM,CAAC,GAAG,CAAC,QAAQ,CAAC;IAClB,MAAM,EAAE,MAAM,EAAE,GAAG,KAAK,CAAC,CAAC,IAAI,CAAA;IAC9B,MAAM,OAAO,GAAG,MAAM,CAAC,IAAI,CAAC,MAAM,EAAE,MAAM,CAAC,cAAc,CAAC,OAAO,CAAC,MAAM,CAAC,CAAC,CAAA;IAC1E,KAAK,MAAM,KAAK,IAAI,OAAO;QAAE,KAAK,CAAC,CAAC,OAAO,CAAC,GAAG,CAAC,OAAO,CAAC,IAAI,CAAC,CAAC,CAAC,IAAI,CAAC,SAAS,CAAC,KAAK,CAAC,CAAC,CAAC,CAAC,KAAK,CAAC,EAAE,CAAC,CAAA;AAClG,CAAC,CAAC,CACH,CAAC,IAAI,CAAC,OAAO,CAAC,eAAe,CAAC,yCAAyC,CAAC,CAAC,CAAA;AAE1E,MAAM,CAAC,MAAM,OAAO,GAAG,OAAO,CAAC,IAAI,CAAC,cAAc,CAAC,CAAC,IAAI,CACtD,OAAO,CAAC,eAAe,CAAC,4BAA4B,CAAC,EACrD,OAAO,CAAC,eAAe,CAAC,CAAC,GAAG,EAAE,KAAK,EAAE,IAAI,EAAE,cAAc,CAAC,CAAC,CAC5D,CAAA;AAED,MAAM,CAAC,MAAM,IAAI,GAAG,OAAO,CAAC,GAAG,CAAC,OAAO,EAAE,EAAE,OAAO,EAAE,OAAO,EAAE,CAAC,CAAA"}
@@ -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"}
@@ -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,3 @@
1
+ #!/usr/bin/env node
2
+ export {};
3
+ //# sourceMappingURL=bin.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"bin.d.ts","sourceRoot":"","sources":["../../src/bin.ts"],"names":[],"mappings":""}
@@ -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,3 @@
1
+ export * as BenchConfig from './Config.ts';
2
+ export { command, main } from './Cli.ts';
3
+ //# sourceMappingURL=index.d.ts.map
@@ -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,3 @@
1
+ export * as BenchConfig from './Config.js';
2
+ export { command, main } from './Cli.js';
3
+ //# sourceMappingURL=index.js.map
@@ -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-stage",
4
- "stub": true,
5
- "description": "Temporary package placeholder for staged publishing"
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
@@ -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
@@ -0,0 +1,2 @@
1
+ export * as BenchConfig from './Config.ts'
2
+ export { command, main } from './Cli.ts'
package/README.md DELETED
@@ -1,3 +0,0 @@
1
- # Temporary Holding Version
2
-
3
- This version is a temporary placeholder for this package. An operational version to replace this has been submitted for review and is awaiting a staged release.