@deeeed/metamask-harness 0.34.2 → 0.34.4
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 +22 -0
- package/dist/adapters/mobile/source-freshness.js +51 -14
- package/dist/adapters.js +5 -2
- package/dist/recipe-security.js +1 -0
- package/library/actions/core/perps/_controller.mjs +75 -23
- package/library/actions/core/perps/read_account.mjs +6 -12
- package/library/actions/core/perps/read_snapshot.mjs +66 -0
- package/library/actions/extension/performance/_navigation-memory.mjs +372 -0
- package/library/actions/extension/performance/compare_idle_navigation_memory.mjs +18 -0
- package/library/actions/extension/performance/measure_detached_dom.mjs +18 -0
- package/library/actions/extension/performance/measure_navigation_memory.mjs +18 -0
- package/library/actions/mobile/app/network-control.mjs +24 -12
- package/library/manifests/core.action-manifest.json +55 -0
- package/library/manifests/extension.action-manifest.json +100 -0
- package/library/recipes/core/perps/snapshot.recipe.json +25 -0
- package/library/recipes/extension/performance/navigation-memory.recipe.json +175 -0
- package/package.json +1 -1
- package/scripts/site-contrast.mjs +66 -10
- package/site/architecture.html +22 -16
- package/site/assets/progress.mjs +47 -2
- package/site/assets/style.css +181 -1
- package/site/cheatsheet.html +14 -12
- package/site/how-it-works.html +693 -0
- package/site/index.html +133 -643
- package/site/perps.html +7 -6
- package/site/recipes.html +23 -17
- package/site/reviewers.html +7 -6
- package/site/tutorials/index.html +7 -6
- package/site/tutorials/v1.html +12 -11
- package/site/tutorials/v2.html +7 -6
- package/site/tutorials/v3.html +15 -11
- package/site/tutorials/v4.html +6 -5
- package/site/tutorials/v5.html +6 -5
- package/site/tutorials/v6.html +6 -5
- package/site/tutorials/v7.html +6 -5
|
@@ -0,0 +1,372 @@
|
|
|
1
|
+
const DEFAULT_SCREEN_TIMEOUT_MS = 15_000;
|
|
2
|
+
const DEFAULT_SETTLE_MS = 1_500;
|
|
3
|
+
const DEFAULT_GC_SETTLE_MS = 300;
|
|
4
|
+
const DEFAULT_ACTION_TIMEOUT_MS = 400_000;
|
|
5
|
+
const DEFAULT_ROUND_DURATION_MS = 10_000;
|
|
6
|
+
|
|
7
|
+
function finiteNumber(value, fallback, label) {
|
|
8
|
+
const parsed = value ?? fallback;
|
|
9
|
+
if (!Number.isFinite(parsed)) throw new Error(`${label} must be a finite number.`);
|
|
10
|
+
return parsed;
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
function positiveInteger(value, fallback, label, minimum = 1) {
|
|
14
|
+
const parsed = finiteNumber(value, fallback, label);
|
|
15
|
+
if (!Number.isInteger(parsed) || parsed < minimum) {
|
|
16
|
+
throw new Error(`${label} must be an integer >= ${minimum}.`);
|
|
17
|
+
}
|
|
18
|
+
return parsed;
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
function requiredText(value, label) {
|
|
22
|
+
if (typeof value !== 'string' || value.trim().length === 0) {
|
|
23
|
+
throw new Error(`${label} is required.`);
|
|
24
|
+
}
|
|
25
|
+
return value.trim();
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
function optionalHash(value, fallback) {
|
|
29
|
+
const hash = typeof value === 'string' && value.trim() ? value.trim() : fallback;
|
|
30
|
+
return hash.startsWith('#') ? hash : `#${hash}`;
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
export function leastSquaresSlope(values) {
|
|
34
|
+
if (!Array.isArray(values) || values.length < 2 || values.some((value) => !Number.isFinite(value))) {
|
|
35
|
+
throw new Error('leastSquaresSlope requires at least two finite samples.');
|
|
36
|
+
}
|
|
37
|
+
const meanX = (values.length - 1) / 2;
|
|
38
|
+
const meanY = values.reduce((sum, value) => sum + value, 0) / values.length;
|
|
39
|
+
let numerator = 0;
|
|
40
|
+
let denominator = 0;
|
|
41
|
+
for (let index = 0; index < values.length; index += 1) {
|
|
42
|
+
numerator += (index - meanX) * (values[index] - meanY);
|
|
43
|
+
denominator += (index - meanX) ** 2;
|
|
44
|
+
}
|
|
45
|
+
return numerator / denominator;
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
export function assessNavigationSlope(slopes, bounds) {
|
|
49
|
+
return {
|
|
50
|
+
nodes: slopes.nodes <= bounds.maxNodesPerCycle,
|
|
51
|
+
listeners: slopes.listeners <= bounds.maxListenersPerCycle,
|
|
52
|
+
};
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
export function countDetachedNodes(snapshot) {
|
|
56
|
+
const fields = snapshot?.snapshot?.meta?.node_fields;
|
|
57
|
+
const nodes = snapshot?.nodes;
|
|
58
|
+
if (!Array.isArray(fields) || !Array.isArray(nodes)) {
|
|
59
|
+
throw new Error('Heap snapshot is missing node metadata.');
|
|
60
|
+
}
|
|
61
|
+
const detachedIndex = fields.indexOf('detachedness');
|
|
62
|
+
if (detachedIndex < 0) throw new Error('Heap snapshot has no detachedness field.');
|
|
63
|
+
let count = 0;
|
|
64
|
+
for (let index = 0; index < nodes.length; index += fields.length) {
|
|
65
|
+
if (nodes[index + detachedIndex] === 2) count += 1;
|
|
66
|
+
}
|
|
67
|
+
return count;
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
export function navigationMemoryOptions(node, countField = 'cycles', defaultCount = 8) {
|
|
71
|
+
const screenHash = optionalHash(node.screen_hash ?? node.screenHash, '');
|
|
72
|
+
if (screenHash === '#') throw new Error('screen_hash is required.');
|
|
73
|
+
const screenTestId = requiredText(node.screen_test_id ?? node.screenTestId, 'screen_test_id');
|
|
74
|
+
return {
|
|
75
|
+
screenHash,
|
|
76
|
+
screenTestId,
|
|
77
|
+
returnHash: optionalHash(node.return_hash ?? node.returnHash, '#/'),
|
|
78
|
+
count: positiveInteger(node[countField], defaultCount, countField, 2),
|
|
79
|
+
settleMs: positiveInteger(node.settle_ms ?? node.settleMs, DEFAULT_SETTLE_MS, 'settle_ms', 0),
|
|
80
|
+
gcSettleMs: positiveInteger(node.gc_settle_ms ?? node.gcSettleMs, DEFAULT_GC_SETTLE_MS, 'gc_settle_ms', 0),
|
|
81
|
+
screenTimeoutMs: positiveInteger(
|
|
82
|
+
node.screen_timeout_ms ?? node.screenTimeoutMs,
|
|
83
|
+
DEFAULT_SCREEN_TIMEOUT_MS,
|
|
84
|
+
'screen_timeout_ms',
|
|
85
|
+
),
|
|
86
|
+
timeoutMs: positiveInteger(node.timeout_ms ?? node.timeoutMs, DEFAULT_ACTION_TIMEOUT_MS, 'timeout_ms'),
|
|
87
|
+
roundDurationMs: positiveInteger(
|
|
88
|
+
node.round_duration_ms ?? node.roundDurationMs,
|
|
89
|
+
DEFAULT_ROUND_DURATION_MS,
|
|
90
|
+
'round_duration_ms',
|
|
91
|
+
),
|
|
92
|
+
};
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
function deadlineIn(timeoutMs) {
|
|
96
|
+
return Date.now() + timeoutMs;
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
function remaining(deadline, label) {
|
|
100
|
+
const value = deadline - Date.now();
|
|
101
|
+
if (value <= 0) throw new Error(`${label} timed out.`);
|
|
102
|
+
return value;
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
async function boundedSleep(ms, deadline, label) {
|
|
106
|
+
if (ms === 0) return;
|
|
107
|
+
const available = remaining(deadline, label);
|
|
108
|
+
if (ms > available) throw new Error(`${label} timed out.`);
|
|
109
|
+
await new Promise((resolve) => setTimeout(resolve, ms));
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
async function navigateAndSettle(page, hash, testId, options, deadline, label) {
|
|
113
|
+
await page.navigateHash(hash, Math.min(options.screenTimeoutMs, remaining(deadline, label)));
|
|
114
|
+
if (testId) {
|
|
115
|
+
await page.waitForSelector(`[data-testid=${JSON.stringify(testId)}]`, {
|
|
116
|
+
timeoutMs: Math.min(options.screenTimeoutMs, remaining(deadline, label)),
|
|
117
|
+
});
|
|
118
|
+
}
|
|
119
|
+
await boundedSleep(options.settleMs, deadline, label);
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
export async function forceGarbageCollection(session, settleMs, deadline) {
|
|
123
|
+
for (let pass = 0; pass < 2; pass += 1) {
|
|
124
|
+
await session.call('HeapProfiler.collectGarbage', {}, {
|
|
125
|
+
timeoutMs: remaining(deadline, 'Forced garbage collection'),
|
|
126
|
+
});
|
|
127
|
+
await boundedSleep(settleMs, deadline, 'Forced garbage collection');
|
|
128
|
+
}
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
async function readPerformanceMetrics(session, deadline) {
|
|
132
|
+
const result = await session.call('Performance.getMetrics', {}, {
|
|
133
|
+
timeoutMs: remaining(deadline, 'Performance metric sampling'),
|
|
134
|
+
});
|
|
135
|
+
const metrics = Object.fromEntries((result?.metrics ?? []).map(({ name, value }) => [name, value]));
|
|
136
|
+
for (const name of ['Nodes', 'JSEventListeners', 'JSHeapUsedSize']) {
|
|
137
|
+
if (!Number.isFinite(metrics[name])) throw new Error(`CDP metric ${name} is unavailable.`);
|
|
138
|
+
}
|
|
139
|
+
return metrics;
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
async function enableMemoryDomains(session, deadline, includePerformance) {
|
|
143
|
+
if (includePerformance) {
|
|
144
|
+
await session.call('Performance.enable', {}, {
|
|
145
|
+
timeoutMs: remaining(deadline, 'Performance domain setup'),
|
|
146
|
+
});
|
|
147
|
+
}
|
|
148
|
+
await session.call('HeapProfiler.enable', {}, {
|
|
149
|
+
timeoutMs: remaining(deadline, 'Heap profiler setup'),
|
|
150
|
+
});
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
async function samplePostGcMetrics(page, options, deadline, index, navigate, roundDurationMs) {
|
|
154
|
+
const startedAt = Date.now();
|
|
155
|
+
if (navigate) {
|
|
156
|
+
await navigateAndSettle(page, options.returnHash, null, options, deadline, `Navigation cycle ${index}`);
|
|
157
|
+
await navigateAndSettle(
|
|
158
|
+
page,
|
|
159
|
+
options.screenHash,
|
|
160
|
+
options.screenTestId,
|
|
161
|
+
options,
|
|
162
|
+
deadline,
|
|
163
|
+
`Navigation cycle ${index}`,
|
|
164
|
+
);
|
|
165
|
+
}
|
|
166
|
+
if (roundDurationMs !== undefined) {
|
|
167
|
+
const waitMs = roundDurationMs - (Date.now() - startedAt);
|
|
168
|
+
if (waitMs < 0) {
|
|
169
|
+
throw new Error(
|
|
170
|
+
`${navigate ? 'Navigation' : 'Idle'} round ${index} exceeded round_duration_ms=${roundDurationMs}.`,
|
|
171
|
+
);
|
|
172
|
+
}
|
|
173
|
+
await boundedSleep(waitMs, deadline, `${navigate ? 'Navigation' : 'Idle'} sample ${index}`);
|
|
174
|
+
}
|
|
175
|
+
await forceGarbageCollection(page.session, options.gcSettleMs, deadline);
|
|
176
|
+
const metrics = await readPerformanceMetrics(page.session, deadline);
|
|
177
|
+
return {
|
|
178
|
+
index,
|
|
179
|
+
nodes: metrics.Nodes,
|
|
180
|
+
listeners: metrics.JSEventListeners,
|
|
181
|
+
heapBytes: metrics.JSHeapUsedSize,
|
|
182
|
+
};
|
|
183
|
+
}
|
|
184
|
+
|
|
185
|
+
function sampleSlopes(samples) {
|
|
186
|
+
return {
|
|
187
|
+
nodes: leastSquaresSlope(samples.map(({ nodes }) => nodes)),
|
|
188
|
+
listeners: leastSquaresSlope(samples.map(({ listeners }) => listeners)),
|
|
189
|
+
heapBytes: leastSquaresSlope(samples.map(({ heapBytes }) => heapBytes)),
|
|
190
|
+
};
|
|
191
|
+
}
|
|
192
|
+
|
|
193
|
+
export async function measureNavigationSlope(page, options, bounds) {
|
|
194
|
+
const deadline = deadlineIn(options.timeoutMs);
|
|
195
|
+
await enableMemoryDomains(page.session, deadline, true);
|
|
196
|
+
await navigateAndSettle(
|
|
197
|
+
page,
|
|
198
|
+
options.screenHash,
|
|
199
|
+
options.screenTestId,
|
|
200
|
+
options,
|
|
201
|
+
deadline,
|
|
202
|
+
'Initial screen navigation',
|
|
203
|
+
);
|
|
204
|
+
const samples = [];
|
|
205
|
+
for (let cycle = 1; cycle <= options.count; cycle += 1) {
|
|
206
|
+
samples.push(await samplePostGcMetrics(page, options, deadline, cycle, true));
|
|
207
|
+
}
|
|
208
|
+
const slopes = sampleSlopes(samples);
|
|
209
|
+
const passed = assessNavigationSlope(slopes, bounds);
|
|
210
|
+
const result = {
|
|
211
|
+
measurement: 'post-gc-navigation-slope',
|
|
212
|
+
screen: { hash: options.screenHash, testId: options.screenTestId },
|
|
213
|
+
cycles: options.count,
|
|
214
|
+
samples,
|
|
215
|
+
slopes,
|
|
216
|
+
bounds,
|
|
217
|
+
passed,
|
|
218
|
+
};
|
|
219
|
+
if (!passed.nodes || !passed.listeners) {
|
|
220
|
+
throw new Error(
|
|
221
|
+
`Navigation memory bounds exceeded: nodes ${slopes.nodes.toFixed(2)}/${bounds.maxNodesPerCycle} per cycle; listeners ${slopes.listeners.toFixed(2)}/${bounds.maxListenersPerCycle} per cycle.`,
|
|
222
|
+
);
|
|
223
|
+
}
|
|
224
|
+
return result;
|
|
225
|
+
}
|
|
226
|
+
|
|
227
|
+
async function takeHeapSnapshot(session, gcSettleMs, deadline) {
|
|
228
|
+
const chunks = [];
|
|
229
|
+
const unsubscribe = session.on('HeapProfiler.addHeapSnapshotChunk', (params) => {
|
|
230
|
+
if (typeof params?.chunk === 'string') chunks.push(params.chunk);
|
|
231
|
+
});
|
|
232
|
+
try {
|
|
233
|
+
await forceGarbageCollection(session, gcSettleMs, deadline);
|
|
234
|
+
await session.call(
|
|
235
|
+
'HeapProfiler.takeHeapSnapshot',
|
|
236
|
+
{ reportProgress: false, captureNumericValue: false },
|
|
237
|
+
{ timeoutMs: remaining(deadline, 'Heap snapshot') },
|
|
238
|
+
);
|
|
239
|
+
} finally {
|
|
240
|
+
unsubscribe();
|
|
241
|
+
}
|
|
242
|
+
if (chunks.length === 0) throw new Error('Heap snapshot produced no chunks.');
|
|
243
|
+
return JSON.parse(chunks.join(''));
|
|
244
|
+
}
|
|
245
|
+
|
|
246
|
+
export async function measureDetachedDom(page, options, maxDetachedGrowth) {
|
|
247
|
+
const deadline = deadlineIn(options.timeoutMs);
|
|
248
|
+
await enableMemoryDomains(page.session, deadline, false);
|
|
249
|
+
await navigateAndSettle(
|
|
250
|
+
page,
|
|
251
|
+
options.screenHash,
|
|
252
|
+
options.screenTestId,
|
|
253
|
+
options,
|
|
254
|
+
deadline,
|
|
255
|
+
'Initial screen navigation',
|
|
256
|
+
);
|
|
257
|
+
const before = countDetachedNodes(
|
|
258
|
+
await takeHeapSnapshot(page.session, options.gcSettleMs, deadline),
|
|
259
|
+
);
|
|
260
|
+
for (let cycle = 1; cycle <= options.count; cycle += 1) {
|
|
261
|
+
await navigateAndSettle(page, options.returnHash, null, options, deadline, `Navigation cycle ${cycle}`);
|
|
262
|
+
await navigateAndSettle(
|
|
263
|
+
page,
|
|
264
|
+
options.screenHash,
|
|
265
|
+
options.screenTestId,
|
|
266
|
+
options,
|
|
267
|
+
deadline,
|
|
268
|
+
`Navigation cycle ${cycle}`,
|
|
269
|
+
);
|
|
270
|
+
}
|
|
271
|
+
const after = countDetachedNodes(
|
|
272
|
+
await takeHeapSnapshot(page.session, options.gcSettleMs, deadline),
|
|
273
|
+
);
|
|
274
|
+
const result = {
|
|
275
|
+
measurement: 'post-gc-detached-dom',
|
|
276
|
+
screen: { hash: options.screenHash, testId: options.screenTestId },
|
|
277
|
+
cycles: options.count,
|
|
278
|
+
before,
|
|
279
|
+
after,
|
|
280
|
+
growth: after - before,
|
|
281
|
+
bound: { maxDetachedGrowth },
|
|
282
|
+
passed: after - before <= maxDetachedGrowth,
|
|
283
|
+
};
|
|
284
|
+
if (!result.passed) {
|
|
285
|
+
throw new Error(
|
|
286
|
+
`Detached DOM bound exceeded: growth ${result.growth}/${maxDetachedGrowth} nodes.`,
|
|
287
|
+
);
|
|
288
|
+
}
|
|
289
|
+
return result;
|
|
290
|
+
}
|
|
291
|
+
|
|
292
|
+
async function measureArm(page, options, deadline, navigate) {
|
|
293
|
+
await navigateAndSettle(
|
|
294
|
+
page,
|
|
295
|
+
options.screenHash,
|
|
296
|
+
options.screenTestId,
|
|
297
|
+
options,
|
|
298
|
+
deadline,
|
|
299
|
+
`${navigate ? 'Navigation' : 'Idle'} arm setup`,
|
|
300
|
+
);
|
|
301
|
+
const samples = [];
|
|
302
|
+
for (let round = 1; round <= options.count; round += 1) {
|
|
303
|
+
samples.push(
|
|
304
|
+
await samplePostGcMetrics(
|
|
305
|
+
page,
|
|
306
|
+
options,
|
|
307
|
+
deadline,
|
|
308
|
+
round,
|
|
309
|
+
navigate,
|
|
310
|
+
options.roundDurationMs,
|
|
311
|
+
),
|
|
312
|
+
);
|
|
313
|
+
}
|
|
314
|
+
return { samples, slopes: sampleSlopes(samples) };
|
|
315
|
+
}
|
|
316
|
+
|
|
317
|
+
export async function compareIdleNavigation(page, options, maxAttributableListenersPerCycle) {
|
|
318
|
+
const deadline = deadlineIn(options.timeoutMs);
|
|
319
|
+
await enableMemoryDomains(page.session, deadline, true);
|
|
320
|
+
const idle = await measureArm(page, options, deadline, false);
|
|
321
|
+
const navigation = await measureArm(page, options, deadline, true);
|
|
322
|
+
const idleListenersPerCycle = Math.max(0, idle.slopes.listeners);
|
|
323
|
+
const attributableListenersPerCycle = navigation.slopes.listeners - idleListenersPerCycle;
|
|
324
|
+
const result = {
|
|
325
|
+
measurement: 'post-gc-idle-control',
|
|
326
|
+
screen: { hash: options.screenHash, testId: options.screenTestId },
|
|
327
|
+
rounds: options.count,
|
|
328
|
+
idle,
|
|
329
|
+
navigation,
|
|
330
|
+
idleListenersPerCycle,
|
|
331
|
+
attributableListenersPerCycle,
|
|
332
|
+
bound: { maxAttributableListenersPerCycle },
|
|
333
|
+
passed: attributableListenersPerCycle <= maxAttributableListenersPerCycle,
|
|
334
|
+
};
|
|
335
|
+
if (!result.passed) {
|
|
336
|
+
throw new Error(
|
|
337
|
+
`Idle-control bound exceeded: attributable listeners ${attributableListenersPerCycle.toFixed(2)}/${maxAttributableListenersPerCycle} per cycle (navigation ${navigation.slopes.listeners.toFixed(2)}, idle ${idleListenersPerCycle.toFixed(2)}).`,
|
|
338
|
+
);
|
|
339
|
+
}
|
|
340
|
+
return result;
|
|
341
|
+
}
|
|
342
|
+
|
|
343
|
+
export function navigationSlopeBounds(node) {
|
|
344
|
+
return {
|
|
345
|
+
maxNodesPerCycle: finiteNumber(
|
|
346
|
+
node.max_nodes_per_cycle ?? node.maxNodesPerCycle,
|
|
347
|
+
2,
|
|
348
|
+
'max_nodes_per_cycle',
|
|
349
|
+
),
|
|
350
|
+
maxListenersPerCycle: finiteNumber(
|
|
351
|
+
node.max_listeners_per_cycle ?? node.maxListenersPerCycle,
|
|
352
|
+
4,
|
|
353
|
+
'max_listeners_per_cycle',
|
|
354
|
+
),
|
|
355
|
+
};
|
|
356
|
+
}
|
|
357
|
+
|
|
358
|
+
export function detachedDomBound(node) {
|
|
359
|
+
return finiteNumber(
|
|
360
|
+
node.max_detached_growth ?? node.maxDetachedGrowth,
|
|
361
|
+
50,
|
|
362
|
+
'max_detached_growth',
|
|
363
|
+
);
|
|
364
|
+
}
|
|
365
|
+
|
|
366
|
+
export function idleControlBound(node) {
|
|
367
|
+
return finiteNumber(
|
|
368
|
+
node.max_attributable_listeners_per_cycle ?? node.maxAttributableListenersPerCycle,
|
|
369
|
+
2,
|
|
370
|
+
'max_attributable_listeners_per_cycle',
|
|
371
|
+
);
|
|
372
|
+
}
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
import { runAdapter, withExtensionPage } from '../platform/cdp.mjs';
|
|
2
|
+
import {
|
|
3
|
+
compareIdleNavigation,
|
|
4
|
+
idleControlBound,
|
|
5
|
+
navigationMemoryOptions,
|
|
6
|
+
} from './_navigation-memory.mjs';
|
|
7
|
+
|
|
8
|
+
runAdapter((input) =>
|
|
9
|
+
withExtensionPage(input, async (page) => ({
|
|
10
|
+
action: input.action,
|
|
11
|
+
status: 'pass',
|
|
12
|
+
...(await compareIdleNavigation(
|
|
13
|
+
page,
|
|
14
|
+
navigationMemoryOptions(input.node, 'rounds', 8),
|
|
15
|
+
idleControlBound(input.node),
|
|
16
|
+
)),
|
|
17
|
+
})),
|
|
18
|
+
);
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
import { runAdapter, withExtensionPage } from '../platform/cdp.mjs';
|
|
2
|
+
import {
|
|
3
|
+
detachedDomBound,
|
|
4
|
+
measureDetachedDom,
|
|
5
|
+
navigationMemoryOptions,
|
|
6
|
+
} from './_navigation-memory.mjs';
|
|
7
|
+
|
|
8
|
+
runAdapter((input) =>
|
|
9
|
+
withExtensionPage(input, async (page) => ({
|
|
10
|
+
action: input.action,
|
|
11
|
+
status: 'pass',
|
|
12
|
+
...(await measureDetachedDom(
|
|
13
|
+
page,
|
|
14
|
+
navigationMemoryOptions(input.node, 'cycles', 5),
|
|
15
|
+
detachedDomBound(input.node),
|
|
16
|
+
)),
|
|
17
|
+
})),
|
|
18
|
+
);
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
import { runAdapter, withExtensionPage } from '../platform/cdp.mjs';
|
|
2
|
+
import {
|
|
3
|
+
measureNavigationSlope,
|
|
4
|
+
navigationMemoryOptions,
|
|
5
|
+
navigationSlopeBounds,
|
|
6
|
+
} from './_navigation-memory.mjs';
|
|
7
|
+
|
|
8
|
+
runAdapter((input) =>
|
|
9
|
+
withExtensionPage(input, async (page) => ({
|
|
10
|
+
action: input.action,
|
|
11
|
+
status: 'pass',
|
|
12
|
+
...(await measureNavigationSlope(
|
|
13
|
+
page,
|
|
14
|
+
navigationMemoryOptions(input.node),
|
|
15
|
+
navigationSlopeBounds(input.node),
|
|
16
|
+
)),
|
|
17
|
+
})),
|
|
18
|
+
);
|
|
@@ -42,6 +42,12 @@ async function defaultRun(file, args) {
|
|
|
42
42
|
return execFileAsync(file, args, { timeout: 15_000, encoding: 'utf8' });
|
|
43
43
|
}
|
|
44
44
|
|
|
45
|
+
function isAppFocused(output, appId) {
|
|
46
|
+
return String(output ?? '')
|
|
47
|
+
.split('\n')
|
|
48
|
+
.some((line) => line.includes('mCurrentFocus=') && line.includes(appId));
|
|
49
|
+
}
|
|
50
|
+
|
|
45
51
|
export async function setMobileNetworkState(input, deps = {}) {
|
|
46
52
|
const state = String(input.node?.state ?? '').toLowerCase();
|
|
47
53
|
if (state !== 'offline' && state !== 'online') {
|
|
@@ -87,19 +93,26 @@ export async function setMobileNetworkState(input, deps = {}) {
|
|
|
87
93
|
`app.network requested ${state}, but Android airplane_mode_on was ${JSON.stringify(observed)} instead of ${expected}.`,
|
|
88
94
|
);
|
|
89
95
|
}
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
await run(adb, [
|
|
96
|
+
const focusBefore = await run(adb, [
|
|
93
97
|
'-s',
|
|
94
98
|
serial,
|
|
95
99
|
'shell',
|
|
96
|
-
'
|
|
97
|
-
'
|
|
98
|
-
appId,
|
|
99
|
-
'-c',
|
|
100
|
-
'android.intent.category.LAUNCHER',
|
|
101
|
-
'1',
|
|
100
|
+
'dumpsys',
|
|
101
|
+
'window',
|
|
102
102
|
]);
|
|
103
|
+
if (!isAppFocused(focusBefore.stdout, appId)) {
|
|
104
|
+
await run(adb, [
|
|
105
|
+
'-s',
|
|
106
|
+
serial,
|
|
107
|
+
'shell',
|
|
108
|
+
'monkey',
|
|
109
|
+
'-p',
|
|
110
|
+
appId,
|
|
111
|
+
'-c',
|
|
112
|
+
'android.intent.category.LAUNCHER',
|
|
113
|
+
'1',
|
|
114
|
+
]);
|
|
115
|
+
}
|
|
103
116
|
|
|
104
117
|
const fallbackSettleMs = state === 'offline' ? 3000 : 10000;
|
|
105
118
|
const settleMs = Number(input.node?.settle_ms ?? fallbackSettleMs);
|
|
@@ -113,10 +126,9 @@ export async function setMobileNetworkState(input, deps = {}) {
|
|
|
113
126
|
serial,
|
|
114
127
|
'shell',
|
|
115
128
|
'dumpsys',
|
|
116
|
-
'
|
|
117
|
-
'activities',
|
|
129
|
+
'window',
|
|
118
130
|
]);
|
|
119
|
-
if (!
|
|
131
|
+
if (!isAppFocused(foreground.stdout, appId)) {
|
|
120
132
|
throw new Error(
|
|
121
133
|
`app.network changed Android networking but ${appId} was not foregrounded afterward.`,
|
|
122
134
|
);
|
|
@@ -728,6 +728,61 @@
|
|
|
728
728
|
],
|
|
729
729
|
"execution_capabilities": []
|
|
730
730
|
},
|
|
731
|
+
"metamask.perps.read_snapshot": {
|
|
732
|
+
"description": "Read global market and account snapshots through the public PerpsController API used by wallet clients.",
|
|
733
|
+
"schema": {
|
|
734
|
+
"type": "object",
|
|
735
|
+
"properties": {
|
|
736
|
+
"scope": {
|
|
737
|
+
"type": "string",
|
|
738
|
+
"enum": [
|
|
739
|
+
"markets",
|
|
740
|
+
"account",
|
|
741
|
+
"all"
|
|
742
|
+
],
|
|
743
|
+
"default": "all",
|
|
744
|
+
"description": "Select the public controller snapshot calls to execute."
|
|
745
|
+
},
|
|
746
|
+
"account": {
|
|
747
|
+
"type": "string",
|
|
748
|
+
"description": "Wallet fixture account name or EVM address; defaults to dev1."
|
|
749
|
+
},
|
|
750
|
+
"account_name": {
|
|
751
|
+
"type": "string",
|
|
752
|
+
"description": "Alias for a wallet fixture account name."
|
|
753
|
+
},
|
|
754
|
+
"network": {
|
|
755
|
+
"type": "string",
|
|
756
|
+
"enum": [
|
|
757
|
+
"testnet",
|
|
758
|
+
"mainnet"
|
|
759
|
+
],
|
|
760
|
+
"default": "testnet"
|
|
761
|
+
},
|
|
762
|
+
"terminal_market_data_url": {
|
|
763
|
+
"type": "string",
|
|
764
|
+
"description": "Optional full URL for the legacy Terminal market-data endpoint."
|
|
765
|
+
},
|
|
766
|
+
"terminal_global_snapshot_url": {
|
|
767
|
+
"type": "string",
|
|
768
|
+
"description": "Optional full URL for the Terminal schema-v2 global snapshot endpoint."
|
|
769
|
+
},
|
|
770
|
+
"timeout_ms": {
|
|
771
|
+
"type": "number"
|
|
772
|
+
}
|
|
773
|
+
},
|
|
774
|
+
"additionalProperties": false
|
|
775
|
+
},
|
|
776
|
+
"examples": [
|
|
777
|
+
{
|
|
778
|
+
"action": "metamask.perps.read_snapshot",
|
|
779
|
+
"scope": "all",
|
|
780
|
+
"intent": "Read coherent Perps snapshots through the public controller API",
|
|
781
|
+
"next": "done"
|
|
782
|
+
}
|
|
783
|
+
],
|
|
784
|
+
"execution_capabilities": []
|
|
785
|
+
},
|
|
731
786
|
"metamask.perps.place_order": {
|
|
732
787
|
"description": "core Place a real Perps order on HyperLiquid testnet by driving the headless perps controller placeOrder() through the full signing/provider path. Supports market (default), resting limit orders (order_type=limit with price/offset_pct), and resting trigger placements (stop_market | stop_limit | take_profit_market | take_profit_limit with trigger_price/trigger_offset_pct), plus reduce_only and attached/partial TP/SL (take_profit_price/take_profit_size, stop_loss_price/stop_loss_size, tpsl_linkage). Venue is selected with network (testnet default; mainnet signs with REAL funds and also requires CORE_PERPS_ALLOW_MAINNET_WRITES=1). Plain limit orders accept time_in_force GTC | ALO (post-only).",
|
|
733
788
|
"schema": {
|
|
@@ -1221,6 +1221,106 @@
|
|
|
1221
1221
|
],
|
|
1222
1222
|
"execution_capabilities": []
|
|
1223
1223
|
},
|
|
1224
|
+
"metamask.performance.measure_navigation_memory": {
|
|
1225
|
+
"description": "Measure retained DOM nodes and listeners across repeated Extension navigation after forced garbage collection, using least-squares slopes and separate declared bounds.",
|
|
1226
|
+
"schema": {
|
|
1227
|
+
"type": "object",
|
|
1228
|
+
"properties": {
|
|
1229
|
+
"screen_hash": { "type": "string" },
|
|
1230
|
+
"screen_test_id": { "type": "string" },
|
|
1231
|
+
"return_hash": { "type": "string", "default": "#/" },
|
|
1232
|
+
"cycles": { "type": "integer", "minimum": 2, "default": 8 },
|
|
1233
|
+
"max_nodes_per_cycle": { "type": "number", "default": 2 },
|
|
1234
|
+
"max_listeners_per_cycle": { "type": "number", "default": 4 },
|
|
1235
|
+
"settle_ms": { "type": "integer", "minimum": 0, "default": 1500 },
|
|
1236
|
+
"gc_settle_ms": { "type": "integer", "minimum": 0, "default": 300 },
|
|
1237
|
+
"screen_timeout_ms": { "type": "integer", "minimum": 1, "default": 15000 },
|
|
1238
|
+
"timeout_ms": { "type": "integer", "minimum": 1, "default": 400000 }
|
|
1239
|
+
},
|
|
1240
|
+
"required": ["screen_hash", "screen_test_id"],
|
|
1241
|
+
"additionalProperties": false
|
|
1242
|
+
},
|
|
1243
|
+
"examples": [
|
|
1244
|
+
{
|
|
1245
|
+
"action": "metamask.performance.measure_navigation_memory",
|
|
1246
|
+
"screen_hash": "#/settings",
|
|
1247
|
+
"screen_test_id": "settings-tab-bar-grouped",
|
|
1248
|
+
"return_hash": "#/",
|
|
1249
|
+
"cycles": 8,
|
|
1250
|
+
"max_nodes_per_cycle": 2,
|
|
1251
|
+
"max_listeners_per_cycle": 4,
|
|
1252
|
+
"intent": "Measure post-GC retained-memory slopes across repeated navigation.",
|
|
1253
|
+
"next": "done"
|
|
1254
|
+
}
|
|
1255
|
+
],
|
|
1256
|
+
"execution_capabilities": []
|
|
1257
|
+
},
|
|
1258
|
+
"metamask.performance.measure_detached_dom": {
|
|
1259
|
+
"description": "Compare retained detached-DOM counts before and after repeated Extension navigation, with forced garbage collection before each heap snapshot.",
|
|
1260
|
+
"schema": {
|
|
1261
|
+
"type": "object",
|
|
1262
|
+
"properties": {
|
|
1263
|
+
"screen_hash": { "type": "string" },
|
|
1264
|
+
"screen_test_id": { "type": "string" },
|
|
1265
|
+
"return_hash": { "type": "string", "default": "#/" },
|
|
1266
|
+
"cycles": { "type": "integer", "minimum": 2, "default": 5 },
|
|
1267
|
+
"max_detached_growth": { "type": "number", "default": 50 },
|
|
1268
|
+
"settle_ms": { "type": "integer", "minimum": 0, "default": 1500 },
|
|
1269
|
+
"gc_settle_ms": { "type": "integer", "minimum": 0, "default": 300 },
|
|
1270
|
+
"screen_timeout_ms": { "type": "integer", "minimum": 1, "default": 15000 },
|
|
1271
|
+
"timeout_ms": { "type": "integer", "minimum": 1, "default": 400000 }
|
|
1272
|
+
},
|
|
1273
|
+
"required": ["screen_hash", "screen_test_id"],
|
|
1274
|
+
"additionalProperties": false
|
|
1275
|
+
},
|
|
1276
|
+
"examples": [
|
|
1277
|
+
{
|
|
1278
|
+
"action": "metamask.performance.measure_detached_dom",
|
|
1279
|
+
"screen_hash": "#/settings",
|
|
1280
|
+
"screen_test_id": "settings-tab-bar-grouped",
|
|
1281
|
+
"return_hash": "#/",
|
|
1282
|
+
"cycles": 5,
|
|
1283
|
+
"max_detached_growth": 50,
|
|
1284
|
+
"intent": "Measure detached DOM retained after navigation and forced garbage collection.",
|
|
1285
|
+
"next": "done"
|
|
1286
|
+
}
|
|
1287
|
+
],
|
|
1288
|
+
"execution_capabilities": []
|
|
1289
|
+
},
|
|
1290
|
+
"metamask.performance.compare_idle_navigation_memory": {
|
|
1291
|
+
"description": "Compare equal-duration idle and navigating Extension arms to isolate navigation-attributable listener growth after forced garbage collection.",
|
|
1292
|
+
"schema": {
|
|
1293
|
+
"type": "object",
|
|
1294
|
+
"properties": {
|
|
1295
|
+
"screen_hash": { "type": "string" },
|
|
1296
|
+
"screen_test_id": { "type": "string" },
|
|
1297
|
+
"return_hash": { "type": "string", "default": "#/" },
|
|
1298
|
+
"rounds": { "type": "integer", "minimum": 2, "default": 8 },
|
|
1299
|
+
"round_duration_ms": { "type": "integer", "minimum": 1, "default": 10000 },
|
|
1300
|
+
"max_attributable_listeners_per_cycle": { "type": "number", "default": 2 },
|
|
1301
|
+
"settle_ms": { "type": "integer", "minimum": 0, "default": 1500 },
|
|
1302
|
+
"gc_settle_ms": { "type": "integer", "minimum": 0, "default": 300 },
|
|
1303
|
+
"screen_timeout_ms": { "type": "integer", "minimum": 1, "default": 15000 },
|
|
1304
|
+
"timeout_ms": { "type": "integer", "minimum": 1, "default": 400000 }
|
|
1305
|
+
},
|
|
1306
|
+
"required": ["screen_hash", "screen_test_id"],
|
|
1307
|
+
"additionalProperties": false
|
|
1308
|
+
},
|
|
1309
|
+
"examples": [
|
|
1310
|
+
{
|
|
1311
|
+
"action": "metamask.performance.compare_idle_navigation_memory",
|
|
1312
|
+
"screen_hash": "#/settings",
|
|
1313
|
+
"screen_test_id": "settings-tab-bar-grouped",
|
|
1314
|
+
"return_hash": "#/",
|
|
1315
|
+
"rounds": 8,
|
|
1316
|
+
"round_duration_ms": 10000,
|
|
1317
|
+
"max_attributable_listeners_per_cycle": 2,
|
|
1318
|
+
"intent": "Separate navigation-attributable listener growth from equal-duration idle growth.",
|
|
1319
|
+
"next": "done"
|
|
1320
|
+
}
|
|
1321
|
+
],
|
|
1322
|
+
"execution_capabilities": []
|
|
1323
|
+
},
|
|
1224
1324
|
"metamask.perps.read_positions": {
|
|
1225
1325
|
"description": "extension Read live Perps positions; without a selector, return all live positions.",
|
|
1226
1326
|
"schema": {
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
{
|
|
2
|
+
"$schema": "https://farmslot.io/schemas/recipe-v1.schema.json",
|
|
3
|
+
"title": "MetaMask core Perps snapshots",
|
|
4
|
+
"description": "Reads global market and account snapshots through the public PerpsController API used by wallet clients. Configure CORE_PERPS_TERMINAL_GLOBAL_SNAPSHOT_URL to exercise a Terminal schema-v2 endpoint.",
|
|
5
|
+
"workflow": {
|
|
6
|
+
"entry": "status",
|
|
7
|
+
"nodes": {
|
|
8
|
+
"status": {
|
|
9
|
+
"action": "app.status",
|
|
10
|
+
"next": "read_snapshot",
|
|
11
|
+
"intent": "Resolve the Core checkout and report headless compatibility"
|
|
12
|
+
},
|
|
13
|
+
"read_snapshot": {
|
|
14
|
+
"action": "metamask.perps.read_snapshot",
|
|
15
|
+
"scope": "all",
|
|
16
|
+
"next": "done",
|
|
17
|
+
"intent": "Read coherent market and account snapshots through public controller methods"
|
|
18
|
+
},
|
|
19
|
+
"done": {
|
|
20
|
+
"action": "end",
|
|
21
|
+
"status": "pass"
|
|
22
|
+
}
|
|
23
|
+
}
|
|
24
|
+
}
|
|
25
|
+
}
|