@deeeed/metamask-harness 0.40.0 → 0.41.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 +22 -0
- package/adapters/manifest.json +8 -0
- package/adapters/mobile/bridge-runtime/cdp-bridge.cjs +5 -4
- package/adapters/mobile/bridge-runtime/lib/target-discovery.cjs +24 -5
- package/adapters/mobile/metro-config.cjs +94 -0
- package/adapters/mobile/start-metro.sh +13 -1
- package/dist/commands/device-target.js +3 -0
- package/dist/commands/run-engine.js +4 -14
- package/dist/funding-execution-context.js +35 -28
- package/dist/heal-bounds.js +1 -0
- package/dist/live-adapter-contract.js +2 -1
- package/dist/mm-harness-cli.js +1 -1
- package/docs/RECIPES.md +25 -19
- package/library/actions/extension/perps/mutation-receipt.mjs +12 -8
- package/library/actions/extension/perps/select_activity_filter.mjs +1 -1
- package/library/actions/extension/perps/select_market_filter.mjs +1 -1
- package/library/actions/mobile/perps/capture_performance.mjs +2 -2
- package/library/actions/mobile/perps/performance-capture.mjs +375 -17
- package/library/actions/mobile/perps/perps.mjs +56 -10
- package/library/actions/mobile/platform/bridge.mjs +59 -1
- package/library/actions/mobile/wallet/ensure_unlocked.mjs +60 -26
- package/library/actions/mobile/wallet/import.mjs +2 -0
- package/library/actions/mobile/wallet/select_account.mjs +21 -2
- package/library/manifests/mobile.action-manifest.json +47 -6
- package/library/recipes/mobile/perps/performance.recipe.json +680 -26
- package/package.json +1 -1
- package/library/recipes/mobile/perps/performance.background-resume.recipe.json +0 -56
- package/library/recipes/mobile/perps/performance.cold-start.recipe.json +0 -56
- package/library/recipes/mobile/perps/performance.homepage.android-background-reconnect.recipe.json +0 -159
- package/library/recipes/mobile/perps/performance.homepage.android-background-short.recipe.json +0 -157
- package/library/recipes/mobile/perps/performance.homepage.android-cold-disk-cache.recipe.json +0 -165
- package/library/recipes/mobile/perps/performance.homepage.android-cold-no-cache.recipe.json +0 -139
- package/library/recipes/mobile/perps/performance.homepage.android-network-recovery.recipe.json +0 -149
- package/library/recipes/mobile/perps/performance.homepage.cold-position-sample.recipe.json +0 -120
- package/library/recipes/mobile/perps/performance.homepage.ios-background-reconnect.recipe.json +0 -111
- package/library/recipes/mobile/perps/performance.homepage.ios-background-short.recipe.json +0 -108
- package/library/recipes/mobile/perps/performance.homepage.ios-cold-disk-cache.recipe.json +0 -122
- package/library/recipes/mobile/perps/performance.homepage.ios-cold-no-cache.recipe.json +0 -95
- package/library/recipes/mobile/perps/performance.warm-start.recipe.json +0 -49
|
@@ -1,8 +1,11 @@
|
|
|
1
1
|
import { mkdir, readFile, rm, stat, writeFile } from "node:fs/promises";
|
|
2
2
|
import path from "node:path";
|
|
3
3
|
|
|
4
|
-
const MARKER = "[
|
|
4
|
+
const MARKER = "[PerpsPerf]";
|
|
5
|
+
const LEGACY_MARKER = "[HomepagePerf]";
|
|
6
|
+
const DETAILED_MARKERS = [MARKER, LEGACY_MARKER];
|
|
5
7
|
const STARTUP_MARKER = "[StartupPerf]";
|
|
8
|
+
const LOAD_PROOF_MARKER = "[PerpsLoadProof]";
|
|
6
9
|
const STATE_FILE = ".homepage-performance-capture.json";
|
|
7
10
|
const FRESH_SOURCES = new Set([
|
|
8
11
|
"fresh_socket",
|
|
@@ -44,6 +47,201 @@ function parseRecord(line, marker) {
|
|
|
44
47
|
}
|
|
45
48
|
}
|
|
46
49
|
|
|
50
|
+
function detailedMarker(line) {
|
|
51
|
+
return DETAILED_MARKERS.find((marker) => line.includes(marker)) ?? null;
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
function parseDetailedRecord(line) {
|
|
55
|
+
const marker = detailedMarker(line);
|
|
56
|
+
return marker ? parseRecord(line, marker) : null;
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
function latestPerpsBootstrapStartRecord(content) {
|
|
60
|
+
return content
|
|
61
|
+
.split(/\r?\n/u)
|
|
62
|
+
.filter((line) => line.includes(LOAD_PROOF_MARKER))
|
|
63
|
+
.map((line) => parseRecord(line, LOAD_PROOF_MARKER))
|
|
64
|
+
.filter((record) => record?.stage === "perps_bootstrap_start")
|
|
65
|
+
.at(-1) ?? null;
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
function consoleClockMs(line) {
|
|
69
|
+
const match = line.match(/\b(\d{2}):(\d{2}):(\d{2})\.(\d{3})\b/u);
|
|
70
|
+
if (!match) return null;
|
|
71
|
+
return (
|
|
72
|
+
Number(match[1]) * 3_600_000 +
|
|
73
|
+
Number(match[2]) * 60_000 +
|
|
74
|
+
Number(match[3]) * 1_000 +
|
|
75
|
+
Number(match[4])
|
|
76
|
+
);
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
function existingLiveStreamRecords(segmentLines, markerRecords) {
|
|
80
|
+
let bootstrapLineIndex = -1;
|
|
81
|
+
segmentLines.forEach((line, index) => {
|
|
82
|
+
if (
|
|
83
|
+
line.includes(LOAD_PROOF_MARKER) &&
|
|
84
|
+
line.includes('"stage":"perps_bootstrap_start"')
|
|
85
|
+
) {
|
|
86
|
+
bootstrapLineIndex = index;
|
|
87
|
+
}
|
|
88
|
+
});
|
|
89
|
+
const bootstrapLine =
|
|
90
|
+
bootstrapLineIndex >= 0 ? segmentLines[bootstrapLineIndex] : undefined;
|
|
91
|
+
const liveLines =
|
|
92
|
+
bootstrapLineIndex >= 0
|
|
93
|
+
? segmentLines.slice(bootstrapLineIndex + 1)
|
|
94
|
+
: segmentLines;
|
|
95
|
+
const bootstrapClock = bootstrapLine ? consoleClockMs(bootstrapLine) : null;
|
|
96
|
+
const bootstrapRecord = markerRecords
|
|
97
|
+
.filter((record) => record.stage === "perps_bootstrap_start")
|
|
98
|
+
.at(-1);
|
|
99
|
+
const patterns = new Map([
|
|
100
|
+
["PerpsWS: First price data received", "prices"],
|
|
101
|
+
["PerpsWS: First position data received", "positions"],
|
|
102
|
+
["PerpsWS: First order data received", "orders"],
|
|
103
|
+
["PerpsWS: First account data received", "account"],
|
|
104
|
+
]);
|
|
105
|
+
const records = [];
|
|
106
|
+
for (const [message, stream] of patterns) {
|
|
107
|
+
const line = liveLines.find((candidate) => candidate.includes(message));
|
|
108
|
+
if (!line) continue;
|
|
109
|
+
const eventClock = consoleClockMs(line);
|
|
110
|
+
let elapsedMs = null;
|
|
111
|
+
if (bootstrapClock !== null && eventClock !== null) {
|
|
112
|
+
elapsedMs = eventClock - bootstrapClock;
|
|
113
|
+
if (elapsedMs < 0) elapsedMs += 24 * 3_600_000;
|
|
114
|
+
}
|
|
115
|
+
records.push({
|
|
116
|
+
stage: "values_ready",
|
|
117
|
+
stream,
|
|
118
|
+
source: "fresh_socket",
|
|
119
|
+
item_count: null,
|
|
120
|
+
elapsed_ms: elapsedMs,
|
|
121
|
+
monotonic_ms:
|
|
122
|
+
elapsedMs !== null &&
|
|
123
|
+
Number.isFinite(Number(bootstrapRecord?.monotonic_ms))
|
|
124
|
+
? Number(bootstrapRecord.monotonic_ms) + elapsedMs
|
|
125
|
+
: null,
|
|
126
|
+
evidence: "existing_sentry_ws_marker",
|
|
127
|
+
});
|
|
128
|
+
}
|
|
129
|
+
return records;
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
function mergeLoadProofRecords(records) {
|
|
133
|
+
const byStageStreamSource = new Map();
|
|
134
|
+
for (const record of records) {
|
|
135
|
+
const key = `${record.stage ?? "unknown"}:${record.stream ?? ""}:${record.source ?? ""}`;
|
|
136
|
+
const current = byStageStreamSource.get(key);
|
|
137
|
+
if (record.stage === "perps_bootstrap_start") {
|
|
138
|
+
if (
|
|
139
|
+
!current ||
|
|
140
|
+
Number(record.monotonic_ms) > Number(current.monotonic_ms)
|
|
141
|
+
) {
|
|
142
|
+
byStageStreamSource.set(key, record);
|
|
143
|
+
}
|
|
144
|
+
continue;
|
|
145
|
+
}
|
|
146
|
+
const recordIsFallback =
|
|
147
|
+
record.evidence === "existing_sentry_ws_marker";
|
|
148
|
+
const currentIsFallback =
|
|
149
|
+
current?.evidence === "existing_sentry_ws_marker";
|
|
150
|
+
const recordElapsedMs = Number(record.elapsed_ms);
|
|
151
|
+
const currentElapsedMs = Number(current?.elapsed_ms);
|
|
152
|
+
const sameEvidenceClassEarlier =
|
|
153
|
+
recordIsFallback === currentIsFallback &&
|
|
154
|
+
record.elapsed_ms !== null &&
|
|
155
|
+
record.elapsed_ms !== undefined &&
|
|
156
|
+
Number.isFinite(recordElapsedMs) &&
|
|
157
|
+
current?.elapsed_ms !== null &&
|
|
158
|
+
current?.elapsed_ms !== undefined &&
|
|
159
|
+
Number.isFinite(currentElapsedMs) &&
|
|
160
|
+
recordElapsedMs < currentElapsedMs;
|
|
161
|
+
if (
|
|
162
|
+
!current ||
|
|
163
|
+
(currentIsFallback && !recordIsFallback) ||
|
|
164
|
+
sameEvidenceClassEarlier
|
|
165
|
+
) {
|
|
166
|
+
byStageStreamSource.set(key, record);
|
|
167
|
+
}
|
|
168
|
+
}
|
|
169
|
+
return [...byStageStreamSource.values()];
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
async function waitForPerformanceRecord({ node, sourceFile, offset }) {
|
|
173
|
+
const stage = node.wait_for_stage;
|
|
174
|
+
const requiredLiveStreams = Array.isArray(node.required_live_streams)
|
|
175
|
+
? node.required_live_streams.map(String)
|
|
176
|
+
: [];
|
|
177
|
+
if (stage === undefined && requiredLiveStreams.length === 0) return;
|
|
178
|
+
|
|
179
|
+
const expected =
|
|
180
|
+
stage === undefined
|
|
181
|
+
? null
|
|
182
|
+
: {
|
|
183
|
+
stage: String(stage),
|
|
184
|
+
...(node.wait_for_source !== undefined
|
|
185
|
+
? { source: String(node.wait_for_source) }
|
|
186
|
+
: {}),
|
|
187
|
+
...(node.wait_for_lifecycle !== undefined
|
|
188
|
+
? { lifecycle: String(node.wait_for_lifecycle) }
|
|
189
|
+
: {}),
|
|
190
|
+
};
|
|
191
|
+
const timeoutMs = Math.max(1, Number(node.wait_timeout_ms ?? 60_000));
|
|
192
|
+
const pollIntervalMs = Math.max(
|
|
193
|
+
10,
|
|
194
|
+
Number(node.wait_poll_interval_ms ?? 100),
|
|
195
|
+
);
|
|
196
|
+
const deadline = Date.now() + timeoutMs;
|
|
197
|
+
|
|
198
|
+
while (true) {
|
|
199
|
+
const content = await readFile(sourceFile);
|
|
200
|
+
const start = offset <= content.length ? offset : 0;
|
|
201
|
+
const segmentLines = content
|
|
202
|
+
.subarray(start)
|
|
203
|
+
.toString("utf8")
|
|
204
|
+
.split(/\r?\n/u);
|
|
205
|
+
const performanceRecords = segmentLines
|
|
206
|
+
.filter(detailedMarker)
|
|
207
|
+
.map(parseDetailedRecord)
|
|
208
|
+
.filter(Boolean);
|
|
209
|
+
const stageMatched =
|
|
210
|
+
expected === null ||
|
|
211
|
+
performanceRecords.some((record) =>
|
|
212
|
+
Object.entries(expected).every(
|
|
213
|
+
([key, value]) => String(record[key] ?? "") === value,
|
|
214
|
+
),
|
|
215
|
+
);
|
|
216
|
+
const markerLoadProofRecords = segmentLines
|
|
217
|
+
.filter((line) => line.includes(LOAD_PROOF_MARKER))
|
|
218
|
+
.map((line) => parseRecord(line, LOAD_PROOF_MARKER))
|
|
219
|
+
.filter(Boolean);
|
|
220
|
+
const liveStreams = new Set(
|
|
221
|
+
mergeLoadProofRecords([
|
|
222
|
+
...markerLoadProofRecords,
|
|
223
|
+
...existingLiveStreamRecords(segmentLines, markerLoadProofRecords),
|
|
224
|
+
])
|
|
225
|
+
.filter(
|
|
226
|
+
(record) =>
|
|
227
|
+
record.stage === "values_ready" &&
|
|
228
|
+
record.source === "fresh_socket",
|
|
229
|
+
)
|
|
230
|
+
.map((record) => String(record.stream ?? "")),
|
|
231
|
+
);
|
|
232
|
+
const liveStreamsMatched = requiredLiveStreams.every((stream) =>
|
|
233
|
+
liveStreams.has(stream),
|
|
234
|
+
);
|
|
235
|
+
if (stageMatched && liveStreamsMatched) return;
|
|
236
|
+
if (Date.now() >= deadline) {
|
|
237
|
+
throw new Error(
|
|
238
|
+
`Timed out waiting ${timeoutMs}ms for ${MARKER} stage ${JSON.stringify(expected)} and live streams ${JSON.stringify(requiredLiveStreams)}.`,
|
|
239
|
+
);
|
|
240
|
+
}
|
|
241
|
+
await new Promise((resolve) => setTimeout(resolve, pollIntervalMs));
|
|
242
|
+
}
|
|
243
|
+
}
|
|
244
|
+
|
|
47
245
|
function startupMeasurements(records) {
|
|
48
246
|
return records.map((record) => {
|
|
49
247
|
const processToStageMs = Number(
|
|
@@ -72,6 +270,60 @@ function startupMeasurements(records) {
|
|
|
72
270
|
});
|
|
73
271
|
}
|
|
74
272
|
|
|
273
|
+
function perpsBootstrapBoundary(homepageRecords, loadProofRecords) {
|
|
274
|
+
const demandTimes = homepageRecords
|
|
275
|
+
.filter((record) => record.stage === "surface_demand")
|
|
276
|
+
.map((record) => Number(record.monotonic_ms))
|
|
277
|
+
.filter(Number.isFinite);
|
|
278
|
+
const firstDemandAt = demandTimes.length > 0 ? Math.min(...demandTimes) : null;
|
|
279
|
+
const bootstrapTimes = loadProofRecords
|
|
280
|
+
.filter((record) => record.stage === "perps_bootstrap_start")
|
|
281
|
+
.map((record) => Number(record.monotonic_ms))
|
|
282
|
+
.filter(
|
|
283
|
+
(value) =>
|
|
284
|
+
Number.isFinite(value) &&
|
|
285
|
+
(firstDemandAt === null || value <= firstDemandAt),
|
|
286
|
+
);
|
|
287
|
+
const bootstrapAt =
|
|
288
|
+
bootstrapTimes.length > 0 ? Math.max(...bootstrapTimes) : null;
|
|
289
|
+
const failureReason =
|
|
290
|
+
firstDemandAt === null
|
|
291
|
+
? "missing_surface_demand"
|
|
292
|
+
: bootstrapAt === null
|
|
293
|
+
? "missing_prior_perps_bootstrap_start"
|
|
294
|
+
: null;
|
|
295
|
+
return {
|
|
296
|
+
status: failureReason === null ? "pass" : "fail",
|
|
297
|
+
perpsBootstrapStartAtMonotonicMs: bootstrapAt,
|
|
298
|
+
firstDemandAtMonotonicMs: firstDemandAt,
|
|
299
|
+
demandAfterPerpsBootstrapStartMs:
|
|
300
|
+
bootstrapAt !== null && firstDemandAt !== null
|
|
301
|
+
? roundMs(firstDemandAt - bootstrapAt)
|
|
302
|
+
: null,
|
|
303
|
+
failureReason,
|
|
304
|
+
};
|
|
305
|
+
}
|
|
306
|
+
|
|
307
|
+
function loadProofMeasurements(records) {
|
|
308
|
+
return records
|
|
309
|
+
.filter((record) => record.stage === "values_ready")
|
|
310
|
+
.map((record) => {
|
|
311
|
+
const elapsedMs = finiteNumberOrNull(record.elapsed_ms);
|
|
312
|
+
return {
|
|
313
|
+
stream: String(record.stream ?? "unknown"),
|
|
314
|
+
source: String(record.source ?? "unknown"),
|
|
315
|
+
evidence: String(record.evidence ?? LOAD_PROOF_MARKER),
|
|
316
|
+
itemCount: finiteNumberOrNull(record.item_count),
|
|
317
|
+
elapsedAfterPerpsBootstrapStartMs:
|
|
318
|
+
elapsedMs === null ? null : roundMs(elapsedMs),
|
|
319
|
+
mainMarketCount: finiteNumberOrNull(record.main_market_count),
|
|
320
|
+
hip3MarketCount: finiteNumberOrNull(record.hip3_market_count),
|
|
321
|
+
pricedMarketCount: finiteNumberOrNull(record.priced_market_count),
|
|
322
|
+
trendMarketCount: finiteNumberOrNull(record.trend_market_count),
|
|
323
|
+
};
|
|
324
|
+
});
|
|
325
|
+
}
|
|
326
|
+
|
|
75
327
|
function increment(counts, value) {
|
|
76
328
|
const key = value == null || value === "" ? "unknown" : String(value);
|
|
77
329
|
counts[key] = (counts[key] ?? 0) + 1;
|
|
@@ -81,6 +333,12 @@ function roundMs(value) {
|
|
|
81
333
|
return Number(value.toFixed(3));
|
|
82
334
|
}
|
|
83
335
|
|
|
336
|
+
function finiteNumberOrNull(value) {
|
|
337
|
+
if (value === null || value === undefined) return null;
|
|
338
|
+
const number = Number(value);
|
|
339
|
+
return Number.isFinite(number) ? number : null;
|
|
340
|
+
}
|
|
341
|
+
|
|
84
342
|
function contentVariantSatisfies(actual, required) {
|
|
85
343
|
if (actual === required) return true;
|
|
86
344
|
return (
|
|
@@ -139,7 +397,7 @@ function frameIsCached(record) {
|
|
|
139
397
|
function visibleFrameGroups(records) {
|
|
140
398
|
const demandLifecycles = new Map(
|
|
141
399
|
records
|
|
142
|
-
.filter((record) => record.stage === "
|
|
400
|
+
.filter((record) => record.stage === "surface_demand")
|
|
143
401
|
.map((record) => [
|
|
144
402
|
String(record.demand_id ?? ""),
|
|
145
403
|
record.lifecycle ?? "unknown",
|
|
@@ -215,7 +473,7 @@ function cacheTakeoverMeasurement(records, requiredContentVariants = []) {
|
|
|
215
473
|
const failureReason = !cached
|
|
216
474
|
? "missing_cached_visible_frame"
|
|
217
475
|
: !fresh
|
|
218
|
-
? "
|
|
476
|
+
? "missing_later_surface_live_frame"
|
|
219
477
|
: cacheHydratedAt === null
|
|
220
478
|
? "missing_disk_cache_hydrated"
|
|
221
479
|
: null;
|
|
@@ -248,7 +506,7 @@ function cacheTakeoverMeasurement(records, requiredContentVariants = []) {
|
|
|
248
506
|
|
|
249
507
|
function visibleMeasurements(records, requiredFreshContentVariants = []) {
|
|
250
508
|
const demands = records.filter(
|
|
251
|
-
(record) => record.stage === "
|
|
509
|
+
(record) => record.stage === "surface_demand",
|
|
252
510
|
);
|
|
253
511
|
return demands.map((demand) => {
|
|
254
512
|
const demandId = String(demand.demand_id ?? "");
|
|
@@ -265,13 +523,16 @@ function visibleMeasurements(records, requiredFreshContentVariants = []) {
|
|
|
265
523
|
(record) =>
|
|
266
524
|
record.stage === "next_frame_checkpoint" && belongsToDemand(record),
|
|
267
525
|
);
|
|
526
|
+
const commits = records.filter(
|
|
527
|
+
(record) => record.stage === "react_commit" && belongsToDemand(record),
|
|
528
|
+
);
|
|
268
529
|
const firstVisibleBoundary = records.find(
|
|
269
530
|
(record) =>
|
|
270
|
-
record.stage === "
|
|
531
|
+
record.stage === "surface_resolved_recorded" && belongsToDemand(record),
|
|
271
532
|
);
|
|
272
533
|
const freshVisibleBoundary = records.find(
|
|
273
534
|
(record) =>
|
|
274
|
-
record.stage === "
|
|
535
|
+
record.stage === "surface_live_recorded" && belongsToDemand(record),
|
|
275
536
|
);
|
|
276
537
|
const frameTimes = frames
|
|
277
538
|
.map((record) =>
|
|
@@ -352,9 +613,12 @@ function visibleMeasurements(records, requiredFreshContentVariants = []) {
|
|
|
352
613
|
: null,
|
|
353
614
|
freshVisibleAtMonotonicMs: freshVisibleAt,
|
|
354
615
|
freshContentVariant,
|
|
355
|
-
dataReadyAtDemand:
|
|
356
|
-
|
|
357
|
-
|
|
616
|
+
dataReadyAtDemand:
|
|
617
|
+
firstVisibleBoundary?.data_ready_at_demand === true ||
|
|
618
|
+
commits.some(
|
|
619
|
+
(commit) =>
|
|
620
|
+
commit.data_ready_at_demand === true && frameIsFresh(commit),
|
|
621
|
+
),
|
|
358
622
|
dfdMs: Number.isFinite(Number(freshVisibleBoundary?.duration_ms))
|
|
359
623
|
? roundMs(Number(freshVisibleBoundary.duration_ms))
|
|
360
624
|
: Number.isFinite(startedAt) && freshVisibleAt != null
|
|
@@ -527,6 +791,7 @@ function firstFreshDeliveryChecks(records, requirements) {
|
|
|
527
791
|
function summarize(
|
|
528
792
|
records,
|
|
529
793
|
startupRecords,
|
|
794
|
+
loadProofRecords,
|
|
530
795
|
sourcePath,
|
|
531
796
|
offset,
|
|
532
797
|
bytesScanned,
|
|
@@ -554,6 +819,10 @@ function summarize(
|
|
|
554
819
|
for (const record of startupRecords) {
|
|
555
820
|
increment(startupStages, record.stage);
|
|
556
821
|
}
|
|
822
|
+
const loadProofStages = {};
|
|
823
|
+
for (const record of loadProofRecords) {
|
|
824
|
+
increment(loadProofStages, record.stage);
|
|
825
|
+
}
|
|
557
826
|
return {
|
|
558
827
|
schemaVersion: 1,
|
|
559
828
|
marker: MARKER,
|
|
@@ -565,6 +834,11 @@ function summarize(
|
|
|
565
834
|
startupRecordCount: startupRecords.length,
|
|
566
835
|
startupStages,
|
|
567
836
|
startupMeasurements: startupMeasurements(startupRecords),
|
|
837
|
+
loadProofMarker: LOAD_PROOF_MARKER,
|
|
838
|
+
loadProofRecordCount: loadProofRecords.length,
|
|
839
|
+
loadProofStages,
|
|
840
|
+
loadProofMeasurements: loadProofMeasurements(loadProofRecords),
|
|
841
|
+
perpsBootstrapBoundary: perpsBootstrapBoundary(records, loadProofRecords),
|
|
568
842
|
stages,
|
|
569
843
|
lifecycles,
|
|
570
844
|
streams,
|
|
@@ -587,7 +861,7 @@ function summarize(
|
|
|
587
861
|
};
|
|
588
862
|
}
|
|
589
863
|
|
|
590
|
-
export async function
|
|
864
|
+
export async function capturePerpsPerformance(input) {
|
|
591
865
|
const node = input.node ?? {};
|
|
592
866
|
const phase = String(node.phase ?? "").toLowerCase();
|
|
593
867
|
if (phase !== "start" && phase !== "end") {
|
|
@@ -612,9 +886,21 @@ export async function captureHomepagePerformance(input) {
|
|
|
612
886
|
|
|
613
887
|
if (phase === "start") {
|
|
614
888
|
const offset = await fileSize(sourceFile);
|
|
889
|
+
const existingContent =
|
|
890
|
+
offset > 0 ? (await readFile(sourceFile)).toString("utf8") : "";
|
|
615
891
|
await writeFile(
|
|
616
892
|
stateFile,
|
|
617
|
-
`${JSON.stringify(
|
|
893
|
+
`${JSON.stringify(
|
|
894
|
+
{
|
|
895
|
+
sourcePath,
|
|
896
|
+
offset,
|
|
897
|
+
captureStartedAtEpochMs: Date.now(),
|
|
898
|
+
perpsBootstrapStartRecord:
|
|
899
|
+
latestPerpsBootstrapStartRecord(existingContent),
|
|
900
|
+
},
|
|
901
|
+
null,
|
|
902
|
+
2,
|
|
903
|
+
)}\n`,
|
|
618
904
|
);
|
|
619
905
|
return { phase, sourcePath, offset };
|
|
620
906
|
}
|
|
@@ -631,27 +917,52 @@ export async function captureHomepagePerformance(input) {
|
|
|
631
917
|
if (
|
|
632
918
|
state.sourcePath !== sourcePath ||
|
|
633
919
|
!Number.isInteger(state.offset) ||
|
|
634
|
-
state.offset < 0
|
|
920
|
+
state.offset < 0 ||
|
|
921
|
+
!Number.isFinite(state.captureStartedAtEpochMs)
|
|
635
922
|
) {
|
|
636
923
|
throw new Error(
|
|
637
924
|
"metamask.perps.capture_performance capture state is invalid or uses a different source_path.",
|
|
638
925
|
);
|
|
639
926
|
}
|
|
640
927
|
|
|
928
|
+
await waitForPerformanceRecord({
|
|
929
|
+
node,
|
|
930
|
+
sourceFile,
|
|
931
|
+
offset: state.offset,
|
|
932
|
+
});
|
|
933
|
+
|
|
641
934
|
const content = await readFile(sourceFile);
|
|
642
935
|
const offset = state.offset <= content.length ? state.offset : 0;
|
|
643
936
|
const segment = content.subarray(offset).toString("utf8");
|
|
644
|
-
const
|
|
645
|
-
|
|
646
|
-
.filter(
|
|
937
|
+
const segmentLines = segment.split(/\r?\n/u);
|
|
938
|
+
const lines = segmentLines
|
|
939
|
+
.filter(
|
|
940
|
+
(line) =>
|
|
941
|
+
detailedMarker(line) ||
|
|
942
|
+
line.includes(STARTUP_MARKER) ||
|
|
943
|
+
line.includes(LOAD_PROOF_MARKER),
|
|
944
|
+
);
|
|
647
945
|
const records = lines
|
|
648
|
-
.filter(
|
|
649
|
-
.map(
|
|
946
|
+
.filter(detailedMarker)
|
|
947
|
+
.map(parseDetailedRecord)
|
|
650
948
|
.filter(Boolean);
|
|
651
949
|
const startupRecords = lines
|
|
652
950
|
.filter((line) => line.includes(STARTUP_MARKER))
|
|
653
951
|
.map((line) => parseRecord(line, STARTUP_MARKER))
|
|
654
952
|
.filter(Boolean);
|
|
953
|
+
const markerLoadProofRecords = [
|
|
954
|
+
...(state.perpsBootstrapStartRecord
|
|
955
|
+
? [state.perpsBootstrapStartRecord]
|
|
956
|
+
: []),
|
|
957
|
+
...lines
|
|
958
|
+
.filter((line) => line.includes(LOAD_PROOF_MARKER))
|
|
959
|
+
.map((line) => parseRecord(line, LOAD_PROOF_MARKER))
|
|
960
|
+
.filter(Boolean),
|
|
961
|
+
];
|
|
962
|
+
const loadProofRecords = mergeLoadProofRecords([
|
|
963
|
+
...markerLoadProofRecords,
|
|
964
|
+
...existingLiveStreamRecords(segmentLines, markerLoadProofRecords),
|
|
965
|
+
]);
|
|
655
966
|
const requireRecords = node.require_records !== false;
|
|
656
967
|
const requiredStages = Array.isArray(node.required_stages)
|
|
657
968
|
? node.required_stages.map(String)
|
|
@@ -732,6 +1043,26 @@ export async function captureHomepagePerformance(input) {
|
|
|
732
1043
|
(check) => check.status === "fail",
|
|
733
1044
|
);
|
|
734
1045
|
const requireCacheBeforeFresh = node.require_cache_before_fresh === true;
|
|
1046
|
+
const requirePerpsBootstrapStartBeforeDemand =
|
|
1047
|
+
node.require_perps_bootstrap_start_before_demand === true;
|
|
1048
|
+
const requiredLiveStreams = Array.isArray(node.required_live_streams)
|
|
1049
|
+
? node.required_live_streams.map(String)
|
|
1050
|
+
: [];
|
|
1051
|
+
const presentLiveStreams = new Set(
|
|
1052
|
+
loadProofRecords
|
|
1053
|
+
.filter(
|
|
1054
|
+
(record) =>
|
|
1055
|
+
record.stage === "values_ready" && record.source === "fresh_socket",
|
|
1056
|
+
)
|
|
1057
|
+
.map((record) => String(record.stream ?? "")),
|
|
1058
|
+
);
|
|
1059
|
+
const missingLiveStreams = requiredLiveStreams.filter(
|
|
1060
|
+
(stream) => !presentLiveStreams.has(stream),
|
|
1061
|
+
);
|
|
1062
|
+
const measuredPerpsBootstrapBoundary = perpsBootstrapBoundary(
|
|
1063
|
+
records,
|
|
1064
|
+
loadProofRecords,
|
|
1065
|
+
);
|
|
735
1066
|
const cacheTakeover = cacheTakeoverMeasurement(
|
|
736
1067
|
records,
|
|
737
1068
|
requiredFreshContentVariants,
|
|
@@ -756,6 +1087,9 @@ export async function captureHomepagePerformance(input) {
|
|
|
756
1087
|
missingFreshLifecycles.length === 0 &&
|
|
757
1088
|
missingFreshContentVariants.length === 0 &&
|
|
758
1089
|
failedFirstFreshDeliveries.length === 0 &&
|
|
1090
|
+
missingLiveStreams.length === 0 &&
|
|
1091
|
+
(!requirePerpsBootstrapStartBeforeDemand ||
|
|
1092
|
+
measuredPerpsBootstrapBoundary.status === "pass") &&
|
|
759
1093
|
(!requireCacheBeforeFresh || cacheTakeover.status === "pass")
|
|
760
1094
|
? "pass"
|
|
761
1095
|
: "fail",
|
|
@@ -767,20 +1101,31 @@ export async function captureHomepagePerformance(input) {
|
|
|
767
1101
|
missingFreshLifecycles,
|
|
768
1102
|
missingFreshContentVariants,
|
|
769
1103
|
failedFirstFreshDeliveries,
|
|
1104
|
+
missingLiveStreams,
|
|
1105
|
+
perpsBootstrapStartBeforeDemand: requirePerpsBootstrapStartBeforeDemand
|
|
1106
|
+
? measuredPerpsBootstrapBoundary.status
|
|
1107
|
+
: "not_required",
|
|
770
1108
|
cacheBeforeFresh: requireCacheBeforeFresh
|
|
771
1109
|
? cacheTakeover.status
|
|
772
1110
|
: "not_required",
|
|
773
1111
|
};
|
|
1112
|
+
const captureEndedAtEpochMs = Date.now();
|
|
774
1113
|
const summary = {
|
|
775
1114
|
...summarize(
|
|
776
1115
|
records,
|
|
777
1116
|
startupRecords,
|
|
1117
|
+
loadProofRecords,
|
|
778
1118
|
sourcePath,
|
|
779
1119
|
offset,
|
|
780
1120
|
content.length - offset,
|
|
781
1121
|
requiredFreshContentVariants,
|
|
782
1122
|
firstFreshDeliveryRequirements,
|
|
783
1123
|
),
|
|
1124
|
+
captureWindow: {
|
|
1125
|
+
startedAtEpochMs: state.captureStartedAtEpochMs,
|
|
1126
|
+
endedAtEpochMs: captureEndedAtEpochMs,
|
|
1127
|
+
durationMs: captureEndedAtEpochMs - state.captureStartedAtEpochMs,
|
|
1128
|
+
},
|
|
784
1129
|
validation,
|
|
785
1130
|
};
|
|
786
1131
|
const nodeId = String(input.context?.nodeId ?? "capture-performance");
|
|
@@ -804,6 +1149,19 @@ export async function captureHomepagePerformance(input) {
|
|
|
804
1149
|
`Missing required ${STARTUP_MARKER} stages: ${missingStartupStages.join(", ")}.`,
|
|
805
1150
|
);
|
|
806
1151
|
}
|
|
1152
|
+
if (
|
|
1153
|
+
requirePerpsBootstrapStartBeforeDemand &&
|
|
1154
|
+
measuredPerpsBootstrapBoundary.status !== "pass"
|
|
1155
|
+
) {
|
|
1156
|
+
throw new Error(
|
|
1157
|
+
`Perps bootstrap boundary requirement failed: ${measuredPerpsBootstrapBoundary.failureReason}.`,
|
|
1158
|
+
);
|
|
1159
|
+
}
|
|
1160
|
+
if (missingLiveStreams.length > 0) {
|
|
1161
|
+
throw new Error(
|
|
1162
|
+
`Missing required live Perps streams: ${missingLiveStreams.join(", ")}.`,
|
|
1163
|
+
);
|
|
1164
|
+
}
|
|
807
1165
|
if (missingLifecycles.length > 0) {
|
|
808
1166
|
throw new Error(
|
|
809
1167
|
`Missing required ${MARKER} lifecycles: ${missingLifecycles.join(", ")}.`,
|
|
@@ -6,6 +6,7 @@ import {
|
|
|
6
6
|
runAdapter,
|
|
7
7
|
} from '../platform/bridge.mjs';
|
|
8
8
|
import { ensureUnlocked } from '../wallet/ensure_unlocked.mjs';
|
|
9
|
+
import { selectAccount } from '../wallet/select_account.mjs';
|
|
9
10
|
|
|
10
11
|
function sleep(ms) {
|
|
11
12
|
return new Promise((resolve) => setTimeout(resolve, ms));
|
|
@@ -961,29 +962,74 @@ async function applyTutorialState(input, config) {
|
|
|
961
962
|
export async function startState(input) {
|
|
962
963
|
const params = paramsForState(input);
|
|
963
964
|
const config = mergeStateConfig(profileDefaults(params.profile), params);
|
|
965
|
+
const timeoutMs = Math.max(1, Number(input.node?.timeout_ms ?? 30_000));
|
|
966
|
+
const deadline = Date.now() + timeoutMs;
|
|
967
|
+
const stateInput = () => {
|
|
968
|
+
const remainingMs = Math.max(1, deadline - Date.now());
|
|
969
|
+
const configuredTargetMs = Number(input.node?.target_timeout_ms);
|
|
970
|
+
const targetTimeoutMs = Math.min(
|
|
971
|
+
remainingMs,
|
|
972
|
+
Number.isFinite(configuredTargetMs) && configuredTargetMs > 0
|
|
973
|
+
? configuredTargetMs
|
|
974
|
+
: Math.min(30_000, Math.ceil(remainingMs / 3)),
|
|
975
|
+
);
|
|
976
|
+
const configuredUnlockMs = Number(input.node?.unlock_timeout_ms);
|
|
977
|
+
const unlockTimeoutMs = Math.min(
|
|
978
|
+
Math.max(1, remainingMs - targetTimeoutMs),
|
|
979
|
+
Number.isFinite(configuredUnlockMs) && configuredUnlockMs > 0
|
|
980
|
+
? configuredUnlockMs
|
|
981
|
+
: remainingMs,
|
|
982
|
+
);
|
|
983
|
+
return {
|
|
984
|
+
...input,
|
|
985
|
+
node: {
|
|
986
|
+
...input.node,
|
|
987
|
+
timeout_ms: remainingMs,
|
|
988
|
+
bridge_timeout_ms: remainingMs,
|
|
989
|
+
cdp_timeout_ms: remainingMs,
|
|
990
|
+
controller_ready_timeout_ms: remainingMs,
|
|
991
|
+
target_timeout_ms: targetTimeoutMs,
|
|
992
|
+
unlock_timeout_ms: unlockTimeoutMs,
|
|
993
|
+
},
|
|
994
|
+
};
|
|
995
|
+
};
|
|
964
996
|
// Product edits require an app restart before proof. Mobile relocks on restart,
|
|
965
997
|
// so a deterministic Perps start state must restore the fixture-backed wallet
|
|
966
998
|
// prerequisite before it navigates or converges controller state.
|
|
999
|
+
const walletInput = stateInput();
|
|
967
1000
|
const wallet = await ensureUnlocked({
|
|
968
|
-
...
|
|
1001
|
+
...walletInput,
|
|
969
1002
|
action: 'metamask.wallet.ensure_unlocked',
|
|
970
|
-
node: { ...
|
|
1003
|
+
node: { ...walletInput.node, action: 'metamask.wallet.ensure_unlocked' },
|
|
971
1004
|
});
|
|
1005
|
+
const accountInput = stateInput();
|
|
1006
|
+
const account = typeof config.account === 'string' && config.account.trim()
|
|
1007
|
+
? await selectAccount({
|
|
1008
|
+
...accountInput,
|
|
1009
|
+
action: 'metamask.wallet.select_account',
|
|
1010
|
+
node: {
|
|
1011
|
+
...accountInput.node,
|
|
1012
|
+
action: 'metamask.wallet.select_account',
|
|
1013
|
+
name: config.account.trim(),
|
|
1014
|
+
},
|
|
1015
|
+
})
|
|
1016
|
+
: { skipped: true };
|
|
972
1017
|
// Entering Perps initializes the provider client on a freshly launched app.
|
|
973
1018
|
// State reads before this navigation fail with CLIENT_NOT_INITIALIZED.
|
|
974
|
-
const navigation = await applyStateNavigation(
|
|
975
|
-
const provider = await ensureProvider(
|
|
976
|
-
const network = await ensureNetwork(
|
|
977
|
-
const tutorial = await applyTutorialState(
|
|
978
|
-
const readyToTrade = await assertReadyToTrade(
|
|
979
|
-
const balance = await assertBalance(
|
|
980
|
-
const orders = await applyOrdersState(
|
|
981
|
-
const positions = await applyPositionsState(
|
|
1019
|
+
const navigation = await applyStateNavigation(stateInput(), config);
|
|
1020
|
+
const provider = await ensureProvider(stateInput(), config);
|
|
1021
|
+
const network = await ensureNetwork(stateInput(), config);
|
|
1022
|
+
const tutorial = await applyTutorialState(stateInput(), config);
|
|
1023
|
+
const readyToTrade = await assertReadyToTrade(stateInput(), config);
|
|
1024
|
+
const balance = await assertBalance(stateInput(), config);
|
|
1025
|
+
const orders = await applyOrdersState(stateInput(), config.orders);
|
|
1026
|
+
const positions = await applyPositionsState(stateInput(), config.positions);
|
|
982
1027
|
return {
|
|
983
1028
|
action: input.action,
|
|
984
1029
|
profile: config.profile ?? params.profile ?? 'clean_market_testnet',
|
|
985
1030
|
phase: 'start_state',
|
|
986
1031
|
wallet,
|
|
1032
|
+
account,
|
|
987
1033
|
provider,
|
|
988
1034
|
network,
|
|
989
1035
|
tutorial,
|