@buoy-gg/mcp 7.0.21 → 7.0.23
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/dist/{chunk-JQ5GZ4LG.mjs → chunk-SGRL6UKT.mjs} +1391 -110
- package/dist/cli.js +1391 -110
- package/dist/cli.mjs +1 -1
- package/dist/index.d.mts +167 -11
- package/dist/index.d.ts +167 -11
- package/dist/index.js +1391 -110
- package/dist/index.mjs +1 -1
- package/package.json +5 -3
package/dist/cli.mjs
CHANGED
package/dist/index.d.mts
CHANGED
|
@@ -24,6 +24,14 @@ interface DeviceInfo {
|
|
|
24
24
|
isConnected: boolean;
|
|
25
25
|
/** Whether the connected app reported an active Buoy Pro license. */
|
|
26
26
|
isPro: boolean;
|
|
27
|
+
/**
|
|
28
|
+
* The app's access tier. Apps older than the tier split report only `isPro`,
|
|
29
|
+
* which maps exactly — a key could only ever grant Pro back then.
|
|
30
|
+
*
|
|
31
|
+
* Surfaced so `list_devices` can say WHY the MCP is blocked: "signed in on
|
|
32
|
+
* the free tier" and "no account at all" are different problems.
|
|
33
|
+
*/
|
|
34
|
+
tier: "anonymous" | "free" | "pro";
|
|
27
35
|
tools: ToolCapability[];
|
|
28
36
|
}
|
|
29
37
|
/** How the client obtained its broker connection. */
|
|
@@ -57,11 +65,31 @@ declare class SyncClient {
|
|
|
57
65
|
private pendingActions;
|
|
58
66
|
/** Callbacks waiting for the next snapshot of `${deviceId}::${toolId}`. */
|
|
59
67
|
private snapshotWaiters;
|
|
68
|
+
/** When each cached snapshot arrived, keyed by snapKey (staleness reporting). */
|
|
69
|
+
private snapshotAt;
|
|
70
|
+
/** Liveness per device — see {@link noteTimeout}. */
|
|
71
|
+
private health;
|
|
72
|
+
/** Dev-server origin per device — see {@link noteDevServerOrigin}. */
|
|
73
|
+
private devServerOrigins;
|
|
74
|
+
/** Devices whose origin probe already ran (a production bundle has none). */
|
|
75
|
+
private originProbed;
|
|
76
|
+
/** Last time each device appeared in the broker's device list. */
|
|
77
|
+
private lastSeen;
|
|
60
78
|
constructor(opts?: SyncClientOptions);
|
|
61
79
|
private readonly autoSpawn;
|
|
62
80
|
/** Connect to a running broker, or spawn one in-process, then handshake. */
|
|
63
81
|
start(): Promise<BrokerMode>;
|
|
64
82
|
private connectSocket;
|
|
83
|
+
/**
|
|
84
|
+
* Drop everything remembered about devices that have been gone a while.
|
|
85
|
+
*
|
|
86
|
+
* Deliberately NOT on disconnect: a crashed app often drops off the broker,
|
|
87
|
+
* and its cached console snapshot is the crash evidence — throwing that away
|
|
88
|
+
* the instant it disconnects would delete the answer. But device ids change
|
|
89
|
+
* with every simulator erase and fresh dev client, and each one can hold
|
|
90
|
+
* multi-MB snapshots, so a long-lived server has to forget eventually.
|
|
91
|
+
*/
|
|
92
|
+
private pruneStaleDevices;
|
|
65
93
|
/** Disconnect and tear down a spawned broker, if any. */
|
|
66
94
|
stop(): Promise<void>;
|
|
67
95
|
/** All devices the broker knows about, merged with their advertised tools. */
|
|
@@ -91,6 +119,61 @@ declare class SyncClient {
|
|
|
91
119
|
* is blocked.
|
|
92
120
|
*/
|
|
93
121
|
requireProDevice(deviceId?: string): string;
|
|
122
|
+
private healthOf;
|
|
123
|
+
/** The device answered — it's alive. */
|
|
124
|
+
private noteOk;
|
|
125
|
+
/**
|
|
126
|
+
* The device didn't answer. A crashed app never answers again, so after the
|
|
127
|
+
* first miss we stop paying the full timeout on every subsequent call.
|
|
128
|
+
*/
|
|
129
|
+
private noteTimeout;
|
|
130
|
+
/** True once a device has missed a call and hasn't answered since. */
|
|
131
|
+
isUnresponsive(deviceId: string): boolean;
|
|
132
|
+
/**
|
|
133
|
+
* Full timeout normally; a short one for a device already not answering.
|
|
134
|
+
*
|
|
135
|
+
* With one escape hatch: every RECOVERY_PROBE_MS a flagged device gets the
|
|
136
|
+
* full budget again. Without it the flag is self-sustaining — a device whose
|
|
137
|
+
* work legitimately takes longer than the short budget (a big snapshot, a
|
|
138
|
+
* slow simulator) can never answer, so it can never clear the flag, and every
|
|
139
|
+
* later reply is a confident "most likely CRASHED" about a healthy app.
|
|
140
|
+
*/
|
|
141
|
+
private timeoutFor;
|
|
142
|
+
/**
|
|
143
|
+
* Plain-language diagnosis for a device that isn't answering, including the
|
|
144
|
+
* app's last words if the console tool got a crash flush out in time.
|
|
145
|
+
*
|
|
146
|
+
* A silent timeout is the worst possible answer here: the app is on a redbox,
|
|
147
|
+
* the reason is already captured, and without this the caller reads "timed
|
|
148
|
+
* out" and keeps retrying the same dead device.
|
|
149
|
+
*/
|
|
150
|
+
unresponsiveNote(deviceId: string): string;
|
|
151
|
+
/**
|
|
152
|
+
* The newest error line the console tool synced for this device, if any.
|
|
153
|
+
* Reads the cached console snapshot rather than the device (which, by the
|
|
154
|
+
* time this matters, is not answering).
|
|
155
|
+
*/
|
|
156
|
+
private lastFatalFor;
|
|
157
|
+
/** Age of the cached snapshot for a tool, in ms (null = never received). */
|
|
158
|
+
snapshotAgeMs(toolId: string, deviceId: string): number | null;
|
|
159
|
+
/**
|
|
160
|
+
* Remember where a device's bundle came from, reported by core's `app`
|
|
161
|
+
* adapter while the app is healthy. It is the only way to reload an app that
|
|
162
|
+
* has crashed: the in-app reload needs the React tree that the crash
|
|
163
|
+
* destroyed, but the dev server's reload is handled natively.
|
|
164
|
+
*/
|
|
165
|
+
noteDevServerOrigin(deviceId: string, origin: unknown): void;
|
|
166
|
+
/**
|
|
167
|
+
* Ask a freshly-announced device where its bundle came from, once.
|
|
168
|
+
*
|
|
169
|
+
* Deliberately eager: this is the piece of information that can ONLY be
|
|
170
|
+
* collected while the app is healthy, and is only ever needed after it has
|
|
171
|
+
* stopped answering. One cheap round-trip per device buys the ability to
|
|
172
|
+
* recover a crashed app instead of telling the caller to go do it by hand.
|
|
173
|
+
*/
|
|
174
|
+
private learnDevServerOrigin;
|
|
175
|
+
/** The dev-server origin last reported by this device, if any. */
|
|
176
|
+
devServerOrigin(deviceId: string): string | null;
|
|
94
177
|
/**
|
|
95
178
|
* Keep the device RECORDING this tool, without asking it to stream.
|
|
96
179
|
*
|
|
@@ -135,10 +218,14 @@ declare class SyncClient {
|
|
|
135
218
|
*/
|
|
136
219
|
getSnapshot(toolId: string, deviceId?: string, opts?: {
|
|
137
220
|
timeoutMs?: number;
|
|
221
|
+
silent?: boolean;
|
|
138
222
|
}): Promise<{
|
|
139
223
|
deviceId: string;
|
|
140
224
|
payload: unknown;
|
|
225
|
+
stale?: string;
|
|
141
226
|
}>;
|
|
227
|
+
/** Header line for data served from cache because the device went quiet. */
|
|
228
|
+
private staleNote;
|
|
142
229
|
private waitForSnapshot;
|
|
143
230
|
/**
|
|
144
231
|
* Invoke a remote action on a device and await its result. Pass deviceId to
|
|
@@ -146,14 +233,10 @@ declare class SyncClient {
|
|
|
146
233
|
*/
|
|
147
234
|
callAction(toolId: string, action: string, params?: unknown, deviceId?: string, opts?: {
|
|
148
235
|
timeoutMs?: number;
|
|
236
|
+
silent?: boolean;
|
|
149
237
|
}): Promise<unknown>;
|
|
150
238
|
}
|
|
151
239
|
|
|
152
|
-
/**
|
|
153
|
-
* Build the Buoy MCP server and register all tools against a connected
|
|
154
|
-
* {@link SyncClient}. Tool packs (perf-monitor, etc.) are layered on top of the
|
|
155
|
-
* generic core here.
|
|
156
|
-
*/
|
|
157
240
|
declare function createMcpServer(sync: SyncClient): McpServer;
|
|
158
241
|
|
|
159
242
|
/**
|
|
@@ -351,6 +434,12 @@ interface AutomationConfig {
|
|
|
351
434
|
runsPerCase?: number;
|
|
352
435
|
coolDownMs?: number;
|
|
353
436
|
discardWarmupRuns?: number;
|
|
437
|
+
/**
|
|
438
|
+
* Run a throwaway case at the front of the batch and drop it from the
|
|
439
|
+
* results, so the batch's cold start doesn't land on real data. Default true.
|
|
440
|
+
* See the perf-monitor AutomationConfig for the reproduction.
|
|
441
|
+
*/
|
|
442
|
+
discardWarmupCase?: boolean;
|
|
354
443
|
shuffleCases?: boolean;
|
|
355
444
|
/** Capture per-component render counts/durations each run. Default true. */
|
|
356
445
|
captureRenders?: boolean;
|
|
@@ -378,13 +467,17 @@ interface IndexEntry {
|
|
|
378
467
|
caseId?: string;
|
|
379
468
|
runIndex?: number;
|
|
380
469
|
isMedianRun?: boolean;
|
|
470
|
+
/** Thermal state at run start; a throttled run reads cheap AND janky. */
|
|
471
|
+
thermalState?: "nominal" | "fair" | "serious" | "critical";
|
|
381
472
|
/** Render-capture mirror (absent when capture was unavailable/disabled). */
|
|
382
473
|
renderCommits?: number;
|
|
383
474
|
renderTotalMs?: number;
|
|
475
|
+
renderWasted?: number;
|
|
384
476
|
topRenderers?: {
|
|
385
477
|
name: string;
|
|
386
478
|
renders: number;
|
|
387
479
|
totalMs: number;
|
|
480
|
+
cause?: string;
|
|
388
481
|
}[];
|
|
389
482
|
}
|
|
390
483
|
/** Per-metric min/max/avg/p95 channel. */
|
|
@@ -406,12 +499,40 @@ interface BenchmarkReportStats {
|
|
|
406
499
|
uiJankFrames: number;
|
|
407
500
|
deviceMaxRefreshRate: number;
|
|
408
501
|
}
|
|
409
|
-
|
|
410
502
|
/**
|
|
411
|
-
*
|
|
412
|
-
*
|
|
413
|
-
*
|
|
503
|
+
* Why components rendered, counted over a run.
|
|
504
|
+
*
|
|
505
|
+
* `propsUnstable` is the actionable one: props changed by IDENTITY only —
|
|
506
|
+
* a fresh arrow or object literal handed down every render — while every
|
|
507
|
+
* value stayed equal. Reporting that as plain "props" sends you into the
|
|
508
|
+
* child, when the fix is always in the parent.
|
|
414
509
|
*/
|
|
510
|
+
interface RenderCauseCounts {
|
|
511
|
+
mount?: number;
|
|
512
|
+
props?: number;
|
|
513
|
+
propsUnstable?: number;
|
|
514
|
+
hooks?: number;
|
|
515
|
+
parent?: number;
|
|
516
|
+
}
|
|
517
|
+
interface RenderCapture {
|
|
518
|
+
totalCommits: number;
|
|
519
|
+
totalRenders: number;
|
|
520
|
+
capturedDurations: boolean;
|
|
521
|
+
totalRenderMs?: number;
|
|
522
|
+
/** Parent cascades + identity-only prop churn: the removable share. */
|
|
523
|
+
wastedRenders?: number;
|
|
524
|
+
causes?: RenderCauseCounts;
|
|
525
|
+
topComponents: {
|
|
526
|
+
name: string;
|
|
527
|
+
renders: number;
|
|
528
|
+
totalMs: number;
|
|
529
|
+
maxMs: number;
|
|
530
|
+
avgMs: number;
|
|
531
|
+
causes?: RenderCauseCounts;
|
|
532
|
+
changedProps?: string[];
|
|
533
|
+
parentName?: string;
|
|
534
|
+
}[];
|
|
535
|
+
}
|
|
415
536
|
|
|
416
537
|
interface BuildConfigInput {
|
|
417
538
|
targetRoute: string;
|
|
@@ -423,12 +544,13 @@ interface BuildConfigInput {
|
|
|
423
544
|
coolDownMs?: number;
|
|
424
545
|
navTimeoutMs?: number;
|
|
425
546
|
discardWarmupRuns?: number;
|
|
547
|
+
discardWarmupCase?: boolean;
|
|
426
548
|
shuffleCases?: boolean;
|
|
427
549
|
reloadBetweenCases?: boolean;
|
|
428
550
|
captureRenders?: boolean;
|
|
429
551
|
}
|
|
430
552
|
/** The settings subset a driver inherits from the device's saved config. */
|
|
431
|
-
type BenchmarkSettings = Partial<Pick<AutomationConfig, "perCaseDurationMs" | "settleMs" | "navTimeoutMs" | "runsPerCase" | "coolDownMs" | "discardWarmupRuns" | "shuffleCases" | "reloadBetweenCases" | "captureRenders">>;
|
|
553
|
+
type BenchmarkSettings = Partial<Pick<AutomationConfig, "perCaseDurationMs" | "settleMs" | "navTimeoutMs" | "runsPerCase" | "coolDownMs" | "discardWarmupRuns" | "discardWarmupCase" | "shuffleCases" | "reloadBetweenCases" | "captureRenders">>;
|
|
432
554
|
/**
|
|
433
555
|
* Build + validate the AutomationConfig the device expects. Throws on bad input.
|
|
434
556
|
*
|
|
@@ -461,13 +583,17 @@ interface RankRow {
|
|
|
461
583
|
deltaJsFps: number | null;
|
|
462
584
|
/** At-risk axes for this case (e.g. "UI thread drops frames", "CPU near cap"). */
|
|
463
585
|
flags: string[];
|
|
586
|
+
/** Thermal state at the median run's start, when the device reported one. */
|
|
587
|
+
thermalState?: "nominal" | "fair" | "serious" | "critical";
|
|
464
588
|
/** React commits during the median run (null when capture was off/unavailable). */
|
|
465
589
|
renderCommits: number | null;
|
|
466
590
|
/** Top components by total render self-time during the median run. */
|
|
591
|
+
renderWasted?: number;
|
|
467
592
|
topRenderers: {
|
|
468
593
|
name: string;
|
|
469
594
|
renders: number;
|
|
470
595
|
totalMs: number;
|
|
596
|
+
cause?: string;
|
|
471
597
|
}[];
|
|
472
598
|
}
|
|
473
599
|
interface BatchRanking {
|
|
@@ -506,14 +632,44 @@ declare function rankBatchFromIndex(index: IndexEntry[], batchId: string, casesO
|
|
|
506
632
|
}[]): BatchRanking;
|
|
507
633
|
/** Render a batch ranking as a trade-off-aware report (no single false winner). */
|
|
508
634
|
declare function formatRankingMarkdown(ranking: BatchRanking): string;
|
|
509
|
-
/**
|
|
635
|
+
/**
|
|
636
|
+
* Side-by-side comparison of two full reports.
|
|
637
|
+
*
|
|
638
|
+
* Renders are part of the comparison, not an afterthought: when someone runs
|
|
639
|
+
* this after a memoization fix, "did the render count actually drop" IS the
|
|
640
|
+
* question, and for a long while this function silently dropped the block
|
|
641
|
+
* that answers it.
|
|
642
|
+
*/
|
|
510
643
|
declare function compareReportStats(a: {
|
|
511
644
|
name: string;
|
|
512
645
|
stats: BenchmarkReportStats;
|
|
646
|
+
renders?: RenderCapture;
|
|
513
647
|
}, b: {
|
|
514
648
|
name: string;
|
|
515
649
|
stats: BenchmarkReportStats;
|
|
650
|
+
renders?: RenderCapture;
|
|
516
651
|
}): {
|
|
652
|
+
renders?: {
|
|
653
|
+
total: {
|
|
654
|
+
a: number;
|
|
655
|
+
b: number;
|
|
656
|
+
delta: number;
|
|
657
|
+
};
|
|
658
|
+
commits: {
|
|
659
|
+
a: number;
|
|
660
|
+
b: number;
|
|
661
|
+
delta: number;
|
|
662
|
+
};
|
|
663
|
+
wasted: {
|
|
664
|
+
a: number;
|
|
665
|
+
b: number;
|
|
666
|
+
delta: number;
|
|
667
|
+
};
|
|
668
|
+
causes: {
|
|
669
|
+
a: RenderCauseCounts;
|
|
670
|
+
b: RenderCauseCounts;
|
|
671
|
+
};
|
|
672
|
+
} | undefined;
|
|
517
673
|
a: string;
|
|
518
674
|
b: string;
|
|
519
675
|
uiFps: {
|
package/dist/index.d.ts
CHANGED
|
@@ -24,6 +24,14 @@ interface DeviceInfo {
|
|
|
24
24
|
isConnected: boolean;
|
|
25
25
|
/** Whether the connected app reported an active Buoy Pro license. */
|
|
26
26
|
isPro: boolean;
|
|
27
|
+
/**
|
|
28
|
+
* The app's access tier. Apps older than the tier split report only `isPro`,
|
|
29
|
+
* which maps exactly — a key could only ever grant Pro back then.
|
|
30
|
+
*
|
|
31
|
+
* Surfaced so `list_devices` can say WHY the MCP is blocked: "signed in on
|
|
32
|
+
* the free tier" and "no account at all" are different problems.
|
|
33
|
+
*/
|
|
34
|
+
tier: "anonymous" | "free" | "pro";
|
|
27
35
|
tools: ToolCapability[];
|
|
28
36
|
}
|
|
29
37
|
/** How the client obtained its broker connection. */
|
|
@@ -57,11 +65,31 @@ declare class SyncClient {
|
|
|
57
65
|
private pendingActions;
|
|
58
66
|
/** Callbacks waiting for the next snapshot of `${deviceId}::${toolId}`. */
|
|
59
67
|
private snapshotWaiters;
|
|
68
|
+
/** When each cached snapshot arrived, keyed by snapKey (staleness reporting). */
|
|
69
|
+
private snapshotAt;
|
|
70
|
+
/** Liveness per device — see {@link noteTimeout}. */
|
|
71
|
+
private health;
|
|
72
|
+
/** Dev-server origin per device — see {@link noteDevServerOrigin}. */
|
|
73
|
+
private devServerOrigins;
|
|
74
|
+
/** Devices whose origin probe already ran (a production bundle has none). */
|
|
75
|
+
private originProbed;
|
|
76
|
+
/** Last time each device appeared in the broker's device list. */
|
|
77
|
+
private lastSeen;
|
|
60
78
|
constructor(opts?: SyncClientOptions);
|
|
61
79
|
private readonly autoSpawn;
|
|
62
80
|
/** Connect to a running broker, or spawn one in-process, then handshake. */
|
|
63
81
|
start(): Promise<BrokerMode>;
|
|
64
82
|
private connectSocket;
|
|
83
|
+
/**
|
|
84
|
+
* Drop everything remembered about devices that have been gone a while.
|
|
85
|
+
*
|
|
86
|
+
* Deliberately NOT on disconnect: a crashed app often drops off the broker,
|
|
87
|
+
* and its cached console snapshot is the crash evidence — throwing that away
|
|
88
|
+
* the instant it disconnects would delete the answer. But device ids change
|
|
89
|
+
* with every simulator erase and fresh dev client, and each one can hold
|
|
90
|
+
* multi-MB snapshots, so a long-lived server has to forget eventually.
|
|
91
|
+
*/
|
|
92
|
+
private pruneStaleDevices;
|
|
65
93
|
/** Disconnect and tear down a spawned broker, if any. */
|
|
66
94
|
stop(): Promise<void>;
|
|
67
95
|
/** All devices the broker knows about, merged with their advertised tools. */
|
|
@@ -91,6 +119,61 @@ declare class SyncClient {
|
|
|
91
119
|
* is blocked.
|
|
92
120
|
*/
|
|
93
121
|
requireProDevice(deviceId?: string): string;
|
|
122
|
+
private healthOf;
|
|
123
|
+
/** The device answered — it's alive. */
|
|
124
|
+
private noteOk;
|
|
125
|
+
/**
|
|
126
|
+
* The device didn't answer. A crashed app never answers again, so after the
|
|
127
|
+
* first miss we stop paying the full timeout on every subsequent call.
|
|
128
|
+
*/
|
|
129
|
+
private noteTimeout;
|
|
130
|
+
/** True once a device has missed a call and hasn't answered since. */
|
|
131
|
+
isUnresponsive(deviceId: string): boolean;
|
|
132
|
+
/**
|
|
133
|
+
* Full timeout normally; a short one for a device already not answering.
|
|
134
|
+
*
|
|
135
|
+
* With one escape hatch: every RECOVERY_PROBE_MS a flagged device gets the
|
|
136
|
+
* full budget again. Without it the flag is self-sustaining — a device whose
|
|
137
|
+
* work legitimately takes longer than the short budget (a big snapshot, a
|
|
138
|
+
* slow simulator) can never answer, so it can never clear the flag, and every
|
|
139
|
+
* later reply is a confident "most likely CRASHED" about a healthy app.
|
|
140
|
+
*/
|
|
141
|
+
private timeoutFor;
|
|
142
|
+
/**
|
|
143
|
+
* Plain-language diagnosis for a device that isn't answering, including the
|
|
144
|
+
* app's last words if the console tool got a crash flush out in time.
|
|
145
|
+
*
|
|
146
|
+
* A silent timeout is the worst possible answer here: the app is on a redbox,
|
|
147
|
+
* the reason is already captured, and without this the caller reads "timed
|
|
148
|
+
* out" and keeps retrying the same dead device.
|
|
149
|
+
*/
|
|
150
|
+
unresponsiveNote(deviceId: string): string;
|
|
151
|
+
/**
|
|
152
|
+
* The newest error line the console tool synced for this device, if any.
|
|
153
|
+
* Reads the cached console snapshot rather than the device (which, by the
|
|
154
|
+
* time this matters, is not answering).
|
|
155
|
+
*/
|
|
156
|
+
private lastFatalFor;
|
|
157
|
+
/** Age of the cached snapshot for a tool, in ms (null = never received). */
|
|
158
|
+
snapshotAgeMs(toolId: string, deviceId: string): number | null;
|
|
159
|
+
/**
|
|
160
|
+
* Remember where a device's bundle came from, reported by core's `app`
|
|
161
|
+
* adapter while the app is healthy. It is the only way to reload an app that
|
|
162
|
+
* has crashed: the in-app reload needs the React tree that the crash
|
|
163
|
+
* destroyed, but the dev server's reload is handled natively.
|
|
164
|
+
*/
|
|
165
|
+
noteDevServerOrigin(deviceId: string, origin: unknown): void;
|
|
166
|
+
/**
|
|
167
|
+
* Ask a freshly-announced device where its bundle came from, once.
|
|
168
|
+
*
|
|
169
|
+
* Deliberately eager: this is the piece of information that can ONLY be
|
|
170
|
+
* collected while the app is healthy, and is only ever needed after it has
|
|
171
|
+
* stopped answering. One cheap round-trip per device buys the ability to
|
|
172
|
+
* recover a crashed app instead of telling the caller to go do it by hand.
|
|
173
|
+
*/
|
|
174
|
+
private learnDevServerOrigin;
|
|
175
|
+
/** The dev-server origin last reported by this device, if any. */
|
|
176
|
+
devServerOrigin(deviceId: string): string | null;
|
|
94
177
|
/**
|
|
95
178
|
* Keep the device RECORDING this tool, without asking it to stream.
|
|
96
179
|
*
|
|
@@ -135,10 +218,14 @@ declare class SyncClient {
|
|
|
135
218
|
*/
|
|
136
219
|
getSnapshot(toolId: string, deviceId?: string, opts?: {
|
|
137
220
|
timeoutMs?: number;
|
|
221
|
+
silent?: boolean;
|
|
138
222
|
}): Promise<{
|
|
139
223
|
deviceId: string;
|
|
140
224
|
payload: unknown;
|
|
225
|
+
stale?: string;
|
|
141
226
|
}>;
|
|
227
|
+
/** Header line for data served from cache because the device went quiet. */
|
|
228
|
+
private staleNote;
|
|
142
229
|
private waitForSnapshot;
|
|
143
230
|
/**
|
|
144
231
|
* Invoke a remote action on a device and await its result. Pass deviceId to
|
|
@@ -146,14 +233,10 @@ declare class SyncClient {
|
|
|
146
233
|
*/
|
|
147
234
|
callAction(toolId: string, action: string, params?: unknown, deviceId?: string, opts?: {
|
|
148
235
|
timeoutMs?: number;
|
|
236
|
+
silent?: boolean;
|
|
149
237
|
}): Promise<unknown>;
|
|
150
238
|
}
|
|
151
239
|
|
|
152
|
-
/**
|
|
153
|
-
* Build the Buoy MCP server and register all tools against a connected
|
|
154
|
-
* {@link SyncClient}. Tool packs (perf-monitor, etc.) are layered on top of the
|
|
155
|
-
* generic core here.
|
|
156
|
-
*/
|
|
157
240
|
declare function createMcpServer(sync: SyncClient): McpServer;
|
|
158
241
|
|
|
159
242
|
/**
|
|
@@ -351,6 +434,12 @@ interface AutomationConfig {
|
|
|
351
434
|
runsPerCase?: number;
|
|
352
435
|
coolDownMs?: number;
|
|
353
436
|
discardWarmupRuns?: number;
|
|
437
|
+
/**
|
|
438
|
+
* Run a throwaway case at the front of the batch and drop it from the
|
|
439
|
+
* results, so the batch's cold start doesn't land on real data. Default true.
|
|
440
|
+
* See the perf-monitor AutomationConfig for the reproduction.
|
|
441
|
+
*/
|
|
442
|
+
discardWarmupCase?: boolean;
|
|
354
443
|
shuffleCases?: boolean;
|
|
355
444
|
/** Capture per-component render counts/durations each run. Default true. */
|
|
356
445
|
captureRenders?: boolean;
|
|
@@ -378,13 +467,17 @@ interface IndexEntry {
|
|
|
378
467
|
caseId?: string;
|
|
379
468
|
runIndex?: number;
|
|
380
469
|
isMedianRun?: boolean;
|
|
470
|
+
/** Thermal state at run start; a throttled run reads cheap AND janky. */
|
|
471
|
+
thermalState?: "nominal" | "fair" | "serious" | "critical";
|
|
381
472
|
/** Render-capture mirror (absent when capture was unavailable/disabled). */
|
|
382
473
|
renderCommits?: number;
|
|
383
474
|
renderTotalMs?: number;
|
|
475
|
+
renderWasted?: number;
|
|
384
476
|
topRenderers?: {
|
|
385
477
|
name: string;
|
|
386
478
|
renders: number;
|
|
387
479
|
totalMs: number;
|
|
480
|
+
cause?: string;
|
|
388
481
|
}[];
|
|
389
482
|
}
|
|
390
483
|
/** Per-metric min/max/avg/p95 channel. */
|
|
@@ -406,12 +499,40 @@ interface BenchmarkReportStats {
|
|
|
406
499
|
uiJankFrames: number;
|
|
407
500
|
deviceMaxRefreshRate: number;
|
|
408
501
|
}
|
|
409
|
-
|
|
410
502
|
/**
|
|
411
|
-
*
|
|
412
|
-
*
|
|
413
|
-
*
|
|
503
|
+
* Why components rendered, counted over a run.
|
|
504
|
+
*
|
|
505
|
+
* `propsUnstable` is the actionable one: props changed by IDENTITY only —
|
|
506
|
+
* a fresh arrow or object literal handed down every render — while every
|
|
507
|
+
* value stayed equal. Reporting that as plain "props" sends you into the
|
|
508
|
+
* child, when the fix is always in the parent.
|
|
414
509
|
*/
|
|
510
|
+
interface RenderCauseCounts {
|
|
511
|
+
mount?: number;
|
|
512
|
+
props?: number;
|
|
513
|
+
propsUnstable?: number;
|
|
514
|
+
hooks?: number;
|
|
515
|
+
parent?: number;
|
|
516
|
+
}
|
|
517
|
+
interface RenderCapture {
|
|
518
|
+
totalCommits: number;
|
|
519
|
+
totalRenders: number;
|
|
520
|
+
capturedDurations: boolean;
|
|
521
|
+
totalRenderMs?: number;
|
|
522
|
+
/** Parent cascades + identity-only prop churn: the removable share. */
|
|
523
|
+
wastedRenders?: number;
|
|
524
|
+
causes?: RenderCauseCounts;
|
|
525
|
+
topComponents: {
|
|
526
|
+
name: string;
|
|
527
|
+
renders: number;
|
|
528
|
+
totalMs: number;
|
|
529
|
+
maxMs: number;
|
|
530
|
+
avgMs: number;
|
|
531
|
+
causes?: RenderCauseCounts;
|
|
532
|
+
changedProps?: string[];
|
|
533
|
+
parentName?: string;
|
|
534
|
+
}[];
|
|
535
|
+
}
|
|
415
536
|
|
|
416
537
|
interface BuildConfigInput {
|
|
417
538
|
targetRoute: string;
|
|
@@ -423,12 +544,13 @@ interface BuildConfigInput {
|
|
|
423
544
|
coolDownMs?: number;
|
|
424
545
|
navTimeoutMs?: number;
|
|
425
546
|
discardWarmupRuns?: number;
|
|
547
|
+
discardWarmupCase?: boolean;
|
|
426
548
|
shuffleCases?: boolean;
|
|
427
549
|
reloadBetweenCases?: boolean;
|
|
428
550
|
captureRenders?: boolean;
|
|
429
551
|
}
|
|
430
552
|
/** The settings subset a driver inherits from the device's saved config. */
|
|
431
|
-
type BenchmarkSettings = Partial<Pick<AutomationConfig, "perCaseDurationMs" | "settleMs" | "navTimeoutMs" | "runsPerCase" | "coolDownMs" | "discardWarmupRuns" | "shuffleCases" | "reloadBetweenCases" | "captureRenders">>;
|
|
553
|
+
type BenchmarkSettings = Partial<Pick<AutomationConfig, "perCaseDurationMs" | "settleMs" | "navTimeoutMs" | "runsPerCase" | "coolDownMs" | "discardWarmupRuns" | "discardWarmupCase" | "shuffleCases" | "reloadBetweenCases" | "captureRenders">>;
|
|
432
554
|
/**
|
|
433
555
|
* Build + validate the AutomationConfig the device expects. Throws on bad input.
|
|
434
556
|
*
|
|
@@ -461,13 +583,17 @@ interface RankRow {
|
|
|
461
583
|
deltaJsFps: number | null;
|
|
462
584
|
/** At-risk axes for this case (e.g. "UI thread drops frames", "CPU near cap"). */
|
|
463
585
|
flags: string[];
|
|
586
|
+
/** Thermal state at the median run's start, when the device reported one. */
|
|
587
|
+
thermalState?: "nominal" | "fair" | "serious" | "critical";
|
|
464
588
|
/** React commits during the median run (null when capture was off/unavailable). */
|
|
465
589
|
renderCommits: number | null;
|
|
466
590
|
/** Top components by total render self-time during the median run. */
|
|
591
|
+
renderWasted?: number;
|
|
467
592
|
topRenderers: {
|
|
468
593
|
name: string;
|
|
469
594
|
renders: number;
|
|
470
595
|
totalMs: number;
|
|
596
|
+
cause?: string;
|
|
471
597
|
}[];
|
|
472
598
|
}
|
|
473
599
|
interface BatchRanking {
|
|
@@ -506,14 +632,44 @@ declare function rankBatchFromIndex(index: IndexEntry[], batchId: string, casesO
|
|
|
506
632
|
}[]): BatchRanking;
|
|
507
633
|
/** Render a batch ranking as a trade-off-aware report (no single false winner). */
|
|
508
634
|
declare function formatRankingMarkdown(ranking: BatchRanking): string;
|
|
509
|
-
/**
|
|
635
|
+
/**
|
|
636
|
+
* Side-by-side comparison of two full reports.
|
|
637
|
+
*
|
|
638
|
+
* Renders are part of the comparison, not an afterthought: when someone runs
|
|
639
|
+
* this after a memoization fix, "did the render count actually drop" IS the
|
|
640
|
+
* question, and for a long while this function silently dropped the block
|
|
641
|
+
* that answers it.
|
|
642
|
+
*/
|
|
510
643
|
declare function compareReportStats(a: {
|
|
511
644
|
name: string;
|
|
512
645
|
stats: BenchmarkReportStats;
|
|
646
|
+
renders?: RenderCapture;
|
|
513
647
|
}, b: {
|
|
514
648
|
name: string;
|
|
515
649
|
stats: BenchmarkReportStats;
|
|
650
|
+
renders?: RenderCapture;
|
|
516
651
|
}): {
|
|
652
|
+
renders?: {
|
|
653
|
+
total: {
|
|
654
|
+
a: number;
|
|
655
|
+
b: number;
|
|
656
|
+
delta: number;
|
|
657
|
+
};
|
|
658
|
+
commits: {
|
|
659
|
+
a: number;
|
|
660
|
+
b: number;
|
|
661
|
+
delta: number;
|
|
662
|
+
};
|
|
663
|
+
wasted: {
|
|
664
|
+
a: number;
|
|
665
|
+
b: number;
|
|
666
|
+
delta: number;
|
|
667
|
+
};
|
|
668
|
+
causes: {
|
|
669
|
+
a: RenderCauseCounts;
|
|
670
|
+
b: RenderCauseCounts;
|
|
671
|
+
};
|
|
672
|
+
} | undefined;
|
|
517
673
|
a: string;
|
|
518
674
|
b: string;
|
|
519
675
|
uiFps: {
|