@electron-stagewright/plugin-budget 0.0.0-stage → 0.2.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/LICENSE +21 -0
- package/README.md +214 -2
- package/dist/budget.d.ts +143 -0
- package/dist/budget.d.ts.map +1 -0
- package/dist/budget.js +134 -0
- package/dist/budget.js.map +1 -0
- package/dist/bundle.d.ts +15 -0
- package/dist/bundle.d.ts.map +1 -0
- package/dist/bundle.js +64 -0
- package/dist/bundle.js.map +1 -0
- package/dist/check-api.d.ts +15 -0
- package/dist/check-api.d.ts.map +1 -0
- package/dist/check-api.js +11 -0
- package/dist/check-api.js.map +1 -0
- package/dist/check.d.ts +50 -0
- package/dist/check.d.ts.map +1 -0
- package/dist/check.js +198 -0
- package/dist/check.js.map +1 -0
- package/dist/cli.d.ts +85 -0
- package/dist/cli.d.ts.map +1 -0
- package/dist/cli.js +464 -0
- package/dist/cli.js.map +1 -0
- package/dist/index.d.ts +24 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +17 -0
- package/dist/index.js.map +1 -0
- package/dist/measure.d.ts +81 -0
- package/dist/measure.d.ts.map +1 -0
- package/dist/measure.js +269 -0
- package/dist/measure.js.map +1 -0
- package/dist/memory.d.ts +43 -0
- package/dist/memory.d.ts.map +1 -0
- package/dist/memory.js +159 -0
- package/dist/memory.js.map +1 -0
- package/dist/plugin.d.ts +13 -0
- package/dist/plugin.d.ts.map +1 -0
- package/dist/plugin.js +456 -0
- package/dist/plugin.js.map +1 -0
- package/dist/probe.d.ts +74 -0
- package/dist/probe.d.ts.map +1 -0
- package/dist/probe.js +181 -0
- package/dist/probe.js.map +1 -0
- package/dist/render.d.ts +21 -0
- package/dist/render.d.ts.map +1 -0
- package/dist/render.js +63 -0
- package/dist/render.js.map +1 -0
- package/dist/run.d.ts +267 -0
- package/dist/run.d.ts.map +1 -0
- package/dist/run.js +257 -0
- package/dist/run.js.map +1 -0
- package/dist/session.d.ts +79 -0
- package/dist/session.d.ts.map +1 -0
- package/dist/session.js +263 -0
- package/dist/session.js.map +1 -0
- package/package.json +67 -3
package/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 Electron Stagewright contributors
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
package/README.md
CHANGED
|
@@ -1,3 +1,215 @@
|
|
|
1
|
-
#
|
|
1
|
+
# @electron-stagewright/plugin-budget
|
|
2
2
|
|
|
3
|
-
|
|
3
|
+
Performance budgets for Electron apps: write the limits in one JSON file, measure a build, and fail
|
|
4
|
+
the change that goes over. An agent asks `budget_check` whether its change broke a budget; CI runs the
|
|
5
|
+
`electron-stagewright-budget` CLI. Both read the same budget file and the same run format.
|
|
6
|
+
|
|
7
|
+
It measures startup to first window, memory per process type in the platform's own metric,
|
|
8
|
+
main-thread blocking, IPC round-trip latency, and bundle size. The app needs no code change: the
|
|
9
|
+
plugin samples through the session Stagewright already holds.
|
|
10
|
+
|
|
11
|
+
## Load it
|
|
12
|
+
|
|
13
|
+
```sh
|
|
14
|
+
electron-stagewright --plugin budget --allow-eval
|
|
15
|
+
```
|
|
16
|
+
|
|
17
|
+
```js
|
|
18
|
+
import { createServer } from '@electron-stagewright/core'
|
|
19
|
+
import budgetPlugin from '@electron-stagewright/plugin-budget'
|
|
20
|
+
|
|
21
|
+
const server = await createServer({
|
|
22
|
+
plugins: [budgetPlugin],
|
|
23
|
+
allowEval: true,
|
|
24
|
+
pluginConfigs: { budget: { budgetFile: '/abs/path/perf-budget.json' } },
|
|
25
|
+
})
|
|
26
|
+
```
|
|
27
|
+
|
|
28
|
+
Config (`pluginConfigs.budget`): `budgetFile` (default budget for `budget_check`), `root` (where
|
|
29
|
+
bundle paths resolve; defaults to the budget file's directory), `windowMs` (idle window, default 2000).
|
|
30
|
+
|
|
31
|
+
| Metric | Needs |
|
|
32
|
+
| ---------------------------- | -------------------------------------------------- |
|
|
33
|
+
| Memory, main-thread blocking | A session and main eval (`--allow-eval=main`) |
|
|
34
|
+
| IPC latency, startup | The above plus renderer eval (bare `--allow-eval`) |
|
|
35
|
+
| Bundle size | Nothing: no session, no eval |
|
|
36
|
+
|
|
37
|
+
The bodies evaluated in the app are fixed. The one caller-supplied expression is the IPC probe (from
|
|
38
|
+
`ipcProbe` or the budget file): it needs the renderer grant, passes the same blocklist as
|
|
39
|
+
`electron_eval_renderer`, and leaves the same audit line. Bundle entries must resolve inside `root`,
|
|
40
|
+
and with `--app-root` set the budget file and its entries must sit inside it.
|
|
41
|
+
|
|
42
|
+
## Tools
|
|
43
|
+
|
|
44
|
+
- **`budget_check`** `{ budgetPath?, sessionId?, windowMs?, strict?, requireMeasurement? }` — measure
|
|
45
|
+
the running app and the bundle entries and compare them to the budget. Returns
|
|
46
|
+
`{ passed, reason?, summary, results }`; each result is `pass`, `fail`, or `unmeasured` with the
|
|
47
|
+
reason. `passed` is false when a limit is exceeded and when nothing could be measured. Visible
|
|
48
|
+
without eval; then only the bundle is checked.
|
|
49
|
+
- **`budget_start`** `{ budgetPath?, ipcProbe?, ipcIntervalMs?, sessionId? }` — open a measurement
|
|
50
|
+
window, then drive the app with the other tools. The next `budget_measure` or `budget_check` closes
|
|
51
|
+
it, so blocking and IPC latency cover the interaction instead of an idle app. A window nobody
|
|
52
|
+
reads stops itself after 10 minutes. Budget calls on one session run one at a time.
|
|
53
|
+
- **`budget_measure`** `{ budgetPath?, ipcProbe?, windowMs?, sessionId? }` — the numbers without a
|
|
54
|
+
verdict: `{ measured: [{ id, value, unit, metric? }], unavailable? }`.
|
|
55
|
+
|
|
56
|
+
Main-thread blocking is the lateness of a 10 ms timer in the main process: `blockedPercent` is the
|
|
57
|
+
share of the window it ran 10 ms or more late, `maxStallMs` the worst single stall. The IPC probe
|
|
58
|
+
times one round trip of the budget's `ipc.probe` expression every `intervalMs` (default 25).
|
|
59
|
+
Startup is process creation to the end of the load event of the first window still open, whichever
|
|
60
|
+
window is selected; it is `unmeasured` once that window reloaded or navigated. Memory is read on
|
|
61
|
+
this host only when the host's process with the app's main pid started with the app, so an app in
|
|
62
|
+
another pid namespace reports memory `unmeasured` instead of a stranger's numbers.
|
|
63
|
+
|
|
64
|
+
## Budget file
|
|
65
|
+
|
|
66
|
+
```jsonc
|
|
67
|
+
{
|
|
68
|
+
"thresholdPercent": 10,
|
|
69
|
+
"startup": { "maxFirstWindowMs": 3000 },
|
|
70
|
+
"memory": {
|
|
71
|
+
"darwin": {
|
|
72
|
+
"metric": "footprint",
|
|
73
|
+
"phase": "warm",
|
|
74
|
+
"perTypeMb": { "main": 100, "renderer": 80, "utility": 10 },
|
|
75
|
+
"totalWithoutGpuMb": 160,
|
|
76
|
+
},
|
|
77
|
+
"win32": { "metric": "privateBytes", "perTypeMb": { "main": 90 } },
|
|
78
|
+
"linux": { "metric": "pss", "perTypeMb": { "main": 90 } },
|
|
79
|
+
},
|
|
80
|
+
"mainThread": { "windowMs": 5000, "maxBlockedPercent": 5, "maxStallMs": 100 },
|
|
81
|
+
"ipc": { "probe": "window.api.getVersion()", "maxP99Ms": 15 },
|
|
82
|
+
"bundle": {
|
|
83
|
+
"entries": [{ "path": "dist/renderer", "extensions": [".js"], "gzip": true, "maxKb": 500 }],
|
|
84
|
+
},
|
|
85
|
+
}
|
|
86
|
+
```
|
|
87
|
+
|
|
88
|
+
- **Memory has one block per platform.** `footprint` (macOS), `pss` (Linux), `privateBytes`
|
|
89
|
+
(Windows) and `workingSet` count different things, so megabytes are only ever compared within the
|
|
90
|
+
metric the budget names. Every table prints the metric next to the value.
|
|
91
|
+
- **Processes are summed per type** (`main`, `renderer`, `gpu`, `utility`, `other`). A type the
|
|
92
|
+
budget does not name is reported and never gates; GPU memory swings widely between machines, so
|
|
93
|
+
leave it out unless you mean to gate it.
|
|
94
|
+
- **`thresholdPercent`** is the tolerance over each limit; a section may override it.
|
|
95
|
+
- **`aggregate`** in a memory block is `end` (default) or `peak`; see Peak memory below.
|
|
96
|
+
- **`memoryProbe`** (top level) is a renderer expression that resolves to `app.getAppMetrics()`,
|
|
97
|
+
for builds whose main process cannot be inspected.
|
|
98
|
+
- **Phase.** `startup` reads `cold` unless it names a phase. A `memory` block that names none reads
|
|
99
|
+
the run's `warm` rounds, else `live`, else `cold`: a run of fresh-profile launches
|
|
100
|
+
(`--phases cold`) is gated on its cold rounds.
|
|
101
|
+
- **Startup is not launch-to-ready.** `maxFirstWindowMs` is process creation to the end of the
|
|
102
|
+
first window's load event. Work the app does after that (opening a database, fetching data,
|
|
103
|
+
starting a server) is not in it, so a wall-clock launch budget converted to it gets looser
|
|
104
|
+
without saying so. Time that work in a workload, or keep it in its own gate.
|
|
105
|
+
- **`$comment`** is allowed in every object. Unknown keys are rejected, so a typo cannot silently
|
|
106
|
+
disable a limit.
|
|
107
|
+
|
|
108
|
+
## CLI
|
|
109
|
+
|
|
110
|
+
```sh
|
|
111
|
+
electron-stagewright-budget measure --main dist/main.js --budget perf-budget.json \
|
|
112
|
+
--rounds 5 --out run.json --check
|
|
113
|
+
electron-stagewright-budget check run.json --budget perf-budget.json [--no-tolerance] [--require-measurement]
|
|
114
|
+
electron-stagewright-budget compare before.json after.json
|
|
115
|
+
electron-stagewright-budget bundle --budget perf-budget.json [--root <dir>] [--out run.json]
|
|
116
|
+
```
|
|
117
|
+
|
|
118
|
+
- `measure` launches the app (`--main <main.js>`, or `--executable <path>` for a packaged build;
|
|
119
|
+
repeat `--arg` and `--env K=V` to pass them through) on an empty throwaway profile. Each round is
|
|
120
|
+
a `cold` launch and a `warm` relaunch on the same profile (`--phases` picks one). It waits for
|
|
121
|
+
the window to load, settles (`--settle-ms`, default 3000), measures a window (`--window-ms`), and
|
|
122
|
+
writes the run. `--check` gates it in the same command; `--phase` reads every metric from one
|
|
123
|
+
measured phase, and a metric whose phase was not measured is `unmeasured`. On Linux CI run it
|
|
124
|
+
under `xvfb-run`.
|
|
125
|
+
- `--workload <script.mjs>` replaces the idle window with your interaction. The module
|
|
126
|
+
default-exports `async ({ dispatch, sessionId, phase }) => { ... }`, where `dispatch(tool, args)`
|
|
127
|
+
calls any core Stagewright tool (plugin tools are not loaded) on the launched app. The workload
|
|
128
|
+
runs with your privileges and eval enabled, so pass only a script you trust. One that has not
|
|
129
|
+
finished after `--workload-timeout-ms` (default 120000) fails the command:
|
|
130
|
+
|
|
131
|
+
```js
|
|
132
|
+
export default async ({ dispatch, sessionId }) => {
|
|
133
|
+
await dispatch('electron_click', { sessionId, selector: '#new-sale' })
|
|
134
|
+
}
|
|
135
|
+
```
|
|
136
|
+
|
|
137
|
+
- `--before-round <script.mjs>` prepares each round before its first launch. The module
|
|
138
|
+
default-exports `async ({ round, profile }) => ({ env, args })` (string values only); the
|
|
139
|
+
returned `env` and `args` are added to that round's launches. Use it to hand every launch its own copy of a fixture, so
|
|
140
|
+
rounds that share state do not skew the median.
|
|
141
|
+
- `--skip-failed-rounds` reports a round whose launch or measurement failed and goes on. Pair it
|
|
142
|
+
with `--min-samples N` on the check: a metric with fewer than `N` measured launches is left
|
|
143
|
+
unmeasured instead of gated, and `--require-measurement` turns that into a failure. If every
|
|
144
|
+
round fails, the command fails with the first round's error (exit `2`):
|
|
145
|
+
|
|
146
|
+
```sh
|
|
147
|
+
electron-stagewright-budget measure --main dist/main.js --budget perf-budget.json \
|
|
148
|
+
--rounds 3 --phases cold --before-round fresh-fixture.mjs --skip-failed-rounds \
|
|
149
|
+
--check --min-samples 2
|
|
150
|
+
```
|
|
151
|
+
|
|
152
|
+
- `bundle` measures the `bundle` entries (paths resolve from `--root`, default the budget file's
|
|
153
|
+
directory) and gates them. It needs no Electron and no display.
|
|
154
|
+
- `check` compares run files to the budget. Several files (`check a.json b.json c.json`) are merged
|
|
155
|
+
into one run first, and values are the median over the rounds of a phase. `--min-samples N`
|
|
156
|
+
leaves a runtime metric with fewer than `N` measured rounds `unmeasured`. In code,
|
|
157
|
+
`mergeRuns(runs)` and `checkRun(run, budget, { minSamples })` do the same, and `measureApp` takes
|
|
158
|
+
`beforeRound` and `failedRounds: 'skip'`.
|
|
159
|
+
- `compare` prints a before/after table of two runs.
|
|
160
|
+
- `--no-tolerance` ignores `thresholdPercent`, so the limit itself gates. It does not turn warnings
|
|
161
|
+
into failures: every verdict already fails the command. `--strict` is a deprecated alias that
|
|
162
|
+
prints a warning. `--require-measurement` fails a budgeted metric the run did not measure;
|
|
163
|
+
without it that metric is listed as `unmeasured`.
|
|
164
|
+
- `--json` prints the machine report instead of the table.
|
|
165
|
+
|
|
166
|
+
To run the CLI from a script, resolve it instead of guessing a path inside `dist/`:
|
|
167
|
+
|
|
168
|
+
```js
|
|
169
|
+
const cli = fileURLToPath(import.meta.resolve('@electron-stagewright/plugin-budget/cli'))
|
|
170
|
+
// or, from CommonJS: require.resolve('@electron-stagewright/plugin-budget/cli')
|
|
171
|
+
spawn(process.execPath, [cli, 'measure', '--main', 'dist/main.js', '--out', 'run.json'])
|
|
172
|
+
```
|
|
173
|
+
|
|
174
|
+
Exit codes: `0` within budget, `1` over budget **or nothing measured**, `2` usage error, unreadable
|
|
175
|
+
input, or any other error. A bundle entry that matches no file is `unmeasured`, never zero bytes,
|
|
176
|
+
and fails `bundle`. `measure` writes no run when no round produced a measurement.
|
|
177
|
+
|
|
178
|
+
The inspector Stagewright attaches is part of what `measure` measures, so calibrate budgets with
|
|
179
|
+
this tool.
|
|
180
|
+
|
|
181
|
+
### Packaged builds with the inspect fuse off
|
|
182
|
+
|
|
183
|
+
A release build with the `EnableNodeCliInspectArguments` fuse off has no main-process inspector.
|
|
184
|
+
`measure --executable` still launches it (over CDP, through the renderer), and then:
|
|
185
|
+
|
|
186
|
+
- **Memory** comes from the app itself. Expose `app.getAppMetrics()` to the renderer (for example
|
|
187
|
+
`ipcMain.handle('app-metrics', () => app.getAppMetrics())` plus a preload bridge), and name the
|
|
188
|
+
expression in the budget as `"memoryProbe": "window.api.appMetrics()"`. The host still reads
|
|
189
|
+
each pid in the platform metric. Keep the bridge out of builds users get if you do not want it
|
|
190
|
+
there.
|
|
191
|
+
- **IPC latency** works as usual.
|
|
192
|
+
- **Main-thread blocking and startup** are `unmeasured`, with the reason.
|
|
193
|
+
|
|
194
|
+
### Peak memory
|
|
195
|
+
|
|
196
|
+
`"aggregate": "peak"` in a memory block gates the heaviest moment of the window instead of its
|
|
197
|
+
end: `measure` samples memory every second (`--memory-sample-ms` to change it, or to sample
|
|
198
|
+
without the budget asking) and keeps the sample with the most memory outside the GPU as
|
|
199
|
+
`memoryPeak`. Check rows are then `memoryPeak.main`, `memoryPeak.totalWithoutGpu`, and so on, and
|
|
200
|
+
the median is taken over the rounds' peaks. Sampling through main adds a little main-thread work
|
|
201
|
+
of its own, and a `memoryProbe` adds IPC round trips, so calibrate blocking and IPC budgets with
|
|
202
|
+
the same setting. A round where no sample succeeded leaves `memoryPeak` unmeasured.
|
|
203
|
+
|
|
204
|
+
## Library
|
|
205
|
+
|
|
206
|
+
`@electron-stagewright/plugin-budget/check` holds the budget and run formats, `checkRun`, and the
|
|
207
|
+
report renderers. It loads neither core nor the plugin, so a gate that only checks runs without the
|
|
208
|
+
MCP SDK. The root export adds `measureApp`, `measureSession` and the plugin.
|
|
209
|
+
|
|
210
|
+
```ts
|
|
211
|
+
import { checkRun, loadBudget, loadRun } from '@electron-stagewright/plugin-budget/check'
|
|
212
|
+
|
|
213
|
+
const report = checkRun(await loadRun('run.json'), await loadBudget('perf-budget.json'))
|
|
214
|
+
if (!report.passed) console.error(report.results.filter((r) => r.status === 'fail'))
|
|
215
|
+
```
|
package/dist/budget.d.ts
ADDED
|
@@ -0,0 +1,143 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The budget file: one JSON document naming the limits a build must stay under.
|
|
3
|
+
*
|
|
4
|
+
* @module
|
|
5
|
+
*/
|
|
6
|
+
import { z } from 'zod';
|
|
7
|
+
/** OS memory metrics. They are not interchangeable, so a budget names exactly one per platform. */
|
|
8
|
+
export declare const MEMORY_METRICS: readonly ["footprint", "pss", "privateBytes", "workingSet"];
|
|
9
|
+
export type MemoryMetric = (typeof MEMORY_METRICS)[number];
|
|
10
|
+
/** Process buckets, normalised from Electron's `app.getAppMetrics()` types. */
|
|
11
|
+
export declare const PROCESS_TYPES: readonly ["main", "renderer", "gpu", "utility", "other"];
|
|
12
|
+
export type ProcessType = (typeof PROCESS_TYPES)[number];
|
|
13
|
+
/** `cold` is a first launch on an empty profile, `warm` a relaunch on it, `live` an agent session. */
|
|
14
|
+
export declare const PHASES: readonly ["cold", "warm", "live"];
|
|
15
|
+
export type Phase = (typeof PHASES)[number];
|
|
16
|
+
export declare const BUDGET_PLATFORMS: readonly ["darwin", "win32", "linux"];
|
|
17
|
+
export type BudgetPlatform = (typeof BUDGET_PLATFORMS)[number];
|
|
18
|
+
declare const memoryPlatformSchema: z.ZodObject<{
|
|
19
|
+
metric: z.ZodEnum<{
|
|
20
|
+
footprint: "footprint";
|
|
21
|
+
pss: "pss";
|
|
22
|
+
privateBytes: "privateBytes";
|
|
23
|
+
workingSet: "workingSet";
|
|
24
|
+
}>;
|
|
25
|
+
phase: z.ZodOptional<z.ZodEnum<{
|
|
26
|
+
cold: "cold";
|
|
27
|
+
warm: "warm";
|
|
28
|
+
live: "live";
|
|
29
|
+
}>>;
|
|
30
|
+
aggregate: z.ZodOptional<z.ZodEnum<{
|
|
31
|
+
end: "end";
|
|
32
|
+
peak: "peak";
|
|
33
|
+
}>>;
|
|
34
|
+
thresholdPercent: z.ZodOptional<z.ZodNumber>;
|
|
35
|
+
perTypeMb: z.ZodOptional<z.ZodRecord<z.ZodEnum<{
|
|
36
|
+
main: "main";
|
|
37
|
+
renderer: "renderer";
|
|
38
|
+
gpu: "gpu";
|
|
39
|
+
utility: "utility";
|
|
40
|
+
other: "other";
|
|
41
|
+
}> & z.core.$partial, z.ZodNumber>>;
|
|
42
|
+
totalWithoutGpuMb: z.ZodOptional<z.ZodNumber>;
|
|
43
|
+
totalMb: z.ZodOptional<z.ZodNumber>;
|
|
44
|
+
$comment: z.ZodOptional<z.ZodString>;
|
|
45
|
+
}, z.core.$strict>;
|
|
46
|
+
declare const bundleEntrySchema: z.ZodObject<{
|
|
47
|
+
name: z.ZodOptional<z.ZodString>;
|
|
48
|
+
path: z.ZodString;
|
|
49
|
+
extensions: z.ZodOptional<z.ZodArray<z.ZodString>>;
|
|
50
|
+
gzip: z.ZodOptional<z.ZodBoolean>;
|
|
51
|
+
maxKb: z.ZodNumber;
|
|
52
|
+
$comment: z.ZodOptional<z.ZodString>;
|
|
53
|
+
}, z.core.$strict>;
|
|
54
|
+
export declare const budgetSchema: z.ZodObject<{
|
|
55
|
+
$schema: z.ZodOptional<z.ZodString>;
|
|
56
|
+
thresholdPercent: z.ZodOptional<z.ZodNumber>;
|
|
57
|
+
memoryProbe: z.ZodOptional<z.ZodString>;
|
|
58
|
+
startup: z.ZodOptional<z.ZodObject<{
|
|
59
|
+
phase: z.ZodOptional<z.ZodEnum<{
|
|
60
|
+
cold: "cold";
|
|
61
|
+
warm: "warm";
|
|
62
|
+
live: "live";
|
|
63
|
+
}>>;
|
|
64
|
+
thresholdPercent: z.ZodOptional<z.ZodNumber>;
|
|
65
|
+
maxFirstWindowMs: z.ZodNumber;
|
|
66
|
+
$comment: z.ZodOptional<z.ZodString>;
|
|
67
|
+
}, z.core.$strict>>;
|
|
68
|
+
memory: z.ZodOptional<z.ZodRecord<z.ZodEnum<{
|
|
69
|
+
darwin: "darwin";
|
|
70
|
+
win32: "win32";
|
|
71
|
+
linux: "linux";
|
|
72
|
+
}> & z.core.$partial, z.ZodObject<{
|
|
73
|
+
metric: z.ZodEnum<{
|
|
74
|
+
footprint: "footprint";
|
|
75
|
+
pss: "pss";
|
|
76
|
+
privateBytes: "privateBytes";
|
|
77
|
+
workingSet: "workingSet";
|
|
78
|
+
}>;
|
|
79
|
+
phase: z.ZodOptional<z.ZodEnum<{
|
|
80
|
+
cold: "cold";
|
|
81
|
+
warm: "warm";
|
|
82
|
+
live: "live";
|
|
83
|
+
}>>;
|
|
84
|
+
aggregate: z.ZodOptional<z.ZodEnum<{
|
|
85
|
+
end: "end";
|
|
86
|
+
peak: "peak";
|
|
87
|
+
}>>;
|
|
88
|
+
thresholdPercent: z.ZodOptional<z.ZodNumber>;
|
|
89
|
+
perTypeMb: z.ZodOptional<z.ZodRecord<z.ZodEnum<{
|
|
90
|
+
main: "main";
|
|
91
|
+
renderer: "renderer";
|
|
92
|
+
gpu: "gpu";
|
|
93
|
+
utility: "utility";
|
|
94
|
+
other: "other";
|
|
95
|
+
}> & z.core.$partial, z.ZodNumber>>;
|
|
96
|
+
totalWithoutGpuMb: z.ZodOptional<z.ZodNumber>;
|
|
97
|
+
totalMb: z.ZodOptional<z.ZodNumber>;
|
|
98
|
+
$comment: z.ZodOptional<z.ZodString>;
|
|
99
|
+
}, z.core.$strict>>>;
|
|
100
|
+
mainThread: z.ZodOptional<z.ZodObject<{
|
|
101
|
+
thresholdPercent: z.ZodOptional<z.ZodNumber>;
|
|
102
|
+
windowMs: z.ZodOptional<z.ZodNumber>;
|
|
103
|
+
maxBlockedPercent: z.ZodOptional<z.ZodNumber>;
|
|
104
|
+
maxStallMs: z.ZodOptional<z.ZodNumber>;
|
|
105
|
+
$comment: z.ZodOptional<z.ZodString>;
|
|
106
|
+
}, z.core.$strict>>;
|
|
107
|
+
ipc: z.ZodOptional<z.ZodObject<{
|
|
108
|
+
thresholdPercent: z.ZodOptional<z.ZodNumber>;
|
|
109
|
+
probe: z.ZodString;
|
|
110
|
+
intervalMs: z.ZodOptional<z.ZodNumber>;
|
|
111
|
+
maxP99Ms: z.ZodOptional<z.ZodNumber>;
|
|
112
|
+
maxP95Ms: z.ZodOptional<z.ZodNumber>;
|
|
113
|
+
$comment: z.ZodOptional<z.ZodString>;
|
|
114
|
+
}, z.core.$strict>>;
|
|
115
|
+
bundle: z.ZodOptional<z.ZodObject<{
|
|
116
|
+
thresholdPercent: z.ZodOptional<z.ZodNumber>;
|
|
117
|
+
entries: z.ZodArray<z.ZodObject<{
|
|
118
|
+
name: z.ZodOptional<z.ZodString>;
|
|
119
|
+
path: z.ZodString;
|
|
120
|
+
extensions: z.ZodOptional<z.ZodArray<z.ZodString>>;
|
|
121
|
+
gzip: z.ZodOptional<z.ZodBoolean>;
|
|
122
|
+
maxKb: z.ZodNumber;
|
|
123
|
+
$comment: z.ZodOptional<z.ZodString>;
|
|
124
|
+
}, z.core.$strict>>;
|
|
125
|
+
$comment: z.ZodOptional<z.ZodString>;
|
|
126
|
+
}, z.core.$strict>>;
|
|
127
|
+
$comment: z.ZodOptional<z.ZodString>;
|
|
128
|
+
}, z.core.$strict>;
|
|
129
|
+
export type Budget = z.infer<typeof budgetSchema>;
|
|
130
|
+
export type MemoryPlatformBudget = z.infer<typeof memoryPlatformSchema>;
|
|
131
|
+
export type BundleEntryBudget = z.infer<typeof bundleEntrySchema>;
|
|
132
|
+
/** A budget file that is missing, not JSON, or does not match {@link budgetSchema}. */
|
|
133
|
+
export declare class BudgetFileError extends Error {
|
|
134
|
+
readonly code: 'BUDGET_NOT_FOUND' | 'BUDGET_INVALID';
|
|
135
|
+
readonly budgetPath?: string | undefined;
|
|
136
|
+
constructor(code: 'BUDGET_NOT_FOUND' | 'BUDGET_INVALID', message: string, budgetPath?: string | undefined);
|
|
137
|
+
}
|
|
138
|
+
/** Validate an already-parsed budget document. */
|
|
139
|
+
export declare function parseBudget(raw: unknown, budgetPath?: string): Budget;
|
|
140
|
+
/** Read and validate a budget file. */
|
|
141
|
+
export declare function loadBudget(budgetPath: string): Promise<Budget>;
|
|
142
|
+
export {};
|
|
143
|
+
//# sourceMappingURL=budget.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"budget.d.ts","sourceRoot":"","sources":["../src/budget.ts"],"names":[],"mappings":"AAAA;;;;GAIG;AAIH,OAAO,EAAE,CAAC,EAAE,MAAM,KAAK,CAAA;AAEvB,mGAAmG;AACnG,eAAO,MAAM,cAAc,6DAA8D,CAAA;AACzF,MAAM,MAAM,YAAY,GAAG,CAAC,OAAO,cAAc,CAAC,CAAC,MAAM,CAAC,CAAA;AAE1D,+EAA+E;AAC/E,eAAO,MAAM,aAAa,0DAA2D,CAAA;AACrF,MAAM,MAAM,WAAW,GAAG,CAAC,OAAO,aAAa,CAAC,CAAC,MAAM,CAAC,CAAA;AAExD,sGAAsG;AACtG,eAAO,MAAM,MAAM,mCAAoC,CAAA;AACvD,MAAM,MAAM,KAAK,GAAG,CAAC,OAAO,MAAM,CAAC,CAAC,MAAM,CAAC,CAAA;AAE3C,eAAO,MAAM,gBAAgB,uCAAwC,CAAA;AACrE,MAAM,MAAM,cAAc,GAAG,CAAC,OAAO,gBAAgB,CAAC,CAAC,MAAM,CAAC,CAAA;AAM9D,QAAA,MAAM,oBAAoB;;;;;;;;;;;;;;;;;;;;;;;;;;;kBAWxB,CAAA;AAEF,QAAA,MAAM,iBAAiB;;;;;;;kBAQrB,CAAA;AAEF,eAAO,MAAM,YAAY;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;kBAsDvB,CAAA;AAEF,MAAM,MAAM,MAAM,GAAG,CAAC,CAAC,KAAK,CAAC,OAAO,YAAY,CAAC,CAAA;AACjD,MAAM,MAAM,oBAAoB,GAAG,CAAC,CAAC,KAAK,CAAC,OAAO,oBAAoB,CAAC,CAAA;AACvE,MAAM,MAAM,iBAAiB,GAAG,CAAC,CAAC,KAAK,CAAC,OAAO,iBAAiB,CAAC,CAAA;AAEjE,uFAAuF;AACvF,qBAAa,eAAgB,SAAQ,KAAK;IAEtC,QAAQ,CAAC,IAAI,EAAE,kBAAkB,GAAG,gBAAgB;IAEpD,QAAQ,CAAC,UAAU,CAAC,EAAE,MAAM;gBAFnB,IAAI,EAAE,kBAAkB,GAAG,gBAAgB,EACpD,OAAO,EAAE,MAAM,EACN,UAAU,CAAC,EAAE,MAAM,YAAA;CAK/B;AAED,kDAAkD;AAClD,wBAAgB,WAAW,CAAC,GAAG,EAAE,OAAO,EAAE,UAAU,CAAC,EAAE,MAAM,GAAG,MAAM,CAOrE;AAED,uCAAuC;AACvC,wBAAsB,UAAU,CAAC,UAAU,EAAE,MAAM,GAAG,OAAO,CAAC,MAAM,CAAC,CAuBpE"}
|
package/dist/budget.js
ADDED
|
@@ -0,0 +1,134 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The budget file: one JSON document naming the limits a build must stay under.
|
|
3
|
+
*
|
|
4
|
+
* @module
|
|
5
|
+
*/
|
|
6
|
+
import { readFile } from 'node:fs/promises';
|
|
7
|
+
import { z } from 'zod';
|
|
8
|
+
/** OS memory metrics. They are not interchangeable, so a budget names exactly one per platform. */
|
|
9
|
+
export const MEMORY_METRICS = ['footprint', 'pss', 'privateBytes', 'workingSet'];
|
|
10
|
+
/** Process buckets, normalised from Electron's `app.getAppMetrics()` types. */
|
|
11
|
+
export const PROCESS_TYPES = ['main', 'renderer', 'gpu', 'utility', 'other'];
|
|
12
|
+
/** `cold` is a first launch on an empty profile, `warm` a relaunch on it, `live` an agent session. */
|
|
13
|
+
export const PHASES = ['cold', 'warm', 'live'];
|
|
14
|
+
export const BUDGET_PLATFORMS = ['darwin', 'win32', 'linux'];
|
|
15
|
+
const comment = { $comment: z.string().optional() };
|
|
16
|
+
const limit = z.number().positive();
|
|
17
|
+
const thresholdPercent = z.number().min(0).max(1000).optional();
|
|
18
|
+
const memoryPlatformSchema = z.strictObject({
|
|
19
|
+
...comment,
|
|
20
|
+
metric: z.enum(MEMORY_METRICS),
|
|
21
|
+
phase: z.enum(PHASES).optional(),
|
|
22
|
+
// `end`: the sample taken when the window closes. `peak`: the window's highest sample outside GPU.
|
|
23
|
+
aggregate: z.enum(['end', 'peak']).optional(),
|
|
24
|
+
thresholdPercent,
|
|
25
|
+
// Summed per type. A type that is not named (GPU, typically) is reported but never gates.
|
|
26
|
+
perTypeMb: z.partialRecord(z.enum(PROCESS_TYPES), limit).optional(),
|
|
27
|
+
totalWithoutGpuMb: limit.optional(),
|
|
28
|
+
totalMb: limit.optional(),
|
|
29
|
+
});
|
|
30
|
+
const bundleEntrySchema = z.strictObject({
|
|
31
|
+
...comment,
|
|
32
|
+
name: z.string().min(1).optional(),
|
|
33
|
+
// A file or a directory (summed recursively), relative to the budget file unless absolute.
|
|
34
|
+
path: z.string().min(1),
|
|
35
|
+
extensions: z.array(z.string().regex(/^\./, 'extensions start with a dot')).min(1).optional(),
|
|
36
|
+
gzip: z.boolean().optional(),
|
|
37
|
+
maxKb: limit,
|
|
38
|
+
});
|
|
39
|
+
export const budgetSchema = z.strictObject({
|
|
40
|
+
...comment,
|
|
41
|
+
$schema: z.string().optional(),
|
|
42
|
+
thresholdPercent,
|
|
43
|
+
// Renderer expression resolving to `app.getAppMetrics()`, for a build whose main process cannot
|
|
44
|
+
// be inspected (the EnableNodeCliInspectArguments fuse off).
|
|
45
|
+
memoryProbe: z.string().min(1).optional(),
|
|
46
|
+
startup: z
|
|
47
|
+
.strictObject({
|
|
48
|
+
...comment,
|
|
49
|
+
phase: z.enum(PHASES).optional(),
|
|
50
|
+
thresholdPercent,
|
|
51
|
+
maxFirstWindowMs: limit,
|
|
52
|
+
})
|
|
53
|
+
.optional(),
|
|
54
|
+
memory: z.partialRecord(z.enum(BUDGET_PLATFORMS), memoryPlatformSchema).optional(),
|
|
55
|
+
mainThread: z
|
|
56
|
+
.strictObject({
|
|
57
|
+
...comment,
|
|
58
|
+
thresholdPercent,
|
|
59
|
+
windowMs: z.number().int().min(250).max(120_000).optional(),
|
|
60
|
+
maxBlockedPercent: z.number().min(0).max(100).optional(),
|
|
61
|
+
maxStallMs: limit.optional(),
|
|
62
|
+
})
|
|
63
|
+
.optional(),
|
|
64
|
+
ipc: z
|
|
65
|
+
.strictObject({
|
|
66
|
+
...comment,
|
|
67
|
+
thresholdPercent,
|
|
68
|
+
// Renderer expression that performs one round trip, e.g. `window.api.getVersion()`.
|
|
69
|
+
probe: z.string().min(1),
|
|
70
|
+
intervalMs: z.number().int().min(1).max(1000).optional(),
|
|
71
|
+
maxP99Ms: limit.optional(),
|
|
72
|
+
maxP95Ms: limit.optional(),
|
|
73
|
+
})
|
|
74
|
+
.optional(),
|
|
75
|
+
bundle: z
|
|
76
|
+
.strictObject({ ...comment, thresholdPercent, entries: z.array(bundleEntrySchema).min(1) })
|
|
77
|
+
.superRefine((bundle, ctx) => {
|
|
78
|
+
// Entries are matched to their measurement by `name ?? path`, so that id must be unique.
|
|
79
|
+
const seen = new Set();
|
|
80
|
+
bundle.entries.forEach((entry, index) => {
|
|
81
|
+
const id = entry.name ?? entry.path;
|
|
82
|
+
if (seen.has(id)) {
|
|
83
|
+
ctx.addIssue({
|
|
84
|
+
code: 'custom',
|
|
85
|
+
path: ['entries', index],
|
|
86
|
+
message: `duplicate bundle entry "${id}"; give each entry a unique name`,
|
|
87
|
+
});
|
|
88
|
+
}
|
|
89
|
+
seen.add(id);
|
|
90
|
+
});
|
|
91
|
+
})
|
|
92
|
+
.optional(),
|
|
93
|
+
});
|
|
94
|
+
/** A budget file that is missing, not JSON, or does not match {@link budgetSchema}. */
|
|
95
|
+
export class BudgetFileError extends Error {
|
|
96
|
+
code;
|
|
97
|
+
budgetPath;
|
|
98
|
+
constructor(code, message, budgetPath) {
|
|
99
|
+
super(message);
|
|
100
|
+
this.code = code;
|
|
101
|
+
this.budgetPath = budgetPath;
|
|
102
|
+
this.name = 'BudgetFileError';
|
|
103
|
+
}
|
|
104
|
+
}
|
|
105
|
+
/** Validate an already-parsed budget document. */
|
|
106
|
+
export function parseBudget(raw, budgetPath) {
|
|
107
|
+
const parsed = budgetSchema.safeParse(raw);
|
|
108
|
+
if (parsed.success)
|
|
109
|
+
return parsed.data;
|
|
110
|
+
const issues = parsed.error.issues
|
|
111
|
+
.map((issue) => `${issue.path.join('.') || '(root)'}: ${issue.message}`)
|
|
112
|
+
.join('; ');
|
|
113
|
+
throw new BudgetFileError('BUDGET_INVALID', `Invalid budget: ${issues}`, budgetPath);
|
|
114
|
+
}
|
|
115
|
+
/** Read and validate a budget file. */
|
|
116
|
+
export async function loadBudget(budgetPath) {
|
|
117
|
+
let text;
|
|
118
|
+
try {
|
|
119
|
+
text = await readFile(budgetPath, 'utf8');
|
|
120
|
+
}
|
|
121
|
+
catch (error) {
|
|
122
|
+
throw new BudgetFileError('BUDGET_NOT_FOUND', `Cannot read budget file ${budgetPath}: ${error instanceof Error ? error.message : String(error)}`, budgetPath);
|
|
123
|
+
}
|
|
124
|
+
let raw;
|
|
125
|
+
try {
|
|
126
|
+
raw = JSON.parse(text);
|
|
127
|
+
}
|
|
128
|
+
catch {
|
|
129
|
+
// The parser's message quotes the file, and this path can be agent-supplied.
|
|
130
|
+
throw new BudgetFileError('BUDGET_INVALID', `Budget file ${budgetPath} is not valid JSON`, budgetPath);
|
|
131
|
+
}
|
|
132
|
+
return parseBudget(raw, budgetPath);
|
|
133
|
+
}
|
|
134
|
+
//# sourceMappingURL=budget.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"budget.js","sourceRoot":"","sources":["../src/budget.ts"],"names":[],"mappings":"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"}
|
package/dist/bundle.d.ts
ADDED
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Bundle-size measurement: bytes on disk for the files a budget names. Needs no Electron.
|
|
3
|
+
*
|
|
4
|
+
* @module
|
|
5
|
+
*/
|
|
6
|
+
import type { BundleEntryBudget } from './budget.js';
|
|
7
|
+
import type { BundleEntryMeasurement } from './run.js';
|
|
8
|
+
/**
|
|
9
|
+
* Measure each entry under `root`. A missing path, or one with no matching file, is reported as
|
|
10
|
+
* `unavailable` instead of zero bytes: an empty measurement must never read as "within budget".
|
|
11
|
+
*/
|
|
12
|
+
export declare function measureBundle(root: string, entries: readonly BundleEntryBudget[]): Promise<{
|
|
13
|
+
entries: BundleEntryMeasurement[];
|
|
14
|
+
}>;
|
|
15
|
+
//# sourceMappingURL=bundle.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"bundle.d.ts","sourceRoot":"","sources":["../src/bundle.ts"],"names":[],"mappings":"AAAA;;;;GAIG;AAMH,OAAO,KAAK,EAAE,iBAAiB,EAAE,MAAM,aAAa,CAAA;AACpD,OAAO,KAAK,EAAE,sBAAsB,EAAE,MAAM,UAAU,CAAA;AAatD;;;GAGG;AACH,wBAAsB,aAAa,CACjC,IAAI,EAAE,MAAM,EACZ,OAAO,EAAE,SAAS,iBAAiB,EAAE,GACpC,OAAO,CAAC;IAAE,OAAO,EAAE,sBAAsB,EAAE,CAAA;CAAE,CAAC,CAqChD"}
|
package/dist/bundle.js
ADDED
|
@@ -0,0 +1,64 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Bundle-size measurement: bytes on disk for the files a budget names. Needs no Electron.
|
|
3
|
+
*
|
|
4
|
+
* @module
|
|
5
|
+
*/
|
|
6
|
+
import { readFile, readdir, stat } from 'node:fs/promises';
|
|
7
|
+
import path from 'node:path';
|
|
8
|
+
import { gzipSync } from 'node:zlib';
|
|
9
|
+
async function listFiles(target) {
|
|
10
|
+
const info = await stat(target);
|
|
11
|
+
if (info.isFile())
|
|
12
|
+
return [target];
|
|
13
|
+
if (!info.isDirectory())
|
|
14
|
+
return [];
|
|
15
|
+
const entries = await readdir(target, { recursive: true, withFileTypes: true });
|
|
16
|
+
return entries
|
|
17
|
+
.filter((entry) => entry.isFile())
|
|
18
|
+
.map((entry) => path.join(entry.parentPath, entry.name))
|
|
19
|
+
.sort();
|
|
20
|
+
}
|
|
21
|
+
/**
|
|
22
|
+
* Measure each entry under `root`. A missing path, or one with no matching file, is reported as
|
|
23
|
+
* `unavailable` instead of zero bytes: an empty measurement must never read as "within budget".
|
|
24
|
+
*/
|
|
25
|
+
export async function measureBundle(root, entries) {
|
|
26
|
+
const measured = [];
|
|
27
|
+
for (const entry of entries) {
|
|
28
|
+
const target = path.resolve(root, entry.path);
|
|
29
|
+
const gzip = entry.gzip === true;
|
|
30
|
+
const base = { id: entry.name ?? entry.path, path: target, gzip };
|
|
31
|
+
let files;
|
|
32
|
+
try {
|
|
33
|
+
files = await listFiles(target);
|
|
34
|
+
}
|
|
35
|
+
catch (error) {
|
|
36
|
+
const unavailable = error.code === 'ENOENT'
|
|
37
|
+
? `${target} does not exist`
|
|
38
|
+
: `cannot read ${target}: ${error instanceof Error ? error.message : String(error)}`;
|
|
39
|
+
measured.push({ ...base, files: 0, bytes: 0, unavailable });
|
|
40
|
+
continue;
|
|
41
|
+
}
|
|
42
|
+
const extensions = entry.extensions;
|
|
43
|
+
const matching = extensions === undefined
|
|
44
|
+
? files
|
|
45
|
+
: files.filter((file) => extensions.includes(path.extname(file)));
|
|
46
|
+
if (matching.length === 0) {
|
|
47
|
+
measured.push({ ...base, files: 0, bytes: 0, unavailable: `no file matched under ${target}` });
|
|
48
|
+
continue;
|
|
49
|
+
}
|
|
50
|
+
let bytes = 0;
|
|
51
|
+
if (gzip) {
|
|
52
|
+
// Sequential: holding every file buffer (and fd) at once does not scale to large trees.
|
|
53
|
+
for (const file of matching)
|
|
54
|
+
bytes += gzipSync(await readFile(file)).byteLength;
|
|
55
|
+
}
|
|
56
|
+
else {
|
|
57
|
+
const sizes = await Promise.all(matching.map(async (file) => (await stat(file)).size));
|
|
58
|
+
bytes = sizes.reduce((sum, size) => sum + size, 0);
|
|
59
|
+
}
|
|
60
|
+
measured.push({ ...base, files: matching.length, bytes });
|
|
61
|
+
}
|
|
62
|
+
return { entries: measured };
|
|
63
|
+
}
|
|
64
|
+
//# sourceMappingURL=bundle.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"bundle.js","sourceRoot":"","sources":["../src/bundle.ts"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* `@electron-stagewright/plugin-budget/check`: the budget and run formats and the check, without
|
|
3
|
+
* core, the plugin, or anything that launches Electron.
|
|
4
|
+
*
|
|
5
|
+
* @module
|
|
6
|
+
*/
|
|
7
|
+
export { BUDGET_PLATFORMS, BudgetFileError, MEMORY_METRICS, PHASES, PROCESS_TYPES, budgetSchema, loadBudget, parseBudget, } from './budget.js';
|
|
8
|
+
export type { Budget, BudgetPlatform, BundleEntryBudget, MemoryMetric, MemoryPlatformBudget, Phase, ProcessType, } from './budget.js';
|
|
9
|
+
export { checkRun } from './check.js';
|
|
10
|
+
export type { CheckOptions, CheckReport, CheckResult, CheckStatus } from './check.js';
|
|
11
|
+
export { compareRuns, renderCheckReport, renderComparison } from './render.js';
|
|
12
|
+
export type { ComparisonRow } from './render.js';
|
|
13
|
+
export { RUN_FORMAT, RUN_SCHEMA_VERSION, RunFileError, createRun, loadRun, memoryByType, mergeRuns, runSchema, summarizeLatencies, summarizeRun, } from './run.js';
|
|
14
|
+
export type { BudgetRun, BundleEntryMeasurement, LatencySummary, Measurement, MemoryMeasurement, ProcessMemory, StallSummary, SummaryRow, } from './run.js';
|
|
15
|
+
//# sourceMappingURL=check-api.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"check-api.d.ts","sourceRoot":"","sources":["../src/check-api.ts"],"names":[],"mappings":"AAAA;;;;;GAKG;AAEH,OAAO,EACL,gBAAgB,EAChB,eAAe,EACf,cAAc,EACd,MAAM,EACN,aAAa,EACb,YAAY,EACZ,UAAU,EACV,WAAW,GACZ,MAAM,aAAa,CAAA;AACpB,YAAY,EACV,MAAM,EACN,cAAc,EACd,iBAAiB,EACjB,YAAY,EACZ,oBAAoB,EACpB,KAAK,EACL,WAAW,GACZ,MAAM,aAAa,CAAA;AACpB,OAAO,EAAE,QAAQ,EAAE,MAAM,YAAY,CAAA;AACrC,YAAY,EAAE,YAAY,EAAE,WAAW,EAAE,WAAW,EAAE,WAAW,EAAE,MAAM,YAAY,CAAA;AACrF,OAAO,EAAE,WAAW,EAAE,iBAAiB,EAAE,gBAAgB,EAAE,MAAM,aAAa,CAAA;AAC9E,YAAY,EAAE,aAAa,EAAE,MAAM,aAAa,CAAA;AAChD,OAAO,EACL,UAAU,EACV,kBAAkB,EAClB,YAAY,EACZ,SAAS,EACT,OAAO,EACP,YAAY,EACZ,SAAS,EACT,SAAS,EACT,kBAAkB,EAClB,YAAY,GACb,MAAM,UAAU,CAAA;AACjB,YAAY,EACV,SAAS,EACT,sBAAsB,EACtB,cAAc,EACd,WAAW,EACX,iBAAiB,EACjB,aAAa,EACb,YAAY,EACZ,UAAU,GACX,MAAM,UAAU,CAAA"}
|