@effect-bench/core 0.0.2 → 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.
- package/README.md +22 -3
- package/dist/src/Bench.d.ts +63 -2
- package/dist/src/Bench.d.ts.map +1 -1
- package/dist/src/Bench.js +69 -7
- package/dist/src/Bench.js.map +1 -1
- package/dist/src/BenchResult.d.ts +15 -7
- package/dist/src/BenchResult.d.ts.map +1 -1
- package/dist/src/BenchResult.js +10 -2
- package/dist/src/BenchResult.js.map +1 -1
- package/dist/src/Measure.d.ts +11 -6
- package/dist/src/Measure.d.ts.map +1 -1
- package/dist/src/Measure.js +157 -34
- package/dist/src/Measure.js.map +1 -1
- package/dist/src/Reporter.d.ts.map +1 -1
- package/dist/src/Reporter.js +12 -1
- package/dist/src/Reporter.js.map +1 -1
- package/dist/src/Runner.d.ts +20 -6
- package/dist/src/Runner.d.ts.map +1 -1
- package/dist/src/Runner.js +46 -22
- package/dist/src/Runner.js.map +1 -1
- package/dist/src/Services.d.ts +8 -0
- package/dist/src/Services.d.ts.map +1 -1
- package/dist/src/Services.js +4 -1
- package/dist/src/Services.js.map +1 -1
- package/dist/src/Stats.d.ts +1 -0
- package/dist/src/Stats.d.ts.map +1 -1
- package/dist/src/Stats.js +10 -0
- package/dist/src/Stats.js.map +1 -1
- package/package.json +1 -1
- package/src/Bench.ts +211 -15
- package/src/BenchResult.ts +13 -2
- package/src/Measure.ts +179 -35
- package/src/Reporter.ts +15 -1
- package/src/Runner.ts +48 -22
- package/src/Services.ts +13 -1
- package/src/Stats.ts +11 -0
package/src/Reporter.ts
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import { Console, Effect, Layer } from 'effect'
|
|
2
2
|
import { type BenchResult, schemaVersion } from './BenchResult.ts'
|
|
3
3
|
import { BenchReporter } from './Services.ts'
|
|
4
|
-
import { formatNs } from './Stats.ts'
|
|
4
|
+
import { formatBytes, formatNs } from './Stats.ts'
|
|
5
5
|
|
|
6
6
|
const json = Layer.succeed(BenchReporter, {
|
|
7
7
|
onResult: result => Console.log(JSON.stringify({ type: 'result', schemaVersion, ...result })),
|
|
@@ -28,6 +28,14 @@ const row = (result: BenchResult): ReadonlyArray<string> => {
|
|
|
28
28
|
]
|
|
29
29
|
}
|
|
30
30
|
|
|
31
|
+
/** The key suffix gives the unit: `firstItemNs`, `peakRssBytes`, `phase:compileNs`. */
|
|
32
|
+
const metric = ([key, value]: [string, number]) =>
|
|
33
|
+
key.endsWith('Ns')
|
|
34
|
+
? `${key.slice(0, -2)} ${formatNs(value)}`
|
|
35
|
+
: key.endsWith('Bytes')
|
|
36
|
+
? `${key.slice(0, -5)} ${formatBytes(value)}`
|
|
37
|
+
: `${key} ${value.toLocaleString('en-US', { maximumFractionDigits: 2 })}`
|
|
38
|
+
|
|
31
39
|
const render = (rows: ReadonlyArray<ReadonlyArray<string>>) => {
|
|
32
40
|
const widths = rows[0]!.map((_, column) => Math.max(...rows.map(cells => cells[column]!.length)))
|
|
33
41
|
return rows
|
|
@@ -46,6 +54,12 @@ const table = Layer.succeed(BenchReporter, {
|
|
|
46
54
|
Effect.gen(function* () {
|
|
47
55
|
if (results.length === 0) return yield* Console.log('No benches matched.')
|
|
48
56
|
yield* Console.log(render([['bench', 'median', 'p99', 'rme', 'ops/s', 'change', 'budget'], ...results.map(row)]))
|
|
57
|
+
const withMetrics = results.flatMap(result =>
|
|
58
|
+
result.status === 'ok' && result.metrics !== null && Object.keys(result.metrics).length > 0
|
|
59
|
+
? [[result.id, Object.entries(result.metrics).map(metric).join(' ')]]
|
|
60
|
+
: []
|
|
61
|
+
)
|
|
62
|
+
if (withMetrics.length > 0) yield* Console.log(`\n${render([['bench', 'metrics'], ...withMetrics])}`)
|
|
49
63
|
for (const result of results)
|
|
50
64
|
if (result.status === 'failed') yield* Console.log(`\n${result.id}:\n${result.message}`)
|
|
51
65
|
yield* Console.log(
|
package/src/Runner.ts
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
|
-
import { Cause, type Context, Data, Duration, Effect, Exit, Layer, Option, type Scope } from 'effect'
|
|
2
|
-
import { type Bench, benchId, type Options, type Suite } from './Bench.ts'
|
|
1
|
+
import { Cause, type Context, Data, Duration, Effect, Exit, Layer, Option, type Scope, Stream } from 'effect'
|
|
2
|
+
import { type Bench, benchId, type Options, type Size, type Suite } from './Bench.ts'
|
|
3
3
|
import type {
|
|
4
4
|
Baseline,
|
|
5
5
|
BaselineEntry,
|
|
@@ -15,9 +15,9 @@ import type {
|
|
|
15
15
|
} from './BenchResult.ts'
|
|
16
16
|
import { schemaVersion } from './BenchResult.ts'
|
|
17
17
|
import { compare } from './Compare.ts'
|
|
18
|
-
import { measure } from './Measure.ts'
|
|
18
|
+
import { CheckFailed, measure } from './Measure.ts'
|
|
19
19
|
import { BaselineError, BaselineStore, BenchEnvironment, BenchReporter } from './Services.ts'
|
|
20
|
-
import { compute, formatNs, median } from './Stats.ts'
|
|
20
|
+
import { compute, formatBytes, formatNs, median } from './Stats.ts'
|
|
21
21
|
|
|
22
22
|
export interface MeasureOptions {
|
|
23
23
|
/** Only run benches whose id contains this text, or that carry this tag. */
|
|
@@ -57,6 +57,14 @@ class BenchFailure extends Data.TaggedError('BenchFailure')<{
|
|
|
57
57
|
|
|
58
58
|
const toNs = (input: Duration.Input) => Number(Duration.toNanosUnsafe(input))
|
|
59
59
|
|
|
60
|
+
const bytesPer = { B: 1, KB: 1e3, KiB: 1024, MB: 1e6, MiB: 1024 ** 2, GB: 1e9, GiB: 1024 ** 3 } as const
|
|
61
|
+
|
|
62
|
+
const toBytes = (size: Size): number => {
|
|
63
|
+
if (typeof size === 'number') return size
|
|
64
|
+
const [amount, unit] = size.split(' ') as [string, keyof typeof bytesPer]
|
|
65
|
+
return Number(amount) * bytesPer[unit]
|
|
66
|
+
}
|
|
67
|
+
|
|
60
68
|
const failWith = (phase: FailedResult['phase']) => (cause: Cause.Cause<unknown>) =>
|
|
61
69
|
new BenchFailure({ phase, message: Cause.pretty(cause) })
|
|
62
70
|
|
|
@@ -77,14 +85,19 @@ export const list = (suites: ReadonlyArray<Suite>, filter?: string): ReadonlyArr
|
|
|
77
85
|
suite.benches.map(bench => plannedOf(suite, bench, undefined)).filter(planned => matches(planned, filter))
|
|
78
86
|
)
|
|
79
87
|
|
|
80
|
-
const checkBudget = (
|
|
81
|
-
if (
|
|
82
|
-
const violations =
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
+
const checkBudget = (limits: RawOk['limits'], stats: Stats, heapDeltaBytes: number | null): BudgetCheck | null => {
|
|
89
|
+
if (limits === null) return null
|
|
90
|
+
const violations = [
|
|
91
|
+
...(limits.p50Ns !== null && stats.medianNs > limits.p50Ns
|
|
92
|
+
? [`p50 ${formatNs(stats.medianNs)} > ${formatNs(limits.p50Ns)}`]
|
|
93
|
+
: []),
|
|
94
|
+
...(limits.p99Ns !== null && stats.p99Ns > limits.p99Ns
|
|
95
|
+
? [`p99 ${formatNs(stats.p99Ns)} > ${formatNs(limits.p99Ns)}`]
|
|
96
|
+
: []),
|
|
97
|
+
...(limits.heapDeltaBytes !== null && heapDeltaBytes !== null && heapDeltaBytes > limits.heapDeltaBytes
|
|
98
|
+
? [`heapDelta ${formatBytes(heapDeltaBytes)} > ${formatBytes(limits.heapDeltaBytes)}`]
|
|
99
|
+
: []),
|
|
100
|
+
]
|
|
88
101
|
return { status: violations.length === 0 ? 'ok' : 'exceeded', violations }
|
|
89
102
|
}
|
|
90
103
|
|
|
@@ -103,11 +116,15 @@ const runBench = Effect.fn('effectBench/runBench')(function* (
|
|
|
103
116
|
) {
|
|
104
117
|
const operation = yield* bench.prepare(ctx).pipe(Effect.catchCause(cause => Effect.fail(failWith('prepare')(cause))))
|
|
105
118
|
|
|
106
|
-
if (bench.check !== undefined) {
|
|
119
|
+
if (bench.check !== undefined && operation._tag !== 'Scenario') {
|
|
107
120
|
const value =
|
|
108
121
|
operation._tag === 'Sync'
|
|
109
122
|
? yield* Effect.try({ try: operation.run, catch: error => error })
|
|
110
|
-
:
|
|
123
|
+
: operation._tag === 'Effect'
|
|
124
|
+
? yield* operation.single ?? operation.run
|
|
125
|
+
: operation._tag === 'PerOp'
|
|
126
|
+
? yield* Effect.flatMap(operation.before, operation.run)
|
|
127
|
+
: yield* Stream.runCount(operation.stream())
|
|
111
128
|
if (!bench.check(value)) {
|
|
112
129
|
return yield* new BenchFailure({ phase: 'check', message: `check rejected the result: ${String(value)}` })
|
|
113
130
|
}
|
|
@@ -117,7 +134,7 @@ const runBench = Effect.fn('effectBench/runBench')(function* (
|
|
|
117
134
|
samples: options.samples ?? defaults.samples,
|
|
118
135
|
minTimeNs: toNs(options.minTime ?? defaults.minTime),
|
|
119
136
|
warmupNs: toNs(options.warmup ?? defaults.warmup),
|
|
120
|
-
memory: options.memory
|
|
137
|
+
memory: options.memory === true || options.budget?.heapDelta !== undefined,
|
|
121
138
|
})
|
|
122
139
|
|
|
123
140
|
const { budget } = options
|
|
@@ -132,18 +149,21 @@ const runBench = Effect.fn('effectBench/runBench')(function* (
|
|
|
132
149
|
batchSize: measurement.batchSize,
|
|
133
150
|
overheadNs: measurement.overheadNs,
|
|
134
151
|
heapDeltaBytes: measurement.heapDeltaBytes,
|
|
135
|
-
|
|
152
|
+
metrics: measurement.metrics,
|
|
153
|
+
limits:
|
|
136
154
|
budget === undefined
|
|
137
155
|
? null
|
|
138
156
|
: {
|
|
139
|
-
|
|
140
|
-
|
|
157
|
+
p50Ns: budget.p50 === undefined ? null : toNs(budget.p50),
|
|
158
|
+
p99Ns: budget.p99 === undefined ? null : toNs(budget.p99),
|
|
159
|
+
heapDeltaBytes: budget.heapDelta === undefined ? null : toBytes(budget.heapDelta),
|
|
141
160
|
},
|
|
142
161
|
} satisfies RawOk
|
|
143
162
|
})
|
|
144
163
|
|
|
145
164
|
const failureOf = (cause: Cause.Cause<unknown>): BenchFailure => {
|
|
146
165
|
const error = Cause.squash(cause)
|
|
166
|
+
if (error instanceof CheckFailed) return new BenchFailure({ phase: 'check', message: error.message })
|
|
147
167
|
return error instanceof BenchFailure ? error : failWith('run')(cause)
|
|
148
168
|
}
|
|
149
169
|
|
|
@@ -177,7 +197,7 @@ const runSuite = Effect.fn('effectBench/runSuite')(function* (
|
|
|
177
197
|
|
|
178
198
|
const results: Array<RawResult> = []
|
|
179
199
|
for (const { bench, planned } of benches) {
|
|
180
|
-
const options = { ...defaults, ...mode, ...suite.options, ...bench.options }
|
|
200
|
+
const options = { ...defaults, ...bench.defaults, ...mode, ...suite.options, ...bench.options }
|
|
181
201
|
const exit = yield* Effect.exit(
|
|
182
202
|
provideSuite(runBench(bench, planned, setup.value.ctx, options), setup.value.context)
|
|
183
203
|
)
|
|
@@ -222,7 +242,9 @@ const merge = (
|
|
|
222
242
|
const stats = compute(forks.flat())
|
|
223
243
|
const heap = oks.flatMap(run => (run.heapDeltaBytes === null ? [] : [run.heapDeltaBytes]))
|
|
224
244
|
const recorded = baseline?.entries.find(entry => entry.id === first.id)
|
|
225
|
-
const { relativeTo, samplesNs,
|
|
245
|
+
const { relativeTo, samplesNs, limits, metrics: _, ...identity } = first
|
|
246
|
+
const metricKeys = [...new Set(oks.flatMap(run => Object.keys(run.metrics ?? {})))]
|
|
247
|
+
const heapDeltaBytes = heap.length === 0 ? null : median(heap)
|
|
226
248
|
|
|
227
249
|
return {
|
|
228
250
|
result: {
|
|
@@ -231,8 +253,12 @@ const merge = (
|
|
|
231
253
|
forks: forks.length,
|
|
232
254
|
batchSize: median(oks.map(run => run.batchSize)),
|
|
233
255
|
overheadNs: median(oks.map(run => run.overheadNs)),
|
|
234
|
-
heapDeltaBytes
|
|
235
|
-
|
|
256
|
+
heapDeltaBytes,
|
|
257
|
+
metrics:
|
|
258
|
+
metricKeys.length === 0
|
|
259
|
+
? null
|
|
260
|
+
: Object.fromEntries(metricKeys.map(key => [key, median(oks.flatMap(run => run.metrics?.[key] ?? []))])),
|
|
261
|
+
budget: checkBudget(limits, stats, heapDeltaBytes),
|
|
236
262
|
comparison:
|
|
237
263
|
recorded === undefined || baseline === undefined
|
|
238
264
|
? null
|
package/src/Services.ts
CHANGED
|
@@ -12,10 +12,22 @@ export const BenchClock = Context.Reference<Clock>('effect-bench/BenchClock', {
|
|
|
12
12
|
export interface Memory {
|
|
13
13
|
readonly gc: (() => void) | undefined
|
|
14
14
|
readonly heapUsed: (() => number) | undefined
|
|
15
|
+
/** Resident set size of the process, for stream peaks. */
|
|
16
|
+
readonly rss: (() => number) | undefined
|
|
15
17
|
}
|
|
16
18
|
|
|
17
19
|
export const BenchMemory = Context.Reference<Memory>('effect-bench/BenchMemory', {
|
|
18
|
-
defaultValue: () => ({ gc: undefined, heapUsed: undefined }),
|
|
20
|
+
defaultValue: () => ({ gc: undefined, heapUsed: undefined, rss: undefined }),
|
|
21
|
+
})
|
|
22
|
+
|
|
23
|
+
export interface PhaseRecorder {
|
|
24
|
+
/** Path of the enclosing phase, empty at the top of a run. */
|
|
25
|
+
readonly path: string
|
|
26
|
+
readonly record: (path: string, ns: number) => void
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
export const BenchPhases = Context.Reference<PhaseRecorder>('effect-bench/BenchPhases', {
|
|
30
|
+
defaultValue: () => ({ path: '', record: () => {} }),
|
|
19
31
|
})
|
|
20
32
|
|
|
21
33
|
export interface EnvironmentProbe {
|
package/src/Stats.ts
CHANGED
|
@@ -50,3 +50,14 @@ export const formatNs = (ns: number): string => {
|
|
|
50
50
|
for (const [scale, unit] of units) if (Math.abs(ns) >= scale) return `${(ns / scale).toPrecision(3)} ${unit}`
|
|
51
51
|
return `${ns.toPrecision(3)} ns`
|
|
52
52
|
}
|
|
53
|
+
|
|
54
|
+
export const formatBytes = (bytes: number): string => {
|
|
55
|
+
const units = ['B', 'KiB', 'MiB', 'GiB'] as const
|
|
56
|
+
let value = Math.abs(bytes)
|
|
57
|
+
let unit = 0
|
|
58
|
+
while (value >= 1024 && unit < units.length - 1) {
|
|
59
|
+
value /= 1024
|
|
60
|
+
unit++
|
|
61
|
+
}
|
|
62
|
+
return `${bytes < 0 ? '-' : ''}${unit === 0 ? value.toFixed(0) : value.toPrecision(3)} ${units[unit]}`
|
|
63
|
+
}
|