@effect-bench/core 0.0.1 → 0.1.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.
@@ -1 +1 @@
1
- {"version":3,"file":"Services.js","sourceRoot":"","sources":["../../src/Services.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,OAAO,EAAE,IAAI,EAAE,MAAM,EAAE,MAAM,EAAE,MAAM,QAAQ,CAAA;AAOtD,MAAM,CAAC,MAAM,UAAU,GAAG,OAAO,CAAC,SAAS,CAAQ,yBAAyB,EAAE;IAC5E,YAAY,EAAE,GAAG,EAAE,CAAC,CAAC,EAAE,KAAK,EAAE,GAAG,EAAE,CAAC,WAAW,CAAC,GAAG,EAAE,GAAG,GAAG,EAAE,CAAC;CAC/D,CAAC,CAAA;AAOF,MAAM,CAAC,MAAM,WAAW,GAAG,OAAO,CAAC,SAAS,CAAS,0BAA0B,EAAE;IAC/E,YAAY,EAAE,GAAG,EAAE,CAAC,CAAC,EAAE,EAAE,EAAE,SAAS,EAAE,QAAQ,EAAE,SAAS,EAAE,CAAC;CAC7D,CAAC,CAAA;AAMF,MAAM,CAAC,MAAM,gBAAgB,GAAG,OAAO,CAAC,SAAS,CAAmB,+BAA+B,EAAE;IACnG,YAAY,EAAE,GAAG,EAAE,CAAC,CAAC;QACnB,WAAW,EAAE,MAAM,CAAC,OAAO,CAAC;YAC1B,OAAO,EAAE,SAAS;YAClB,cAAc,EAAE,SAAS;YACzB,QAAQ,EAAE,SAAS;YACnB,IAAI,EAAE,SAAS;YACf,GAAG,EAAE,SAAS;YACd,IAAI,EAAE,CAAC;YACP,MAAM,EAAE,IAAI;YACZ,QAAQ,EAAE,IAAI;SACf,CAAC;KACH,CAAC;CACH,CAAC,CAAA;AAEF,MAAM,OAAO,aAAc,SAAQ,IAAI,CAAC,WAAW,CAAC,eAAe,CAIjE;CAAG;AAOL,MAAM,CAAC,MAAM,eAAe,GAAG,GAAU,EAAE;IACzC,MAAM,SAAS,GAAG,IAAI,GAAG,EAAoB,CAAA;IAC7C,OAAO;QACL,IAAI,EAAE,QAAQ,CAAC,EAAE,CAAC,MAAM,CAAC,IAAI,CAAC,GAAG,EAAE,CAAC,KAAK,SAAS,CAAC,GAAG,CAAC,QAAQ,CAAC,IAAI,EAAE,QAAQ,CAAC,CAAC;QAChF,IAAI,EAAE,IAAI,CAAC,EAAE,CAAC,MAAM,CAAC,IAAI,CAAC,GAAG,EAAE,CAAC,MAAM,CAAC,eAAe,CAAC,SAAS,CAAC,GAAG,CAAC,IAAI,CAAC,CAAC,CAAC;KAC7E,CAAA;AACH,CAAC,CAAA;AAED,MAAM,CAAC,MAAM,aAAa,GAAG,OAAO,CAAC,SAAS,CAAQ,4BAA4B,EAAE;IAClF,YAAY,EAAE,eAAe;CAC9B,CAAC,CAAA;AAOF,MAAM,CAAC,MAAM,aAAa,GAAG,OAAO,CAAC,SAAS,CAAW,4BAA4B,EAAE;IACrF,YAAY,EAAE,GAAG,EAAE,CAAC,CAAC,EAAE,QAAQ,EAAE,GAAG,EAAE,CAAC,MAAM,CAAC,IAAI,EAAE,KAAK,EAAE,GAAG,EAAE,CAAC,MAAM,CAAC,IAAI,EAAE,CAAC;CAChF,CAAC,CAAA"}
1
+ {"version":3,"file":"Services.js","sourceRoot":"","sources":["../../src/Services.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,OAAO,EAAE,IAAI,EAAE,MAAM,EAAE,MAAM,EAAE,MAAM,QAAQ,CAAA;AAOtD,MAAM,CAAC,MAAM,UAAU,GAAG,OAAO,CAAC,SAAS,CAAQ,yBAAyB,EAAE;IAC5E,YAAY,EAAE,GAAG,EAAE,CAAC,CAAC,EAAE,KAAK,EAAE,GAAG,EAAE,CAAC,WAAW,CAAC,GAAG,EAAE,GAAG,GAAG,EAAE,CAAC;CAC/D,CAAC,CAAA;AASF,MAAM,CAAC,MAAM,WAAW,GAAG,OAAO,CAAC,SAAS,CAAS,0BAA0B,EAAE;IAC/E,YAAY,EAAE,GAAG,EAAE,CAAC,CAAC,EAAE,EAAE,EAAE,SAAS,EAAE,QAAQ,EAAE,SAAS,EAAE,GAAG,EAAE,SAAS,EAAE,CAAC;CAC7E,CAAC,CAAA;AAQF,MAAM,CAAC,MAAM,WAAW,GAAG,OAAO,CAAC,SAAS,CAAgB,0BAA0B,EAAE;IACtF,YAAY,EAAE,GAAG,EAAE,CAAC,CAAC,EAAE,IAAI,EAAE,EAAE,EAAE,MAAM,EAAE,GAAG,EAAE,GAAE,CAAC,EAAE,CAAC;CACrD,CAAC,CAAA;AAMF,MAAM,CAAC,MAAM,gBAAgB,GAAG,OAAO,CAAC,SAAS,CAAmB,+BAA+B,EAAE;IACnG,YAAY,EAAE,GAAG,EAAE,CAAC,CAAC;QACnB,WAAW,EAAE,MAAM,CAAC,OAAO,CAAC;YAC1B,OAAO,EAAE,SAAS;YAClB,cAAc,EAAE,SAAS;YACzB,QAAQ,EAAE,SAAS;YACnB,IAAI,EAAE,SAAS;YACf,GAAG,EAAE,SAAS;YACd,IAAI,EAAE,CAAC;YACP,MAAM,EAAE,IAAI;YACZ,QAAQ,EAAE,IAAI;SACf,CAAC;KACH,CAAC;CACH,CAAC,CAAA;AAEF,MAAM,OAAO,aAAc,SAAQ,IAAI,CAAC,WAAW,CAAC,eAAe,CAIjE;CAAG;AAOL,MAAM,CAAC,MAAM,eAAe,GAAG,GAAU,EAAE;IACzC,MAAM,SAAS,GAAG,IAAI,GAAG,EAAoB,CAAA;IAC7C,OAAO;QACL,IAAI,EAAE,QAAQ,CAAC,EAAE,CAAC,MAAM,CAAC,IAAI,CAAC,GAAG,EAAE,CAAC,KAAK,SAAS,CAAC,GAAG,CAAC,QAAQ,CAAC,IAAI,EAAE,QAAQ,CAAC,CAAC;QAChF,IAAI,EAAE,IAAI,CAAC,EAAE,CAAC,MAAM,CAAC,IAAI,CAAC,GAAG,EAAE,CAAC,MAAM,CAAC,eAAe,CAAC,SAAS,CAAC,GAAG,CAAC,IAAI,CAAC,CAAC,CAAC;KAC7E,CAAA;AACH,CAAC,CAAA;AAED,MAAM,CAAC,MAAM,aAAa,GAAG,OAAO,CAAC,SAAS,CAAQ,4BAA4B,EAAE;IAClF,YAAY,EAAE,eAAe;CAC9B,CAAC,CAAA;AAOF,MAAM,CAAC,MAAM,aAAa,GAAG,OAAO,CAAC,SAAS,CAAW,4BAA4B,EAAE;IACrF,YAAY,EAAE,GAAG,EAAE,CAAC,CAAC,EAAE,QAAQ,EAAE,GAAG,EAAE,CAAC,MAAM,CAAC,IAAI,EAAE,KAAK,EAAE,GAAG,EAAE,CAAC,MAAM,CAAC,IAAI,EAAE,CAAC;CAChF,CAAC,CAAA"}
@@ -4,4 +4,5 @@ export declare const median: (values: ReadonlyArray<number>) => number;
4
4
  /** Drops Tukey outliers (outside 1.5 × IQR) before summarising. */
5
5
  export declare const compute: (samplesNs: ReadonlyArray<number>) => StatsType;
6
6
  export declare const formatNs: (ns: number) => string;
7
+ export declare const formatBytes: (bytes: number) => string;
7
8
  //# sourceMappingURL=Stats.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"Stats.d.ts","sourceRoot":"","sources":["../../src/Stats.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,KAAK,IAAI,SAAS,EAAE,MAAM,kBAAkB,CAAA;AAE1D,eAAO,MAAM,QAAQ,WAAY,aAAa,CAAC,MAAM,CAAC,KAAK,MAAM,KAAG,MAMnE,CAAA;AAED,eAAO,MAAM,MAAM,WAAY,aAAa,CAAC,MAAM,CAAC,KAAG,MAIpD,CAAA;AAEH,mEAAmE;AACnE,eAAO,MAAM,OAAO,cAAe,aAAa,CAAC,MAAM,CAAC,KAAG,SAuB1D,CAAA;AAQD,eAAO,MAAM,QAAQ,OAAQ,MAAM,KAAG,MAGrC,CAAA"}
1
+ {"version":3,"file":"Stats.d.ts","sourceRoot":"","sources":["../../src/Stats.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,KAAK,IAAI,SAAS,EAAE,MAAM,kBAAkB,CAAA;AAE1D,eAAO,MAAM,QAAQ,WAAY,aAAa,CAAC,MAAM,CAAC,KAAK,MAAM,KAAG,MAMnE,CAAA;AAED,eAAO,MAAM,MAAM,WAAY,aAAa,CAAC,MAAM,CAAC,KAAG,MAIpD,CAAA;AAEH,mEAAmE;AACnE,eAAO,MAAM,OAAO,cAAe,aAAa,CAAC,MAAM,CAAC,KAAG,SAuB1D,CAAA;AAQD,eAAO,MAAM,QAAQ,OAAQ,MAAM,KAAG,MAGrC,CAAA;AAED,eAAO,MAAM,WAAW,UAAW,MAAM,KAAG,MAS3C,CAAA"}
package/dist/src/Stats.js CHANGED
@@ -43,4 +43,14 @@ export const formatNs = (ns) => {
43
43
  return `${(ns / scale).toPrecision(3)} ${unit}`;
44
44
  return `${ns.toPrecision(3)} ns`;
45
45
  };
46
+ export const formatBytes = (bytes) => {
47
+ const units = ['B', 'KiB', 'MiB', 'GiB'];
48
+ let value = Math.abs(bytes);
49
+ let unit = 0;
50
+ while (value >= 1024 && unit < units.length - 1) {
51
+ value /= 1024;
52
+ unit++;
53
+ }
54
+ return `${bytes < 0 ? '-' : ''}${unit === 0 ? value.toFixed(0) : value.toPrecision(3)} ${units[unit]}`;
55
+ };
46
56
  //# sourceMappingURL=Stats.js.map
@@ -1 +1 @@
1
- {"version":3,"file":"Stats.js","sourceRoot":"","sources":["../../src/Stats.ts"],"names":[],"mappings":"AAEA,MAAM,CAAC,MAAM,QAAQ,GAAG,CAAC,MAA6B,EAAE,CAAS,EAAU,EAAE;IAC3E,IAAI,MAAM,CAAC,MAAM,KAAK,CAAC;QAAE,OAAO,MAAM,CAAC,GAAG,CAAA;IAC1C,MAAM,QAAQ,GAAG,CAAC,MAAM,CAAC,MAAM,GAAG,CAAC,CAAC,GAAG,CAAC,CAAA;IACxC,MAAM,KAAK,GAAG,IAAI,CAAC,KAAK,CAAC,QAAQ,CAAC,CAAA;IAClC,MAAM,KAAK,GAAG,IAAI,CAAC,IAAI,CAAC,QAAQ,CAAC,CAAA;IACjC,OAAO,MAAM,CAAC,KAAK,CAAE,GAAG,CAAC,MAAM,CAAC,KAAK,CAAE,GAAG,MAAM,CAAC,KAAK,CAAE,CAAC,GAAG,CAAC,QAAQ,GAAG,KAAK,CAAC,CAAA;AAChF,CAAC,CAAA;AAED,MAAM,CAAC,MAAM,MAAM,GAAG,CAAC,MAA6B,EAAU,EAAE,CAC9D,QAAQ,CACN,CAAC,GAAG,MAAM,CAAC,CAAC,IAAI,CAAC,CAAC,CAAC,EAAE,CAAC,EAAE,EAAE,CAAC,CAAC,GAAG,CAAC,CAAC,EACjC,GAAG,CACJ,CAAA;AAEH,mEAAmE;AACnE,MAAM,CAAC,MAAM,OAAO,GAAG,CAAC,SAAgC,EAAa,EAAE;IACrE,MAAM,MAAM,GAAG,CAAC,GAAG,SAAS,CAAC,CAAC,IAAI,CAAC,CAAC,CAAC,EAAE,CAAC,EAAE,EAAE,CAAC,CAAC,GAAG,CAAC,CAAC,CAAA;IACnD,MAAM,EAAE,GAAG,QAAQ,CAAC,MAAM,EAAE,IAAI,CAAC,CAAA;IACjC,MAAM,EAAE,GAAG,QAAQ,CAAC,MAAM,EAAE,IAAI,CAAC,CAAA;IACjC,MAAM,KAAK,GAAG,GAAG,GAAG,CAAC,EAAE,GAAG,EAAE,CAAC,CAAA;IAC7B,MAAM,IAAI,GAAG,MAAM,CAAC,MAAM,CAAC,KAAK,CAAC,EAAE,CAAC,KAAK,IAAI,EAAE,GAAG,KAAK,IAAI,KAAK,IAAI,EAAE,GAAG,KAAK,CAAC,CAAA;IAC/E,MAAM,CAAC,GAAG,IAAI,CAAC,MAAM,CAAA;IACrB,MAAM,IAAI,GAAG,IAAI,CAAC,MAAM,CAAC,CAAC,GAAG,EAAE,KAAK,EAAE,EAAE,CAAC,GAAG,GAAG,KAAK,EAAE,CAAC,CAAC,GAAG,CAAC,CAAA;IAC5D,MAAM,QAAQ,GAAG,CAAC,GAAG,CAAC,CAAC,CAAC,CAAC,IAAI,CAAC,MAAM,CAAC,CAAC,GAAG,EAAE,KAAK,EAAE,EAAE,CAAC,GAAG,GAAG,CAAC,KAAK,GAAG,IAAI,CAAC,IAAI,CAAC,EAAE,CAAC,CAAC,GAAG,CAAC,CAAC,GAAG,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,CAAA;IAChG,MAAM,QAAQ,GAAG,QAAQ,CAAC,IAAI,EAAE,GAAG,CAAC,CAAA;IACpC,OAAO;QACL,OAAO,EAAE,CAAC;QACV,QAAQ,EAAE,MAAM,CAAC,MAAM,GAAG,CAAC;QAC3B,MAAM,EAAE,IAAI;QACZ,QAAQ;QACR,KAAK,EAAE,QAAQ,CAAC,IAAI,EAAE,IAAI,CAAC;QAC3B,KAAK,EAAE,QAAQ,CAAC,IAAI,EAAE,IAAI,CAAC;QAC3B,KAAK,EAAE,IAAI,CAAC,CAAC,CAAC,IAAI,MAAM,CAAC,GAAG;QAC5B,KAAK,EAAE,IAAI,CAAC,CAAC,GAAG,CAAC,CAAC,IAAI,MAAM,CAAC,GAAG;QAChC,KAAK,EAAE,MAAM,CAAC,IAAI,CAAC,GAAG,CAAC,KAAK,CAAC,EAAE,CAAC,IAAI,CAAC,GAAG,CAAC,KAAK,GAAG,QAAQ,CAAC,CAAC,CAAC;QAC5D,GAAG,EAAE,IAAI,KAAK,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,IAAI,GAAG,IAAI,CAAC,IAAI,CAAC,QAAQ,CAAC,CAAC,GAAG,IAAI,CAAC,IAAI,CAAC,CAAC,CAAC,GAAG,IAAI,CAAC,GAAG,GAAG;QAChF,SAAS,EAAE,QAAQ,GAAG,CAAC,CAAC,CAAC,CAAC,GAAG,GAAG,QAAQ,CAAC,CAAC,CAAC,IAAI;KAChD,CAAA;AACH,CAAC,CAAA;AAED,MAAM,KAAK,GAAG;IACZ,CAAC,GAAG,EAAE,GAAG,CAAC;IACV,CAAC,GAAG,EAAE,IAAI,CAAC;IACX,CAAC,GAAG,EAAE,IAAI,CAAC;CACH,CAAA;AAEV,MAAM,CAAC,MAAM,QAAQ,GAAG,CAAC,EAAU,EAAU,EAAE;IAC7C,KAAK,MAAM,CAAC,KAAK,EAAE,IAAI,CAAC,IAAI,KAAK;QAAE,IAAI,IAAI,CAAC,GAAG,CAAC,EAAE,CAAC,IAAI,KAAK;YAAE,OAAO,GAAG,CAAC,EAAE,GAAG,KAAK,CAAC,CAAC,WAAW,CAAC,CAAC,CAAC,IAAI,IAAI,EAAE,CAAA;IAC7G,OAAO,GAAG,EAAE,CAAC,WAAW,CAAC,CAAC,CAAC,KAAK,CAAA;AAClC,CAAC,CAAA"}
1
+ {"version":3,"file":"Stats.js","sourceRoot":"","sources":["../../src/Stats.ts"],"names":[],"mappings":"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"}
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@effect-bench/core",
3
- "version": "0.0.1",
3
+ "version": "0.1.0",
4
4
  "type": "module",
5
5
  "exports": {
6
6
  ".": {
package/src/Bench.ts CHANGED
@@ -1,13 +1,18 @@
1
1
  import type { Duration, Scope } from 'effect'
2
- import { Effect, Layer } from 'effect'
2
+ import { Effect, Layer, Stream } from 'effect'
3
+ import { BenchClock, BenchPhases } from './Services.ts'
3
4
 
4
5
  export type ParamValue = string | number | boolean
5
6
 
6
7
  export type Params = Readonly<Record<string, ParamValue>>
7
8
 
9
+ export type Size = number | `${number} ${'B' | 'KB' | 'KiB' | 'MB' | 'MiB' | 'GB' | 'GiB'}`
10
+
8
11
  export interface Budget {
9
12
  readonly p50?: Duration.Input
10
13
  readonly p99?: Duration.Input
14
+ /** Heap growth per operation. Setting it turns on `memory`. */
15
+ readonly heapDelta?: Size
11
16
  }
12
17
 
13
18
  export interface Options {
@@ -24,7 +29,26 @@ export interface Options {
24
29
 
25
30
  export type Operation =
26
31
  | { readonly _tag: 'Sync'; readonly run: () => unknown }
27
- | { readonly _tag: 'Effect'; readonly run: Effect.Effect<unknown, unknown> }
32
+ | {
33
+ readonly _tag: 'Effect'
34
+ readonly run: Effect.Effect<unknown, unknown>
35
+ /** Operations one run performs; stats are reported per operation. */
36
+ readonly units?: number
37
+ /** A single operation, for `check`, when `run` performs several. */
38
+ readonly single?: Effect.Effect<unknown, unknown>
39
+ }
40
+ | {
41
+ readonly _tag: 'PerOp'
42
+ readonly before: Effect.Effect<unknown, unknown>
43
+ readonly run: (input: unknown) => Effect.Effect<unknown, unknown>
44
+ }
45
+ | { readonly _tag: 'Stream'; readonly stream: () => Stream.Stream<unknown, unknown> }
46
+ | {
47
+ readonly _tag: 'Scenario'
48
+ readonly before: Effect.Effect<unknown, unknown> | undefined
49
+ readonly run: (input: unknown) => Effect.Effect<unknown, unknown>
50
+ readonly verify: ((value: unknown) => boolean) | undefined
51
+ }
28
52
 
29
53
  export interface Bench<in Ctx = any, out R = any> {
30
54
  readonly _tag: 'Bench'
@@ -32,6 +56,8 @@ export interface Bench<in Ctx = any, out R = any> {
32
56
  readonly params: Params
33
57
  readonly group: { readonly name: string; readonly baseline: string } | undefined
34
58
  readonly options: Options
59
+ /** Defaults for this kind of bench; mode, suite and bench options override them. */
60
+ readonly defaults: Options
35
61
  readonly check: ((value: unknown) => boolean) | undefined
36
62
  readonly prepare: (ctx: Ctx) => Effect.Effect<Operation, unknown, R>
37
63
  }
@@ -73,26 +99,77 @@ const makeBench = <Ctx, R>(
73
99
  name: string,
74
100
  prepare: (ctx: Ctx) => Effect.Effect<Operation, unknown, R>,
75
101
  { check, ...options }: BenchOptions<any>,
76
- params: Params = {}
77
- ): Bench<Ctx, R> => ({ _tag: 'Bench', name, params, group: undefined, options, check, prepare })
102
+ { params = {}, defaults = {} }: { readonly params?: Params; readonly defaults?: Options } = {}
103
+ ): Bench<Ctx, R> => ({ _tag: 'Bench', name, params, group: undefined, options, defaults, check, prepare })
104
+
105
+ export interface Spec<Ctx, I, B, A, E, R> extends BenchOptions<A> {
106
+ /** Runs once, untimed. */
107
+ readonly prepare?: (ctx: Ctx) => Effect.Effect<I, unknown, R>
108
+ /** Runs before every operation, untimed; `run` receives its value. */
109
+ readonly before?: (input: I, ctx: Ctx) => Effect.Effect<B, unknown, R>
110
+ readonly run: (input: B, ctx: Ctx) => Effect.Effect<A, E, R>
111
+ }
78
112
 
79
113
  /**
80
114
  * An Effect value is re-run as is; a function is called for every operation, so building the Effect is measured too.
81
115
  */
82
- export const make = <Ctx = unknown, A = unknown, E = never, R = never>(
116
+ export const make: {
117
+ <Ctx = unknown, A = unknown, E = never, R = never>(
118
+ name: string,
119
+ run: Effect.Effect<A, E, R> | ((ctx: Ctx) => Effect.Effect<A, E, R>),
120
+ options?: BenchOptions<A>
121
+ ): Bench<Ctx, R>
122
+ <Ctx = unknown, I = undefined, B = I, A = unknown, E = never, R = never>(
123
+ name: string,
124
+ spec: Spec<Ctx, I, B, A, E, R>
125
+ ): Bench<Ctx, R>
126
+ } = (
83
127
  name: string,
84
- run: Effect.Effect<A, E, R> | ((ctx: Ctx) => Effect.Effect<A, E, R>),
85
- options: BenchOptions<A> = {}
86
- ): Bench<Ctx, R> =>
87
- makeBench(
128
+ run:
129
+ | Effect.Effect<unknown, unknown, unknown>
130
+ | ((ctx: unknown) => Effect.Effect<unknown, unknown, unknown>)
131
+ | Spec<unknown, unknown, unknown, unknown, unknown, unknown>,
132
+ options: BenchOptions<unknown> = {}
133
+ ): Bench<any, any> => {
134
+ if (Effect.isEffect(run) || typeof run === 'function') {
135
+ return makeBench(
136
+ name,
137
+ ctx =>
138
+ Effect.map(Effect.context<unknown>(), context => ({
139
+ _tag: 'Effect' as const,
140
+ run: Effect.provideContext(Effect.isEffect(run) ? run : Effect.suspend(() => run(ctx)), context),
141
+ })),
142
+ options
143
+ )
144
+ }
145
+ const { prepare, before, run: runOne, ...specOptions } = run
146
+ return makeBench(
88
147
  name,
89
148
  ctx =>
90
- Effect.map(Effect.context<R>(), context => ({
91
- _tag: 'Effect' as const,
92
- run: Effect.provideContext(Effect.isEffect(run) ? run : Effect.suspend(() => run(ctx)), context),
93
- })),
94
- options
149
+ Effect.gen(function* () {
150
+ const input = prepare === undefined ? undefined : yield* prepare(ctx)
151
+ const context = yield* Effect.context<unknown>()
152
+ if (before === undefined) {
153
+ return {
154
+ _tag: 'Effect' as const,
155
+ run: Effect.provideContext(
156
+ Effect.suspend(() => runOne(input, ctx)),
157
+ context
158
+ ),
159
+ }
160
+ }
161
+ return {
162
+ _tag: 'PerOp' as const,
163
+ before: Effect.provideContext(
164
+ Effect.suspend(() => before(input, ctx)),
165
+ context
166
+ ),
167
+ run: (value: unknown) => Effect.provideContext(runOne(value, ctx), context),
168
+ }
169
+ }),
170
+ specOptions
95
171
  )
172
+ }
96
173
 
97
174
  export const sync = <Ctx = unknown, A = unknown>(
98
175
  name: string,
@@ -151,10 +228,129 @@ export const params: {
151
228
  return { _tag: 'Effect' as const, run: Effect.provideContext(run!(input, ctx), context) }
152
229
  }),
153
230
  options,
154
- combo
231
+ { params: combo }
155
232
  )
156
233
  )
157
234
 
235
+ export interface ThroughputSpec<Ctx, I, A, E, R> extends BenchOptions<A> {
236
+ /** Operations per run; stats are reported per operation. */
237
+ readonly ops: number
238
+ /** Fibers running operations at once. An array makes one bench per value. */
239
+ readonly concurrency: number | ReadonlyArray<number>
240
+ readonly prepare?: (ctx: Ctx) => Effect.Effect<I, unknown, R>
241
+ readonly run: (index: number, input: I, ctx: Ctx) => Effect.Effect<A, E, R>
242
+ }
243
+
244
+ /** Runs `ops` operations with the given concurrency; ids carry it, e.g. `enqueue[concurrency=50]`. */
245
+ export const throughput = <Ctx = unknown, I = undefined, A = unknown, E = never, R = never>(
246
+ name: string,
247
+ { ops, concurrency, prepare, run, ...options }: ThroughputSpec<Ctx, I, A, E, R>
248
+ ): ReadonlyArray<Bench<Ctx, R>> => {
249
+ const indexes = Array.from({ length: ops }, (_, index) => index)
250
+ return (typeof concurrency === 'number' ? [concurrency] : concurrency).map(fibers =>
251
+ makeBench(
252
+ name,
253
+ (ctx: Ctx) =>
254
+ Effect.gen(function* () {
255
+ const input = (prepare === undefined ? undefined : yield* prepare(ctx)) as I
256
+ const context = yield* Effect.context<R>()
257
+ return {
258
+ _tag: 'Effect' as const,
259
+ units: ops,
260
+ single: Effect.provideContext(
261
+ Effect.suspend(() => run(0, input, ctx)),
262
+ context
263
+ ),
264
+ run: Effect.provideContext(
265
+ Effect.forEach(indexes, index => run(index, input, ctx), { concurrency: fibers, discard: true }),
266
+ context
267
+ ),
268
+ }
269
+ }),
270
+ options,
271
+ { params: { concurrency: fibers } }
272
+ )
273
+ )
274
+ }
275
+
276
+ export interface StreamSpec<Ctx, I, A, E, R> extends Options {
277
+ readonly prepare?: (ctx: Ctx) => Effect.Effect<I, unknown, R>
278
+ /** Built fresh for every run. */
279
+ readonly stream: (input: I, ctx: Ctx) => Stream.Stream<A, E, R>
280
+ }
281
+
282
+ /** Runs the stream to the end; stats are per item, with time to first item and peak RSS as metrics. */
283
+ export const stream = <Ctx = unknown, I = undefined, A = unknown, E = never, R = never>(
284
+ name: string,
285
+ { prepare, stream: build, ...options }: StreamSpec<Ctx, I, A, E, R>
286
+ ): Bench<Ctx, R> =>
287
+ makeBench(
288
+ name,
289
+ (ctx: Ctx) =>
290
+ Effect.gen(function* () {
291
+ const input = (prepare === undefined ? undefined : yield* prepare(ctx)) as I
292
+ const context = yield* Effect.context<R>()
293
+ return { _tag: 'Stream' as const, stream: () => Stream.provideContext(build(input, ctx), context) }
294
+ }),
295
+ options,
296
+ { defaults: { samples: 10 } }
297
+ )
298
+
299
+ export interface ScenarioSpec<Ctx, I, B, A, E, R> extends Options {
300
+ /** Runs once, untimed. */
301
+ readonly prepare?: (ctx: Ctx) => Effect.Effect<I, unknown, R>
302
+ /** Runs before every run, untimed; `run` receives its value. */
303
+ readonly before?: (input: I, ctx: Ctx) => Effect.Effect<B, unknown, R>
304
+ readonly run: (input: B, ctx: Ctx) => Effect.Effect<A, E, R>
305
+ /** Checked after every run, untimed. */
306
+ readonly verify?: (value: A) => boolean
307
+ }
308
+
309
+ /** A longer flow timed once per sample, with `Bench.phase` timings as metrics. */
310
+ export const scenario = <Ctx = unknown, I = undefined, B = I, A = unknown, E = never, R = never>(
311
+ name: string,
312
+ { prepare, before, run, verify, ...options }: ScenarioSpec<Ctx, I, B, A, E, R>
313
+ ): Bench<Ctx, R> =>
314
+ makeBench(
315
+ name,
316
+ (ctx: Ctx) =>
317
+ Effect.gen(function* () {
318
+ const input = (prepare === undefined ? undefined : yield* prepare(ctx)) as I
319
+ const context = yield* Effect.context<R>()
320
+ return {
321
+ _tag: 'Scenario' as const,
322
+ before:
323
+ before === undefined
324
+ ? undefined
325
+ : Effect.provideContext(
326
+ Effect.suspend(() => before(input, ctx)),
327
+ context
328
+ ),
329
+ run: (value: unknown) =>
330
+ Effect.provideContext(
331
+ Effect.suspend(() => run((before === undefined ? input : value) as B, ctx)),
332
+ context
333
+ ),
334
+ verify: verify as ((value: unknown) => boolean) | undefined,
335
+ }
336
+ }),
337
+ options,
338
+ { defaults: { samples: 10, minTime: 0, warmup: 0 } }
339
+ )
340
+
341
+ /** Times part of a scenario. Nested phases are reported as `outer/inner`; outside a scenario it just runs the effect. */
342
+ export const phase = <A, E, R>(name: string, effect: Effect.Effect<A, E, R>): Effect.Effect<A, E, R> =>
343
+ Effect.gen(function* () {
344
+ const recorder = yield* BenchPhases
345
+ const clock = yield* BenchClock
346
+ const path = recorder.path === '' ? name : `${recorder.path}/${name}`
347
+ const startedAt = clock.nowNs()
348
+ return yield* effect.pipe(
349
+ Effect.provideService(BenchPhases, { ...recorder, path }),
350
+ Effect.ensuring(Effect.sync(() => recorder.record(path, clock.nowNs() - startedAt)))
351
+ )
352
+ })
353
+
158
354
  export const group =
159
355
  (name: string, options: { readonly baseline: string }) =>
160
356
  <Ctx, R>(...entries: ReadonlyArray<Entry<Ctx, R>>): ReadonlyArray<Bench<Ctx, R>> =>
@@ -1,6 +1,6 @@
1
1
  import { Schema } from 'effect'
2
2
 
3
- export const schemaVersion = 2
3
+ export const schemaVersion = 3
4
4
 
5
5
  export const Stats = Schema.Struct({
6
6
  samples: Schema.Number,
@@ -56,6 +56,9 @@ export const Relative = Schema.Struct({
56
56
  })
57
57
  export type Relative = typeof Relative.Type
58
58
 
59
+ /** Extra numbers per bench, such as stream time to first item or scenario phases; the key suffix gives the unit. */
60
+ export const Metrics = Schema.NullOr(Schema.Record(Schema.String, Schema.Number))
61
+
59
62
  const identity = {
60
63
  id: Schema.String,
61
64
  suite: Schema.String,
@@ -74,6 +77,7 @@ export const OkResult = Schema.Struct({
74
77
  batchSize: Schema.Number,
75
78
  overheadNs: Schema.Number,
76
79
  heapDeltaBytes: Schema.NullOr(Schema.Number),
80
+ metrics: Metrics,
77
81
  budget: Schema.NullOr(BudgetCheck),
78
82
  comparison: Schema.NullOr(Comparison),
79
83
  relative: Schema.NullOr(Relative),
@@ -100,7 +104,14 @@ export const RawOk = Schema.Struct({
100
104
  batchSize: Schema.Number,
101
105
  overheadNs: Schema.Number,
102
106
  heapDeltaBytes: Schema.NullOr(Schema.Number),
103
- budgetNs: Schema.NullOr(Schema.Struct({ p50: Schema.NullOr(Schema.Number), p99: Schema.NullOr(Schema.Number) })),
107
+ metrics: Metrics,
108
+ limits: Schema.NullOr(
109
+ Schema.Struct({
110
+ p50Ns: Schema.NullOr(Schema.Number),
111
+ p99Ns: Schema.NullOr(Schema.Number),
112
+ heapDeltaBytes: Schema.NullOr(Schema.Number),
113
+ })
114
+ ),
104
115
  })
105
116
  export type RawOk = typeof RawOk.Type
106
117
 
package/src/Measure.ts CHANGED
@@ -1,6 +1,6 @@
1
- import { Effect } from 'effect'
1
+ import { Data, Effect, Stream } from 'effect'
2
2
  import type { Operation } from './Bench.ts'
3
- import { BenchClock, BenchMemory } from './Services.ts'
3
+ import { BenchClock, BenchMemory, BenchPhases, type Clock, type Memory } from './Services.ts'
4
4
  import { median } from './Stats.ts'
5
5
 
6
6
  export interface MeasureOptions {
@@ -19,78 +19,222 @@ export interface Measurement {
19
19
  readonly batchSize: number
20
20
  readonly overheadNs: number
21
21
  readonly heapDeltaBytes: number | null
22
+ readonly metrics: Readonly<Record<string, number>> | null
22
23
  }
23
24
 
25
+ /** A scenario's `verify` rejected a run's result. */
26
+ export class CheckFailed extends Data.TaggedError('CheckFailed')<{ readonly message: string }> {}
27
+
24
28
  /** Keeps return values reachable so the JIT cannot drop the measured work. */
25
29
  export let blackhole: unknown
26
30
 
31
+ interface Timer {
32
+ /** Returns the start time. */
33
+ readonly start: () => number
34
+ readonly stop: () => number
35
+ readonly heapDeltas: Array<number>
36
+ }
37
+
38
+ /** With memory, collects garbage before starting and records heap growth on stop. */
39
+ const makeTimer = (clock: Clock, memory: Memory | undefined): Timer => {
40
+ let startedAt = 0
41
+ let heapBefore = 0
42
+ const heapDeltas: Array<number> = []
43
+ return {
44
+ heapDeltas,
45
+ start: () => {
46
+ memory?.gc?.()
47
+ heapBefore = memory?.heapUsed?.() ?? 0
48
+ startedAt = clock.nowNs()
49
+ return startedAt
50
+ },
51
+ stop: () => {
52
+ const elapsed = clock.nowNs() - startedAt
53
+ if (memory?.heapUsed !== undefined) heapDeltas.push(memory.heapUsed() - heapBefore)
54
+ return elapsed
55
+ },
56
+ }
57
+ }
58
+
59
+ type Batched = Extract<Operation, { readonly _tag: 'Sync' | 'Effect' | 'PerOp' }>
60
+ type SingleShot = Extract<Operation, { readonly _tag: 'Stream' | 'Scenario' }>
61
+
62
+ /** Returns the elapsed time of the timed part only; `PerOp` inputs are built before the timer starts. */
27
63
  const makeBatch =
28
- (operation: Operation) =>
29
- (n: number): Effect.Effect<void, unknown> => {
30
- if (operation._tag === 'Sync') {
31
- const run = operation.run
32
- return Effect.sync(() => {
33
- for (let i = 0; i < n; i++) blackhole = run()
34
- })
64
+ (operation: Batched) =>
65
+ (n: number, timer: Timer): Effect.Effect<number, unknown> => {
66
+ switch (operation._tag) {
67
+ case 'Sync': {
68
+ const run = operation.run
69
+ return Effect.sync(() => {
70
+ timer.start()
71
+ for (let i = 0; i < n; i++) blackhole = run()
72
+ return timer.stop()
73
+ })
74
+ }
75
+ case 'Effect': {
76
+ const run = operation.run
77
+ return Effect.gen(function* () {
78
+ timer.start()
79
+ for (let i = 0; i < n; i++) blackhole = yield* run
80
+ return timer.stop()
81
+ })
82
+ }
83
+ case 'PerOp': {
84
+ const { before, run } = operation
85
+ return Effect.gen(function* () {
86
+ const inputs: Array<unknown> = []
87
+ for (let i = 0; i < n; i++) inputs.push(yield* before)
88
+ timer.start()
89
+ for (const input of inputs) blackhole = yield* run(input)
90
+ return timer.stop()
91
+ })
92
+ }
35
93
  }
36
- const run = operation.run
37
- return Effect.gen(function* () {
38
- for (let i = 0; i < n; i++) blackhole = yield* run
39
- })
40
94
  }
41
95
 
42
- const emptyOperation = (operation: Operation): Operation =>
43
- operation._tag === 'Sync' ? { _tag: 'Sync', run: () => undefined } : { _tag: 'Effect', run: Effect.void }
96
+ const emptyOperation = (operation: Batched): Batched => {
97
+ switch (operation._tag) {
98
+ case 'Sync':
99
+ return { _tag: 'Sync', run: () => undefined }
100
+ case 'Effect':
101
+ return { _tag: 'Effect', run: Effect.void }
102
+ case 'PerOp':
103
+ return { _tag: 'PerOp', before: Effect.void, run: () => Effect.void }
104
+ }
105
+ }
44
106
 
45
- export const measure = Effect.fn('effectBench/measure')(function* (operation: Operation, options: MeasureOptions) {
107
+ const measureBatched = Effect.fn('effectBench/measureBatched')(function* (operation: Batched, options: MeasureOptions) {
46
108
  const clock = yield* BenchClock
47
109
  const memory = yield* BenchMemory
48
110
  const targetSampleNs = options.targetSampleNs ?? 1_000_000
49
111
  const maxSamples = options.maxSamples ?? 10_000
112
+ const units = operation._tag === 'Effect' ? (operation.units ?? 1) : 1
50
113
  const batch = makeBatch(operation)
51
114
  const empty = makeBatch(emptyOperation(operation))
52
-
53
- const time = (run: (n: number) => Effect.Effect<void, unknown>, n: number) =>
54
- Effect.suspend(() => {
55
- const startedAt = clock.nowNs()
56
- return Effect.map(run(n), () => clock.nowNs() - startedAt)
57
- })
115
+ const plain = makeTimer(clock, undefined)
116
+ const sampling = makeTimer(clock, options.memory ? memory : undefined)
58
117
 
59
118
  const warmupStartedAt = clock.nowNs()
60
- do yield* batch(1)
119
+ do yield* batch(1, plain)
61
120
  while (clock.nowNs() - warmupStartedAt < options.warmupNs)
62
121
 
63
122
  let batchSize = 1
64
123
  for (;;) {
65
- const elapsed = yield* time(batch, batchSize)
124
+ const elapsed = yield* batch(batchSize, plain)
66
125
  if (elapsed >= targetSampleNs || batchSize >= 2 ** 30) break
67
126
  batchSize = Math.min(batchSize * 100, Math.max(batchSize * 2, Math.ceil((batchSize * targetSampleNs) / elapsed)))
68
127
  }
69
128
 
70
129
  const overheads: Array<number> = []
71
- for (let i = 0; i < 5; i++) overheads.push(yield* time(empty, batchSize))
130
+ for (let i = 0; i < 5; i++) overheads.push(yield* empty(batchSize, plain))
72
131
  const overheadNs = median(overheads)
73
132
 
133
+ const operations = batchSize * units
74
134
  const samplesNs: Array<number> = []
75
- const heapDeltas: Array<number> = []
76
- const gc = options.memory ? memory.gc : undefined
77
- const heapUsed = options.memory ? memory.heapUsed : undefined
78
135
  const startedAt = clock.nowNs()
79
136
  while (
80
137
  samplesNs.length < maxSamples &&
81
138
  (samplesNs.length < options.samples || clock.nowNs() - startedAt < options.minTimeNs)
82
139
  ) {
83
- gc?.()
84
- const heapBefore = heapUsed?.()
85
- const elapsed = yield* time(batch, batchSize)
86
- if (heapBefore !== undefined) heapDeltas.push((heapUsed!() - heapBefore) / batchSize)
87
- samplesNs.push(Math.max(0, elapsed - overheadNs) / batchSize)
140
+ const elapsed = yield* batch(batchSize, sampling)
141
+ samplesNs.push(Math.max(0, elapsed - overheadNs) / operations)
88
142
  }
89
143
 
90
144
  return {
91
145
  samplesNs,
92
146
  batchSize,
93
- overheadNs: overheadNs / batchSize,
94
- heapDeltaBytes: heapDeltas.length === 0 ? null : median(heapDeltas),
147
+ overheadNs: overheadNs / operations,
148
+ heapDeltaBytes:
149
+ sampling.heapDeltas.length === 0 ? null : median(sampling.heapDeltas.map(delta => delta / operations)),
150
+ metrics: null,
151
+ } satisfies Measurement
152
+ })
153
+
154
+ interface Run {
155
+ readonly elapsedNs: number
156
+ /** Work units in the run: stream items, or 1 for a scenario. */
157
+ readonly units: number
158
+ readonly metrics: Readonly<Record<string, number>>
159
+ }
160
+
161
+ const runOnce = (operation: SingleShot, clock: Clock, memory: Memory, timer: Timer): Effect.Effect<Run, unknown> => {
162
+ if (operation._tag === 'Stream') {
163
+ return Effect.gen(function* () {
164
+ const stream = operation.stream()
165
+ const rss = memory.rss
166
+ let items = 0
167
+ let chunks = 0
168
+ let firstItemNs = Number.NaN
169
+ let peakRss = rss?.() ?? 0
170
+ const startedAt = timer.start()
171
+ yield* Stream.runForEachArray(stream, chunk =>
172
+ Effect.sync(() => {
173
+ if (items === 0) firstItemNs = clock.nowNs() - startedAt
174
+ items += chunk.length
175
+ blackhole = chunk
176
+ // Reading RSS is a syscall; sampling every 64th chunk keeps it out of the timing.
177
+ if (rss !== undefined && (chunks++ & 63) === 0) peakRss = Math.max(peakRss, rss())
178
+ })
179
+ )
180
+ const elapsedNs = timer.stop()
181
+ const metrics: Record<string, number> = { firstItemNs: items === 0 ? elapsedNs : firstItemNs, items }
182
+ if (rss !== undefined) metrics['peakRssBytes'] = Math.max(peakRss, rss())
183
+ return { elapsedNs, units: Math.max(items, 1), metrics }
184
+ })
185
+ }
186
+ return Effect.gen(function* () {
187
+ const input = operation.before === undefined ? undefined : yield* operation.before
188
+ const phases = new Map<string, number>()
189
+ const recorder = { path: '', record: (path: string, ns: number) => phases.set(path, (phases.get(path) ?? 0) + ns) }
190
+ timer.start()
191
+ const value = yield* Effect.provideService(operation.run(input), BenchPhases, recorder)
192
+ const elapsedNs = timer.stop()
193
+ if (operation.verify !== undefined && !operation.verify(value)) {
194
+ return yield* new CheckFailed({ message: `verify rejected the result: ${String(value)}` })
195
+ }
196
+ return {
197
+ elapsedNs,
198
+ units: 1,
199
+ metrics: Object.fromEntries([...phases].map(([path, ns]) => [`phase:${path}Ns`, ns])),
200
+ }
201
+ })
202
+ }
203
+
204
+ /** Streams and scenarios run whole once per sample; they are too slow to batch. */
205
+ const measureRuns = Effect.fn('effectBench/measureRuns')(function* (operation: SingleShot, options: MeasureOptions) {
206
+ const clock = yield* BenchClock
207
+ const memory = yield* BenchMemory
208
+ const maxSamples = options.maxSamples ?? 10_000
209
+ const plain = makeTimer(clock, undefined)
210
+ const sampling = makeTimer(clock, options.memory ? memory : undefined)
211
+
212
+ const warmupStartedAt = clock.nowNs()
213
+ do yield* runOnce(operation, clock, memory, plain)
214
+ while (clock.nowNs() - warmupStartedAt < options.warmupNs)
215
+
216
+ const samplesNs: Array<number> = []
217
+ const metrics: Record<string, Array<number>> = {}
218
+ const startedAt = clock.nowNs()
219
+ while (
220
+ samplesNs.length < maxSamples &&
221
+ (samplesNs.length < options.samples || clock.nowNs() - startedAt < options.minTimeNs)
222
+ ) {
223
+ const run = yield* runOnce(operation, clock, memory, sampling)
224
+ samplesNs.push(run.elapsedNs / run.units)
225
+ for (const [key, value] of Object.entries(run.metrics)) (metrics[key] ??= []).push(value)
226
+ }
227
+
228
+ return {
229
+ samplesNs,
230
+ batchSize: 1,
231
+ overheadNs: 0,
232
+ heapDeltaBytes: sampling.heapDeltas.length === 0 ? null : median(sampling.heapDeltas),
233
+ metrics: Object.fromEntries(Object.entries(metrics).map(([key, values]) => [key, median(values)])),
95
234
  } satisfies Measurement
96
235
  })
236
+
237
+ export const measure = (operation: Operation, options: MeasureOptions): Effect.Effect<Measurement, unknown> =>
238
+ operation._tag === 'Stream' || operation._tag === 'Scenario'
239
+ ? measureRuns(operation, options)
240
+ : measureBatched(operation, options)