@hona/strobe-bench 0.0.0-stage → 0.1.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 +37 -2
- package/dist/index.d.ts +290 -0
- package/dist/index.js +31 -0
- package/package.json +27 -4
package/README.md
CHANGED
|
@@ -1,3 +1,38 @@
|
|
|
1
|
-
#
|
|
1
|
+
# @hona/strobe-bench
|
|
2
2
|
|
|
3
|
-
|
|
3
|
+
Types and helpers for [strobe](https://github.com/Hona/strobe) bench files.
|
|
4
|
+
|
|
5
|
+
strobe runs a bench file with its own copy of this package, so a bench file runs without installing
|
|
6
|
+
anything. Install the package for your editor and `tsc`: the types catch a renamed option before a
|
|
7
|
+
long run.
|
|
8
|
+
|
|
9
|
+
```sh
|
|
10
|
+
bun add -d @hona/strobe-bench
|
|
11
|
+
```
|
|
12
|
+
|
|
13
|
+
```ts
|
|
14
|
+
import { bench, web } from "@hona/strobe-bench"
|
|
15
|
+
|
|
16
|
+
export default bench({
|
|
17
|
+
targets: { app: web({ url: "/" }) },
|
|
18
|
+
cases: {
|
|
19
|
+
"open settings": {
|
|
20
|
+
primary: "main.work",
|
|
21
|
+
run: (ctx) => ctx.page.click("[data-action=settings]", { until: "[role=dialog]" }),
|
|
22
|
+
reset: (ctx) => ctx.page.press("Escape", { until: "idle" }),
|
|
23
|
+
},
|
|
24
|
+
},
|
|
25
|
+
})
|
|
26
|
+
```
|
|
27
|
+
|
|
28
|
+
```sh
|
|
29
|
+
strobe bench perf/settings.bench.ts --check # each case once: does it work?
|
|
30
|
+
strobe bench perf/settings.bench.ts --base git:main # is this branch faster?
|
|
31
|
+
```
|
|
32
|
+
|
|
33
|
+
- **Targets:** `web()`, `url()`, `app()`, `exe()`, `command()` (a dev command that launches an app)
|
|
34
|
+
and `script()` (a Bun or Node module whose exports cases call).
|
|
35
|
+
- **Cases:** `run` measures work inside an open target. `launch` measures the launch itself, from
|
|
36
|
+
process spawn to a selector, with milestones on the way.
|
|
37
|
+
|
|
38
|
+
The package version matches the strobe version whose SDK it describes.
|
package/dist/index.d.ts
ADDED
|
@@ -0,0 +1,290 @@
|
|
|
1
|
+
/** A visible element: a selector, or one whose text contains `text` (or is exactly it, trimmed). */
|
|
2
|
+
export type Selector = string | {
|
|
3
|
+
selector: string;
|
|
4
|
+
text?: string;
|
|
5
|
+
exact?: boolean;
|
|
6
|
+
};
|
|
7
|
+
/** When an input call resolves: `"idle"` (no DOM change or long frame for 300 ms), `"frame"` (after next paint), or a visible element. */
|
|
8
|
+
export type Until = "idle" | "frame" | Selector;
|
|
9
|
+
export interface Options {
|
|
10
|
+
until?: Until;
|
|
11
|
+
/** Milliseconds before the call fails the block. Default 30000. */
|
|
12
|
+
timeout?: number;
|
|
13
|
+
}
|
|
14
|
+
export interface Page {
|
|
15
|
+
/** Opens `url`; a relative path resolves against the current page. */
|
|
16
|
+
goto(url: string, opts?: Options): Promise<void>;
|
|
17
|
+
click(selector: string, opts?: Options): Promise<void>;
|
|
18
|
+
/** Moves the pointer onto `selector` without a click: reveals hover-only controls, or parks the pointer. */
|
|
19
|
+
hover(selector: string, opts?: Options): Promise<void>;
|
|
20
|
+
/** Moves the pointer to a point in the viewport (CSS pixels), such as away from anything that reacts to hover. */
|
|
21
|
+
move(x: number, y: number, opts?: Options): Promise<void>;
|
|
22
|
+
/** Focuses `selector` (clicking it if focus is elsewhere), then sends one key event per character. */
|
|
23
|
+
type(selector: string, text: string, opts?: Options & {
|
|
24
|
+
delay?: number;
|
|
25
|
+
}): Promise<void>;
|
|
26
|
+
/** A key or chord: `"Enter"`, `"Control+a"`, `"ControlOrMeta+Shift+P"`. */
|
|
27
|
+
press(key: string, opts?: Options): Promise<void>;
|
|
28
|
+
scroll(selector: string, delta: {
|
|
29
|
+
by?: number;
|
|
30
|
+
byX?: number;
|
|
31
|
+
speed?: number;
|
|
32
|
+
}, opts?: Options): Promise<void>;
|
|
33
|
+
/** Resolves when a visible element matches `selector` (holding `text`, if given). Re-checked on every DOM and focus change, so `:focus` works. */
|
|
34
|
+
waitFor(selector: string, opts?: {
|
|
35
|
+
text?: string;
|
|
36
|
+
exact?: boolean;
|
|
37
|
+
timeout?: number;
|
|
38
|
+
}): Promise<void>;
|
|
39
|
+
/** Resolves when nothing in the DOM changed and no frame ran long for 300 ms. */
|
|
40
|
+
idle(opts?: {
|
|
41
|
+
timeout?: number;
|
|
42
|
+
}): Promise<void>;
|
|
43
|
+
wait(ms: number): Promise<void>;
|
|
44
|
+
/** Runs `fn` in the page. Only in `prepare`, `setup` and `reset`: it costs main-thread time. */
|
|
45
|
+
eval<A extends unknown[], T>(fn: (...args: A) => T, ...args: A): Promise<Awaited<T>>;
|
|
46
|
+
}
|
|
47
|
+
export interface Context<P> {
|
|
48
|
+
page: Page;
|
|
49
|
+
/** The case this block runs: a block always belongs to one case. */
|
|
50
|
+
case: string;
|
|
51
|
+
/** Marks a named span inside a run; every metric can be sliced by it. */
|
|
52
|
+
phase<T>(name: string, fn: () => T | Promise<T>): Promise<T>;
|
|
53
|
+
/** Adds `n` (default 1) to the `count.<name>` metric of the current run. Outside a run it counts nothing. */
|
|
54
|
+
count(name: string, n?: number): void;
|
|
55
|
+
/** Calls an export of a `script()` target's module in its process and returns what it returned (JSON). */
|
|
56
|
+
call<T = unknown>(name: string, ...args: unknown[]): Promise<T>;
|
|
57
|
+
params: P;
|
|
58
|
+
target: string;
|
|
59
|
+
variant: "base" | "head";
|
|
60
|
+
/** The URL this block opened (empty for app, command and script targets). */
|
|
61
|
+
url: string;
|
|
62
|
+
/** The variant's folder: a git ref's worktree, or the directory it names. */
|
|
63
|
+
root: string;
|
|
64
|
+
/** What the target's `prepare` returned for this block. */
|
|
65
|
+
prepared: any;
|
|
66
|
+
}
|
|
67
|
+
/** A request a `mock` handler answers. */
|
|
68
|
+
export interface MockRequest {
|
|
69
|
+
url: string;
|
|
70
|
+
method: string;
|
|
71
|
+
headers: Record<string, string>;
|
|
72
|
+
/** The request body as text, if it had one. */
|
|
73
|
+
body?: string;
|
|
74
|
+
json<T = any>(): T;
|
|
75
|
+
}
|
|
76
|
+
/** A mocked response: `body` as text, bytes, or any other value as JSON. */
|
|
77
|
+
export interface MockResponse {
|
|
78
|
+
status?: number;
|
|
79
|
+
headers?: Record<string, string>;
|
|
80
|
+
body?: string | Uint8Array | unknown;
|
|
81
|
+
}
|
|
82
|
+
/** Answers a request, or returns `undefined` to let it through to the network. */
|
|
83
|
+
export type MockHandler = (request: MockRequest) => MockResponse | undefined | Promise<MockResponse | undefined>;
|
|
84
|
+
export interface PrepareContext<P> {
|
|
85
|
+
/** The case this block runs, so one file can give each case its own fixture. */
|
|
86
|
+
case: string;
|
|
87
|
+
params: P;
|
|
88
|
+
target: string;
|
|
89
|
+
variant: "base" | "head";
|
|
90
|
+
/** The URL strobe opens after `prepare`, such as the variant's server; mock its API under `new URL(url).origin`. */
|
|
91
|
+
url: string;
|
|
92
|
+
/** The variant's folder: a git ref's worktree, or the directory it names. */
|
|
93
|
+
root: string;
|
|
94
|
+
/** Playwright, connected to this block's browser. Only on targets with a debug port. */
|
|
95
|
+
playwright: {
|
|
96
|
+
browser: any;
|
|
97
|
+
context: any;
|
|
98
|
+
page: any;
|
|
99
|
+
};
|
|
100
|
+
/**
|
|
101
|
+
* Answers requests whose URL matches `pattern` (`*` is any run of characters) in this block's page.
|
|
102
|
+
* Unlike Playwright's `page.route`, the browser's HTTP cache stays on: scripts and styles a reload
|
|
103
|
+
* fetches come from the cache, as for a person. Later calls add patterns and are asked first; a
|
|
104
|
+
* handler that returns `undefined` passes the request to earlier ones, as Playwright's routes do.
|
|
105
|
+
*/
|
|
106
|
+
mock(pattern: string, handler: MockHandler): Promise<void>;
|
|
107
|
+
}
|
|
108
|
+
/** What a launch's `before` hook gets: the launch has not started yet. */
|
|
109
|
+
export interface LaunchContext<P> {
|
|
110
|
+
case: string;
|
|
111
|
+
params: P;
|
|
112
|
+
target: string;
|
|
113
|
+
variant: "base" | "head";
|
|
114
|
+
/** The variant's folder: a git ref's worktree, or the directory it names. */
|
|
115
|
+
root: string;
|
|
116
|
+
/** The private home the app launches in (its `HOME`, `APPDATA`, XDG folders); null for web targets. Ready before the hook: seed and profile files are in. */
|
|
117
|
+
home: string | null;
|
|
118
|
+
/** App and exe targets: the whole environment the app launches with, to start what it expects to find (a background service) as it would. */
|
|
119
|
+
env: Record<string, string> | null;
|
|
120
|
+
/** App and exe targets: the executable this variant launches. */
|
|
121
|
+
exe: string | null;
|
|
122
|
+
}
|
|
123
|
+
/** A point in a launch: the first line matching `match` in `in`, timed from process spawn. */
|
|
124
|
+
export interface Milestone {
|
|
125
|
+
/** Text the line contains, or a regular expression it matches. A group named `at` holding epoch ms (`/entry: (?<at>\d{13})/`) is the time, for marks an app writes into its log; otherwise the time strobe read the line. */
|
|
126
|
+
match: string | RegExp;
|
|
127
|
+
/** `"output"` (stdout and stderr, the default), `"page"` (the page's `performance.mark` names), or a glob of files under the launch's home. */
|
|
128
|
+
in?: "output" | "page" | (string & {});
|
|
129
|
+
}
|
|
130
|
+
export interface Launch<P> {
|
|
131
|
+
/** The launch is ready when this is visible in any of its windows. The primary is `launch.ready`. */
|
|
132
|
+
until: Selector;
|
|
133
|
+
/** Named points on the way, each a metric `launch.<name>`. One a case declares must appear in every launch. */
|
|
134
|
+
milestones?: Record<string, Milestone>;
|
|
135
|
+
/** `fresh` (default): a new home every launch. `warm`: one home per series and variant, kept across its launches, so caches and state from earlier launches are there. */
|
|
136
|
+
profile?: "fresh" | "warm";
|
|
137
|
+
/** Before every launch, never timed: delete caches for a cold start, start or stop a service, seed the home. */
|
|
138
|
+
before?(ctx: LaunchContext<P>): unknown;
|
|
139
|
+
/** Starts the main process with `--inspect` and reads Node's own bootstrap timing after the launch (`launch.node.*`). App and exe targets. */
|
|
140
|
+
inspect?: boolean;
|
|
141
|
+
/** Times `launch.painted` and `launch.settled` from the window's pixels (Windows; default on for apps). */
|
|
142
|
+
screen?: boolean;
|
|
143
|
+
/** Milliseconds before a launch that never shows `until` fails. Default 120000. */
|
|
144
|
+
timeout?: number;
|
|
145
|
+
}
|
|
146
|
+
interface CaseBase {
|
|
147
|
+
/** The metric this case is about, declared before the run. */
|
|
148
|
+
primary: string;
|
|
149
|
+
/** Extra secondary metrics to report: any catalog metric, `count.<name>`, `net.requests{<url glob>}`, `layout.shift`, `launch.<milestone>`, `js.heap.retained` (scripts). */
|
|
150
|
+
metrics?: string[];
|
|
151
|
+
/** The targets this case runs on. Default: every target. */
|
|
152
|
+
targets?: string[];
|
|
153
|
+
}
|
|
154
|
+
/** A case of runs inside an open target: measured `run`, unmeasured `reset`. */
|
|
155
|
+
export interface RunCase<P> extends CaseBase {
|
|
156
|
+
/** Measured. */
|
|
157
|
+
run(ctx: Context<P>): unknown;
|
|
158
|
+
/** Puts the app back between runs. Never measured. */
|
|
159
|
+
reset?(ctx: Context<P>): unknown;
|
|
160
|
+
launch?: never;
|
|
161
|
+
}
|
|
162
|
+
/** A case that measures the launch itself: one cold launch per block. */
|
|
163
|
+
export interface LaunchCase<P> extends CaseBase {
|
|
164
|
+
launch: Launch<P>;
|
|
165
|
+
run?: never;
|
|
166
|
+
reset?: never;
|
|
167
|
+
}
|
|
168
|
+
export type Case<P> = RunCase<P> | LaunchCase<P>;
|
|
169
|
+
/** The first page a block opens: a path, or a function of what `prepare` returned. */
|
|
170
|
+
export type FirstPage<P> = string | ((ctx: {
|
|
171
|
+
case: string;
|
|
172
|
+
prepared: any;
|
|
173
|
+
params: P;
|
|
174
|
+
}) => string | Promise<string>);
|
|
175
|
+
interface Hooks<P> {
|
|
176
|
+
prepare?: (ctx: PrepareContext<P>) => unknown;
|
|
177
|
+
/** When the block closes, for what `prepare` started (a server, a temporary folder). */
|
|
178
|
+
teardown?: (ctx: PrepareContext<P>) => unknown;
|
|
179
|
+
}
|
|
180
|
+
export type Target<P> = ({
|
|
181
|
+
kind: "web";
|
|
182
|
+
url: FirstPage<P>;
|
|
183
|
+
viewport?: Viewport;
|
|
184
|
+
scale?: number;
|
|
185
|
+
} & Hooks<P>) | ({
|
|
186
|
+
kind: "url";
|
|
187
|
+
href: string;
|
|
188
|
+
viewport?: Viewport;
|
|
189
|
+
scale?: number;
|
|
190
|
+
} & Hooks<P>) | {
|
|
191
|
+
kind: "app";
|
|
192
|
+
id: string;
|
|
193
|
+
home?: string;
|
|
194
|
+
viewport?: Viewport;
|
|
195
|
+
} | {
|
|
196
|
+
kind: "exe";
|
|
197
|
+
path: string;
|
|
198
|
+
app?: string;
|
|
199
|
+
home?: string;
|
|
200
|
+
viewport?: Viewport;
|
|
201
|
+
} | ({
|
|
202
|
+
kind: "command";
|
|
203
|
+
} & CommandOptions) | ({
|
|
204
|
+
kind: "script";
|
|
205
|
+
} & ScriptOptions);
|
|
206
|
+
export interface Viewport {
|
|
207
|
+
width: number;
|
|
208
|
+
height: number;
|
|
209
|
+
}
|
|
210
|
+
export interface CommandOptions {
|
|
211
|
+
/** The command line, run by the shell in `cwd`, such as `bun run dev:desktop`. */
|
|
212
|
+
run: string;
|
|
213
|
+
/** Relative to the variant's folder. Default: the folder itself. */
|
|
214
|
+
cwd?: string;
|
|
215
|
+
/** Extra environment; `{home}` (the launch's private home) and `{port}` (a free port) are filled in. */
|
|
216
|
+
env?: Record<string, string>;
|
|
217
|
+
/** Environment variables to remove; `PREFIX_*` removes a family. */
|
|
218
|
+
unset?: string[];
|
|
219
|
+
/** The text before the DevTools endpoint in the command's output. Default `"DevTools listening on"`. */
|
|
220
|
+
attach?: string;
|
|
221
|
+
/** Milliseconds the command may take to print its endpoint. Default 600000. */
|
|
222
|
+
timeout?: number;
|
|
223
|
+
/** Run once per variant before its first block, in its folder. */
|
|
224
|
+
build?: string;
|
|
225
|
+
viewport?: Viewport;
|
|
226
|
+
}
|
|
227
|
+
export interface ScriptOptions {
|
|
228
|
+
/**
|
|
229
|
+
* The harness module whose exports cases call (`ctx.call`), relative to the bench file. strobe
|
|
230
|
+
* runs the same file for both variants, placed at the same path in a git ref's worktree, so its
|
|
231
|
+
* relative imports reach that variant's code. Keep the harness in one file.
|
|
232
|
+
*/
|
|
233
|
+
entry: string;
|
|
234
|
+
/** Default `"bun"`. */
|
|
235
|
+
runtime?: "bun" | "node";
|
|
236
|
+
env?: Record<string, string>;
|
|
237
|
+
/** Run once per variant before its first block, in its folder. */
|
|
238
|
+
build?: string;
|
|
239
|
+
}
|
|
240
|
+
/** A parameter's values: all run, or `run` by default and `more` with `--params all`. */
|
|
241
|
+
export type ParamValues = readonly unknown[] | {
|
|
242
|
+
readonly run: readonly unknown[];
|
|
243
|
+
readonly more?: readonly unknown[];
|
|
244
|
+
};
|
|
245
|
+
type Values<T> = {
|
|
246
|
+
[K in keyof T]: T[K] extends readonly (infer V)[] ? V : T[K] extends {
|
|
247
|
+
run: readonly (infer V)[];
|
|
248
|
+
more: readonly (infer W)[];
|
|
249
|
+
} ? V | W : T[K] extends {
|
|
250
|
+
run: readonly (infer V)[];
|
|
251
|
+
} ? V : never;
|
|
252
|
+
};
|
|
253
|
+
export interface Bench<Params extends Record<string, ParamValues>> {
|
|
254
|
+
targets: Record<string, Target<Values<Params>>>;
|
|
255
|
+
/** Every combination is its own series. */
|
|
256
|
+
params?: Params;
|
|
257
|
+
/** Once per block, after load. Never measured. */
|
|
258
|
+
setup?(ctx: Context<Values<Params>>): unknown;
|
|
259
|
+
/** Once per block, when it closes. Never measured. */
|
|
260
|
+
teardown?(ctx: Context<Values<Params>>): unknown;
|
|
261
|
+
cases: Record<string, Case<Values<Params>>>;
|
|
262
|
+
}
|
|
263
|
+
/** The project's production build, served per variant from `strobe.json`, in a strobe-launched Chromium. */
|
|
264
|
+
export declare function web<P = any>(opts?: {
|
|
265
|
+
url?: FirstPage<P>;
|
|
266
|
+
viewport?: Viewport;
|
|
267
|
+
scale?: number;
|
|
268
|
+
} & Hooks<P>): Target<P>;
|
|
269
|
+
/** A fixed page; the same for both variants unless a variant is `url:<href>`. */
|
|
270
|
+
export declare function url<P = any>(href: string, opts?: {
|
|
271
|
+
viewport?: Viewport;
|
|
272
|
+
scale?: number;
|
|
273
|
+
} & Hooks<P>): Target<P>;
|
|
274
|
+
/** An installed Electron app, by its profile in strobe.json `apps`, in a fresh isolated home seeded from `home`. */
|
|
275
|
+
export declare function app<P = any>(id: string, opts?: {
|
|
276
|
+
home?: string;
|
|
277
|
+
viewport?: Viewport;
|
|
278
|
+
}): Target<P>;
|
|
279
|
+
/** A packaged Electron app by path; `app` borrows a profile from strobe.json `apps` for it. */
|
|
280
|
+
export declare function exe<P = any>(path: string, opts?: {
|
|
281
|
+
app?: string;
|
|
282
|
+
home?: string;
|
|
283
|
+
viewport?: Viewport;
|
|
284
|
+
}): Target<P>;
|
|
285
|
+
/** An app started by a command in the variant's folder (a dev server that launches it), attached through the DevTools endpoint it prints. */
|
|
286
|
+
export declare function command<P = any>(run: string, opts?: Omit<CommandOptions, "run">): Target<P>;
|
|
287
|
+
/** A Bun or Node module of the variant, loaded in a fresh process per block; cases call its exports with `ctx.call`. */
|
|
288
|
+
export declare function script<P = any>(entry: string, opts?: Omit<ScriptOptions, "entry">): Target<P>;
|
|
289
|
+
export declare function bench<const Params extends Record<string, ParamValues> = {}>(spec: Bench<Params>): Bench<Params>;
|
|
290
|
+
export {};
|
package/dist/index.js
ADDED
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
// src/index.ts
|
|
2
|
+
function web(opts = {}) {
|
|
3
|
+
return { kind: "web", url: opts.url ?? "/", prepare: opts.prepare, teardown: opts.teardown, viewport: opts.viewport, scale: opts.scale };
|
|
4
|
+
}
|
|
5
|
+
function url(href, opts = {}) {
|
|
6
|
+
return { kind: "url", href, ...opts };
|
|
7
|
+
}
|
|
8
|
+
function app(id, opts = {}) {
|
|
9
|
+
return { kind: "app", id, ...opts };
|
|
10
|
+
}
|
|
11
|
+
function exe(path, opts = {}) {
|
|
12
|
+
return { kind: "exe", path, ...opts };
|
|
13
|
+
}
|
|
14
|
+
function command(run, opts = {}) {
|
|
15
|
+
return { kind: "command", run, ...opts };
|
|
16
|
+
}
|
|
17
|
+
function script(entry, opts = {}) {
|
|
18
|
+
return { kind: "script", entry, ...opts };
|
|
19
|
+
}
|
|
20
|
+
function bench(spec) {
|
|
21
|
+
return spec;
|
|
22
|
+
}
|
|
23
|
+
export {
|
|
24
|
+
app,
|
|
25
|
+
bench,
|
|
26
|
+
command,
|
|
27
|
+
exe,
|
|
28
|
+
script,
|
|
29
|
+
url,
|
|
30
|
+
web
|
|
31
|
+
};
|
package/package.json
CHANGED
|
@@ -1,6 +1,29 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@hona/strobe-bench",
|
|
3
|
-
"version": "0.
|
|
4
|
-
"
|
|
5
|
-
"
|
|
6
|
-
|
|
3
|
+
"version": "0.1.1",
|
|
4
|
+
"description": "Types and helpers for strobe bench files: measure web pages, Electron apps and scripts with paired, valid statistics",
|
|
5
|
+
"type": "module",
|
|
6
|
+
"types": "./dist/index.d.ts",
|
|
7
|
+
"exports": {
|
|
8
|
+
".": {
|
|
9
|
+
"types": "./dist/index.d.ts",
|
|
10
|
+
"import": "./dist/index.js",
|
|
11
|
+
"default": "./dist/index.js"
|
|
12
|
+
},
|
|
13
|
+
"./package.json": "./package.json"
|
|
14
|
+
},
|
|
15
|
+
"files": ["dist", "README.md"],
|
|
16
|
+
"sideEffects": false,
|
|
17
|
+
"scripts": {
|
|
18
|
+
"build": "bun build src/index.ts --outdir dist --target node --format esm && tsc -p tsconfig.json",
|
|
19
|
+
"prepublishOnly": "bun run build"
|
|
20
|
+
},
|
|
21
|
+
"devDependencies": {
|
|
22
|
+
"typescript": "^5.9.0"
|
|
23
|
+
},
|
|
24
|
+
"keywords": ["benchmark", "performance", "strobe", "playwright", "electron", "chromium"],
|
|
25
|
+
"license": "MIT",
|
|
26
|
+
"publishConfig": {
|
|
27
|
+
"access": "public"
|
|
28
|
+
}
|
|
29
|
+
}
|