@effect-bench/cli 0.0.0-stage → 0.0.1
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 +87 -2
- 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 +31 -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
CHANGED
|
@@ -1,3 +1,88 @@
|
|
|
1
|
-
#
|
|
1
|
+
# effect-bench
|
|
2
2
|
|
|
3
|
-
|
|
3
|
+
Benchmarks for [Effect](https://effect.website) code, with JSON output and baseline verdicts built for agents and CI.
|
|
4
|
+
|
|
5
|
+
Community project, not affiliated with the Effect team.
|
|
6
|
+
|
|
7
|
+
```ts
|
|
8
|
+
// bench/schema.bench.ts
|
|
9
|
+
import { Bench } from '@effect-bench/core'
|
|
10
|
+
import { Effect } from 'effect'
|
|
11
|
+
|
|
12
|
+
export default Bench.suite('workflowCompiler', { layer: NodeRegistry.layer })(
|
|
13
|
+
Bench.params('compile', { nodes: [10, 100, 1000] }, {
|
|
14
|
+
prepare: ({ nodes }) => Effect.succeed(makeDraft(nodes)), // untimed
|
|
15
|
+
run: draft => compile(draft), // timed
|
|
16
|
+
}),
|
|
17
|
+
Bench.group('decode', { baseline: 'sync' })(
|
|
18
|
+
Bench.sync('sync', () => decodeSync(lead)),
|
|
19
|
+
Bench.make('effect', () => decodeEffect(lead)),
|
|
20
|
+
),
|
|
21
|
+
Bench.sync('encode', () => encode(payload), { budget: { p99: '5 millis' } }),
|
|
22
|
+
)
|
|
23
|
+
```
|
|
24
|
+
|
|
25
|
+
```bash
|
|
26
|
+
effect-bench list
|
|
27
|
+
effect-bench run --save before # store a baseline
|
|
28
|
+
effect-bench run --compare before --json
|
|
29
|
+
effect-bench check --compare before # exit 1 on a regression or blown budget
|
|
30
|
+
effect-bench run --rigorous --forks 8 # more processes for a tighter interval
|
|
31
|
+
```
|
|
32
|
+
|
|
33
|
+
## Writing benches
|
|
34
|
+
|
|
35
|
+
| Constructor | Measures |
|
|
36
|
+
| --- | --- |
|
|
37
|
+
| `Bench.make(name, effect)` | re-runs one Effect value |
|
|
38
|
+
| `Bench.make(name, ctx => effect)` | builds and runs a fresh Effect per operation |
|
|
39
|
+
| `Bench.sync(name, ctx => value)` | a plain function |
|
|
40
|
+
| `Bench.params(name, matrix, { prepare?, run \| sync })` | one bench per combination, ids like `compile[nodes=100]` |
|
|
41
|
+
| `Bench.group(name, { baseline })(...)` | alternatives, reported as a ratio to the baseline |
|
|
42
|
+
|
|
43
|
+
`Bench.suite(name, { layer, setup, ...options })` builds `layer` once per suite and runs `setup` in the suite's scope; its result is the `ctx` each bench receives. Benches can only use services the layer provides — anything else is a type error.
|
|
44
|
+
|
|
45
|
+
Options (suite or bench): `samples`, `minTime`, `warmup`, `memory` (GC + heap delta per sample), `budget: { p50, p99 }`, `tags`, and `check` (validates one result before timing).
|
|
46
|
+
|
|
47
|
+
## Output
|
|
48
|
+
|
|
49
|
+
`--json` prints one record per line on stdout: a `result` per bench and a final `summary`. Logs go to stderr.
|
|
50
|
+
|
|
51
|
+
```json
|
|
52
|
+
{"type":"result","schemaVersion":1,"status":"ok","id":"leadSchema/decode/effect","stats":{"medianNs":858,"p99Ns":943,"opsPerSec":1165161,...},"comparison":{"baseline":"before","change":0.051,"ci":[0.039,0.058],"verdict":"slower","stale":false},"relative":{"baselineId":"leadSchema/decode/sync","ratio":5.1}}
|
|
53
|
+
{"type":"summary","schemaVersion":1,"total":4,"failed":0,"regressions":1,"budgetsExceeded":0,"stale":0,"exitCode":1}
|
|
54
|
+
```
|
|
55
|
+
|
|
56
|
+
## How verdicts work
|
|
57
|
+
|
|
58
|
+
The CLI measures each suite in several fresh processes (`--forks`, default 3; 6 with `--rigorous`), interleaved fork by fork so slow drift spreads across suites. Drift between processes — JIT decisions, CPU frequency — is much larger than noise within one run, so each fork's median counts as one observation: the 95% interval for the change is a pooled t-interval on log medians.
|
|
59
|
+
|
|
60
|
+
`faster`/`slower` only when the whole interval is beyond the noise threshold (`--threshold`, default 3%), `same` when it is inside, `inconclusive` otherwise. On a laptop with 3 forks per side, repeated comparisons of unchanged code gave no false verdicts in 96 tries; a 15% slowdown was flagged every time and a 5% one most of the time (otherwise `inconclusive`). Treat `inconclusive` as "measure more": `--rigorous` or more `--forks`.
|
|
61
|
+
|
|
62
|
+
`--forks 0` measures in the CLI process, and `Runner.run` does the same when called from code. With a single fork on both sides the interval only sees within-run noise and will overstate confidence.
|
|
63
|
+
|
|
64
|
+
Exit codes: `0` ok, `1` regression or blown budget, `2` failed bench or error, `3` baseline recorded on another runtime or machine.
|
|
65
|
+
|
|
66
|
+
## Config
|
|
67
|
+
|
|
68
|
+
Optional `effect-bench.config.ts` in the working directory:
|
|
69
|
+
|
|
70
|
+
```ts
|
|
71
|
+
import { BenchConfig } from '@effect-bench/cli'
|
|
72
|
+
|
|
73
|
+
export default BenchConfig.make({
|
|
74
|
+
include: ['**/bench/*.bench.ts'],
|
|
75
|
+
modes: { ci: { samples: 50, minTime: '1 second', forks: 5 } },
|
|
76
|
+
noiseThreshold: 0.05,
|
|
77
|
+
})
|
|
78
|
+
```
|
|
79
|
+
|
|
80
|
+
## Packages
|
|
81
|
+
|
|
82
|
+
- `@effect-bench/core` — DSL, measuring loop, stats, comparison, runner, reporters. No platform imports.
|
|
83
|
+
- `@effect-bench/node` — hrtime clock, GC and heap, environment fingerprint, file baseline store.
|
|
84
|
+
- `@effect-bench/cli` — `effect-bench` command.
|
|
85
|
+
|
|
86
|
+
## Known limitations
|
|
87
|
+
|
|
88
|
+
- Bench files are loaded with Node's built-in type stripping, so they must use erasable TypeScript syntax only.
|
|
@@ -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,34 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@effect-bench/cli",
|
|
3
|
-
"version": "0.0.
|
|
4
|
-
"
|
|
5
|
-
"
|
|
3
|
+
"version": "0.0.1",
|
|
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.1",
|
|
16
|
+
"@effect-bench/node": "0.0.1"
|
|
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
|
+
},
|
|
33
|
+
"scripts": {}
|
|
6
34
|
}
|
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