@electron-stagewright/plugin-budget 0.0.0-stage → 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.
Files changed (51) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +148 -2
  3. package/dist/budget.d.ts +134 -0
  4. package/dist/budget.d.ts.map +1 -0
  5. package/dist/budget.js +129 -0
  6. package/dist/budget.js.map +1 -0
  7. package/dist/bundle.d.ts +15 -0
  8. package/dist/bundle.d.ts.map +1 -0
  9. package/dist/bundle.js +64 -0
  10. package/dist/bundle.js.map +1 -0
  11. package/dist/check.d.ts +43 -0
  12. package/dist/check.d.ts.map +1 -0
  13. package/dist/check.js +172 -0
  14. package/dist/check.js.map +1 -0
  15. package/dist/cli.d.ts +77 -0
  16. package/dist/cli.d.ts.map +1 -0
  17. package/dist/cli.js +401 -0
  18. package/dist/cli.js.map +1 -0
  19. package/dist/index.d.ts +24 -0
  20. package/dist/index.d.ts.map +1 -0
  21. package/dist/index.js +17 -0
  22. package/dist/index.js.map +1 -0
  23. package/dist/measure.d.ts +47 -0
  24. package/dist/measure.d.ts.map +1 -0
  25. package/dist/measure.js +139 -0
  26. package/dist/measure.js.map +1 -0
  27. package/dist/memory.d.ts +43 -0
  28. package/dist/memory.d.ts.map +1 -0
  29. package/dist/memory.js +159 -0
  30. package/dist/memory.js.map +1 -0
  31. package/dist/plugin.d.ts +13 -0
  32. package/dist/plugin.d.ts.map +1 -0
  33. package/dist/plugin.js +456 -0
  34. package/dist/plugin.js.map +1 -0
  35. package/dist/probe.d.ts +64 -0
  36. package/dist/probe.d.ts.map +1 -0
  37. package/dist/probe.js +161 -0
  38. package/dist/probe.js.map +1 -0
  39. package/dist/render.d.ts +21 -0
  40. package/dist/render.d.ts.map +1 -0
  41. package/dist/render.js +63 -0
  42. package/dist/render.js.map +1 -0
  43. package/dist/run.d.ts +218 -0
  44. package/dist/run.d.ts.map +1 -0
  45. package/dist/run.js +218 -0
  46. package/dist/run.js.map +1 -0
  47. package/dist/session.d.ts +54 -0
  48. package/dist/session.d.ts.map +1 -0
  49. package/dist/session.js +167 -0
  50. package/dist/session.js.map +1 -0
  51. package/package.json +60 -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,149 @@
1
- # Temporary Holding Version
1
+ # @electron-stagewright/plugin-budget
2
2
 
3
- This version is a temporary placeholder for this package. An operational version to replace this has been submitted for review and is awaiting a staged release.
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
+ - **`$comment`** is allowed in every object. Unknown keys are rejected, so a typo cannot silently
96
+ disable a limit.
97
+
98
+ ## CLI
99
+
100
+ ```sh
101
+ electron-stagewright-budget measure --main dist/main.js --budget perf-budget.json \
102
+ --rounds 5 --out run.json --check
103
+ electron-stagewright-budget check run.json --budget perf-budget.json [--strict] [--require-measurement]
104
+ electron-stagewright-budget compare before.json after.json
105
+ electron-stagewright-budget bundle --budget perf-budget.json [--root <dir>] [--out run.json]
106
+ ```
107
+
108
+ - `measure` launches the app (`--main <main.js>`, or `--executable <path>` for a packaged build;
109
+ repeat `--arg` and `--env K=V` to pass them through) on an empty throwaway profile. Each round is
110
+ a `cold` launch and a `warm` relaunch on the same profile (`--phases` picks one). It waits for
111
+ the window to load, settles (`--settle-ms`, default 3000), measures a window (`--window-ms`), and
112
+ writes the run. `--check` gates it in the same command; `--phase` reads every metric from one
113
+ measured phase, and a metric whose phase was not measured is `unmeasured`. On Linux CI run it
114
+ under `xvfb-run`.
115
+ - `--workload <script.mjs>` replaces the idle window with your interaction. The module
116
+ default-exports `async ({ dispatch, sessionId, phase }) => { ... }`, where `dispatch(tool, args)`
117
+ calls any core Stagewright tool (plugin tools are not loaded) on the launched app. The workload
118
+ runs with your privileges and eval enabled, so pass only a script you trust. One that has not
119
+ finished after `--workload-timeout-ms` (default 120000) fails the command:
120
+
121
+ ```js
122
+ export default async ({ dispatch, sessionId }) => {
123
+ await dispatch('electron_click', { sessionId, selector: '#new-sale' })
124
+ }
125
+ ```
126
+
127
+ - `bundle` measures the `bundle` entries (paths resolve from `--root`, default the budget file's
128
+ directory) and gates them. It needs no Electron and no display.
129
+ - `check` compares a run file to the budget. Values are the median over the rounds of a phase.
130
+ - `compare` prints a before/after table of two runs.
131
+ - `--strict` ignores `thresholdPercent`. `--require-measurement` fails a budgeted metric the run did
132
+ not measure; without it that metric is listed as `unmeasured`.
133
+ - `--json` prints the machine report instead of the table.
134
+
135
+ Exit codes: `0` within budget, `1` over budget **or nothing measured**, `2` usage error, unreadable
136
+ input, or any other error. A bundle entry that matches no file is `unmeasured`, never zero bytes,
137
+ and fails `bundle`. `measure` writes no run when no round produced a measurement.
138
+
139
+ The inspector Stagewright attaches is part of what `measure` measures, so calibrate budgets with
140
+ this tool. A packaged build whose fuses forbid inspection cannot be measured yet.
141
+
142
+ ## Library
143
+
144
+ ```ts
145
+ import { checkRun, loadBudget, loadRun } from '@electron-stagewright/plugin-budget'
146
+
147
+ const report = checkRun(await loadRun('run.json'), await loadBudget('perf-budget.json'))
148
+ if (!report.passed) console.error(report.results.filter((r) => r.status === 'fail'))
149
+ ```
@@ -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 { 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
+ thresholdPercent: z.ZodOptional<z.ZodNumber>;
31
+ perTypeMb: z.ZodOptional<z.ZodRecord<z.ZodEnum<{
32
+ main: "main";
33
+ renderer: "renderer";
34
+ gpu: "gpu";
35
+ utility: "utility";
36
+ other: "other";
37
+ }> & z.core.$partial, z.ZodNumber>>;
38
+ totalWithoutGpuMb: z.ZodOptional<z.ZodNumber>;
39
+ totalMb: z.ZodOptional<z.ZodNumber>;
40
+ $comment: z.ZodOptional<z.ZodString>;
41
+ }, z.core.$strict>;
42
+ declare const bundleEntrySchema: z.ZodObject<{
43
+ name: z.ZodOptional<z.ZodString>;
44
+ path: z.ZodString;
45
+ extensions: z.ZodOptional<z.ZodArray<z.ZodString>>;
46
+ gzip: z.ZodOptional<z.ZodBoolean>;
47
+ maxKb: z.ZodNumber;
48
+ $comment: z.ZodOptional<z.ZodString>;
49
+ }, z.core.$strict>;
50
+ export declare const budgetSchema: z.ZodObject<{
51
+ $schema: z.ZodOptional<z.ZodString>;
52
+ thresholdPercent: z.ZodOptional<z.ZodNumber>;
53
+ startup: z.ZodOptional<z.ZodObject<{
54
+ phase: z.ZodOptional<z.ZodEnum<{
55
+ cold: "cold";
56
+ warm: "warm";
57
+ live: "live";
58
+ }>>;
59
+ thresholdPercent: z.ZodOptional<z.ZodNumber>;
60
+ maxFirstWindowMs: z.ZodNumber;
61
+ $comment: z.ZodOptional<z.ZodString>;
62
+ }, z.core.$strict>>;
63
+ memory: z.ZodOptional<z.ZodRecord<z.ZodEnum<{
64
+ darwin: "darwin";
65
+ win32: "win32";
66
+ linux: "linux";
67
+ }> & z.core.$partial, z.ZodObject<{
68
+ metric: z.ZodEnum<{
69
+ footprint: "footprint";
70
+ pss: "pss";
71
+ privateBytes: "privateBytes";
72
+ workingSet: "workingSet";
73
+ }>;
74
+ phase: z.ZodOptional<z.ZodEnum<{
75
+ cold: "cold";
76
+ warm: "warm";
77
+ live: "live";
78
+ }>>;
79
+ thresholdPercent: z.ZodOptional<z.ZodNumber>;
80
+ perTypeMb: z.ZodOptional<z.ZodRecord<z.ZodEnum<{
81
+ main: "main";
82
+ renderer: "renderer";
83
+ gpu: "gpu";
84
+ utility: "utility";
85
+ other: "other";
86
+ }> & z.core.$partial, z.ZodNumber>>;
87
+ totalWithoutGpuMb: z.ZodOptional<z.ZodNumber>;
88
+ totalMb: z.ZodOptional<z.ZodNumber>;
89
+ $comment: z.ZodOptional<z.ZodString>;
90
+ }, z.core.$strict>>>;
91
+ mainThread: z.ZodOptional<z.ZodObject<{
92
+ thresholdPercent: z.ZodOptional<z.ZodNumber>;
93
+ windowMs: z.ZodOptional<z.ZodNumber>;
94
+ maxBlockedPercent: z.ZodOptional<z.ZodNumber>;
95
+ maxStallMs: z.ZodOptional<z.ZodNumber>;
96
+ $comment: z.ZodOptional<z.ZodString>;
97
+ }, z.core.$strict>>;
98
+ ipc: z.ZodOptional<z.ZodObject<{
99
+ thresholdPercent: z.ZodOptional<z.ZodNumber>;
100
+ probe: z.ZodString;
101
+ intervalMs: z.ZodOptional<z.ZodNumber>;
102
+ maxP99Ms: z.ZodOptional<z.ZodNumber>;
103
+ maxP95Ms: z.ZodOptional<z.ZodNumber>;
104
+ $comment: z.ZodOptional<z.ZodString>;
105
+ }, z.core.$strict>>;
106
+ bundle: z.ZodOptional<z.ZodObject<{
107
+ thresholdPercent: z.ZodOptional<z.ZodNumber>;
108
+ entries: z.ZodArray<z.ZodObject<{
109
+ name: z.ZodOptional<z.ZodString>;
110
+ path: z.ZodString;
111
+ extensions: z.ZodOptional<z.ZodArray<z.ZodString>>;
112
+ gzip: z.ZodOptional<z.ZodBoolean>;
113
+ maxKb: z.ZodNumber;
114
+ $comment: z.ZodOptional<z.ZodString>;
115
+ }, z.core.$strict>>;
116
+ $comment: z.ZodOptional<z.ZodString>;
117
+ }, z.core.$strict>>;
118
+ $comment: z.ZodOptional<z.ZodString>;
119
+ }, z.core.$strict>;
120
+ export type Budget = z.infer<typeof budgetSchema>;
121
+ export type MemoryPlatformBudget = z.infer<typeof memoryPlatformSchema>;
122
+ export type BundleEntryBudget = z.infer<typeof bundleEntrySchema>;
123
+ /** A budget file that is missing, not JSON, or does not match {@link budgetSchema}. */
124
+ export declare class BudgetFileError extends Error {
125
+ readonly code: 'BUDGET_NOT_FOUND' | 'BUDGET_INVALID';
126
+ readonly budgetPath?: string | undefined;
127
+ constructor(code: 'BUDGET_NOT_FOUND' | 'BUDGET_INVALID', message: string, budgetPath?: string | undefined);
128
+ }
129
+ /** Validate an already-parsed budget document. */
130
+ export declare function parseBudget(raw: unknown, budgetPath?: string): Budget;
131
+ /** Read and validate a budget file. */
132
+ export declare function loadBudget(budgetPath: string): Promise<Budget>;
133
+ export {};
134
+ //# 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;;;;;;;;;;;;;;;;;;;;;;;kBASxB,CAAA;AAEF,QAAA,MAAM,iBAAiB;;;;;;;kBAQrB,CAAA;AAEF,eAAO,MAAM,YAAY;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;kBAmDvB,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,129 @@
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
+ thresholdPercent,
23
+ // Summed per type. A type that is not named (GPU, typically) is reported but never gates.
24
+ perTypeMb: z.partialRecord(z.enum(PROCESS_TYPES), limit).optional(),
25
+ totalWithoutGpuMb: limit.optional(),
26
+ totalMb: limit.optional(),
27
+ });
28
+ const bundleEntrySchema = z.strictObject({
29
+ ...comment,
30
+ name: z.string().min(1).optional(),
31
+ // A file or a directory (summed recursively), relative to the budget file unless absolute.
32
+ path: z.string().min(1),
33
+ extensions: z.array(z.string().regex(/^\./, 'extensions start with a dot')).min(1).optional(),
34
+ gzip: z.boolean().optional(),
35
+ maxKb: limit,
36
+ });
37
+ export const budgetSchema = z.strictObject({
38
+ ...comment,
39
+ $schema: z.string().optional(),
40
+ thresholdPercent,
41
+ startup: z
42
+ .strictObject({
43
+ ...comment,
44
+ phase: z.enum(PHASES).optional(),
45
+ thresholdPercent,
46
+ maxFirstWindowMs: limit,
47
+ })
48
+ .optional(),
49
+ memory: z.partialRecord(z.enum(BUDGET_PLATFORMS), memoryPlatformSchema).optional(),
50
+ mainThread: z
51
+ .strictObject({
52
+ ...comment,
53
+ thresholdPercent,
54
+ windowMs: z.number().int().min(250).max(120_000).optional(),
55
+ maxBlockedPercent: z.number().min(0).max(100).optional(),
56
+ maxStallMs: limit.optional(),
57
+ })
58
+ .optional(),
59
+ ipc: z
60
+ .strictObject({
61
+ ...comment,
62
+ thresholdPercent,
63
+ // Renderer expression that performs one round trip, e.g. `window.api.getVersion()`.
64
+ probe: z.string().min(1),
65
+ intervalMs: z.number().int().min(1).max(1000).optional(),
66
+ maxP99Ms: limit.optional(),
67
+ maxP95Ms: limit.optional(),
68
+ })
69
+ .optional(),
70
+ bundle: z
71
+ .strictObject({ ...comment, thresholdPercent, entries: z.array(bundleEntrySchema).min(1) })
72
+ .superRefine((bundle, ctx) => {
73
+ // Entries are matched to their measurement by `name ?? path`, so that id must be unique.
74
+ const seen = new Set();
75
+ bundle.entries.forEach((entry, index) => {
76
+ const id = entry.name ?? entry.path;
77
+ if (seen.has(id)) {
78
+ ctx.addIssue({
79
+ code: 'custom',
80
+ path: ['entries', index],
81
+ message: `duplicate bundle entry "${id}"; give each entry a unique name`,
82
+ });
83
+ }
84
+ seen.add(id);
85
+ });
86
+ })
87
+ .optional(),
88
+ });
89
+ /** A budget file that is missing, not JSON, or does not match {@link budgetSchema}. */
90
+ export class BudgetFileError extends Error {
91
+ code;
92
+ budgetPath;
93
+ constructor(code, message, budgetPath) {
94
+ super(message);
95
+ this.code = code;
96
+ this.budgetPath = budgetPath;
97
+ this.name = 'BudgetFileError';
98
+ }
99
+ }
100
+ /** Validate an already-parsed budget document. */
101
+ export function parseBudget(raw, budgetPath) {
102
+ const parsed = budgetSchema.safeParse(raw);
103
+ if (parsed.success)
104
+ return parsed.data;
105
+ const issues = parsed.error.issues
106
+ .map((issue) => `${issue.path.join('.') || '(root)'}: ${issue.message}`)
107
+ .join('; ');
108
+ throw new BudgetFileError('BUDGET_INVALID', `Invalid budget: ${issues}`, budgetPath);
109
+ }
110
+ /** Read and validate a budget file. */
111
+ export async function loadBudget(budgetPath) {
112
+ let text;
113
+ try {
114
+ text = await readFile(budgetPath, 'utf8');
115
+ }
116
+ catch (error) {
117
+ throw new BudgetFileError('BUDGET_NOT_FOUND', `Cannot read budget file ${budgetPath}: ${error instanceof Error ? error.message : String(error)}`, budgetPath);
118
+ }
119
+ let raw;
120
+ try {
121
+ raw = JSON.parse(text);
122
+ }
123
+ catch {
124
+ // The parser's message quotes the file, and this path can be agent-supplied.
125
+ throw new BudgetFileError('BUDGET_INVALID', `Budget file ${budgetPath} is not valid JSON`, budgetPath);
126
+ }
127
+ return parseBudget(raw, budgetPath);
128
+ }
129
+ //# sourceMappingURL=budget.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"budget.js","sourceRoot":"","sources":["../src/budget.ts"],"names":[],"mappings":"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"}
@@ -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,43 @@
1
+ /**
2
+ * Compare a run to a budget. Pure: no I/O, no Electron.
3
+ *
4
+ * @module
5
+ */
6
+ import { type Budget, type Phase } from './budget.js';
7
+ import { type BudgetRun, type SummaryRow } from './run.js';
8
+ export type CheckStatus = 'pass' | 'fail' | 'unmeasured';
9
+ export interface CheckResult {
10
+ readonly id: string;
11
+ readonly status: CheckStatus;
12
+ readonly unit: SummaryRow['unit'];
13
+ readonly limit: number;
14
+ /** `limit` plus the tolerance; the value that actually gates. */
15
+ readonly allowed: number;
16
+ readonly actual?: number;
17
+ readonly phase?: Phase;
18
+ readonly metric?: string;
19
+ /** Why it is unmeasured. */
20
+ readonly detail?: string;
21
+ }
22
+ export interface CheckReport {
23
+ /** False on any failure, and always false when nothing at all was measured. */
24
+ readonly passed: boolean;
25
+ readonly reason?: 'NO_EXPECTATIONS' | 'NO_MEASUREMENTS' | 'UNMEASURED' | 'OVER_BUDGET';
26
+ readonly summary: {
27
+ readonly pass: number;
28
+ readonly fail: number;
29
+ readonly unmeasured: number;
30
+ };
31
+ readonly results: readonly CheckResult[];
32
+ }
33
+ export interface CheckOptions {
34
+ /** Ignore every `thresholdPercent`: the limit itself gates. */
35
+ readonly strict?: boolean;
36
+ /** Treat a budgeted metric the run did not measure as a failure. */
37
+ readonly requireMeasurement?: boolean;
38
+ /** Read every metric from this phase instead of the budget's own phases. */
39
+ readonly phase?: Phase;
40
+ }
41
+ /** Compare `run` to `budget` for the platform the run was measured on. */
42
+ export declare function checkRun(run: BudgetRun, budget: Budget, options?: CheckOptions): CheckReport;
43
+ //# sourceMappingURL=check.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"check.d.ts","sourceRoot":"","sources":["../src/check.ts"],"names":[],"mappings":"AAAA;;;;GAIG;AAEH,OAAO,EAAiB,KAAK,MAAM,EAAE,KAAK,KAAK,EAAE,MAAM,aAAa,CAAA;AACpE,OAAO,EAAgB,KAAK,SAAS,EAAE,KAAK,UAAU,EAAE,MAAM,UAAU,CAAA;AAExE,MAAM,MAAM,WAAW,GAAG,MAAM,GAAG,MAAM,GAAG,YAAY,CAAA;AAExD,MAAM,WAAW,WAAW;IAC1B,QAAQ,CAAC,EAAE,EAAE,MAAM,CAAA;IACnB,QAAQ,CAAC,MAAM,EAAE,WAAW,CAAA;IAC5B,QAAQ,CAAC,IAAI,EAAE,UAAU,CAAC,MAAM,CAAC,CAAA;IACjC,QAAQ,CAAC,KAAK,EAAE,MAAM,CAAA;IACtB,iEAAiE;IACjE,QAAQ,CAAC,OAAO,EAAE,MAAM,CAAA;IACxB,QAAQ,CAAC,MAAM,CAAC,EAAE,MAAM,CAAA;IACxB,QAAQ,CAAC,KAAK,CAAC,EAAE,KAAK,CAAA;IACtB,QAAQ,CAAC,MAAM,CAAC,EAAE,MAAM,CAAA;IACxB,4BAA4B;IAC5B,QAAQ,CAAC,MAAM,CAAC,EAAE,MAAM,CAAA;CACzB;AAED,MAAM,WAAW,WAAW;IAC1B,+EAA+E;IAC/E,QAAQ,CAAC,MAAM,EAAE,OAAO,CAAA;IACxB,QAAQ,CAAC,MAAM,CAAC,EAAE,iBAAiB,GAAG,iBAAiB,GAAG,YAAY,GAAG,aAAa,CAAA;IACtF,QAAQ,CAAC,OAAO,EAAE;QAAE,QAAQ,CAAC,IAAI,EAAE,MAAM,CAAC;QAAC,QAAQ,CAAC,IAAI,EAAE,MAAM,CAAC;QAAC,QAAQ,CAAC,UAAU,EAAE,MAAM,CAAA;KAAE,CAAA;IAC/F,QAAQ,CAAC,OAAO,EAAE,SAAS,WAAW,EAAE,CAAA;CACzC;AAED,MAAM,WAAW,YAAY;IAC3B,+DAA+D;IAC/D,QAAQ,CAAC,MAAM,CAAC,EAAE,OAAO,CAAA;IACzB,oEAAoE;IACpE,QAAQ,CAAC,kBAAkB,CAAC,EAAE,OAAO,CAAA;IACrC,4EAA4E;IAC5E,QAAQ,CAAC,KAAK,CAAC,EAAE,KAAK,CAAA;CACvB;AA+ID,0EAA0E;AAC1E,wBAAgB,QAAQ,CAAC,GAAG,EAAE,SAAS,EAAE,MAAM,EAAE,MAAM,EAAE,OAAO,GAAE,YAAiB,GAAG,WAAW,CA6ChG"}