@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,174 @@
|
|
|
1
|
+
import { WebSocket } from "ws";
|
|
2
|
+
import { withTimeout } from "../../async.js";
|
|
3
|
+
const DEFAULT_TIMEOUT_MS = 1e4;
|
|
4
|
+
async function createBrowserCdpClient(cdpPort) {
|
|
5
|
+
if (!Number.isInteger(cdpPort) || cdpPort <= 0) {
|
|
6
|
+
throw new Error("Extension CDP requires a valid port.");
|
|
7
|
+
}
|
|
8
|
+
const version = asRecord(
|
|
9
|
+
await fetchJson(`http://127.0.0.1:${cdpPort}/json/version`)
|
|
10
|
+
);
|
|
11
|
+
const webSocketUrl = String(version.webSocketDebuggerUrl ?? "");
|
|
12
|
+
if (!webSocketUrl) {
|
|
13
|
+
throw new Error("Extension browser CDP WebSocket is unavailable.");
|
|
14
|
+
}
|
|
15
|
+
const socket = new WebSocket(webSocketUrl);
|
|
16
|
+
await waitForOpen(socket);
|
|
17
|
+
let nextId = 0;
|
|
18
|
+
const pending = /* @__PURE__ */ new Map();
|
|
19
|
+
const handlers = /* @__PURE__ */ new Set();
|
|
20
|
+
const closeHandlers = /* @__PURE__ */ new Set();
|
|
21
|
+
socket.on("message", (data) => {
|
|
22
|
+
let message;
|
|
23
|
+
try {
|
|
24
|
+
message = JSON.parse(String(data));
|
|
25
|
+
} catch {
|
|
26
|
+
return;
|
|
27
|
+
}
|
|
28
|
+
if (message.id && pending.has(message.id)) {
|
|
29
|
+
const command = pending.get(message.id);
|
|
30
|
+
pending.delete(message.id);
|
|
31
|
+
if (!command) return;
|
|
32
|
+
clearTimeout(command.timer);
|
|
33
|
+
if (message.error) {
|
|
34
|
+
command.reject(
|
|
35
|
+
new Error(
|
|
36
|
+
String(message.error.message ?? "Extension CDP command failed")
|
|
37
|
+
)
|
|
38
|
+
);
|
|
39
|
+
} else {
|
|
40
|
+
command.resolve(message.result ?? {});
|
|
41
|
+
}
|
|
42
|
+
return;
|
|
43
|
+
}
|
|
44
|
+
for (const handler of handlers) handler(message);
|
|
45
|
+
});
|
|
46
|
+
socket.on("close", () => {
|
|
47
|
+
for (const command of pending.values()) {
|
|
48
|
+
clearTimeout(command.timer);
|
|
49
|
+
command.reject(new Error("Extension browser CDP connection closed"));
|
|
50
|
+
}
|
|
51
|
+
pending.clear();
|
|
52
|
+
handlers.clear();
|
|
53
|
+
for (const handler of closeHandlers) handler();
|
|
54
|
+
closeHandlers.clear();
|
|
55
|
+
});
|
|
56
|
+
return {
|
|
57
|
+
send(method, params = {}, sessionId, timeoutMs = DEFAULT_TIMEOUT_MS) {
|
|
58
|
+
return new Promise((resolve, reject) => {
|
|
59
|
+
const id = ++nextId;
|
|
60
|
+
const timer = setTimeout(() => {
|
|
61
|
+
pending.delete(id);
|
|
62
|
+
reject(new Error(`Extension CDP command timed out: ${method}`));
|
|
63
|
+
}, timeoutMs);
|
|
64
|
+
pending.set(id, { resolve, reject, timer });
|
|
65
|
+
socket.send(
|
|
66
|
+
JSON.stringify({
|
|
67
|
+
id,
|
|
68
|
+
method,
|
|
69
|
+
params,
|
|
70
|
+
...sessionId ? { sessionId } : {}
|
|
71
|
+
})
|
|
72
|
+
);
|
|
73
|
+
});
|
|
74
|
+
},
|
|
75
|
+
onMessage(handler) {
|
|
76
|
+
handlers.add(handler);
|
|
77
|
+
return () => handlers.delete(handler);
|
|
78
|
+
},
|
|
79
|
+
onClose(handler) {
|
|
80
|
+
closeHandlers.add(handler);
|
|
81
|
+
return () => closeHandlers.delete(handler);
|
|
82
|
+
},
|
|
83
|
+
close() {
|
|
84
|
+
socket.close();
|
|
85
|
+
}
|
|
86
|
+
};
|
|
87
|
+
}
|
|
88
|
+
async function fetchCdpJson(cdpPort, path = "/json") {
|
|
89
|
+
return fetchJson(`http://127.0.0.1:${cdpPort}${path}`);
|
|
90
|
+
}
|
|
91
|
+
function asBrowserCdpTarget(value) {
|
|
92
|
+
const target = asRecord(value);
|
|
93
|
+
const targetId = String(target.targetId ?? target.id ?? "");
|
|
94
|
+
const type = String(target.type ?? "");
|
|
95
|
+
const url = String(target.url ?? "");
|
|
96
|
+
return targetId && type && url ? { targetId, type, url } : null;
|
|
97
|
+
}
|
|
98
|
+
function extensionIdFromCdpTargets(value) {
|
|
99
|
+
const targets = Array.isArray(value) ? value : [];
|
|
100
|
+
for (const candidate of targets) {
|
|
101
|
+
const target = asBrowserCdpTarget(candidate);
|
|
102
|
+
if (!target) continue;
|
|
103
|
+
try {
|
|
104
|
+
const url = new URL(target.url);
|
|
105
|
+
if (url.protocol === "chrome-extension:" && url.hostname) {
|
|
106
|
+
return url.hostname;
|
|
107
|
+
}
|
|
108
|
+
} catch {
|
|
109
|
+
continue;
|
|
110
|
+
}
|
|
111
|
+
}
|
|
112
|
+
return null;
|
|
113
|
+
}
|
|
114
|
+
function selectExtensionUiTarget(value, extensionId) {
|
|
115
|
+
const targets = Array.isArray(value) ? value : [];
|
|
116
|
+
const candidates = targets.map(asBrowserCdpTarget).filter((candidate) => {
|
|
117
|
+
if (!candidate || !["page", "other"].includes(candidate.type)) return false;
|
|
118
|
+
try {
|
|
119
|
+
const url = new URL(candidate.url);
|
|
120
|
+
return url.protocol === "chrome-extension:" && url.hostname === extensionId;
|
|
121
|
+
} catch {
|
|
122
|
+
return false;
|
|
123
|
+
}
|
|
124
|
+
});
|
|
125
|
+
const matchingPath = (target, pathname) => {
|
|
126
|
+
try {
|
|
127
|
+
return new URL(target.url).pathname === pathname;
|
|
128
|
+
} catch {
|
|
129
|
+
return false;
|
|
130
|
+
}
|
|
131
|
+
};
|
|
132
|
+
const home = candidates.filter((target) => matchingPath(target, "/home.html"));
|
|
133
|
+
if (home.length > 0) return home.length === 1 ? home[0] : null;
|
|
134
|
+
const sidepanel = candidates.filter(
|
|
135
|
+
(target) => matchingPath(target, "/sidepanel.html")
|
|
136
|
+
);
|
|
137
|
+
return sidepanel.length === 1 ? sidepanel[0] : null;
|
|
138
|
+
}
|
|
139
|
+
async function fetchJson(url) {
|
|
140
|
+
const response = await fetch(url);
|
|
141
|
+
if (!response.ok) {
|
|
142
|
+
throw new Error(`Extension CDP returned HTTP ${response.status}.`);
|
|
143
|
+
}
|
|
144
|
+
return response.json();
|
|
145
|
+
}
|
|
146
|
+
function waitForOpen(socket) {
|
|
147
|
+
const onOpen = (resolve) => () => resolve();
|
|
148
|
+
let openHandler;
|
|
149
|
+
let errorHandler;
|
|
150
|
+
const opened = new Promise((resolve, reject) => {
|
|
151
|
+
openHandler = onOpen(resolve);
|
|
152
|
+
errorHandler = reject;
|
|
153
|
+
socket.once("open", openHandler);
|
|
154
|
+
socket.once("error", errorHandler);
|
|
155
|
+
});
|
|
156
|
+
return withTimeout(opened, {
|
|
157
|
+
message: "Extension browser CDP connection timed out.",
|
|
158
|
+
timeoutMs: DEFAULT_TIMEOUT_MS,
|
|
159
|
+
onTimeout: () => socket.close()
|
|
160
|
+
}).finally(() => {
|
|
161
|
+
if (openHandler) socket.off("open", openHandler);
|
|
162
|
+
if (errorHandler) socket.off("error", errorHandler);
|
|
163
|
+
});
|
|
164
|
+
}
|
|
165
|
+
function asRecord(value) {
|
|
166
|
+
return value && typeof value === "object" && !Array.isArray(value) ? value : {};
|
|
167
|
+
}
|
|
168
|
+
export {
|
|
169
|
+
asBrowserCdpTarget,
|
|
170
|
+
createBrowserCdpClient,
|
|
171
|
+
extensionIdFromCdpTargets,
|
|
172
|
+
fetchCdpJson,
|
|
173
|
+
selectExtensionUiTarget
|
|
174
|
+
};
|
|
@@ -1,6 +1,11 @@
|
|
|
1
1
|
import path from "node:path";
|
|
2
|
-
import { WebSocket } from "ws";
|
|
3
2
|
import brokerModule from "../../../adapters/mobile/bridge-runtime/lib/cdp-broker.cjs";
|
|
3
|
+
import {
|
|
4
|
+
asBrowserCdpTarget,
|
|
5
|
+
createBrowserCdpClient,
|
|
6
|
+
extensionIdFromCdpTargets,
|
|
7
|
+
fetchCdpJson
|
|
8
|
+
} from "./browser-cdp.js";
|
|
4
9
|
const DEVICE_ID = "extension";
|
|
5
10
|
const COMMAND_TIMEOUT_MS = 1e4;
|
|
6
11
|
const EXTENSION_TARGET_TYPES = /* @__PURE__ */ new Set([
|
|
@@ -14,43 +19,19 @@ async function createExtensionNetworkObserver(cdpPort, runtimeDir) {
|
|
|
14
19
|
if (!Number.isInteger(cdpPort) || cdpPort <= 0) {
|
|
15
20
|
throw new Error("Extension network observation requires a valid CDP port.");
|
|
16
21
|
}
|
|
17
|
-
const
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
]);
|
|
21
|
-
const browserWebSocketUrl = String(
|
|
22
|
-
asRecord(version).webSocketDebuggerUrl ?? ""
|
|
23
|
-
);
|
|
24
|
-
const extensionId = extensionIdFromTargets(targets);
|
|
25
|
-
if (!browserWebSocketUrl || !extensionId) {
|
|
22
|
+
const targets = await fetchCdpJson(cdpPort);
|
|
23
|
+
const extensionId = extensionIdFromCdpTargets(targets);
|
|
24
|
+
if (!extensionId) {
|
|
26
25
|
throw new Error(
|
|
27
26
|
"Extension CDP browser or loaded extension target is unavailable."
|
|
28
27
|
);
|
|
29
28
|
}
|
|
30
|
-
const
|
|
31
|
-
await waitForOpen(socket);
|
|
32
|
-
let nextId = 0;
|
|
29
|
+
const connection = await createBrowserCdpClient(cdpPort);
|
|
33
30
|
let closed = false;
|
|
34
31
|
const brokerRef = {};
|
|
35
|
-
const pending = /* @__PURE__ */ new Map();
|
|
36
32
|
const targetSessions = /* @__PURE__ */ new Map();
|
|
37
33
|
const attaching = /* @__PURE__ */ new Set();
|
|
38
|
-
const send = (method, params = {}, sessionId, timeoutMs = COMMAND_TIMEOUT_MS) =>
|
|
39
|
-
const id = ++nextId;
|
|
40
|
-
const timer = setTimeout(() => {
|
|
41
|
-
pending.delete(id);
|
|
42
|
-
reject(new Error(`Extension CDP command timed out: ${method}`));
|
|
43
|
-
}, timeoutMs);
|
|
44
|
-
pending.set(id, { resolve, reject, timer });
|
|
45
|
-
socket.send(
|
|
46
|
-
JSON.stringify({
|
|
47
|
-
id,
|
|
48
|
-
method,
|
|
49
|
-
params,
|
|
50
|
-
...sessionId ? { sessionId } : {}
|
|
51
|
-
})
|
|
52
|
-
);
|
|
53
|
-
});
|
|
34
|
+
const send = (method, params = {}, sessionId, timeoutMs = COMMAND_TIMEOUT_MS) => connection.send(method, params, sessionId, timeoutMs);
|
|
54
35
|
const relevantTarget = (target) => {
|
|
55
36
|
if (!EXTENSION_TARGET_TYPES.has(String(target.type))) return false;
|
|
56
37
|
try {
|
|
@@ -89,36 +70,14 @@ async function createExtensionNetworkObserver(cdpPort, runtimeDir) {
|
|
|
89
70
|
attaching.delete(target.targetId);
|
|
90
71
|
}
|
|
91
72
|
};
|
|
92
|
-
|
|
93
|
-
let message;
|
|
94
|
-
try {
|
|
95
|
-
message = JSON.parse(String(data));
|
|
96
|
-
} catch {
|
|
97
|
-
return;
|
|
98
|
-
}
|
|
99
|
-
if (message.id && pending.has(message.id)) {
|
|
100
|
-
const command = pending.get(message.id);
|
|
101
|
-
pending.delete(message.id);
|
|
102
|
-
if (!command) return;
|
|
103
|
-
clearTimeout(command.timer);
|
|
104
|
-
if (message.error) {
|
|
105
|
-
command.reject(
|
|
106
|
-
new Error(
|
|
107
|
-
String(message.error.message ?? "Extension CDP command failed")
|
|
108
|
-
)
|
|
109
|
-
);
|
|
110
|
-
} else {
|
|
111
|
-
command.resolve(message.result ?? {});
|
|
112
|
-
}
|
|
113
|
-
return;
|
|
114
|
-
}
|
|
73
|
+
connection.onMessage((message) => {
|
|
115
74
|
if (message.method === "Target.targetCreated") {
|
|
116
|
-
const target =
|
|
75
|
+
const target = asBrowserCdpTarget(message.params?.targetInfo);
|
|
117
76
|
if (target) void attachTarget(target);
|
|
118
77
|
return;
|
|
119
78
|
}
|
|
120
79
|
if (message.method === "Target.attachedToTarget") {
|
|
121
|
-
const target =
|
|
80
|
+
const target = asBrowserCdpTarget(message.params?.targetInfo);
|
|
122
81
|
const sessionId = String(message.params?.sessionId ?? "");
|
|
123
82
|
if (target && sessionId && relevantTarget(target)) {
|
|
124
83
|
targetSessions.set(target.targetId, sessionId);
|
|
@@ -143,22 +102,17 @@ async function createExtensionNetworkObserver(cdpPort, runtimeDir) {
|
|
|
143
102
|
);
|
|
144
103
|
}
|
|
145
104
|
});
|
|
146
|
-
|
|
105
|
+
connection.onClose(() => {
|
|
147
106
|
if (!closed) brokerRef.current?.onSessionClose(DEVICE_ID);
|
|
148
|
-
for (const command of pending.values()) {
|
|
149
|
-
clearTimeout(command.timer);
|
|
150
|
-
command.reject(new Error("Extension browser CDP connection closed"));
|
|
151
|
-
}
|
|
152
|
-
pending.clear();
|
|
153
107
|
});
|
|
154
108
|
await send("Target.setDiscoverTargets", { discover: true });
|
|
155
109
|
const targetResult = asRecord(await send("Target.getTargets"));
|
|
156
110
|
const targetInfos = Array.isArray(targetResult.targetInfos) ? targetResult.targetInfos : [];
|
|
157
111
|
await Promise.all(
|
|
158
|
-
targetInfos.map(
|
|
112
|
+
targetInfos.map(asBrowserCdpTarget).filter((target) => Boolean(target)).map(attachTarget)
|
|
159
113
|
);
|
|
160
114
|
if (targetSessions.size === 0) {
|
|
161
|
-
|
|
115
|
+
connection.close();
|
|
162
116
|
throw new Error("Extension CDP exposes no attachable extension targets.");
|
|
163
117
|
}
|
|
164
118
|
const sessions = /* @__PURE__ */ new Map([[DEVICE_ID, { brokerReady: true }]]);
|
|
@@ -208,7 +162,7 @@ async function createExtensionNetworkObserver(cdpPort, runtimeDir) {
|
|
|
208
162
|
const infos = asRecord(result).targetInfos;
|
|
209
163
|
if (!Array.isArray(infos)) return;
|
|
210
164
|
for (const value of infos) {
|
|
211
|
-
const target =
|
|
165
|
+
const target = asBrowserCdpTarget(value);
|
|
212
166
|
if (target) void attachTarget(target);
|
|
213
167
|
}
|
|
214
168
|
}).catch(() => brokerRef.current?.onSessionClose(DEVICE_ID));
|
|
@@ -234,58 +188,13 @@ async function createExtensionNetworkObserver(cdpPort, runtimeDir) {
|
|
|
234
188
|
closed = true;
|
|
235
189
|
client.close();
|
|
236
190
|
brokerRef.current?.close();
|
|
237
|
-
|
|
191
|
+
connection.close();
|
|
238
192
|
}
|
|
239
193
|
};
|
|
240
194
|
}
|
|
241
|
-
async function fetchJson(url) {
|
|
242
|
-
const response = await fetch(url);
|
|
243
|
-
if (!response.ok)
|
|
244
|
-
throw new Error(`Extension CDP returned HTTP ${response.status}.`);
|
|
245
|
-
return response.json();
|
|
246
|
-
}
|
|
247
|
-
function extensionIdFromTargets(targets) {
|
|
248
|
-
const values = Array.isArray(targets) ? targets : [];
|
|
249
|
-
for (const target of values) {
|
|
250
|
-
const info = asTargetInfo(target);
|
|
251
|
-
if (!info) continue;
|
|
252
|
-
try {
|
|
253
|
-
const url = new URL(info.url);
|
|
254
|
-
if (url.protocol !== "chrome-extension:") continue;
|
|
255
|
-
const extensionId = url.hostname;
|
|
256
|
-
if (extensionId) return extensionId;
|
|
257
|
-
} catch {
|
|
258
|
-
continue;
|
|
259
|
-
}
|
|
260
|
-
}
|
|
261
|
-
return null;
|
|
262
|
-
}
|
|
263
195
|
function asRecord(value) {
|
|
264
196
|
return value && typeof value === "object" && !Array.isArray(value) ? value : {};
|
|
265
197
|
}
|
|
266
|
-
function asTargetInfo(value) {
|
|
267
|
-
const target = asRecord(value);
|
|
268
|
-
const targetId = String(target.targetId ?? target.id ?? "");
|
|
269
|
-
const type = String(target.type ?? "");
|
|
270
|
-
const url = String(target.url ?? "");
|
|
271
|
-
return targetId && type && url ? { targetId, type, url } : null;
|
|
272
|
-
}
|
|
273
|
-
function waitForOpen(socket) {
|
|
274
|
-
return new Promise((resolve, reject) => {
|
|
275
|
-
const timer = setTimeout(() => {
|
|
276
|
-
socket.close();
|
|
277
|
-
reject(new Error("Extension browser CDP connection timed out."));
|
|
278
|
-
}, COMMAND_TIMEOUT_MS);
|
|
279
|
-
socket.once("open", () => {
|
|
280
|
-
clearTimeout(timer);
|
|
281
|
-
resolve();
|
|
282
|
-
});
|
|
283
|
-
socket.once("error", (error) => {
|
|
284
|
-
clearTimeout(timer);
|
|
285
|
-
reject(error);
|
|
286
|
-
});
|
|
287
|
-
});
|
|
288
|
-
}
|
|
289
198
|
async function waitForSocket(socketPath) {
|
|
290
199
|
const { existsSync } = await import("node:fs");
|
|
291
200
|
const deadline = Date.now() + COMMAND_TIMEOUT_MS;
|
|
@@ -0,0 +1,75 @@
|
|
|
1
|
+
import {
|
|
2
|
+
createCdpTraceCollector
|
|
3
|
+
} from "../performance/cdp-trace.js";
|
|
4
|
+
import {
|
|
5
|
+
createBrowserCdpClient,
|
|
6
|
+
fetchCdpJson,
|
|
7
|
+
selectExtensionUiTarget
|
|
8
|
+
} from "./browser-cdp.js";
|
|
9
|
+
import { resolveExtensionId } from "./extension-id.js";
|
|
10
|
+
async function createExtensionPerformanceBackend(cdpPort, target) {
|
|
11
|
+
const extensionId = (await resolveExtensionId(target, { cdpPort })).extensionId;
|
|
12
|
+
if (!extensionId) {
|
|
13
|
+
throw new Error("Extension performance observation could not resolve the MetaMask extension id.");
|
|
14
|
+
}
|
|
15
|
+
const uiTarget = selectExtensionUiTarget(
|
|
16
|
+
await fetchCdpJson(cdpPort),
|
|
17
|
+
extensionId
|
|
18
|
+
);
|
|
19
|
+
if (!uiTarget) {
|
|
20
|
+
throw new Error("Extension performance observation requires an open MetaMask UI target.");
|
|
21
|
+
}
|
|
22
|
+
const browser = await createBrowserCdpClient(cdpPort);
|
|
23
|
+
const attached = asRecord(
|
|
24
|
+
await browser.send(
|
|
25
|
+
"Target.attachToTarget",
|
|
26
|
+
{ targetId: uiTarget.targetId, flatten: true },
|
|
27
|
+
void 0,
|
|
28
|
+
1e4
|
|
29
|
+
)
|
|
30
|
+
);
|
|
31
|
+
const sessionId = String(attached.sessionId ?? "");
|
|
32
|
+
if (!sessionId) {
|
|
33
|
+
browser.close();
|
|
34
|
+
throw new Error("Extension performance observation could not attach to the MetaMask UI target.");
|
|
35
|
+
}
|
|
36
|
+
const client = browserTraceClient(browser);
|
|
37
|
+
return createCdpTraceCollector(client, "extension", async (name) => {
|
|
38
|
+
const before = Date.now();
|
|
39
|
+
await browser.send(
|
|
40
|
+
"Runtime.evaluate",
|
|
41
|
+
{
|
|
42
|
+
expression: `performance.mark(${JSON.stringify(name)})`,
|
|
43
|
+
returnByValue: true
|
|
44
|
+
},
|
|
45
|
+
sessionId,
|
|
46
|
+
1e4
|
|
47
|
+
);
|
|
48
|
+
const after = Date.now();
|
|
49
|
+
return {
|
|
50
|
+
hostEpochMs: (before + after) / 2,
|
|
51
|
+
uncertaintyMs: (after - before) / 2
|
|
52
|
+
};
|
|
53
|
+
});
|
|
54
|
+
}
|
|
55
|
+
function browserTraceClient(browser) {
|
|
56
|
+
return {
|
|
57
|
+
send(method, params = {}, timeoutMs) {
|
|
58
|
+
return browser.send(method, params, void 0, timeoutMs);
|
|
59
|
+
},
|
|
60
|
+
on(method, handler) {
|
|
61
|
+
return browser.onMessage((message) => {
|
|
62
|
+
if (message.method === method) handler(message.params ?? {});
|
|
63
|
+
});
|
|
64
|
+
},
|
|
65
|
+
close() {
|
|
66
|
+
browser.close();
|
|
67
|
+
}
|
|
68
|
+
};
|
|
69
|
+
}
|
|
70
|
+
function asRecord(value) {
|
|
71
|
+
return value && typeof value === "object" && !Array.isArray(value) ? value : {};
|
|
72
|
+
}
|
|
73
|
+
export {
|
|
74
|
+
createExtensionPerformanceBackend
|
|
75
|
+
};
|
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
function summarizeFrames(samples) {
|
|
2
|
+
const validSamples = samples.filter(
|
|
3
|
+
(sample) => Number.isFinite(sample.durationMs) && sample.durationMs > 0
|
|
4
|
+
);
|
|
5
|
+
const durations = validSamples.map((sample) => sample.durationMs).sort((first2, second) => first2 - second);
|
|
6
|
+
if (durations.length === 0) return { frameCount: 0 };
|
|
7
|
+
const jankValues = validSamples.map((sample) => sample.janky).filter((value) => value !== void 0);
|
|
8
|
+
const jankyFrameCount = jankValues.filter(Boolean).length;
|
|
9
|
+
const first = validSamples[0]?.completedAtEpochMs;
|
|
10
|
+
const last = validSamples.at(-1)?.completedAtEpochMs;
|
|
11
|
+
const elapsedMs = first !== void 0 && last !== void 0 && last > first ? last - first : void 0;
|
|
12
|
+
return {
|
|
13
|
+
...elapsedMs ? {
|
|
14
|
+
averageFps: round(
|
|
15
|
+
(validSamples.length - 1) * 1e3 / elapsedMs
|
|
16
|
+
)
|
|
17
|
+
} : {},
|
|
18
|
+
frameCount: durations.length,
|
|
19
|
+
...jankValues.length === validSamples.length ? {
|
|
20
|
+
jankyFrameCount,
|
|
21
|
+
jankyFramePercent: round(
|
|
22
|
+
jankyFrameCount / validSamples.length * 100
|
|
23
|
+
)
|
|
24
|
+
} : {},
|
|
25
|
+
longestFrameMs: round(durations.at(-1) ?? 0),
|
|
26
|
+
p50FrameMs: round(percentile(durations, 0.5)),
|
|
27
|
+
p95FrameMs: round(percentile(durations, 0.95)),
|
|
28
|
+
p99FrameMs: round(percentile(durations, 0.99))
|
|
29
|
+
};
|
|
30
|
+
}
|
|
31
|
+
function percentile(sorted, quantile) {
|
|
32
|
+
if (sorted.length === 1) return sorted[0] ?? 0;
|
|
33
|
+
const position = (sorted.length - 1) * quantile;
|
|
34
|
+
const lower = Math.floor(position);
|
|
35
|
+
const upper = Math.ceil(position);
|
|
36
|
+
const lowerValue = sorted[lower] ?? 0;
|
|
37
|
+
const upperValue = sorted[upper] ?? lowerValue;
|
|
38
|
+
return lowerValue + (upperValue - lowerValue) * (position - lower);
|
|
39
|
+
}
|
|
40
|
+
function round(value) {
|
|
41
|
+
return Math.round(value * 1e3) / 1e3;
|
|
42
|
+
}
|
|
43
|
+
export {
|
|
44
|
+
summarizeFrames
|
|
45
|
+
};
|
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
import {
|
|
2
|
+
createCdpTraceCollector
|
|
3
|
+
} from "../performance/cdp-trace.js";
|
|
4
|
+
import {
|
|
5
|
+
connectMobileBroker
|
|
6
|
+
} from "../../network-observation.js";
|
|
7
|
+
async function createMobilePerformanceBackend(target, env) {
|
|
8
|
+
const { client } = await connectMobileBroker(target, env);
|
|
9
|
+
return createCdpTraceCollector(
|
|
10
|
+
client,
|
|
11
|
+
resolvePlatform(env),
|
|
12
|
+
async (name) => {
|
|
13
|
+
const before = Date.now();
|
|
14
|
+
await markRuntime(client, name);
|
|
15
|
+
const after = Date.now();
|
|
16
|
+
return {
|
|
17
|
+
hostEpochMs: (before + after) / 2,
|
|
18
|
+
uncertaintyMs: (after - before) / 2
|
|
19
|
+
};
|
|
20
|
+
}
|
|
21
|
+
);
|
|
22
|
+
}
|
|
23
|
+
async function markRuntime(client, name) {
|
|
24
|
+
await client.send(
|
|
25
|
+
"Runtime.evaluate",
|
|
26
|
+
{
|
|
27
|
+
expression: `performance.mark(${JSON.stringify(name)})`,
|
|
28
|
+
returnByValue: true,
|
|
29
|
+
awaitPromise: false
|
|
30
|
+
},
|
|
31
|
+
1e4
|
|
32
|
+
);
|
|
33
|
+
}
|
|
34
|
+
function resolvePlatform(env) {
|
|
35
|
+
const explicit = String(
|
|
36
|
+
env.MM_HARNESS_EXPLICIT_PLATFORM ?? env.RECIPE_HARNESS_PLATFORM ?? ""
|
|
37
|
+
).toLowerCase();
|
|
38
|
+
if (explicit === "android" || explicit === "ios") return explicit;
|
|
39
|
+
return env.ADB_SERIAL || env.ANDROID_SERIAL || env.ANDROID_DEVICE ? "android" : "ios";
|
|
40
|
+
}
|
|
41
|
+
export {
|
|
42
|
+
createMobilePerformanceBackend
|
|
43
|
+
};
|