@effect-bench/node 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/NodeBench.d.ts +9 -0
- package/dist/src/NodeBench.d.ts.map +1 -0
- package/dist/src/NodeBench.js +73 -0
- package/dist/src/NodeBench.js.map +1 -0
- package/dist/src/index.d.ts +2 -0
- package/dist/src/index.d.ts.map +1 -0
- package/dist/src/index.js +2 -0
- package/dist/src/index.js.map +1 -0
- package/package.json +27 -3
- package/src/NodeBench.ts +86 -0
- package/src/index.ts +1 -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,9 @@
|
|
|
1
|
+
import { FileSystem, Layer, Path } from 'effect';
|
|
2
|
+
export declare const clock: Layer.Layer<never, never, never>;
|
|
3
|
+
export declare const memory: Layer.Layer<never, never, never>;
|
|
4
|
+
export declare const environment: Layer.Layer<never, never, never>;
|
|
5
|
+
export declare const baselineStore: (directory?: string) => Layer.Layer<never, never, FileSystem.FileSystem | Path.Path>;
|
|
6
|
+
export declare const layer: (options?: {
|
|
7
|
+
readonly baselineDirectory?: string;
|
|
8
|
+
}) => Layer.Layer<never, never, never>;
|
|
9
|
+
//# sourceMappingURL=NodeBench.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"NodeBench.d.ts","sourceRoot":"","sources":["../../src/NodeBench.ts"],"names":[],"mappings":"AAMA,OAAO,EAAU,UAAU,EAAE,KAAK,EAAU,IAAI,EAAU,MAAM,QAAQ,CAAA;AAExE,eAAO,MAAM,KAAK,kCAIhB,CAAA;AAYF,eAAO,MAAM,MAAM,kCAGhB,CAAA;AAUH,eAAO,MAAM,WAAW,kCAetB,CAAA;AAIF,eAAO,MAAM,aAAa,sFAwBvB,CAAA;AAEH,eAAO,MAAM,KAAK,aAAa;IAAE,QAAQ,CAAC,iBAAiB,CAAC,EAAE,MAAM,CAAA;CAAE,qCAGnE,CAAA"}
|
|
@@ -0,0 +1,73 @@
|
|
|
1
|
+
import { execFileSync } from 'node:child_process';
|
|
2
|
+
import os from 'node:os';
|
|
3
|
+
import { setFlagsFromString } from 'node:v8';
|
|
4
|
+
import { runInNewContext } from 'node:vm';
|
|
5
|
+
import { Baseline, BaselineError, BaselineStore, BenchClock, BenchEnvironment, BenchMemory } from '@effect-bench/core';
|
|
6
|
+
import { NodeServices } from '@effect/platform-node';
|
|
7
|
+
import { Effect, FileSystem, Layer, Option, Path, Schema } from 'effect';
|
|
8
|
+
export const clock = Layer.sync(BenchClock, () => {
|
|
9
|
+
// Offset from layer creation so the value stays well inside float precision.
|
|
10
|
+
const origin = process.hrtime.bigint();
|
|
11
|
+
return { nowNs: () => Number(process.hrtime.bigint() - origin) };
|
|
12
|
+
});
|
|
13
|
+
const exposeGc = () => {
|
|
14
|
+
if (typeof globalThis.gc === 'function')
|
|
15
|
+
return globalThis.gc;
|
|
16
|
+
try {
|
|
17
|
+
setFlagsFromString('--expose-gc');
|
|
18
|
+
return runInNewContext('gc');
|
|
19
|
+
}
|
|
20
|
+
catch {
|
|
21
|
+
return undefined;
|
|
22
|
+
}
|
|
23
|
+
};
|
|
24
|
+
export const memory = Layer.sync(BenchMemory, () => ({
|
|
25
|
+
gc: exposeGc(),
|
|
26
|
+
heapUsed: () => process.memoryUsage().heapUsed,
|
|
27
|
+
}));
|
|
28
|
+
const git = (args) => {
|
|
29
|
+
try {
|
|
30
|
+
return execFileSync('git', args, { encoding: 'utf8', stdio: ['ignore', 'pipe', 'ignore'] }).trim();
|
|
31
|
+
}
|
|
32
|
+
catch {
|
|
33
|
+
return null;
|
|
34
|
+
}
|
|
35
|
+
};
|
|
36
|
+
export const environment = Layer.succeed(BenchEnvironment, {
|
|
37
|
+
fingerprint: Effect.sync(() => {
|
|
38
|
+
const cpus = os.cpus();
|
|
39
|
+
const status = git(['status', '--porcelain']);
|
|
40
|
+
return {
|
|
41
|
+
runtime: typeof process.versions['bun'] === 'string' ? 'bun' : 'node',
|
|
42
|
+
runtimeVersion: process.version,
|
|
43
|
+
platform: process.platform,
|
|
44
|
+
arch: process.arch,
|
|
45
|
+
cpu: cpus[0]?.model.trim() ?? 'unknown',
|
|
46
|
+
cpus: cpus.length,
|
|
47
|
+
gitSha: git(['rev-parse', 'HEAD']),
|
|
48
|
+
gitDirty: status === null ? null : status.length > 0,
|
|
49
|
+
};
|
|
50
|
+
}),
|
|
51
|
+
});
|
|
52
|
+
const BaselineJson = Schema.fromJsonString(Baseline);
|
|
53
|
+
export const baselineStore = (directory = '.bench/baselines') => Layer.effect(BaselineStore, Effect.gen(function* () {
|
|
54
|
+
const fs = yield* FileSystem.FileSystem;
|
|
55
|
+
const path = yield* Path.Path;
|
|
56
|
+
const fileOf = (name) => path.join(directory, `${name}.json`);
|
|
57
|
+
const failure = (name, message) => (cause) => new BaselineError({ name, message, cause });
|
|
58
|
+
return {
|
|
59
|
+
save: baseline => Effect.gen(function* () {
|
|
60
|
+
const json = yield* Schema.encodeEffect(BaselineJson)(baseline);
|
|
61
|
+
yield* fs.makeDirectory(directory, { recursive: true });
|
|
62
|
+
yield* fs.writeFileString(fileOf(baseline.name), json);
|
|
63
|
+
}).pipe(Effect.mapError(failure(baseline.name, `Could not save baseline "${baseline.name}".`))),
|
|
64
|
+
load: name => Effect.gen(function* () {
|
|
65
|
+
if (!(yield* fs.exists(fileOf(name))))
|
|
66
|
+
return Option.none();
|
|
67
|
+
const json = yield* fs.readFileString(fileOf(name));
|
|
68
|
+
return Option.some(yield* Schema.decodeUnknownEffect(BaselineJson)(json));
|
|
69
|
+
}).pipe(Effect.mapError(failure(name, `Could not load baseline "${name}".`))),
|
|
70
|
+
};
|
|
71
|
+
}));
|
|
72
|
+
export const layer = (options = {}) => Layer.mergeAll(clock, memory, environment, baselineStore(options.baselineDirectory)).pipe(Layer.provide(NodeServices.layer));
|
|
73
|
+
//# sourceMappingURL=NodeBench.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"NodeBench.js","sourceRoot":"","sources":["../../src/NodeBench.ts"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../src/index.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,SAAS,MAAM,gBAAgB,CAAA"}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"index.js","sourceRoot":"","sources":["../../src/index.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,SAAS,MAAM,gBAAgB,CAAA"}
|
package/package.json
CHANGED
|
@@ -1,6 +1,30 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@effect-bench/node",
|
|
3
|
-
"version": "0.0.
|
|
4
|
-
"
|
|
5
|
-
"
|
|
3
|
+
"version": "0.0.1",
|
|
4
|
+
"type": "module",
|
|
5
|
+
"exports": {
|
|
6
|
+
".": {
|
|
7
|
+
"types": "./dist/src/index.d.ts",
|
|
8
|
+
"default": "./dist/src/index.js"
|
|
9
|
+
}
|
|
10
|
+
},
|
|
11
|
+
"dependencies": {
|
|
12
|
+
"@effect-bench/core": "0.0.1"
|
|
13
|
+
},
|
|
14
|
+
"peerDependencies": {
|
|
15
|
+
"effect": "^4.1.2",
|
|
16
|
+
"@effect/platform-node": "^4.1.2"
|
|
17
|
+
},
|
|
18
|
+
"devDependencies": {
|
|
19
|
+
"effect": "4.1.2",
|
|
20
|
+
"@effect/platform-node": "4.1.2"
|
|
21
|
+
},
|
|
22
|
+
"files": [
|
|
23
|
+
"dist/src",
|
|
24
|
+
"src"
|
|
25
|
+
],
|
|
26
|
+
"publishConfig": {
|
|
27
|
+
"access": "public"
|
|
28
|
+
},
|
|
29
|
+
"scripts": {}
|
|
6
30
|
}
|
package/src/NodeBench.ts
ADDED
|
@@ -0,0 +1,86 @@
|
|
|
1
|
+
import { execFileSync } from 'node:child_process'
|
|
2
|
+
import os from 'node:os'
|
|
3
|
+
import { setFlagsFromString } from 'node:v8'
|
|
4
|
+
import { runInNewContext } from 'node:vm'
|
|
5
|
+
import { Baseline, BaselineError, BaselineStore, BenchClock, BenchEnvironment, BenchMemory } from '@effect-bench/core'
|
|
6
|
+
import { NodeServices } from '@effect/platform-node'
|
|
7
|
+
import { Effect, FileSystem, Layer, Option, Path, Schema } from 'effect'
|
|
8
|
+
|
|
9
|
+
export const clock = Layer.sync(BenchClock, () => {
|
|
10
|
+
// Offset from layer creation so the value stays well inside float precision.
|
|
11
|
+
const origin = process.hrtime.bigint()
|
|
12
|
+
return { nowNs: () => Number(process.hrtime.bigint() - origin) }
|
|
13
|
+
})
|
|
14
|
+
|
|
15
|
+
const exposeGc = (): (() => void) | undefined => {
|
|
16
|
+
if (typeof globalThis.gc === 'function') return globalThis.gc
|
|
17
|
+
try {
|
|
18
|
+
setFlagsFromString('--expose-gc')
|
|
19
|
+
return runInNewContext('gc') as () => void
|
|
20
|
+
} catch {
|
|
21
|
+
return undefined
|
|
22
|
+
}
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
export const memory = Layer.sync(BenchMemory, () => ({
|
|
26
|
+
gc: exposeGc(),
|
|
27
|
+
heapUsed: () => process.memoryUsage().heapUsed,
|
|
28
|
+
}))
|
|
29
|
+
|
|
30
|
+
const git = (args: ReadonlyArray<string>): string | null => {
|
|
31
|
+
try {
|
|
32
|
+
return execFileSync('git', args, { encoding: 'utf8', stdio: ['ignore', 'pipe', 'ignore'] }).trim()
|
|
33
|
+
} catch {
|
|
34
|
+
return null
|
|
35
|
+
}
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
export const environment = Layer.succeed(BenchEnvironment, {
|
|
39
|
+
fingerprint: Effect.sync(() => {
|
|
40
|
+
const cpus = os.cpus()
|
|
41
|
+
const status = git(['status', '--porcelain'])
|
|
42
|
+
return {
|
|
43
|
+
runtime: typeof process.versions['bun'] === 'string' ? 'bun' : 'node',
|
|
44
|
+
runtimeVersion: process.version,
|
|
45
|
+
platform: process.platform,
|
|
46
|
+
arch: process.arch,
|
|
47
|
+
cpu: cpus[0]?.model.trim() ?? 'unknown',
|
|
48
|
+
cpus: cpus.length,
|
|
49
|
+
gitSha: git(['rev-parse', 'HEAD']),
|
|
50
|
+
gitDirty: status === null ? null : status.length > 0,
|
|
51
|
+
}
|
|
52
|
+
}),
|
|
53
|
+
})
|
|
54
|
+
|
|
55
|
+
const BaselineJson = Schema.fromJsonString(Baseline)
|
|
56
|
+
|
|
57
|
+
export const baselineStore = (directory = '.bench/baselines') =>
|
|
58
|
+
Layer.effect(
|
|
59
|
+
BaselineStore,
|
|
60
|
+
Effect.gen(function* () {
|
|
61
|
+
const fs = yield* FileSystem.FileSystem
|
|
62
|
+
const path = yield* Path.Path
|
|
63
|
+
const fileOf = (name: string) => path.join(directory, `${name}.json`)
|
|
64
|
+
const failure = (name: string, message: string) => (cause: unknown) => new BaselineError({ name, message, cause })
|
|
65
|
+
|
|
66
|
+
return {
|
|
67
|
+
save: baseline =>
|
|
68
|
+
Effect.gen(function* () {
|
|
69
|
+
const json = yield* Schema.encodeEffect(BaselineJson)(baseline)
|
|
70
|
+
yield* fs.makeDirectory(directory, { recursive: true })
|
|
71
|
+
yield* fs.writeFileString(fileOf(baseline.name), json)
|
|
72
|
+
}).pipe(Effect.mapError(failure(baseline.name, `Could not save baseline "${baseline.name}".`))),
|
|
73
|
+
load: name =>
|
|
74
|
+
Effect.gen(function* () {
|
|
75
|
+
if (!(yield* fs.exists(fileOf(name)))) return Option.none()
|
|
76
|
+
const json = yield* fs.readFileString(fileOf(name))
|
|
77
|
+
return Option.some(yield* Schema.decodeUnknownEffect(BaselineJson)(json))
|
|
78
|
+
}).pipe(Effect.mapError(failure(name, `Could not load baseline "${name}".`))),
|
|
79
|
+
}
|
|
80
|
+
})
|
|
81
|
+
)
|
|
82
|
+
|
|
83
|
+
export const layer = (options: { readonly baselineDirectory?: string } = {}) =>
|
|
84
|
+
Layer.mergeAll(clock, memory, environment, baselineStore(options.baselineDirectory)).pipe(
|
|
85
|
+
Layer.provide(NodeServices.layer)
|
|
86
|
+
)
|
package/src/index.ts
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export * as NodeBench from './NodeBench.ts'
|