ostia 0.1.7 → 0.2.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 +533 -106
- package/chunk-9dqf5cxf.js +8 -0
- package/chunk-h6579788.js +21 -0
- package/cli.js +148 -62
- package/index.d.ts +363 -34
- package/index.js +1 -1
- package/package.json +1 -1
- package/runner.ts +5 -4
- package/chunk-3qbcj1m9.js +0 -21
- package/chunk-zrck2q0b.js +0 -4
package/index.d.ts
CHANGED
|
@@ -1,16 +1,57 @@
|
|
|
1
|
-
|
|
2
|
-
|
|
3
|
-
|
|
1
|
+
export declare function defineConfig(config: Partial<OstiaConfig>): Partial<OstiaConfig>;
|
|
2
|
+
|
|
3
|
+
export interface CommandSpec {
|
|
4
|
+
command: string | string[];
|
|
5
|
+
label?: string;
|
|
6
|
+
/** Overrides `TimeOptions.prepare` for this command. */
|
|
7
|
+
prepare?: PrepareHook;
|
|
8
|
+
/** Overrides `TimeOptions.timeSource` for this command. */
|
|
9
|
+
timeSource?: TimeSource;
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
export interface TimeOptions {
|
|
13
|
+
commands: (string | string[] | CommandSpec)[];
|
|
14
|
+
/** Runs before every trial of every command (warmup and instrumented
|
|
15
|
+
* trials included), unmeasured, in `cwd`/`env`: a command string / argv
|
|
16
|
+
* array spawned and awaited, or a function. hyperfine's `--prepare`. A
|
|
17
|
+
* `CommandSpec.prepare` overrides it per command. */
|
|
18
|
+
prepare?: PrepareHook;
|
|
19
|
+
/** Take every command's timing from a number in its own output instead of
|
|
20
|
+
* its wall clock (e.g. a build tool's `built in 342ms` line, which
|
|
21
|
+
* excludes runtime startup). A `CommandSpec.timeSource` overrides it per
|
|
22
|
+
* command. The parsed value becomes `timing.samples`; each trial keeps
|
|
23
|
+
* `wallNs` too. */
|
|
24
|
+
timeSource?: TimeSource;
|
|
25
|
+
/** Exact trial count. When set, `budgetMs` is ignored. */
|
|
26
|
+
samples?: number;
|
|
27
|
+
/** Wall-clock time budget for the sampling loop, ms (default: a
|
|
28
|
+
* hyperfine-style ~3s min-total-time loop when neither `samples` nor
|
|
29
|
+
* `budgetMs` is given). */
|
|
30
|
+
budgetMs?: number;
|
|
31
|
+
/** Hard floor on trials when no exact `samples` count is given. */
|
|
32
|
+
minSamples?: number;
|
|
4
33
|
warmup?: number;
|
|
34
|
+
/** Round-robin trials across commands (one trial per command, repeated)
|
|
35
|
+
* instead of running each command's whole trial loop to completion before
|
|
36
|
+
* the next command starts. Default: true when 2+ commands are given (a
|
|
37
|
+
* single command has nothing to interleave against). Spreads any drift
|
|
38
|
+
* over the run's wall-clock span (thermal throttling, a noisy neighbor
|
|
39
|
+
* process) evenly across every command instead of favoring whichever ran
|
|
40
|
+
* first or last. */
|
|
41
|
+
interleave?: boolean;
|
|
5
42
|
cwd?: string;
|
|
6
43
|
env?: Record<string, string>;
|
|
7
44
|
cpu?: boolean;
|
|
8
45
|
heap?: boolean;
|
|
9
46
|
cpuIntervalUs?: number;
|
|
10
47
|
outDir?: string;
|
|
48
|
+
/** Measure this machine's noise floor before the first command (default:
|
|
49
|
+
* true) and stamp it on the document as `environment`. Set false to skip
|
|
50
|
+
* the ~200ms reference measurement. */
|
|
51
|
+
noiseCheck?: boolean;
|
|
11
52
|
}
|
|
12
53
|
|
|
13
|
-
export declare function
|
|
54
|
+
export declare function time(opts: TimeOptions): Promise<ProfileDocument>;
|
|
14
55
|
|
|
15
56
|
interface ProfileOptions {
|
|
16
57
|
intervalUs?: number;
|
|
@@ -19,13 +60,76 @@ interface ProfileOptions {
|
|
|
19
60
|
|
|
20
61
|
interface ProfileResult<T> {
|
|
21
62
|
result: T;
|
|
22
|
-
|
|
63
|
+
measurement: Measurement;
|
|
64
|
+
document: ProfileDocument;
|
|
23
65
|
}
|
|
24
66
|
|
|
25
67
|
export declare function profile<T>(fn: () => T | Promise<T>, opts?: ProfileOptions): Promise<ProfileResult<T>>;
|
|
26
68
|
|
|
69
|
+
export interface WorkloadConfig {
|
|
70
|
+
label?: string;
|
|
71
|
+
command?: string[];
|
|
72
|
+
suites?: string[];
|
|
73
|
+
inputs?: string[];
|
|
74
|
+
/** `command` only. Runs before every trial (warmup included), unmeasured:
|
|
75
|
+
* a command string / argv array in both `.ts` and JSON config, or a
|
|
76
|
+
* function in `ostia.config.ts`. A function-form hook makes the workload
|
|
77
|
+
* uncacheable for `ostia ci` (its effect can't be fingerprinted), so it
|
|
78
|
+
* always executes. */
|
|
79
|
+
prepare?: PrepareHook;
|
|
80
|
+
/** `command` only. Take timing from a number in the command's own output
|
|
81
|
+
* instead of its wall clock; see `TimeSource`. */
|
|
82
|
+
timeSource?: TimeSource;
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
interface BenchConfig {
|
|
86
|
+
/** Suite file globs, resolved with Bun.Glob against the config's directory
|
|
87
|
+
* (e.g. "bench/**\/*.bench.ts"). Ignored when suite files are also given
|
|
88
|
+
* on the command line - CLI args replace this list rather than merging
|
|
89
|
+
* with it. */
|
|
90
|
+
suites?: string[];
|
|
91
|
+
preload?: string[];
|
|
92
|
+
jobs?: number | "auto";
|
|
93
|
+
budgetMs?: number;
|
|
94
|
+
samples?: number;
|
|
95
|
+
minSamples?: number;
|
|
96
|
+
gc?: boolean;
|
|
97
|
+
cpu?: boolean;
|
|
98
|
+
alloc?: boolean;
|
|
99
|
+
filter?: string;
|
|
100
|
+
isolate?: boolean;
|
|
101
|
+
outDir?: string;
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
export interface OstiaConfig {
|
|
105
|
+
runs: number | null;
|
|
106
|
+
warmup: number;
|
|
107
|
+
outDir: string;
|
|
108
|
+
baselineDir: string;
|
|
109
|
+
baseline: string;
|
|
110
|
+
cpuIntervalUs: number;
|
|
111
|
+
thresholds: Thresholds;
|
|
112
|
+
workloads: WorkloadConfig[];
|
|
113
|
+
bench?: BenchConfig;
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
interface Thresholds {
|
|
117
|
+
timingPct: number;
|
|
118
|
+
frameSelfPct: number;
|
|
119
|
+
heapTypePct: number;
|
|
120
|
+
minFrameSelfUs: number;
|
|
121
|
+
/** Significance level for the Mann-Whitney p-value: a `regressed` /
|
|
122
|
+
* `improved` verdict also requires `pValue < alpha`. */
|
|
123
|
+
alpha: number;
|
|
124
|
+
/** Bootstrap resample rounds for the timing CI. Capped work regardless:
|
|
125
|
+
* see `bootstrapMedianDiffCi`. */
|
|
126
|
+
bootstrapIterations: number;
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
export declare function compareDocuments(base: ProfileDocument, cand: ProfileDocument, thresholds?: Thresholds): Comparison[];
|
|
130
|
+
|
|
27
131
|
export interface ProfileDocument {
|
|
28
|
-
schemaVersion:
|
|
132
|
+
schemaVersion: 2;
|
|
29
133
|
toolVersion: string;
|
|
30
134
|
bunVersion: string;
|
|
31
135
|
platform: {
|
|
@@ -34,8 +138,41 @@ export interface ProfileDocument {
|
|
|
34
138
|
};
|
|
35
139
|
createdAt: string;
|
|
36
140
|
workloads: Workload[];
|
|
37
|
-
|
|
141
|
+
measurements: Measurement[];
|
|
38
142
|
comparisons?: Comparison[];
|
|
143
|
+
/** Machine conditions when this document was measured. Additive, no
|
|
144
|
+
* schema bump. Absent when `noiseCheck: false` (or `--no-noise-check`)
|
|
145
|
+
* skipped the reference measurement. */
|
|
146
|
+
environment?: Environment;
|
|
147
|
+
/** Repo state when this document was measured, from `git rev-parse` /
|
|
148
|
+
* `git status --porcelain` in the process's cwd. Additive, no schema
|
|
149
|
+
* bump. Absent outside a git repo (or when `git` itself isn't
|
|
150
|
+
* available). Metadata only: never part of any fingerprint or id, so a
|
|
151
|
+
* commit or a dirty working tree never orphans a cached run or baseline. */
|
|
152
|
+
git?: GitMetadata;
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
interface GitMetadata {
|
|
156
|
+
sha: string;
|
|
157
|
+
branch: string;
|
|
158
|
+
dirty: boolean;
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
interface NoiseFloor {
|
|
162
|
+
/** `mad / median` of the reference workload's trial times, as a percent -
|
|
163
|
+
* how noisy this machine is right now, independent of what's being
|
|
164
|
+
* measured. */
|
|
165
|
+
floorPct: number;
|
|
166
|
+
referenceMedianNs: number;
|
|
167
|
+
samples: number;
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
interface Environment {
|
|
171
|
+
cpuModel: string;
|
|
172
|
+
cores: number;
|
|
173
|
+
loadAvg1: number;
|
|
174
|
+
loadAvg5: number;
|
|
175
|
+
noise: NoiseFloor;
|
|
39
176
|
}
|
|
40
177
|
|
|
41
178
|
export interface Workload {
|
|
@@ -44,6 +181,22 @@ export interface Workload {
|
|
|
44
181
|
label?: string;
|
|
45
182
|
command?: string[];
|
|
46
183
|
shell?: string;
|
|
184
|
+
/** Command form of the `prepare` hook that ran before every trial of this
|
|
185
|
+
* command (`ostia time --prepare`, `time({ prepare })`, config `prepare`).
|
|
186
|
+
* Part of the workload id: the same command with and without a prepare
|
|
187
|
+
* step measures different things. A function-form hook isn't
|
|
188
|
+
* serializable and is omitted here (its source text is still hashed into
|
|
189
|
+
* the id). */
|
|
190
|
+
prepare?: string[];
|
|
191
|
+
/** Where this command's timing samples came from when not the subprocess
|
|
192
|
+
* wall clock: a regex over the command's own output and the unit of the
|
|
193
|
+
* number it captures. Part of the workload id, so the same command timed
|
|
194
|
+
* by wall clock and by its own report are two workloads. */
|
|
195
|
+
timeSource?: {
|
|
196
|
+
pattern: string;
|
|
197
|
+
group?: number;
|
|
198
|
+
unit?: "ns" | "us" | "ms" | "s";
|
|
199
|
+
};
|
|
47
200
|
/** `task` is the "group/name" id the bench registry assigns; `group` is the
|
|
48
201
|
* enclosing `group()` name when there is one. Renderers prefer `group` over
|
|
49
202
|
* splitting `task` on "/", so task names may contain slashes. */
|
|
@@ -67,11 +220,23 @@ export interface Workload {
|
|
|
67
220
|
* on the task, its group, or the suite), vs. sharing its suite file's
|
|
68
221
|
* subprocess with other tasks. */
|
|
69
222
|
isolated?: boolean;
|
|
223
|
+
/** Structured parameters this task point represents (e.g. `{ size: 800,
|
|
224
|
+
* impl: "fast" }`), from `task(name, fn, { params })` or a `sweep()` point.
|
|
225
|
+
* Lets renderers pivot and `compare` match on them instead of only on the
|
|
226
|
+
* task name. Part of the workload id when present, so two points that
|
|
227
|
+
* share a task name (a `sweep()`'s whole point) don't collide. */
|
|
228
|
+
params?: Record<string, string | number | boolean>;
|
|
229
|
+
/** From `task.skip()` or a `group.skip()` this task was inside. The runner
|
|
230
|
+
* never measures it, so this workload has no matching `Measurement`; a
|
|
231
|
+
* renderer prints it as a "- skipped" row instead of omitting it, and
|
|
232
|
+
* `compare` treats it as `unchanged` (with a `skipped` warning) rather
|
|
233
|
+
* than silently passing or failing to match it. */
|
|
234
|
+
skipped?: boolean;
|
|
70
235
|
}
|
|
71
236
|
|
|
72
237
|
type Phase = "timing" | "cpu" | "heap" | "memstats";
|
|
73
238
|
|
|
74
|
-
interface
|
|
239
|
+
interface Measurement {
|
|
75
240
|
id: string;
|
|
76
241
|
workloadId: string;
|
|
77
242
|
phase: Phase;
|
|
@@ -86,12 +251,23 @@ interface Run {
|
|
|
86
251
|
jit?: JitTierBreakdown;
|
|
87
252
|
warnings: Warning[];
|
|
88
253
|
artifacts: ArtifactRef[];
|
|
89
|
-
|
|
254
|
+
baselineMeasurementId?: string;
|
|
255
|
+
/** True when this timing measurement's trials were run round-robin against
|
|
256
|
+
* the other commands in the same `time()` call (`--interleave`, default on
|
|
257
|
+
* for 2+ commands) rather than run to completion before the next command
|
|
258
|
+
* started, so drift over the run's wall-clock span (thermal throttling, a
|
|
259
|
+
* noisy neighbor process) lands on every command equally instead of
|
|
260
|
+
* favoring whichever ran first or last. */
|
|
261
|
+
interleaved?: boolean;
|
|
90
262
|
}
|
|
91
263
|
|
|
92
264
|
interface Trial {
|
|
93
265
|
i: number;
|
|
94
266
|
wallNs: number;
|
|
267
|
+
/** The command's self-reported time (ns) when the workload has a
|
|
268
|
+
* `timeSource`; `timing.samples` are these, not `wallNs`, in that case.
|
|
269
|
+
* `wallNs` stays alongside so a document keeps both. */
|
|
270
|
+
reportedNs?: number;
|
|
95
271
|
exitCode?: number;
|
|
96
272
|
userNs?: number;
|
|
97
273
|
systemNs?: number;
|
|
@@ -110,6 +286,15 @@ interface TimingStats {
|
|
|
110
286
|
mild: number;
|
|
111
287
|
severe: number;
|
|
112
288
|
};
|
|
289
|
+
/** 75th percentile, ns. Optional: absent on documents saved before this
|
|
290
|
+
* field existed (`loadDocument` never backfills it). */
|
|
291
|
+
p75?: number;
|
|
292
|
+
/** 99th percentile, ns. Same caveat as `p75`. */
|
|
293
|
+
p99?: number;
|
|
294
|
+
/** Median absolute deviation, ns: the median of `|sample - median|` across
|
|
295
|
+
* all samples. A robust spread measure that (unlike stddev) isn't skewed
|
|
296
|
+
* by the long right tail typical of wall-clock timings. */
|
|
297
|
+
mad?: number;
|
|
113
298
|
}
|
|
114
299
|
|
|
115
300
|
interface Frame {
|
|
@@ -166,6 +351,11 @@ interface MemoryEvidence {
|
|
|
166
351
|
maxRssBytes?: number;
|
|
167
352
|
peakCommitBytes?: number;
|
|
168
353
|
pageFaults?: number;
|
|
354
|
+
/** Bytes allocated per call, from `ostia bench --alloc`: heap size delta
|
|
355
|
+
* (`bun:jsc`'s `heapStats().heapSize`, falling back to
|
|
356
|
+
* `process.memoryUsage().heapUsed`) around one `Bun.gc(true)`-bracketed
|
|
357
|
+
* batch, divided by the batch size. */
|
|
358
|
+
bytesPerOp?: number;
|
|
169
359
|
}
|
|
170
360
|
|
|
171
361
|
interface JitTierBreakdown {
|
|
@@ -183,7 +373,7 @@ interface JitTierBreakdown {
|
|
|
183
373
|
}[];
|
|
184
374
|
}
|
|
185
375
|
|
|
186
|
-
export type WarningCode = "slow-first-run" | "outliers-detected" | "fast-command" | "nonzero-exit" | "instrumented-timing" | "artifact-missing" | "empty-profile" | "below-timer-resolution" | "cache-fallback-rerun" | "low-sample-count";
|
|
376
|
+
export type WarningCode = "slow-first-run" | "outliers-detected" | "fast-command" | "nonzero-exit" | "instrumented-timing" | "artifact-missing" | "empty-profile" | "below-timer-resolution" | "cache-fallback-rerun" | "low-sample-count" | "thin-comparison" | "noisy-machine" | "skipped" | "jit-cold";
|
|
187
377
|
|
|
188
378
|
export interface Warning {
|
|
189
379
|
code: WarningCode;
|
|
@@ -201,13 +391,28 @@ interface ArtifactRef {
|
|
|
201
391
|
|
|
202
392
|
interface Comparison {
|
|
203
393
|
id: string;
|
|
204
|
-
|
|
205
|
-
|
|
394
|
+
baselineMeasurementId: string;
|
|
395
|
+
candidateMeasurementId: string;
|
|
206
396
|
timing?: {
|
|
207
397
|
medianDeltaPct: number;
|
|
208
398
|
meanDeltaPct: number;
|
|
399
|
+
/** Same value as `medianDeltaPct`, named for what it is used for: the
|
|
400
|
+
* effect size the verdict rule tests against `thresholds.timingPct`. */
|
|
401
|
+
effectPct: number;
|
|
402
|
+
/** 95% bootstrap confidence interval on the difference of medians,
|
|
403
|
+
* percent of the baseline median. Absent when either side had fewer
|
|
404
|
+
* than 5 samples (see the `thin-comparison` warning). */
|
|
405
|
+
ci95?: [number, number];
|
|
406
|
+
/** Two-sided Mann-Whitney U p-value, tie-corrected normal approximation.
|
|
407
|
+
* Same absence condition as `ci95`. */
|
|
408
|
+
pValue?: number;
|
|
409
|
+
/** Seed for the bootstrap's PRNG, so `ci95` is reproducible. */
|
|
410
|
+
seed?: number;
|
|
209
411
|
verdict: "improved" | "regressed" | "unchanged";
|
|
210
412
|
};
|
|
413
|
+
/** Attached when timing fell back to the point-estimate rule (thin
|
|
414
|
+
* samples) or otherwise carries a caveat about this comparison. */
|
|
415
|
+
warnings?: Warning[];
|
|
211
416
|
frames?: {
|
|
212
417
|
frameKey: string;
|
|
213
418
|
name: string;
|
|
@@ -228,15 +433,54 @@ interface Comparison {
|
|
|
228
433
|
frameSelfPct: number;
|
|
229
434
|
heapTypePct: number;
|
|
230
435
|
minFrameSelfUs: number;
|
|
436
|
+
alpha: number;
|
|
437
|
+
bootstrapIterations: number;
|
|
438
|
+
/** `max(timingPct, base.environment.noise.floorPct,
|
|
439
|
+
* cand.environment.noise.floorPct)` - the threshold timing was actually
|
|
440
|
+
* tested against, once machine noise widens it past `timingPct`. */
|
|
441
|
+
effectiveTimingPct: number;
|
|
231
442
|
};
|
|
232
443
|
verdict: "pass" | "fail";
|
|
233
444
|
}
|
|
234
445
|
|
|
446
|
+
export interface PrepareRun {
|
|
447
|
+
phase: "warmup" | "timing" | "cpu" | "heap";
|
|
448
|
+
index: number;
|
|
449
|
+
}
|
|
450
|
+
|
|
451
|
+
export type PrepareFn = (run: PrepareRun) => unknown;
|
|
452
|
+
|
|
453
|
+
export type PrepareHook = string | string[] | PrepareFn;
|
|
454
|
+
|
|
455
|
+
export type TimeUnit = "ns" | "us" | "ms" | "s";
|
|
456
|
+
|
|
457
|
+
export interface TimeSource {
|
|
458
|
+
pattern: string | RegExp;
|
|
459
|
+
group?: number;
|
|
460
|
+
unit?: TimeUnit;
|
|
461
|
+
}
|
|
462
|
+
|
|
463
|
+
export declare function keep(value: unknown): void;
|
|
464
|
+
|
|
235
465
|
interface BenchOptions {
|
|
236
466
|
suites: string[];
|
|
237
|
-
|
|
467
|
+
/** Wall-clock sampling budget per task, ms (default 500). */
|
|
468
|
+
budgetMs?: number;
|
|
469
|
+
/** Exact trial count per task. When set, the budget is ignored - the
|
|
470
|
+
* in-process equivalent of `time()`'s `samples`. */
|
|
471
|
+
samples?: number;
|
|
238
472
|
minSamples?: number;
|
|
239
473
|
gc?: boolean;
|
|
474
|
+
/** Capture one extra `phase: "cpu"` measurement per task (200ms of the
|
|
475
|
+
* task looped under the JSC sampling profiler, JIT tiers included), never
|
|
476
|
+
* mixed into the timing numbers. `TaskOptions.cpu` / `GroupOptions.cpu`
|
|
477
|
+
* override this per task or group. */
|
|
478
|
+
cpu?: boolean;
|
|
479
|
+
/** Capture one extra `phase: "memstats"` measurement per task: bytes
|
|
480
|
+
* allocated per call, from a `Bun.gc(true)`-bracketed batch.
|
|
481
|
+
* `TaskOptions.alloc` / `GroupOptions.alloc` override this per task or
|
|
482
|
+
* group. */
|
|
483
|
+
alloc?: boolean;
|
|
240
484
|
filter?: string;
|
|
241
485
|
/** Suite files to run at once, each still in its own child process (default:
|
|
242
486
|
* 1). Files are independent by design, so this is a wall-clock win for
|
|
@@ -270,27 +514,27 @@ interface BenchOptions {
|
|
|
270
514
|
* resolution condition Bun doesn't set by default (e.g. Svelte/Vue's
|
|
271
515
|
* `browser` vs `default` builds). */
|
|
272
516
|
bunFlags?: string[];
|
|
517
|
+
/** Measure this machine's noise floor before the first task per suite
|
|
518
|
+
* subprocess (default: true) and stamp it on the document as
|
|
519
|
+
* `environment`. Set false to skip the ~200ms reference measurement. */
|
|
520
|
+
noiseCheck?: boolean;
|
|
273
521
|
}
|
|
274
522
|
|
|
275
523
|
export declare function bench(opts: BenchOptions): Promise<ProfileDocument>;
|
|
276
524
|
|
|
277
|
-
interface Thresholds {
|
|
278
|
-
timingPct: number;
|
|
279
|
-
frameSelfPct: number;
|
|
280
|
-
heapTypePct: number;
|
|
281
|
-
minFrameSelfUs: number;
|
|
282
|
-
}
|
|
283
|
-
|
|
284
|
-
export declare function compareDocuments(base: ProfileDocument, cand: ProfileDocument, thresholds?: Thresholds): Comparison[];
|
|
285
|
-
|
|
286
525
|
export declare function range(start: number, end: number, multiplier?: number): number[];
|
|
287
526
|
|
|
527
|
+
type Hook = () => unknown | Promise<unknown>;
|
|
528
|
+
|
|
288
529
|
export interface TaskOptions {
|
|
289
530
|
/** Marks this task as the Relative reference for its group in the table
|
|
290
531
|
* renderer, mirroring mitata's `baseline()`. At most one per group. */
|
|
291
532
|
baseline?: boolean;
|
|
292
|
-
/** Per-task time budget; overrides the suite-wide `--
|
|
293
|
-
|
|
533
|
+
/** Per-task time budget; overrides the suite-wide `--budget` / `budgetMs`. */
|
|
534
|
+
budgetMs?: number;
|
|
535
|
+
/** Per-task exact trial count; overrides the suite-wide `--samples` /
|
|
536
|
+
* `samples`. When set, the budget is ignored for this task. */
|
|
537
|
+
samples?: number;
|
|
294
538
|
/** Per-task hard floor on trials; overrides the suite-wide `--min-samples` /
|
|
295
539
|
* `minSamples`. */
|
|
296
540
|
minSamples?: number;
|
|
@@ -305,6 +549,30 @@ export interface TaskOptions {
|
|
|
305
549
|
/** Overrides the suite-wide `bench({ gc })` / `--gc` (and any
|
|
306
550
|
* `GroupOptions.gc`) for this task only. */
|
|
307
551
|
gc?: boolean;
|
|
552
|
+
/** Overrides the suite-wide `bench({ cpu })` / `--cpu` (and any
|
|
553
|
+
* `GroupOptions.cpu`) for this task only: after the timing measurement,
|
|
554
|
+
* capture one extra `phase: "cpu"` measurement (JIT tiers included) on
|
|
555
|
+
* the same workload, never mixed into the timing numbers. */
|
|
556
|
+
cpu?: boolean;
|
|
557
|
+
/** Overrides the suite-wide `bench({ alloc })` / `--alloc` (and any
|
|
558
|
+
* `GroupOptions.alloc`) for this task only: after the timing measurement,
|
|
559
|
+
* capture one extra `phase: "memstats"` measurement with bytes
|
|
560
|
+
* allocated per call. */
|
|
561
|
+
alloc?: boolean;
|
|
562
|
+
/** Structured parameters this task represents (e.g. `{ size: 800, impl:
|
|
563
|
+
* "fast" }`), written to `Workload.params` and folded into the workload id
|
|
564
|
+
* so points with the same task name don't collide. Inside `sweep()`, the
|
|
565
|
+
* current point is inherited automatically; an explicit `params` here
|
|
566
|
+
* merges over it (explicit keys win). */
|
|
567
|
+
params?: Record<string, string | number | boolean>;
|
|
568
|
+
/** Runs once, unmeasured, immediately before this task's warmup - in the
|
|
569
|
+
* task's own process, so it works with `isolate`. No per-trial hook: that
|
|
570
|
+
* would defeat batching. Use `gc` (Bun.gc between trials) or `isolate`
|
|
571
|
+
* (a fresh process per task) for per-trial concerns instead. */
|
|
572
|
+
before?: Hook;
|
|
573
|
+
/** Runs once, unmeasured, immediately after this task's last trial. Same
|
|
574
|
+
* process/no-per-trial caveats as `before`. */
|
|
575
|
+
after?: Hook;
|
|
308
576
|
}
|
|
309
577
|
|
|
310
578
|
export interface GroupOptions {
|
|
@@ -317,11 +585,57 @@ export interface GroupOptions {
|
|
|
317
585
|
/** Default `gc` for every task in this group, unless a task overrides it
|
|
318
586
|
* with its own `TaskOptions.gc`. */
|
|
319
587
|
gc?: boolean;
|
|
588
|
+
/** Default `cpu` for every task in this group, unless a task overrides it
|
|
589
|
+
* with its own `TaskOptions.cpu`. */
|
|
590
|
+
cpu?: boolean;
|
|
591
|
+
/** Default `alloc` for every task in this group, unless a task overrides
|
|
592
|
+
* it with its own `TaskOptions.alloc`. */
|
|
593
|
+
alloc?: boolean;
|
|
594
|
+
/** Runs once, unmeasured, before the group's first task's warmup (not
|
|
595
|
+
* before every task) - in whichever process runs that task, so it works
|
|
596
|
+
* with `isolate`. */
|
|
597
|
+
before?: Hook;
|
|
598
|
+
/** Runs once, unmeasured, after the group's last task's last trial. */
|
|
599
|
+
after?: Hook;
|
|
320
600
|
}
|
|
321
601
|
|
|
322
|
-
|
|
602
|
+
interface GroupFn {
|
|
603
|
+
(name: string, fn: () => void, opts?: GroupOptions): void;
|
|
604
|
+
/** Registers every task inside as skipped: the runner never measures
|
|
605
|
+
* them, but the document still carries their workloads (marked
|
|
606
|
+
* `Workload.skipped`). */
|
|
607
|
+
skip: (name: string, fn: () => void, opts?: GroupOptions) => void;
|
|
608
|
+
/** When any task or group in the suite uses `.only`, the runner restricts
|
|
609
|
+
* the whole suite file to only those tasks (before `--filter` narrows
|
|
610
|
+
* further) and prints a one-line notice to stderr. */
|
|
611
|
+
only: (name: string, fn: () => void, opts?: GroupOptions) => void;
|
|
612
|
+
}
|
|
613
|
+
|
|
614
|
+
export declare const group: GroupFn;
|
|
615
|
+
|
|
616
|
+
interface TaskFn {
|
|
617
|
+
(name: string, fn: () => unknown | Promise<unknown>, opts?: TaskOptions): void;
|
|
618
|
+
/** Registers the task as skipped: the runner never measures it, but the
|
|
619
|
+
* document still carries its workload (marked `Workload.skipped`) so a
|
|
620
|
+
* renderer or `compare` can say so explicitly instead of the task simply
|
|
621
|
+
* being absent. */
|
|
622
|
+
skip: (name: string, fn: () => unknown | Promise<unknown>, opts?: TaskOptions) => void;
|
|
623
|
+
/** When any task or group in the suite uses `.only`, the runner restricts
|
|
624
|
+
* the whole suite file to only those tasks (before `--filter` narrows
|
|
625
|
+
* further) and prints a one-line notice to stderr. */
|
|
626
|
+
only: (name: string, fn: () => unknown | Promise<unknown>, opts?: TaskOptions) => void;
|
|
627
|
+
}
|
|
628
|
+
|
|
629
|
+
export declare const task: TaskFn;
|
|
630
|
+
|
|
631
|
+
type SweepPoint<T extends Record<string, readonly unknown[]>> = {
|
|
632
|
+
[K in keyof T]: T[K][number];
|
|
633
|
+
};
|
|
323
634
|
|
|
324
|
-
export declare function
|
|
635
|
+
export declare function sweep<T extends Record<string, readonly unknown[]>>(dims: T, fn: (point: SweepPoint<T>) => void): void;
|
|
636
|
+
|
|
637
|
+
export declare function newDocument(workloads: Workload[], measurements: Measurement[], environment?: Environment): ProfileDocument;
|
|
638
|
+
export { newDocument as createDocument };
|
|
325
639
|
|
|
326
640
|
export declare function saveDocument(doc: ProfileDocument, path: string): Promise<void>;
|
|
327
641
|
|
|
@@ -349,14 +663,24 @@ export interface MinimalLine {
|
|
|
349
663
|
group?: string;
|
|
350
664
|
description?: string;
|
|
351
665
|
groupDescription?: string;
|
|
352
|
-
|
|
353
|
-
|
|
354
|
-
|
|
355
|
-
|
|
356
|
-
|
|
357
|
-
|
|
358
|
-
|
|
359
|
-
|
|
666
|
+
/** From `task(name, fn, { params })` or a `sweep()` point. */
|
|
667
|
+
params?: Record<string, string | number | boolean>;
|
|
668
|
+
/** From `task.skip()` / `group.skip()`: no measurement was taken, so every
|
|
669
|
+
* stats field below is absent on this line. */
|
|
670
|
+
skipped?: true;
|
|
671
|
+
unit?: "ns";
|
|
672
|
+
samples?: number;
|
|
673
|
+
mean?: number;
|
|
674
|
+
median?: number;
|
|
675
|
+
stddev?: number;
|
|
676
|
+
stddevPct?: number;
|
|
677
|
+
min?: number;
|
|
678
|
+
max?: number;
|
|
679
|
+
/** 75th/99th percentile and median absolute deviation, ns. Absent on
|
|
680
|
+
* documents saved before these fields existed. */
|
|
681
|
+
p75?: number;
|
|
682
|
+
p99?: number;
|
|
683
|
+
mad?: number;
|
|
360
684
|
/** Median over the group's reference median (its baseline task, else its
|
|
361
685
|
* fastest). Only present when the document has more than one timing run. */
|
|
362
686
|
relative?: number;
|
|
@@ -372,5 +696,10 @@ export interface MinimalLine {
|
|
|
372
696
|
meanPct: number;
|
|
373
697
|
verdict: "improved" | "regressed" | "unchanged";
|
|
374
698
|
pass: boolean;
|
|
699
|
+
/** 95% bootstrap CI on the difference of medians and the Mann-Whitney
|
|
700
|
+
* p-value behind the verdict. Absent on a thin (<5 samples/side)
|
|
701
|
+
* comparison, which falls back to a point-estimate threshold. */
|
|
702
|
+
ci95?: [number, number];
|
|
703
|
+
pValue?: number;
|
|
375
704
|
};
|
|
376
705
|
}
|
package/index.js
CHANGED
|
@@ -1,2 +1,2 @@
|
|
|
1
1
|
// @bun
|
|
2
|
-
import{
|
|
2
|
+
import{c,b,J,V,r,G,C,z}from"./chunk-h6579788.js";import{n,s,t,T,q,K}from"./chunk-9dqf5cxf.js";export{c as bench,b as compareDocuments,n as createDocument,G as defineConfig,q as group,T as keep,t as loadDocument,z as profile,J as range,r as renderers,s as saveDocument,V as sweep,K as task,C as time};
|
package/package.json
CHANGED
package/runner.ts
CHANGED
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
#!/usr/bin/env bun
|
|
2
2
|
// @bun
|
|
3
|
-
import{
|
|
4
|
-
`),2;let
|
|
5
|
-
`),2;let
|
|
6
|
-
`),
|
|
3
|
+
import{n,O,i,a,o,s,d,f,g,U,B,A,h,I,j,_,L,H}from"./chunk-9dqf5cxf.js";import{heapStats as X}from"bun:jsc";var Y=100;function z(){try{return X().heapSize}catch{return process.memoryUsage().heapUsed}}async function Z(l,m=Y){let w=Bun.nanoseconds();Bun.gc(!0);let t=z();for(let u=0;u<m;u++){let p=l();if(p instanceof Promise)await p}Bun.gc(!0);let k=z(),c=Bun.nanoseconds()-w;return{memory:{origin:"heapStats",bytesPerOp:Math.max(0,(k-t)/m)},diagnosticWallNs:c}}var ee=200,te=20;async function q(l,m=ee){let w=m*1e6,t=async()=>{let p=Bun.nanoseconds();while(Bun.nanoseconds()-p<w){let b=l();if(b instanceof Promise)await b}},{cpu:k,jit:c,diagnosticWallNs:u}=await d(t);return{cpu:k,jit:c,diagnosticWallNs:u}}function K(l){let{llint:m,baseline:w,dfg:t,ftl:k}=l.tiers,c=m+w+t+k;if(c===0)return;let u=m/c*100,p=w/c*100,b=t/c*100,S=k/c*100;if(u+p<=te)return;return{code:"jit-cold",message:`${(u+p).toFixed(1)}% of CPU samples were in the llint/baseline tiers: the JIT never warmed this task up.`,data:{llintPct:u,baselinePct:p,dfgPct:b,ftlPct:S}}}async function ne(){let[l,m,w]=process.argv.slice(2);if(!l||!m)return process.stderr.write(`bench runner: usage: runner.ts <suiteFile> <outputPath> [optsJson]
|
|
4
|
+
`),2;let t=w?JSON.parse(w):{};for(let r of t.preload??[])await import(r);A(),await import(l);let k=B();if(k.length===0)return process.stderr.write(`bench runner: ${l} registered no tasks (no task() calls found).
|
|
5
|
+
`),2;let c=k.filter((r)=>r.only),u=c.length>0?c:k;if(c.length>0)process.stderr.write(`bench: ${c.length} task(s) selected by .only
|
|
6
|
+
`);let p=H(u,t.filter);if(t.taskIds){let r=new Set(t.taskIds);p=p.filter((e)=>r.has(h(e)))}if(p.length===0)return process.stderr.write(`bench runner: --filter ${JSON.stringify(t.filter)} matched zero of ${u.length} registered task(s) in ${l}.
|
|
7
|
+
`),2;if(t.planPath){let r=p.map((e)=>({id:h(e),isolate:I(e,t.isolate??!1)}));await Bun.write(t.planPath,JSON.stringify({tasks:r}))}let b=t.taskIds?p:p.filter((r)=>!I(r,t.isolate??!1)),M=b.some((r)=>!r.skipped)&&t.noiseCheck!==!1?f():void 0,v=M?g(M):void 0,W=new Map,x=new Map;b.forEach((r,e)=>{if(r.groupName===void 0||r.skipped)return;if(!W.has(r.groupName))W.set(r.groupName,e);x.set(r.groupName,e)});let F=[],P=[];for(let r=0;r<b.length;r++){let e=b[r],R=h(e),T=O(l,R,{label:R,baseline:e.baseline,group:e.groupName,description:e.opts?.description,groupDescription:e.groupDescription,isolated:t.markIsolated,params:e.params,skipped:e.skipped});if(F.push(T),e.skipped)continue;let Q=e.groupName!==void 0&&W.get(e.groupName)===r,V=e.groupName!==void 0&&x.get(e.groupName)===r;if(Q&&e.groupBefore)await e.groupBefore();if(e.opts?.before)await e.opts.before();let E=e.opts?.budgetMs??t.budgetMs,J=e.opts?.samples??t.samples,D=e.opts?.minSamples??t.minSamples,N={...t,...E!==void 0&&{budgetMs:E},...J!==void 0&&{samples:J},...D!==void 0&&{minSamples:D},gc:j(e,t.gc??!1)},C=await U(e.fn,N);if(P.push(i({workload:T,configFingerprint:o({budgetMs:N.budgetMs??null,samples:N.samples??null,minSamples:N.minSamples??null,gc:N.gc??!1}),trials:C.trials,timing:C.timing,warnings:v&&P.length===0?[...C.warnings,v]:C.warnings})),_(e,t.cpu??!1)){let y=await q(e.fn),G=K(y.jit);P.push(a({workload:T,phase:"cpu",configFingerprint:o({cpu:!0}),diagnosticWallNs:y.diagnosticWallNs,cpu:y.cpu,jit:y.jit,warnings:G?[G]:[],artifacts:[]}))}if(L(e,t.alloc??!1)){let y=await Z(e.fn);P.push(a({workload:T,phase:"memstats",configFingerprint:o({alloc:!0}),diagnosticWallNs:y.diagnosticWallNs,memory:y.memory,warnings:[],artifacts:[]}))}if(e.opts?.after)await e.opts.after();if(V&&e.groupAfter)await e.groupAfter()}return await s(n(F,P,M),m),0}ne().then((l)=>process.exit(l));
|