@deeeed/metamask-harness 0.42.0 → 0.43.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/CHANGELOG.md +17 -0
- package/README.md +5 -0
- package/adapters/mobile/bridge-runtime/cdp-bridge.cjs +59 -21
- package/adapters/mobile/bridge-runtime/console-forwarder.cjs +3 -2
- package/adapters/mobile/bridge-runtime/lib/bridge-errors.cjs +2 -0
- package/adapters/mobile/bridge-runtime/lib/cdp-broker.cjs +194 -25
- package/adapters/mobile/bridge-runtime/lib/config.cjs +14 -4
- package/adapters/mobile/start-console-forwarder.sh +5 -1
- package/dist/adapters/extension/browser-cdp.js +174 -0
- package/dist/adapters/extension/network-observer.js +19 -110
- package/dist/adapters/extension/performance-observer.js +75 -0
- package/dist/adapters/mobile/frame-metrics.js +45 -0
- package/dist/adapters/mobile/performance-observer.js +43 -0
- package/dist/adapters/performance/cdp-trace.js +342 -0
- package/dist/adapters/performance/js-task-metrics.js +35 -0
- package/dist/adapters.js +17 -2
- package/dist/artifact-files.js +92 -0
- package/dist/async.js +19 -0
- package/dist/commands/call.js +23 -2
- package/dist/commands/run-engine.js +18 -0
- package/dist/commands/run.js +20 -1
- package/dist/network-observation.js +59 -47
- package/dist/performance-observation.js +465 -0
- package/docs/PERFORMANCE-CAPTURE.md +33 -0
- package/docs/RECIPES.md +7 -0
- package/library/actions/mobile/platform/bridge.mjs +3 -0
- package/library/manifests/extension.action-manifest.json +85 -0
- package/library/manifests/mobile.action-manifest.json +97 -0
- package/package.json +1 -1
- package/scripts/site-contrast.mjs +43 -27
|
@@ -0,0 +1,342 @@
|
|
|
1
|
+
import {
|
|
2
|
+
summarizeFrames
|
|
3
|
+
} from "../mobile/frame-metrics.js";
|
|
4
|
+
import { withTimeout } from "../../async.js";
|
|
5
|
+
import {
|
|
6
|
+
summarizeJavaScriptTasks
|
|
7
|
+
} from "./js-task-metrics.js";
|
|
8
|
+
const FRAME_BUDGET_MS = 1e3 / 60;
|
|
9
|
+
const MAX_TRACE_BYTES = 64 * 1024 * 1024;
|
|
10
|
+
const MAX_TRACE_EVENTS = 25e4;
|
|
11
|
+
const MOBILE_TRACE_CATEGORIES = [
|
|
12
|
+
"blink.user_timing",
|
|
13
|
+
"disabled-by-default-devtools.timeline.frame"
|
|
14
|
+
];
|
|
15
|
+
const EXTENSION_TRACE_CATEGORIES = [
|
|
16
|
+
"blink.user_timing",
|
|
17
|
+
"devtools.timeline",
|
|
18
|
+
"disabled-by-default-devtools.timeline",
|
|
19
|
+
"disabled-by-default-devtools.timeline.frame",
|
|
20
|
+
"v8.execute"
|
|
21
|
+
];
|
|
22
|
+
function createCdpTraceCollector(client, platform, marker) {
|
|
23
|
+
let active;
|
|
24
|
+
const offData = client.on("Tracing.dataCollected", (params) => {
|
|
25
|
+
if (!active || !Array.isArray(params.value)) return;
|
|
26
|
+
for (const value of params.value) {
|
|
27
|
+
if (!value || typeof value !== "object" || Array.isArray(value)) continue;
|
|
28
|
+
const event = value;
|
|
29
|
+
const bytes = Buffer.byteLength(JSON.stringify(event));
|
|
30
|
+
if (active.events.length >= MAX_TRACE_EVENTS || active.bytes + bytes > MAX_TRACE_BYTES) {
|
|
31
|
+
active.overflow = true;
|
|
32
|
+
continue;
|
|
33
|
+
}
|
|
34
|
+
active.events.push(event);
|
|
35
|
+
active.bytes += bytes;
|
|
36
|
+
}
|
|
37
|
+
});
|
|
38
|
+
const offComplete = client.on("Tracing.tracingComplete", (params) => {
|
|
39
|
+
if (!active) return;
|
|
40
|
+
active.dataLossOccurred = params.dataLossOccurred === true;
|
|
41
|
+
active.complete?.();
|
|
42
|
+
});
|
|
43
|
+
return {
|
|
44
|
+
async start(id) {
|
|
45
|
+
if (active) throw new Error("Only one CDP performance trace may be active.");
|
|
46
|
+
let complete;
|
|
47
|
+
const completePromise = new Promise((resolve) => {
|
|
48
|
+
complete = resolve;
|
|
49
|
+
});
|
|
50
|
+
active = {
|
|
51
|
+
id,
|
|
52
|
+
events: [],
|
|
53
|
+
bytes: 0,
|
|
54
|
+
overflow: false,
|
|
55
|
+
dataLossOccurred: false,
|
|
56
|
+
complete,
|
|
57
|
+
completePromise
|
|
58
|
+
};
|
|
59
|
+
let tracingStarted = false;
|
|
60
|
+
try {
|
|
61
|
+
await client.send(
|
|
62
|
+
"Tracing.start",
|
|
63
|
+
{
|
|
64
|
+
categories: (platform === "extension" ? EXTENSION_TRACE_CATEGORIES : MOBILE_TRACE_CATEGORIES).join(","),
|
|
65
|
+
transferMode: "ReportEvents"
|
|
66
|
+
},
|
|
67
|
+
15e3
|
|
68
|
+
);
|
|
69
|
+
tracingStarted = true;
|
|
70
|
+
const markerName = `mmh-clock-${id}-${Date.now()}`;
|
|
71
|
+
const sync = await marker(markerName);
|
|
72
|
+
active.markerName = markerName;
|
|
73
|
+
active.markerHostEpochMs = sync.hostEpochMs;
|
|
74
|
+
active.markerUncertaintyMs = sync.uncertaintyMs;
|
|
75
|
+
} catch (error) {
|
|
76
|
+
if (tracingStarted) {
|
|
77
|
+
await client.send("Tracing.end", {}, 5e3).catch(() => void 0);
|
|
78
|
+
await withTimeout(active.completePromise, {
|
|
79
|
+
message: "CDP tracing cleanup did not report tracingComplete.",
|
|
80
|
+
timeoutMs: 1e3
|
|
81
|
+
}).catch(() => void 0);
|
|
82
|
+
}
|
|
83
|
+
active = void 0;
|
|
84
|
+
throw error;
|
|
85
|
+
}
|
|
86
|
+
},
|
|
87
|
+
async end(id) {
|
|
88
|
+
const capture = active;
|
|
89
|
+
if (!capture || capture.id !== id) {
|
|
90
|
+
throw new Error(`Performance capture is not active: ${id}`);
|
|
91
|
+
}
|
|
92
|
+
try {
|
|
93
|
+
await client.send("Tracing.end", {}, 15e3);
|
|
94
|
+
await withTimeout(capture.completePromise, {
|
|
95
|
+
message: "CDP tracing did not report tracingComplete.",
|
|
96
|
+
timeoutMs: 3e4
|
|
97
|
+
});
|
|
98
|
+
} finally {
|
|
99
|
+
active = void 0;
|
|
100
|
+
}
|
|
101
|
+
return parseTraceCapture({
|
|
102
|
+
platform,
|
|
103
|
+
events: capture.events,
|
|
104
|
+
markerName: capture.markerName,
|
|
105
|
+
markerHostEpochMs: capture.markerHostEpochMs,
|
|
106
|
+
markerUncertaintyMs: capture.markerUncertaintyMs,
|
|
107
|
+
dataLossOccurred: capture.dataLossOccurred,
|
|
108
|
+
overflow: capture.overflow,
|
|
109
|
+
unavailableReasons: [],
|
|
110
|
+
coverageGapReasons: []
|
|
111
|
+
});
|
|
112
|
+
},
|
|
113
|
+
async close() {
|
|
114
|
+
if (active) {
|
|
115
|
+
await client.send("Tracing.end", {}, 5e3).catch(() => void 0);
|
|
116
|
+
await withTimeout(active.completePromise, {
|
|
117
|
+
message: "CDP tracing cleanup did not report tracingComplete.",
|
|
118
|
+
timeoutMs: 1e3
|
|
119
|
+
}).catch(() => void 0);
|
|
120
|
+
active = void 0;
|
|
121
|
+
}
|
|
122
|
+
offData();
|
|
123
|
+
offComplete();
|
|
124
|
+
client.close();
|
|
125
|
+
}
|
|
126
|
+
};
|
|
127
|
+
}
|
|
128
|
+
function parseTraceCapture(capture) {
|
|
129
|
+
const marker = findClockMarker(capture.events, capture.markerName);
|
|
130
|
+
if (capture.platform === "extension" && !finite(marker?.pid)) {
|
|
131
|
+
const reason = "Extension trace marker did not identify a renderer process.";
|
|
132
|
+
return {
|
|
133
|
+
platform: capture.platform,
|
|
134
|
+
javascript: unavailableJavaScriptSource("cdp-js-runtime-tasks", reason),
|
|
135
|
+
nativeUi: unavailableNativeUiSource("cdp-native-frame-timings", reason),
|
|
136
|
+
trace: traceEvidence(capture, "unresolved")
|
|
137
|
+
};
|
|
138
|
+
}
|
|
139
|
+
const scopedEvents = capture.platform === "extension" && finite(marker?.pid) ? capture.events.filter((event) => event.pid === marker.pid) : capture.events;
|
|
140
|
+
const scope = capture.platform !== "extension" ? "mobile-host" : finite(marker?.pid) ? "extension-renderer" : "unresolved";
|
|
141
|
+
const trace = traceEvidence(
|
|
142
|
+
{ ...capture, events: scopedEvents },
|
|
143
|
+
scope,
|
|
144
|
+
finite(marker?.pid) ? marker.pid : void 0
|
|
145
|
+
);
|
|
146
|
+
if (!finite(marker?.ts) || capture.markerHostEpochMs === void 0 || capture.markerUncertaintyMs === void 0) {
|
|
147
|
+
const reason = "CDP trace clock marker was unavailable.";
|
|
148
|
+
return {
|
|
149
|
+
platform: capture.platform,
|
|
150
|
+
javascript: unavailableJavaScriptSource("cdp-js-runtime-tasks", reason),
|
|
151
|
+
nativeUi: unavailableNativeUiSource(
|
|
152
|
+
"cdp-native-frame-timings",
|
|
153
|
+
reason
|
|
154
|
+
),
|
|
155
|
+
trace
|
|
156
|
+
};
|
|
157
|
+
}
|
|
158
|
+
const traceToEpochMs = (timestampUs) => capture.markerHostEpochMs + (timestampUs - marker.ts) / 1e3;
|
|
159
|
+
const gapReasons = [
|
|
160
|
+
...capture.coverageGapReasons,
|
|
161
|
+
...capture.overflow ? ["CDP trace retention limit was reached."] : [],
|
|
162
|
+
...capture.dataLossOccurred ? ["CDP reported trace data loss."] : []
|
|
163
|
+
];
|
|
164
|
+
const nativeSamples = nativeFrameSamples(
|
|
165
|
+
scopedEvents,
|
|
166
|
+
capture.platform,
|
|
167
|
+
traceToEpochMs
|
|
168
|
+
);
|
|
169
|
+
const javascriptSamples = javascriptTaskSamples(
|
|
170
|
+
scopedEvents,
|
|
171
|
+
traceToEpochMs
|
|
172
|
+
);
|
|
173
|
+
const status = capture.overflow || capture.dataLossOccurred ? "partial" : "complete";
|
|
174
|
+
return {
|
|
175
|
+
platform: capture.platform,
|
|
176
|
+
javascript: javascriptSourceResult(
|
|
177
|
+
"cdp-js-runtime-tasks",
|
|
178
|
+
javascriptSamples,
|
|
179
|
+
status,
|
|
180
|
+
gapReasons,
|
|
181
|
+
traceEventNames(scopedEvents),
|
|
182
|
+
capture.markerUncertaintyMs
|
|
183
|
+
),
|
|
184
|
+
nativeUi: nativeUiSourceResult(
|
|
185
|
+
capture.platform === "extension" ? "chromium-cdp-frame-timings" : "react-native-cdp-frame-timings",
|
|
186
|
+
nativeSamples,
|
|
187
|
+
status,
|
|
188
|
+
gapReasons,
|
|
189
|
+
traceEventNames(scopedEvents),
|
|
190
|
+
capture.markerUncertaintyMs
|
|
191
|
+
),
|
|
192
|
+
trace
|
|
193
|
+
};
|
|
194
|
+
}
|
|
195
|
+
function traceEvidence(capture, scope, rendererProcessId) {
|
|
196
|
+
const counts = /* @__PURE__ */ new Map();
|
|
197
|
+
for (const event of capture.events) {
|
|
198
|
+
const name = String(event.name ?? "");
|
|
199
|
+
counts.set(name, (counts.get(name) ?? 0) + 1);
|
|
200
|
+
}
|
|
201
|
+
return {
|
|
202
|
+
beginFrameCount: counts.get("BeginFrame") ?? 0,
|
|
203
|
+
dataLossOccurred: capture.dataLossOccurred,
|
|
204
|
+
drawFrameCount: counts.get("DrawFrame") ?? 0,
|
|
205
|
+
overflow: capture.overflow,
|
|
206
|
+
profileChunkCount: counts.get("ProfileChunk") ?? 0,
|
|
207
|
+
...rendererProcessId === void 0 ? {} : { rendererProcessId },
|
|
208
|
+
runTaskCount: counts.get("RunTask") ?? 0,
|
|
209
|
+
scope,
|
|
210
|
+
totalEventCount: capture.events.length
|
|
211
|
+
};
|
|
212
|
+
}
|
|
213
|
+
function nativeFrameSamples(events, platform, traceToEpochMs) {
|
|
214
|
+
const begins = /* @__PURE__ */ new Map();
|
|
215
|
+
const frames = [];
|
|
216
|
+
for (const event of events) {
|
|
217
|
+
if (event.name !== "BeginFrame" && event.name !== "DrawFrame") continue;
|
|
218
|
+
const sequence = frameSequence(event);
|
|
219
|
+
if (!sequence || !finite(event.ts)) continue;
|
|
220
|
+
if (event.name === "BeginFrame") {
|
|
221
|
+
begins.set(sequence, event);
|
|
222
|
+
continue;
|
|
223
|
+
}
|
|
224
|
+
const begin = begins.get(sequence);
|
|
225
|
+
if (!begin || !finite(begin.ts)) continue;
|
|
226
|
+
frames.push({ beginUs: begin.ts, endUs: event.ts });
|
|
227
|
+
}
|
|
228
|
+
frames.sort((first, second) => first.beginUs - second.beginUs);
|
|
229
|
+
if (platform === "ios") {
|
|
230
|
+
const samples = [];
|
|
231
|
+
for (let index = 1; index < frames.length; index += 1) {
|
|
232
|
+
const previous = frames[index - 1];
|
|
233
|
+
const current = frames[index];
|
|
234
|
+
if (!previous || !current) continue;
|
|
235
|
+
const durationMs = (current.beginUs - previous.beginUs) / 1e3;
|
|
236
|
+
if (durationMs <= 0) continue;
|
|
237
|
+
samples.push({
|
|
238
|
+
completedAtEpochMs: traceToEpochMs(current.beginUs),
|
|
239
|
+
durationMs,
|
|
240
|
+
janky: durationMs > FRAME_BUDGET_MS * 1.01
|
|
241
|
+
});
|
|
242
|
+
}
|
|
243
|
+
return samples;
|
|
244
|
+
}
|
|
245
|
+
return frames.map((frame) => {
|
|
246
|
+
const durationMs = (frame.endUs - frame.beginUs) / 1e3;
|
|
247
|
+
return {
|
|
248
|
+
completedAtEpochMs: traceToEpochMs(frame.endUs),
|
|
249
|
+
durationMs,
|
|
250
|
+
janky: durationMs > FRAME_BUDGET_MS * 1.01
|
|
251
|
+
};
|
|
252
|
+
}).filter((sample) => sample.durationMs > 0);
|
|
253
|
+
}
|
|
254
|
+
function javascriptTaskSamples(events, traceToEpochMs) {
|
|
255
|
+
return events.filter(
|
|
256
|
+
(event) => event.name === "RunTask" && event.ph === "X" && finite(event.ts) && finite(event.dur) && event.dur > 0
|
|
257
|
+
).map((event) => ({
|
|
258
|
+
completedAtEpochMs: traceToEpochMs(event.ts + event.dur),
|
|
259
|
+
durationMs: event.dur / 1e3
|
|
260
|
+
}));
|
|
261
|
+
}
|
|
262
|
+
function findClockMarker(events, markerName) {
|
|
263
|
+
if (!markerName) return void 0;
|
|
264
|
+
return events.find(
|
|
265
|
+
(candidate) => candidate.name === markerName || asRecord(candidate.args).sync_id === markerName || asRecord(asRecord(candidate.args).data).sync_id === markerName
|
|
266
|
+
);
|
|
267
|
+
}
|
|
268
|
+
function frameSequence(event) {
|
|
269
|
+
const args = asRecord(event.args);
|
|
270
|
+
const value = args.frameSeqId ?? asRecord(args.data).frameSeqId;
|
|
271
|
+
return value === void 0 ? void 0 : String(value);
|
|
272
|
+
}
|
|
273
|
+
function javascriptSourceResult(kind, samples, status, coverageGapReasons, observedEventNames, clockSyncUncertaintyMs) {
|
|
274
|
+
return {
|
|
275
|
+
status: samples.length > 0 ? status : "partial",
|
|
276
|
+
kind,
|
|
277
|
+
unavailableReasons: [],
|
|
278
|
+
coverageGapReasons: [
|
|
279
|
+
...coverageGapReasons,
|
|
280
|
+
...samples.length > 0 ? [] : [
|
|
281
|
+
`No ${kind} samples were recorded. Observed trace events: ${observedEventNames.join(", ") || "none"}.`
|
|
282
|
+
]
|
|
283
|
+
],
|
|
284
|
+
samples,
|
|
285
|
+
summary: summarizeJavaScriptTasks(samples),
|
|
286
|
+
clockSyncUncertaintyMs
|
|
287
|
+
};
|
|
288
|
+
}
|
|
289
|
+
function nativeUiSourceResult(kind, samples, status, coverageGapReasons, observedEventNames, clockSyncUncertaintyMs) {
|
|
290
|
+
return {
|
|
291
|
+
status: samples.length > 0 ? status : "partial",
|
|
292
|
+
kind,
|
|
293
|
+
unavailableReasons: [],
|
|
294
|
+
coverageGapReasons: [
|
|
295
|
+
...coverageGapReasons,
|
|
296
|
+
...samples.length > 0 ? [] : [
|
|
297
|
+
`No ${kind} samples were recorded. Observed trace events: ${observedEventNames.join(", ") || "none"}.`
|
|
298
|
+
]
|
|
299
|
+
],
|
|
300
|
+
samples,
|
|
301
|
+
summary: summarizeFrames(samples),
|
|
302
|
+
clockSyncUncertaintyMs
|
|
303
|
+
};
|
|
304
|
+
}
|
|
305
|
+
function traceEventNames(events) {
|
|
306
|
+
const counts = /* @__PURE__ */ new Map();
|
|
307
|
+
for (const event of events) {
|
|
308
|
+
const name = String(event.name ?? "unknown");
|
|
309
|
+
counts.set(name, (counts.get(name) ?? 0) + 1);
|
|
310
|
+
}
|
|
311
|
+
return [...counts.entries()].sort((first, second) => second[1] - first[1]).slice(0, 20).map(([name, count]) => `${name}(${count})`);
|
|
312
|
+
}
|
|
313
|
+
function unavailableJavaScriptSource(kind, reason) {
|
|
314
|
+
return {
|
|
315
|
+
status: "unavailable",
|
|
316
|
+
kind,
|
|
317
|
+
unavailableReasons: [reason],
|
|
318
|
+
coverageGapReasons: [],
|
|
319
|
+
samples: [],
|
|
320
|
+
summary: { taskCount: 0 }
|
|
321
|
+
};
|
|
322
|
+
}
|
|
323
|
+
function unavailableNativeUiSource(kind, reason) {
|
|
324
|
+
return {
|
|
325
|
+
status: "unavailable",
|
|
326
|
+
kind,
|
|
327
|
+
unavailableReasons: [reason],
|
|
328
|
+
coverageGapReasons: [],
|
|
329
|
+
samples: [],
|
|
330
|
+
summary: { frameCount: 0 }
|
|
331
|
+
};
|
|
332
|
+
}
|
|
333
|
+
function finite(value) {
|
|
334
|
+
return typeof value === "number" && Number.isFinite(value);
|
|
335
|
+
}
|
|
336
|
+
function asRecord(value) {
|
|
337
|
+
return value && typeof value === "object" && !Array.isArray(value) ? value : {};
|
|
338
|
+
}
|
|
339
|
+
export {
|
|
340
|
+
createCdpTraceCollector,
|
|
341
|
+
parseTraceCapture
|
|
342
|
+
};
|
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
const LONG_TASK_MS = 50;
|
|
2
|
+
function summarizeJavaScriptTasks(samples) {
|
|
3
|
+
const durations = samples.map((sample) => sample.durationMs).filter((duration) => Number.isFinite(duration) && duration > 0).sort((first, second) => first - second);
|
|
4
|
+
if (durations.length === 0) return { taskCount: 0 };
|
|
5
|
+
const longTaskCount = durations.filter(
|
|
6
|
+
(duration) => duration >= LONG_TASK_MS
|
|
7
|
+
).length;
|
|
8
|
+
return {
|
|
9
|
+
longestTaskMs: round(durations.at(-1) ?? 0),
|
|
10
|
+
longTaskCount,
|
|
11
|
+
longTaskPercent: round(longTaskCount / durations.length * 100),
|
|
12
|
+
p50TaskMs: round(percentile(durations, 0.5)),
|
|
13
|
+
p95TaskMs: round(percentile(durations, 0.95)),
|
|
14
|
+
p99TaskMs: round(percentile(durations, 0.99)),
|
|
15
|
+
taskCount: durations.length,
|
|
16
|
+
totalTaskTimeMs: round(
|
|
17
|
+
durations.reduce((total, duration) => total + duration, 0)
|
|
18
|
+
)
|
|
19
|
+
};
|
|
20
|
+
}
|
|
21
|
+
function percentile(sorted, quantile) {
|
|
22
|
+
if (sorted.length === 1) return sorted[0] ?? 0;
|
|
23
|
+
const position = (sorted.length - 1) * quantile;
|
|
24
|
+
const lower = Math.floor(position);
|
|
25
|
+
const upper = Math.ceil(position);
|
|
26
|
+
const lowerValue = sorted[lower] ?? 0;
|
|
27
|
+
const upperValue = sorted[upper] ?? lowerValue;
|
|
28
|
+
return lowerValue + (upperValue - lowerValue) * (position - lower);
|
|
29
|
+
}
|
|
30
|
+
function round(value) {
|
|
31
|
+
return Math.round(value * 1e3) / 1e3;
|
|
32
|
+
}
|
|
33
|
+
export {
|
|
34
|
+
summarizeJavaScriptTasks
|
|
35
|
+
};
|
package/dist/adapters.js
CHANGED
|
@@ -13,6 +13,7 @@ import { nativeAgentDeviceStateDir } from "../library/actions/mobile/platform/na
|
|
|
13
13
|
import { resolveMobileToolPath } from "../library/actions/mobile/platform/tool-paths.mjs";
|
|
14
14
|
import { resolveWalletImportCredentials, validateWalletImportOptions } from "../library/actions/shared/wallet/import-source.mjs";
|
|
15
15
|
import { handleRunNetworkAction } from "./network-observation.js";
|
|
16
|
+
import { handleRunPerformanceAction } from "./performance-observation.js";
|
|
16
17
|
const execFileAsync = promisify(execFile);
|
|
17
18
|
const NATIVE_PROVIDER_UI_ACTIONS = /* @__PURE__ */ new Set([
|
|
18
19
|
"ui.press",
|
|
@@ -107,7 +108,9 @@ const LIVE_ONLY_WALLET_ACTIONS = /* @__PURE__ */ new Set([
|
|
|
107
108
|
const LIVE_ONLY_APP_ACTIONS = /* @__PURE__ */ new Set([
|
|
108
109
|
"ui.navigate",
|
|
109
110
|
"app.network_capture",
|
|
110
|
-
"app.network_assert"
|
|
111
|
+
"app.network_assert",
|
|
112
|
+
"app.performance_capture",
|
|
113
|
+
"app.performance_assert"
|
|
111
114
|
]);
|
|
112
115
|
function requiresLiveAdapter(platform, action) {
|
|
113
116
|
return LIVE_ONLY_ACTIONS.has(action) || platform === "core" && CORE_ONLY_PERPS_ACTIONS.has(action) || LIVE_ONLY_WALLET_ACTIONS.has(action) || LIVE_ONLY_APP_ACTIONS.has(action);
|
|
@@ -145,6 +148,13 @@ function liveAdapterPathHint(platform, action) {
|
|
|
145
148
|
return `library/actions/${platform}/${action.replaceAll(".", "/")}.mjs`;
|
|
146
149
|
}
|
|
147
150
|
async function semanticResult(platform, action, node, context, forceLive = false, preparedLiveAdapters) {
|
|
151
|
+
const performanceAction = await handleRunPerformanceAction(
|
|
152
|
+
platform,
|
|
153
|
+
action,
|
|
154
|
+
node,
|
|
155
|
+
context
|
|
156
|
+
);
|
|
157
|
+
if (performanceAction) return performanceAction;
|
|
148
158
|
const networkAction = await handleRunNetworkAction(
|
|
149
159
|
platform,
|
|
150
160
|
action,
|
|
@@ -265,7 +275,12 @@ function createMetaMaskSemanticAdapters(platform, declaredCustomActions = [], pr
|
|
|
265
275
|
];
|
|
266
276
|
const bundledActions = [
|
|
267
277
|
...walletActions,
|
|
268
|
-
...platform !== "core" ? [
|
|
278
|
+
...platform !== "core" ? [
|
|
279
|
+
"app.network_capture",
|
|
280
|
+
"app.network_assert",
|
|
281
|
+
"app.performance_capture",
|
|
282
|
+
"app.performance_assert"
|
|
283
|
+
] : [],
|
|
269
284
|
"metamask.assets.read_visible_state",
|
|
270
285
|
"metamask.assets.open_details",
|
|
271
286
|
"metamask.assets.read_details",
|
|
@@ -0,0 +1,92 @@
|
|
|
1
|
+
import { constants as fsConstants } from "node:fs";
|
|
2
|
+
import {
|
|
3
|
+
lstat,
|
|
4
|
+
mkdir,
|
|
5
|
+
open,
|
|
6
|
+
readFile,
|
|
7
|
+
realpath,
|
|
8
|
+
writeFile
|
|
9
|
+
} from "node:fs/promises";
|
|
10
|
+
import path from "node:path";
|
|
11
|
+
async function writeContainedArtifact(artifactsDir, relativePath, value, label) {
|
|
12
|
+
const file = resolveContainedArtifact(artifactsDir, relativePath, label);
|
|
13
|
+
await mkdir(path.dirname(file), { recursive: true });
|
|
14
|
+
await assertContainedParent(artifactsDir, file, label);
|
|
15
|
+
await assertNotSymlink(file, label);
|
|
16
|
+
const handle = await open(
|
|
17
|
+
file,
|
|
18
|
+
fsConstants.O_WRONLY | fsConstants.O_CREAT | fsConstants.O_TRUNC | fsConstants.O_NOFOLLOW,
|
|
19
|
+
384
|
|
20
|
+
);
|
|
21
|
+
try {
|
|
22
|
+
await handle.writeFile(value);
|
|
23
|
+
} finally {
|
|
24
|
+
await handle.close();
|
|
25
|
+
}
|
|
26
|
+
}
|
|
27
|
+
async function readContainedJsonArtifact(artifactsDir, relativePath, maxBytes, label) {
|
|
28
|
+
const file = resolveContainedArtifact(artifactsDir, relativePath, label);
|
|
29
|
+
await assertContainedParent(artifactsDir, file, label);
|
|
30
|
+
await assertNotSymlink(file, label);
|
|
31
|
+
const handle = await open(file, fsConstants.O_RDONLY | fsConstants.O_NOFOLLOW);
|
|
32
|
+
try {
|
|
33
|
+
const info = await handle.stat();
|
|
34
|
+
if (!info.isFile() || info.size > maxBytes) {
|
|
35
|
+
throw new Error(`${label} is not a bounded regular file.`);
|
|
36
|
+
}
|
|
37
|
+
return JSON.parse(await handle.readFile("utf8"));
|
|
38
|
+
} finally {
|
|
39
|
+
await handle.close();
|
|
40
|
+
}
|
|
41
|
+
}
|
|
42
|
+
async function indexArtifactManifest(manifestPath, entries) {
|
|
43
|
+
const parsed = JSON.parse(await readFile(manifestPath, "utf8"));
|
|
44
|
+
const manifest = asRecord(parsed);
|
|
45
|
+
const replacementPaths = new Set(entries.map((entry) => entry.path));
|
|
46
|
+
const current = Array.isArray(manifest.artifacts) ? manifest.artifacts.filter(
|
|
47
|
+
(artifact) => !replacementPaths.has(String(asRecord(artifact).path ?? ""))
|
|
48
|
+
) : [];
|
|
49
|
+
await writeFile(
|
|
50
|
+
manifestPath,
|
|
51
|
+
`${JSON.stringify(
|
|
52
|
+
{ ...manifest, artifacts: [...current, ...entries] },
|
|
53
|
+
null,
|
|
54
|
+
2
|
|
55
|
+
)}
|
|
56
|
+
`
|
|
57
|
+
);
|
|
58
|
+
}
|
|
59
|
+
function resolveContainedArtifact(artifactsDir, relativePath, label) {
|
|
60
|
+
const root = path.resolve(artifactsDir);
|
|
61
|
+
const file = path.resolve(root, relativePath);
|
|
62
|
+
const relative = path.relative(root, file);
|
|
63
|
+
if (relative.startsWith("..") || path.isAbsolute(relative)) {
|
|
64
|
+
throw new Error(`${label} path must stay inside artifactsDir.`);
|
|
65
|
+
}
|
|
66
|
+
return file;
|
|
67
|
+
}
|
|
68
|
+
async function assertContainedParent(artifactsDir, file, label) {
|
|
69
|
+
const root = await realpath(artifactsDir);
|
|
70
|
+
const parent = await realpath(path.dirname(file));
|
|
71
|
+
const relative = path.relative(root, parent);
|
|
72
|
+
if (relative.startsWith("..") || path.isAbsolute(relative)) {
|
|
73
|
+
throw new Error(`${label} parent must stay inside artifactsDir.`);
|
|
74
|
+
}
|
|
75
|
+
}
|
|
76
|
+
async function assertNotSymlink(file, label) {
|
|
77
|
+
try {
|
|
78
|
+
if ((await lstat(file)).isSymbolicLink()) {
|
|
79
|
+
throw new Error(`${label} must not be a symbolic link.`);
|
|
80
|
+
}
|
|
81
|
+
} catch (error) {
|
|
82
|
+
if (error.code !== "ENOENT") throw error;
|
|
83
|
+
}
|
|
84
|
+
}
|
|
85
|
+
function asRecord(value) {
|
|
86
|
+
return value && typeof value === "object" && !Array.isArray(value) ? value : {};
|
|
87
|
+
}
|
|
88
|
+
export {
|
|
89
|
+
indexArtifactManifest,
|
|
90
|
+
readContainedJsonArtifact,
|
|
91
|
+
writeContainedArtifact
|
|
92
|
+
};
|
package/dist/async.js
ADDED
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
async function withTimeout(promise, options) {
|
|
2
|
+
let timer;
|
|
3
|
+
try {
|
|
4
|
+
return await Promise.race([
|
|
5
|
+
promise,
|
|
6
|
+
new Promise((_resolve, reject) => {
|
|
7
|
+
timer = setTimeout(() => {
|
|
8
|
+
options.onTimeout?.();
|
|
9
|
+
reject(new Error(options.message));
|
|
10
|
+
}, options.timeoutMs);
|
|
11
|
+
})
|
|
12
|
+
]);
|
|
13
|
+
} finally {
|
|
14
|
+
if (timer) clearTimeout(timer);
|
|
15
|
+
}
|
|
16
|
+
}
|
|
17
|
+
export {
|
|
18
|
+
withTimeout
|
|
19
|
+
};
|
package/dist/commands/call.js
CHANGED
|
@@ -46,6 +46,9 @@ import { formatRunDiagnosticsForHuman, readRunDiagnosticsDocument } from "../run
|
|
|
46
46
|
import {
|
|
47
47
|
startRunNetworkObservation
|
|
48
48
|
} from "../network-observation.js";
|
|
49
|
+
import {
|
|
50
|
+
startRunPerformanceObservation
|
|
51
|
+
} from "../performance-observation.js";
|
|
49
52
|
import { recipeTrustFailure } from "../recipe-security.js";
|
|
50
53
|
import { isSensitiveKey, recordCommandEvidence, redactStructuredValue } from "../command-journal.js";
|
|
51
54
|
import { closest } from "../command-contract.js";
|
|
@@ -213,13 +216,17 @@ async function handleCall(argv) {
|
|
|
213
216
|
const requestedRuntimeOptions = runtimeOptionsFromCli(options);
|
|
214
217
|
const inheritedSource = process.env.FARMSLOT_RECIPE_SOURCE_TRUST || process.env.FARMSLOT_RECIPE_SOURCE_KIND || process.env.FARMSLOT_RECIPE_SOURCE_NAME || process.env.FARMSLOT_RECIPE_SOURCE_DIGEST;
|
|
215
218
|
let networkObservation;
|
|
219
|
+
let performanceObservation;
|
|
216
220
|
const callRuntimeOptions = {
|
|
217
221
|
...requestedRuntimeOptions,
|
|
218
222
|
...librarySources ? { librarySources } : {},
|
|
219
223
|
autoHud: false,
|
|
220
224
|
suppressLibraryResolutionLogs: true,
|
|
221
225
|
stdoutIsMachineContract: json,
|
|
222
|
-
onActionEvent: ({ nodeId, action, status }) =>
|
|
226
|
+
onActionEvent: ({ nodeId, action, status }) => {
|
|
227
|
+
networkObservation?.onActionEvent({ nodeId, action, status });
|
|
228
|
+
performanceObservation?.onActionEvent({ nodeId, action, status });
|
|
229
|
+
},
|
|
223
230
|
...requestedRuntimeOptions.source ? { source: requestedRuntimeOptions.source } : inheritedSource ? {} : {
|
|
224
231
|
source: {
|
|
225
232
|
kind: "operator",
|
|
@@ -290,6 +297,16 @@ async function handleCall(argv) {
|
|
|
290
297
|
watcherPort: callRuntimeOptions.watcherPort
|
|
291
298
|
}
|
|
292
299
|
);
|
|
300
|
+
performanceObservation = startRunPerformanceObservation(
|
|
301
|
+
adapter,
|
|
302
|
+
target,
|
|
303
|
+
artifactsDir,
|
|
304
|
+
process.env,
|
|
305
|
+
{
|
|
306
|
+
cdpPort: callRuntimeOptions.cdpPort,
|
|
307
|
+
watcherPort: callRuntimeOptions.watcherPort
|
|
308
|
+
}
|
|
309
|
+
);
|
|
293
310
|
let executionResult;
|
|
294
311
|
try {
|
|
295
312
|
executionResult = await executeWithHealBounds(
|
|
@@ -316,13 +333,17 @@ async function handleCall(argv) {
|
|
|
316
333
|
})
|
|
317
334
|
);
|
|
318
335
|
} catch (error) {
|
|
319
|
-
await networkObservation?.finalize();
|
|
336
|
+
await networkObservation?.finalize().catch(() => void 0);
|
|
320
337
|
networkObservation = void 0;
|
|
338
|
+
await performanceObservation?.finalize().catch(() => void 0);
|
|
339
|
+
performanceObservation = void 0;
|
|
321
340
|
throw error;
|
|
322
341
|
}
|
|
323
342
|
const { result, violation } = executionResult;
|
|
324
343
|
await networkObservation?.finalize(result.artifactManifestPath);
|
|
325
344
|
networkObservation = void 0;
|
|
345
|
+
await performanceObservation?.finalize(result.artifactManifestPath);
|
|
346
|
+
performanceObservation = void 0;
|
|
326
347
|
if (violation !== null) {
|
|
327
348
|
const conciseFailure = violation.originalError ? conciseFailureForHuman(violation.originalError) : "";
|
|
328
349
|
const example = describedAction ? actionExampleCommand(
|
|
@@ -241,7 +241,24 @@ function activateRecipeRuntimeEnvironment(adapter, projectRoot, runtimeOptions)
|
|
|
241
241
|
const previousAndroidPackageId = process.env.ANDROID_PACKAGE_ID;
|
|
242
242
|
const previousAdbSerial = process.env.ADB_SERIAL;
|
|
243
243
|
const previousAndroidSerial = process.env.ANDROID_SERIAL;
|
|
244
|
+
const explicitMobileDeviceEnv = adapter === "mobile" && process.env.MM_HARNESS_EXPLICIT_PLATFORM ? Object.fromEntries(
|
|
245
|
+
[
|
|
246
|
+
"PLATFORM",
|
|
247
|
+
"MM_HARNESS_EXPLICIT_PLATFORM",
|
|
248
|
+
"IOS_SIMULATOR",
|
|
249
|
+
"SIM_UDID",
|
|
250
|
+
"ADB_SERIAL",
|
|
251
|
+
"ANDROID_SERIAL",
|
|
252
|
+
"ANDROID_DEVICE",
|
|
253
|
+
"ANDROID_TARGET_DEVICE_NAME"
|
|
254
|
+
].map((key) => [key, process.env[key]])
|
|
255
|
+
) : void 0;
|
|
244
256
|
getAdapterSurface(adapter).resolveSlotPorts(projectRoot);
|
|
257
|
+
if (explicitMobileDeviceEnv) {
|
|
258
|
+
for (const [key, value] of Object.entries(explicitMobileDeviceEnv)) {
|
|
259
|
+
restoreEnv(key, value);
|
|
260
|
+
}
|
|
261
|
+
}
|
|
245
262
|
if (runtimeOptions.cdpPort) {
|
|
246
263
|
process.env.CDP_PORT = runtimeOptions.cdpPort;
|
|
247
264
|
process.env.RECIPE_CDP_PORT = runtimeOptions.cdpPort;
|
|
@@ -1362,6 +1379,7 @@ function countRecipeNodes(recipe) {
|
|
|
1362
1379
|
return nodes ? Object.keys(nodes).length : void 0;
|
|
1363
1380
|
}
|
|
1364
1381
|
export {
|
|
1382
|
+
activateRecipeRuntimeEnvironment,
|
|
1365
1383
|
countRecipeNodes,
|
|
1366
1384
|
describeRunnableRecipe,
|
|
1367
1385
|
emitHealViolation,
|
package/dist/commands/run.js
CHANGED
|
@@ -44,6 +44,9 @@ import {
|
|
|
44
44
|
import {
|
|
45
45
|
startRunNetworkObservation
|
|
46
46
|
} from "../network-observation.js";
|
|
47
|
+
import {
|
|
48
|
+
startRunPerformanceObservation
|
|
49
|
+
} from "../performance-observation.js";
|
|
47
50
|
async function validationCapabilityRefusals(adapter, findings, librarySources) {
|
|
48
51
|
const actionNames = findings.flatMap((finding) => {
|
|
49
52
|
if (finding.code !== "recipe.action_not_declared_by_manifest") return [];
|
|
@@ -231,6 +234,7 @@ async function handleRunInner({ positional, options }, stream) {
|
|
|
231
234
|
recordCommandEvidence(artifactsDir);
|
|
232
235
|
const librarySources = validated.librarySources;
|
|
233
236
|
let networkObservation;
|
|
237
|
+
let performanceObservation;
|
|
234
238
|
const runtimeOptions = {
|
|
235
239
|
...runtimeOptionsFromCli(options),
|
|
236
240
|
params,
|
|
@@ -239,6 +243,7 @@ async function handleRunInner({ positional, options }, stream) {
|
|
|
239
243
|
onActionEvent: ({ nodeId, action, status }) => {
|
|
240
244
|
stream.node(nodeId, action, status);
|
|
241
245
|
networkObservation?.onActionEvent({ nodeId, action, status });
|
|
246
|
+
performanceObservation?.onActionEvent({ nodeId, action, status });
|
|
242
247
|
}
|
|
243
248
|
};
|
|
244
249
|
stream.phase("authorize");
|
|
@@ -280,6 +285,16 @@ async function handleRunInner({ positional, options }, stream) {
|
|
|
280
285
|
watcherPort: runtimeOptions.watcherPort
|
|
281
286
|
}
|
|
282
287
|
);
|
|
288
|
+
performanceObservation = startRunPerformanceObservation(
|
|
289
|
+
adapter,
|
|
290
|
+
target,
|
|
291
|
+
artifactsDir,
|
|
292
|
+
process.env,
|
|
293
|
+
{
|
|
294
|
+
cdpPort: runtimeOptions.cdpPort,
|
|
295
|
+
watcherPort: runtimeOptions.watcherPort
|
|
296
|
+
}
|
|
297
|
+
);
|
|
283
298
|
stream.phase("execute");
|
|
284
299
|
let executionResult;
|
|
285
300
|
try {
|
|
@@ -307,13 +322,17 @@ async function handleRunInner({ positional, options }, stream) {
|
|
|
307
322
|
(code) => stream.phase("recover", { code })
|
|
308
323
|
);
|
|
309
324
|
} catch (error) {
|
|
310
|
-
await networkObservation?.finalize();
|
|
325
|
+
await networkObservation?.finalize().catch(() => void 0);
|
|
311
326
|
networkObservation = void 0;
|
|
327
|
+
await performanceObservation?.finalize().catch(() => void 0);
|
|
328
|
+
performanceObservation = void 0;
|
|
312
329
|
throw error;
|
|
313
330
|
}
|
|
314
331
|
const { result, violation } = executionResult;
|
|
315
332
|
await networkObservation?.finalize(result.artifactManifestPath);
|
|
316
333
|
networkObservation = void 0;
|
|
334
|
+
await performanceObservation?.finalize(result.artifactManifestPath);
|
|
335
|
+
performanceObservation = void 0;
|
|
317
336
|
for (const mutation of state.mutations) stream.mutation(mutation);
|
|
318
337
|
for (const recovery of state.recovered) stream.recovery(recovery);
|
|
319
338
|
if (violation !== null) {
|