@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.
Files changed (55) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +214 -2
  3. package/dist/budget.d.ts +143 -0
  4. package/dist/budget.d.ts.map +1 -0
  5. package/dist/budget.js +134 -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-api.d.ts +15 -0
  12. package/dist/check-api.d.ts.map +1 -0
  13. package/dist/check-api.js +11 -0
  14. package/dist/check-api.js.map +1 -0
  15. package/dist/check.d.ts +50 -0
  16. package/dist/check.d.ts.map +1 -0
  17. package/dist/check.js +198 -0
  18. package/dist/check.js.map +1 -0
  19. package/dist/cli.d.ts +85 -0
  20. package/dist/cli.d.ts.map +1 -0
  21. package/dist/cli.js +464 -0
  22. package/dist/cli.js.map +1 -0
  23. package/dist/index.d.ts +24 -0
  24. package/dist/index.d.ts.map +1 -0
  25. package/dist/index.js +17 -0
  26. package/dist/index.js.map +1 -0
  27. package/dist/measure.d.ts +81 -0
  28. package/dist/measure.d.ts.map +1 -0
  29. package/dist/measure.js +269 -0
  30. package/dist/measure.js.map +1 -0
  31. package/dist/memory.d.ts +43 -0
  32. package/dist/memory.d.ts.map +1 -0
  33. package/dist/memory.js +159 -0
  34. package/dist/memory.js.map +1 -0
  35. package/dist/plugin.d.ts +13 -0
  36. package/dist/plugin.d.ts.map +1 -0
  37. package/dist/plugin.js +456 -0
  38. package/dist/plugin.js.map +1 -0
  39. package/dist/probe.d.ts +74 -0
  40. package/dist/probe.d.ts.map +1 -0
  41. package/dist/probe.js +181 -0
  42. package/dist/probe.js.map +1 -0
  43. package/dist/render.d.ts +21 -0
  44. package/dist/render.d.ts.map +1 -0
  45. package/dist/render.js +63 -0
  46. package/dist/render.js.map +1 -0
  47. package/dist/run.d.ts +267 -0
  48. package/dist/run.d.ts.map +1 -0
  49. package/dist/run.js +257 -0
  50. package/dist/run.js.map +1 -0
  51. package/dist/session.d.ts +79 -0
  52. package/dist/session.d.ts.map +1 -0
  53. package/dist/session.js +263 -0
  54. package/dist/session.js.map +1 -0
  55. package/package.json +67 -3
@@ -0,0 +1,269 @@
1
+ /**
2
+ * Launch an app and measure it over cold and warm rounds on a throwaway profile.
3
+ *
4
+ * @module
5
+ */
6
+ import { mkdtemp, rm } from 'node:fs/promises';
7
+ import { tmpdir } from 'node:os';
8
+ import path from 'node:path';
9
+ import { NOOP_LOGGER, createServer } from '@electron-stagewright/core';
10
+ import { DEFAULT_WINDOW_MS } from './probe.js';
11
+ import { createRun } from './run.js';
12
+ import { measureSession, heavierMemory, sampleMemory, startWindow, } from './session.js';
13
+ export const DEFAULT_ROUNDS = 3;
14
+ export const DEFAULT_SETTLE_MS = 3_000;
15
+ export const DEFAULT_WORKLOAD_TIMEOUT_MS = 120_000;
16
+ export const DEFAULT_MEMORY_SAMPLE_MS = 1_000;
17
+ const LOAD_TIMEOUT_MS = 30_000;
18
+ // loadEventEnd is only set once the load event has finished.
19
+ const LOADED_BODY = `
20
+ await new Promise((resolve) => {
21
+ if (document.readyState === 'complete') resolve();
22
+ else addEventListener('load', resolve, { once: true });
23
+ });
24
+ await new Promise((resolve) => setTimeout(resolve, 0));
25
+ return true;
26
+ `;
27
+ const sleep = (ms) => new Promise((resolve) => setTimeout(resolve, ms));
28
+ async function withTimeout(work, ms, what) {
29
+ let timer;
30
+ try {
31
+ return await Promise.race([
32
+ work,
33
+ new Promise((_, reject) => {
34
+ timer = setTimeout(() => reject(new Error(`${what} timed out after ${ms} ms`)), ms);
35
+ }),
36
+ ]);
37
+ }
38
+ finally {
39
+ clearTimeout(timer);
40
+ }
41
+ }
42
+ /** Why main eval is unavailable on this session, or undefined when it works. */
43
+ async function mainEvalUnavailable(session) {
44
+ try {
45
+ await session.evaluate('main', 'return true');
46
+ return undefined;
47
+ }
48
+ catch (error) {
49
+ const reason = error instanceof Error ? error.message : String(error);
50
+ return `main eval is unavailable (${reason})`;
51
+ }
52
+ }
53
+ function memorySampleMs(options) {
54
+ if (options.memorySampleMs !== undefined)
55
+ return options.memorySampleMs;
56
+ const platform = process.platform;
57
+ return options.budget?.memory?.[platform]?.aggregate === 'peak'
58
+ ? DEFAULT_MEMORY_SAMPLE_MS
59
+ : undefined;
60
+ }
61
+ /** Bounds the wait for a sample still running when the window closes. */
62
+ const SAMPLER_STOP_MS = 5_000;
63
+ /**
64
+ * Samples memory until stopped; `peak` keeps the heaviest sample outside the GPU, or returns
65
+ * undefined when no sample during the window succeeded.
66
+ */
67
+ export function startSampler(session, source, intervalMs) {
68
+ if (intervalMs === undefined) {
69
+ return { stop: () => Promise.resolve(), peak: () => undefined };
70
+ }
71
+ let best;
72
+ let sampled = false;
73
+ let running = true;
74
+ let wake = () => undefined;
75
+ const keep = (sample) => {
76
+ if (sample !== undefined && (best === undefined || heavierMemory(sample, best)))
77
+ best = sample;
78
+ };
79
+ const loop = (async () => {
80
+ while (running) {
81
+ const sample = await sampleMemory(session, source);
82
+ if (!running)
83
+ break;
84
+ sampled ||= sample !== undefined;
85
+ keep(sample);
86
+ await new Promise((resolve) => {
87
+ const timer = setTimeout(resolve, intervalMs);
88
+ wake = () => {
89
+ clearTimeout(timer);
90
+ resolve();
91
+ };
92
+ });
93
+ }
94
+ })();
95
+ return {
96
+ stop: async () => {
97
+ running = false;
98
+ wake();
99
+ await withTimeout(loop, SAMPLER_STOP_MS, 'memory sampler').catch(() => undefined);
100
+ },
101
+ peak: (end) => {
102
+ if (!sampled)
103
+ return undefined;
104
+ keep(end);
105
+ return best;
106
+ },
107
+ };
108
+ }
109
+ function checkSetup(setup) {
110
+ if (setup === undefined)
111
+ return {};
112
+ const strings = (value) => Array.isArray(value) && value.every((item) => typeof item === 'string');
113
+ const env = setup.env;
114
+ if (typeof setup !== 'object' ||
115
+ (setup.args !== undefined && !strings(setup.args)) ||
116
+ (env !== undefined && (typeof env !== 'object' || env === null || !strings(Object.values(env))))) {
117
+ throw new TypeError('beforeRound must return { env?: Record<string, string>, args?: string[] }');
118
+ }
119
+ return setup;
120
+ }
121
+ /** Launch once on `profile` and, when `measure` is set, take one measurement. */
122
+ async function launchOnce(options, setup, profile, phase, measure) {
123
+ const server = await createServer({ allowEval: true, logger: NOOP_LOGGER });
124
+ const dispatch = (tool, args = {}) => server.dispatcher.dispatch(tool, args);
125
+ try {
126
+ const launched = await dispatch('electron_launch', {
127
+ ...options.launch,
128
+ ...(options.launch.env === undefined && setup.env === undefined
129
+ ? {}
130
+ : { env: { ...options.launch.env, ...setup.env } }),
131
+ args: [...(options.launch.args ?? []), ...(setup.args ?? []), `--user-data-dir=${profile}`],
132
+ });
133
+ if (launched['ok'] !== true) {
134
+ throw new Error(`could not launch the app: ${String(launched['error'] ?? launched['code'])}`);
135
+ }
136
+ const managed = server.sessions.resolve();
137
+ try {
138
+ const session = managed.session;
139
+ await withTimeout(session.evaluate('renderer', LOADED_BODY), LOAD_TIMEOUT_MS, 'window load');
140
+ await sleep(options.settleMs ?? DEFAULT_SETTLE_MS);
141
+ // A priming launch still loads and settles, so it leaves the profile a measured cold one would.
142
+ if (!measure)
143
+ return {};
144
+ const probe = options.budget?.ipc;
145
+ const ipc = probe === undefined
146
+ ? undefined
147
+ : {
148
+ probe: probe.probe,
149
+ ...(probe.intervalMs === undefined ? {} : { intervalMs: probe.intervalMs }),
150
+ };
151
+ // A build with the EnableNodeCliInspectArguments fuse off launches over CDP alone.
152
+ const mainUnavailable = await mainEvalUnavailable(session);
153
+ const open = await startWindow(session, {
154
+ allowRenderer: true,
155
+ ...(ipc === undefined ? {} : { ipc }),
156
+ ...(mainUnavailable === undefined ? {} : { mainUnavailable }),
157
+ });
158
+ const memoryProbe = options.budget?.memoryProbe;
159
+ const source = {
160
+ allowRenderer: true,
161
+ ...(mainUnavailable === undefined ? {} : { mainUnavailable }),
162
+ ...(memoryProbe === undefined ? {} : { memoryProbe }),
163
+ };
164
+ const sampler = startSampler(session, source, memorySampleMs(options));
165
+ const windowMs = options.windowMs ?? options.budget?.mainThread?.windowMs ?? DEFAULT_WINDOW_MS;
166
+ try {
167
+ if (options.workload === undefined)
168
+ await sleep(windowMs);
169
+ else {
170
+ // A timed-out workload keeps running; once the window closes its dispatch rejects, so a
171
+ // polling loop ends instead of keeping the process alive.
172
+ let ended = false;
173
+ const workloadDispatch = (tool, args) => ended
174
+ ? Promise.reject(new Error('the measurement window has closed'))
175
+ : dispatch(tool, args);
176
+ try {
177
+ await withTimeout(options.workload({ sessionId: managed.id, phase, dispatch: workloadDispatch }), options.workloadTimeoutMs ?? DEFAULT_WORKLOAD_TIMEOUT_MS, 'workload');
178
+ }
179
+ finally {
180
+ ended = true;
181
+ }
182
+ }
183
+ }
184
+ finally {
185
+ await sampler.stop();
186
+ }
187
+ const measured = await measureSession(session, {
188
+ allowRenderer: true,
189
+ open,
190
+ windowMs,
191
+ ...(mainUnavailable === undefined ? {} : { mainUnavailable }),
192
+ ...(memoryProbe === undefined ? {} : { memoryProbe }),
193
+ });
194
+ const sampleMs = memorySampleMs(options);
195
+ const peak = sampler.peak(measured.measurement.memory);
196
+ const unavailable = sampleMs !== undefined && peak === undefined
197
+ ? {
198
+ unavailable: {
199
+ ...measured.measurement.unavailable,
200
+ memoryPeak: 'no memory sample succeeded during the window',
201
+ },
202
+ }
203
+ : {};
204
+ return {
205
+ measurement: {
206
+ ...measured.measurement,
207
+ ...unavailable,
208
+ ...(peak === undefined ? {} : { memoryPeak: peak }),
209
+ },
210
+ ...(measured.electronVersion === undefined
211
+ ? {}
212
+ : { electronVersion: measured.electronVersion }),
213
+ };
214
+ }
215
+ finally {
216
+ await dispatch('electron_stop', { sessionId: managed.id }).catch(() => undefined);
217
+ }
218
+ }
219
+ finally {
220
+ await server.close().catch(() => undefined);
221
+ }
222
+ }
223
+ /**
224
+ * Measure `rounds` rounds. Each round gets an empty profile: the first launch on it is `cold`,
225
+ * the relaunch `warm`. Asking only for `warm` still does the cold launch, unmeasured.
226
+ */
227
+ export async function measureApp(options) {
228
+ const phases = options.phases ?? ['cold', 'warm'];
229
+ const rounds = [];
230
+ let electronVersion;
231
+ let firstFailure;
232
+ for (let round = 1; round <= (options.rounds ?? DEFAULT_ROUNDS); round += 1) {
233
+ const profile = await mkdtemp(path.join(tmpdir(), 'sw-budget-profile-'));
234
+ try {
235
+ const setup = checkSetup(await options.beforeRound?.({ round, profile }));
236
+ for (const phase of ['cold', 'warm']) {
237
+ const measure = phases.includes(phase);
238
+ if (!measure && !(phase === 'cold' && phases.includes('warm')))
239
+ continue;
240
+ options.onProgress?.(`round ${round}: ${phase}${measure ? '' : ' (priming, unmeasured)'}`);
241
+ const result = await launchOnce(options, setup, profile, phase, measure);
242
+ electronVersion ??= result.electronVersion;
243
+ if (result.measurement !== undefined) {
244
+ rounds.push({ round, phase, measurement: result.measurement });
245
+ }
246
+ }
247
+ }
248
+ catch (error) {
249
+ if (options.failedRounds !== 'skip')
250
+ throw error;
251
+ firstFailure ??= error;
252
+ // A warm launch shares the cold one's state, so the rest of the round is dropped too.
253
+ options.onProgress?.(`round ${round} failed and was skipped: ${error instanceof Error ? error.message : String(error)}`);
254
+ }
255
+ finally {
256
+ // A process that outlives the app can hold the profile; that must not discard the rounds.
257
+ await rm(profile, { recursive: true, force: true, maxRetries: 5, retryDelay: 200 }).catch((error) => options.onProgress?.(`could not remove ${profile}: ${error instanceof Error ? error.message : String(error)}`));
258
+ }
259
+ }
260
+ // Every round failing is a setup problem (a wrong path, a broken script), not a budget verdict.
261
+ if (rounds.length === 0 && firstFailure !== undefined)
262
+ throw firstFailure;
263
+ return createRun({
264
+ rounds,
265
+ ...(electronVersion === undefined ? {} : { electronVersion }),
266
+ ...(options.label === undefined ? {} : { label: options.label }),
267
+ });
268
+ }
269
+ //# sourceMappingURL=measure.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"measure.js","sourceRoot":"","sources":["../src/measure.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,43 @@
1
+ /**
2
+ * Per-process memory in the platform's own metric, read from the host by pid.
3
+ *
4
+ * `app.getAppMetrics()` supplies the pids, their types, and working set; the OS supplies the
5
+ * number a user sees: footprint on macOS, Pss on Linux, private bytes on Windows.
6
+ *
7
+ * @module
8
+ */
9
+ import type { MemoryMetric, ProcessType } from './budget.js';
10
+ import type { MemoryMeasurement } from './run.js';
11
+ /** One entry of `app.getAppMetrics()`, reduced to what the budget reads. */
12
+ export interface AppProcessMetric {
13
+ readonly pid: number;
14
+ readonly type: string;
15
+ readonly name?: string | null;
16
+ readonly workingSetKb: number;
17
+ /** Windows only. */
18
+ readonly privateKb?: number | null;
19
+ }
20
+ /** Map Electron's process type (`Browser`, `Tab`, `GPU`, `Utility`, ...) to a budget bucket. */
21
+ export declare function classifyProcess(type: string): ProcessType;
22
+ export declare function platformMetric(platform: string): MemoryMetric;
23
+ /** Bytes per pid from `footprint -j` output. Pids that already exited are simply absent. */
24
+ export declare function parseFootprintJson(text: string): Map<number, number>;
25
+ /** Pss in bytes from `/proc/<pid>/smaps_rollup`, or undefined when the line is missing. */
26
+ export declare function parseSmapsRollup(text: string): number | undefined;
27
+ /** Reads the OS metric for a set of pids. Injected so tests never touch the host. */
28
+ export type OsMemoryReader = (pids: readonly number[]) => Promise<Map<number, number>>;
29
+ /** Epoch ms a host pid started, `missing` when no such pid exists, undefined when unknown. */
30
+ export type HostStartTimeReader = (pid: number) => Promise<number | 'missing' | undefined>;
31
+ /** Boot time plus the pid's start ticks: how Chromium computes `process.getCreationTime()`. */
32
+ export declare function readProcStartTime(pid: number, read?: (file: string) => Promise<string>): Promise<number | 'missing' | undefined>;
33
+ export declare function hostStartTimeReader(platform: string): HostStartTimeReader | undefined;
34
+ export declare function osMemoryReader(platform: string): OsMemoryReader | undefined;
35
+ /**
36
+ * Build the memory sample for one moment. `error` explains an OS reader that failed; the sample
37
+ * then still carries working set, and its `metric` values are absent rather than guessed.
38
+ */
39
+ export declare function measureMemory(processes: readonly AppProcessMetric[], platform: string, reader?: OsMemoryReader | undefined): Promise<{
40
+ memory: MemoryMeasurement;
41
+ error?: string;
42
+ }>;
43
+ //# sourceMappingURL=memory.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"memory.d.ts","sourceRoot":"","sources":["../src/memory.ts"],"names":[],"mappings":"AAAA;;;;;;;GAOG;AAOH,OAAO,KAAK,EAAE,YAAY,EAAE,WAAW,EAAE,MAAM,aAAa,CAAA;AAC5D,OAAO,KAAK,EAAE,iBAAiB,EAAE,MAAM,UAAU,CAAA;AAEjD,4EAA4E;AAC5E,MAAM,WAAW,gBAAgB;IAC/B,QAAQ,CAAC,GAAG,EAAE,MAAM,CAAA;IACpB,QAAQ,CAAC,IAAI,EAAE,MAAM,CAAA;IACrB,QAAQ,CAAC,IAAI,CAAC,EAAE,MAAM,GAAG,IAAI,CAAA;IAC7B,QAAQ,CAAC,YAAY,EAAE,MAAM,CAAA;IAC7B,oBAAoB;IACpB,QAAQ,CAAC,SAAS,CAAC,EAAE,MAAM,GAAG,IAAI,CAAA;CACnC;AAKD,gGAAgG;AAChG,wBAAgB,eAAe,CAAC,IAAI,EAAE,MAAM,GAAG,WAAW,CAazD;AAED,wBAAgB,cAAc,CAAC,QAAQ,EAAE,MAAM,GAAG,YAAY,CAK7D;AAED,4FAA4F;AAC5F,wBAAgB,kBAAkB,CAAC,IAAI,EAAE,MAAM,GAAG,GAAG,CAAC,MAAM,EAAE,MAAM,CAAC,CAapE;AAED,2FAA2F;AAC3F,wBAAgB,gBAAgB,CAAC,IAAI,EAAE,MAAM,GAAG,MAAM,GAAG,SAAS,CAGjE;AAED,qFAAqF;AACrF,MAAM,MAAM,cAAc,GAAG,CAAC,IAAI,EAAE,SAAS,MAAM,EAAE,KAAK,OAAO,CAAC,GAAG,CAAC,MAAM,EAAE,MAAM,CAAC,CAAC,CAAA;AAmCtF,8FAA8F;AAC9F,MAAM,MAAM,mBAAmB,GAAG,CAAC,GAAG,EAAE,MAAM,KAAK,OAAO,CAAC,MAAM,GAAG,SAAS,GAAG,SAAS,CAAC,CAAA;AAK1F,+FAA+F;AAC/F,wBAAsB,iBAAiB,CACrC,GAAG,EAAE,MAAM,EACX,IAAI,GAAE,CAAC,IAAI,EAAE,MAAM,KAAK,OAAO,CAAC,MAAM,CAAoC,GACzE,OAAO,CAAC,MAAM,GAAG,SAAS,GAAG,SAAS,CAAC,CASzC;AAqBD,wBAAgB,mBAAmB,CAAC,QAAQ,EAAE,MAAM,GAAG,mBAAmB,GAAG,SAAS,CAIrF;AAED,wBAAgB,cAAc,CAAC,QAAQ,EAAE,MAAM,GAAG,cAAc,GAAG,SAAS,CAI3E;AAED;;;GAGG;AACH,wBAAsB,aAAa,CACjC,SAAS,EAAE,SAAS,gBAAgB,EAAE,EACtC,QAAQ,EAAE,MAAM,EAChB,MAAM,GAAE,cAAc,GAAG,SAAoC,GAC5D,OAAO,CAAC;IAAE,MAAM,EAAE,iBAAiB,CAAC;IAAC,KAAK,CAAC,EAAE,MAAM,CAAA;CAAE,CAAC,CA8BxD"}
package/dist/memory.js ADDED
@@ -0,0 +1,159 @@
1
+ /**
2
+ * Per-process memory in the platform's own metric, read from the host by pid.
3
+ *
4
+ * `app.getAppMetrics()` supplies the pids, their types, and working set; the OS supplies the
5
+ * number a user sees: footprint on macOS, Pss on Linux, private bytes on Windows.
6
+ *
7
+ * @module
8
+ */
9
+ import { execFile } from 'node:child_process';
10
+ import { mkdtemp, readFile, rm } from 'node:fs/promises';
11
+ import { tmpdir } from 'node:os';
12
+ import path from 'node:path';
13
+ const MB = 1024 * 1024;
14
+ const round1 = (value) => Math.round(value * 10) / 10;
15
+ /** Map Electron's process type (`Browser`, `Tab`, `GPU`, `Utility`, ...) to a budget bucket. */
16
+ export function classifyProcess(type) {
17
+ switch (type) {
18
+ case 'Browser':
19
+ return 'main';
20
+ case 'Tab':
21
+ return 'renderer';
22
+ case 'GPU':
23
+ return 'gpu';
24
+ case 'Utility':
25
+ return 'utility';
26
+ default:
27
+ return 'other';
28
+ }
29
+ }
30
+ export function platformMetric(platform) {
31
+ if (platform === 'darwin')
32
+ return 'footprint';
33
+ if (platform === 'linux')
34
+ return 'pss';
35
+ if (platform === 'win32')
36
+ return 'privateBytes';
37
+ return 'workingSet';
38
+ }
39
+ /** Bytes per pid from `footprint -j` output. Pids that already exited are simply absent. */
40
+ export function parseFootprintJson(text) {
41
+ const parsed = JSON.parse(text);
42
+ const unit = parsed['bytes per unit'] ?? 1;
43
+ const bytes = new Map();
44
+ for (const entry of parsed.processes ?? []) {
45
+ if (typeof entry.pid === 'number' && typeof entry.footprint === 'number') {
46
+ bytes.set(entry.pid, entry.footprint * unit);
47
+ }
48
+ }
49
+ return bytes;
50
+ }
51
+ /** Pss in bytes from `/proc/<pid>/smaps_rollup`, or undefined when the line is missing. */
52
+ export function parseSmapsRollup(text) {
53
+ const match = /^Pss:\s+(\d+)\s+kB/m.exec(text);
54
+ return match?.[1] === undefined ? undefined : Number(match[1]) * 1024;
55
+ }
56
+ const FOOTPRINT_TIMEOUT_MS = 15_000;
57
+ async function readFootprint(pids) {
58
+ // footprint only writes JSON to a regular file.
59
+ const dir = await mkdtemp(path.join(tmpdir(), 'sw-budget-fp-'));
60
+ const file = path.join(dir, 'footprint.json');
61
+ try {
62
+ await new Promise((resolve, reject) => {
63
+ execFile('/usr/bin/footprint', ['-j', file, ...pids.map(String)], { timeout: FOOTPRINT_TIMEOUT_MS, maxBuffer: 16 * MB }, (error) => (error === null ? resolve() : reject(error)));
64
+ });
65
+ return parseFootprintJson(await readFile(file, 'utf8'));
66
+ }
67
+ finally {
68
+ await rm(dir, { recursive: true, force: true });
69
+ }
70
+ }
71
+ async function readPss(pids) {
72
+ const bytes = new Map();
73
+ await Promise.all(pids.map(async (pid) => {
74
+ const text = await readFile(`/proc/${pid}/smaps_rollup`, 'utf8').catch(() => undefined);
75
+ const pss = text === undefined ? undefined : parseSmapsRollup(text);
76
+ if (pss !== undefined)
77
+ bytes.set(pid, pss);
78
+ }));
79
+ return bytes;
80
+ }
81
+ /** Linux exposes start times in clock ticks; USER_HZ is 100 on every supported architecture. */
82
+ const CLOCK_TICKS_PER_SECOND = 100;
83
+ /** Boot time plus the pid's start ticks: how Chromium computes `process.getCreationTime()`. */
84
+ export async function readProcStartTime(pid, read = (file) => readFile(file, 'utf8')) {
85
+ const stat = await read(`/proc/${pid}/stat`).catch(() => undefined);
86
+ if (stat === undefined)
87
+ return 'missing';
88
+ const procStat = await read('/proc/stat').catch(() => '');
89
+ // The command name may hold spaces and parentheses; fields resume after the last ')'.
90
+ const ticks = Number(stat.slice(stat.lastIndexOf(')') + 2).split(' ')[19]);
91
+ const bootSeconds = Number(/^btime (\d+)$/m.exec(procStat)?.[1]);
92
+ if (!Number.isFinite(ticks) || !Number.isFinite(bootSeconds))
93
+ return undefined;
94
+ return bootSeconds * 1000 + (ticks / CLOCK_TICKS_PER_SECOND) * 1000;
95
+ }
96
+ function readPsStartTime(pid) {
97
+ return new Promise((resolve) => {
98
+ execFile('ps', ['-o', 'lstart=', '-p', String(pid)],
99
+ // lstart follows the locale; only the C locale is one Date.parse reads.
100
+ { timeout: 5_000, env: { ...process.env, LC_ALL: 'C' } }, (error, stdout, stderr) => {
101
+ // ps exits 1 silently for an unknown pid; a ps that rejects the flags says why on stderr.
102
+ if (error !== null) {
103
+ return resolve(error.code === 1 && stderr.trim() === '' ? 'missing' : undefined);
104
+ }
105
+ const started = Date.parse(stdout.trim());
106
+ resolve(Number.isNaN(started) ? undefined : started);
107
+ });
108
+ });
109
+ }
110
+ export function hostStartTimeReader(platform) {
111
+ if (platform === 'linux')
112
+ return (pid) => readProcStartTime(pid);
113
+ if (platform === 'darwin')
114
+ return readPsStartTime;
115
+ return undefined;
116
+ }
117
+ export function osMemoryReader(platform) {
118
+ if (platform === 'darwin')
119
+ return readFootprint;
120
+ if (platform === 'linux')
121
+ return readPss;
122
+ return undefined;
123
+ }
124
+ /**
125
+ * Build the memory sample for one moment. `error` explains an OS reader that failed; the sample
126
+ * then still carries working set, and its `metric` values are absent rather than guessed.
127
+ */
128
+ export async function measureMemory(processes, platform, reader = osMemoryReader(platform)) {
129
+ const metric = platformMetric(platform);
130
+ let osBytes = new Map();
131
+ let error;
132
+ if (reader !== undefined) {
133
+ try {
134
+ osBytes = await reader(processes.map((entry) => entry.pid));
135
+ }
136
+ catch (cause) {
137
+ error = `could not read ${metric}: ${cause instanceof Error ? cause.message : String(cause)}`;
138
+ }
139
+ }
140
+ const memory = {
141
+ metric,
142
+ processes: processes.map((entry) => {
143
+ const bytes = metric === 'privateBytes'
144
+ ? typeof entry.privateKb === 'number'
145
+ ? entry.privateKb * 1024
146
+ : undefined
147
+ : osBytes.get(entry.pid);
148
+ return {
149
+ pid: entry.pid,
150
+ type: classifyProcess(entry.type),
151
+ ...(typeof entry.name === 'string' && entry.name.length > 0 ? { name: entry.name } : {}),
152
+ ...(bytes === undefined ? {} : { mb: round1(bytes / MB) }),
153
+ workingSetMb: round1(entry.workingSetKb / 1024),
154
+ };
155
+ }),
156
+ };
157
+ return { memory, ...(error === undefined ? {} : { error }) };
158
+ }
159
+ //# sourceMappingURL=memory.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"memory.js","sourceRoot":"","sources":["../src/memory.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,13 @@
1
+ /**
2
+ * The MCP face of the budget: `budget_start`, `budget_measure`, `budget_check`.
3
+ *
4
+ * Runtime metrics run fixed bodies in the app's main process, so they need the main eval grant;
5
+ * the IPC probe and startup timing also need the renderer. Bundle size needs neither, nor a session.
6
+ *
7
+ * @module
8
+ */
9
+ import { type StagewrightPlugin } from '@electron-stagewright/core';
10
+ export declare function createBudgetPlugin(): StagewrightPlugin;
11
+ /** Load with `--plugin budget` or `createServer({ plugins: [budgetPlugin] })`. */
12
+ export declare const budgetPlugin: StagewrightPlugin;
13
+ //# sourceMappingURL=plugin.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"plugin.d.ts","sourceRoot":"","sources":["../src/plugin.ts"],"names":[],"mappings":"AAAA;;;;;;;GAOG;AAKH,OAAO,EAQL,KAAK,iBAAiB,EAIvB,MAAM,4BAA4B,CAAA;AA2EnC,wBAAgB,kBAAkB,IAAI,iBAAiB,CAubtD;AAED,kFAAkF;AAClF,eAAO,MAAM,YAAY,EAAE,iBAe1B,CAAA"}