@deeeed/metamask-harness 0.34.0 → 0.34.2
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 +29 -0
- package/adapters/manifest.json +0 -8
- package/adapters/mobile/bridge-runtime/cdp-bridge.cjs +236 -3
- package/adapters/mobile/bridge-runtime/console-forwarder.cjs +22 -1
- package/adapters/mobile/bridge-runtime/lib/target-discovery.cjs +22 -15
- package/adapters/mobile/bridge-runtime/lib/ws-client.cjs +18 -0
- package/adapters/mobile/launch-metro.cjs +6 -4
- package/adapters/mobile/open-device.sh +170 -19
- package/adapters/mobile/start-metro.sh +165 -93
- package/adapters/mobile/stop-metro.sh +1 -0
- package/dist/adapters/mobile/prepare.js +1 -4
- package/dist/adapters/mobile/video-recorder.js +12 -10
- package/dist/adapters.js +134 -6
- package/dist/commands/launch/index.js +9 -0
- package/dist/commands/launch/mobile.js +17 -3
- package/dist/live-adapter-contract.js +10 -3
- package/dist/recipe-security.js +2 -0
- package/dist/runner.js +53 -8
- package/dist/runtime-context.js +1 -0
- package/library/actions/mobile/perps/measure_homepage_visible.mjs +4 -0
- package/library/actions/mobile/perps/performance-capture.mjs +579 -111
- package/library/actions/mobile/perps/perps.mjs +221 -38
- package/library/actions/mobile/perps/prepare_local_snapshot_endpoint.mjs +58 -0
- package/library/actions/mobile/platform/bridge.mjs +102 -8
- package/library/actions/mobile/wallet/ensure_unlocked.mjs +35 -8
- package/library/manifests/mobile.action-manifest.json +243 -511
- package/library/recipes/mobile/perps/performance.homepage.android-cold-disk-cache.recipe.json +20 -2
- package/library/recipes/mobile/perps/performance.homepage.android-cold-no-cache.recipe.json +2 -0
- package/library/recipes/mobile/perps/performance.homepage.cold-position-sample.recipe.json +120 -0
- package/library/recipes/mobile/perps/performance.homepage.ios-background-reconnect.recipe.json +7 -6
- package/library/recipes/mobile/perps/performance.homepage.ios-cold-no-cache.recipe.json +5 -5
- package/package.json +1 -1
- package/adapters/mobile/metro-config.cjs +0 -93
|
@@ -1,14 +1,21 @@
|
|
|
1
|
-
import { mkdir, readFile, rm, stat, writeFile } from
|
|
2
|
-
import path from
|
|
1
|
+
import { mkdir, readFile, rm, stat, writeFile } from "node:fs/promises";
|
|
2
|
+
import path from "node:path";
|
|
3
3
|
|
|
4
|
-
const MARKER =
|
|
5
|
-
const
|
|
4
|
+
const MARKER = "[HomepagePerf]";
|
|
5
|
+
const STARTUP_MARKER = "[StartupPerf]";
|
|
6
|
+
const STATE_FILE = ".homepage-performance-capture.json";
|
|
7
|
+
const FRESH_SOURCES = new Set([
|
|
8
|
+
"fresh_socket",
|
|
9
|
+
"provider_snapshot",
|
|
10
|
+
"terminal_global_snapshot_v2",
|
|
11
|
+
"provider",
|
|
12
|
+
]);
|
|
6
13
|
|
|
7
14
|
function resolveWithin(root, relativePath, label) {
|
|
8
15
|
const absoluteRoot = path.resolve(root);
|
|
9
16
|
const absolutePath = path.resolve(absoluteRoot, relativePath);
|
|
10
17
|
const relative = path.relative(absoluteRoot, absolutePath);
|
|
11
|
-
if (relative.startsWith(
|
|
18
|
+
if (relative.startsWith("..") || path.isAbsolute(relative)) {
|
|
12
19
|
throw new Error(`${label} must stay within ${absoluteRoot}.`);
|
|
13
20
|
}
|
|
14
21
|
return absolutePath;
|
|
@@ -18,25 +25,55 @@ async function fileSize(file) {
|
|
|
18
25
|
try {
|
|
19
26
|
return (await stat(file)).size;
|
|
20
27
|
} catch (error) {
|
|
21
|
-
if (error?.code ===
|
|
28
|
+
if (error?.code === "ENOENT") return 0;
|
|
22
29
|
throw error;
|
|
23
30
|
}
|
|
24
31
|
}
|
|
25
32
|
|
|
26
|
-
function parseRecord(line) {
|
|
27
|
-
const markerIndex = line.indexOf(
|
|
33
|
+
function parseRecord(line, marker) {
|
|
34
|
+
const markerIndex = line.indexOf(marker);
|
|
28
35
|
if (markerIndex < 0) return null;
|
|
29
|
-
const payload = line.slice(markerIndex +
|
|
36
|
+
const payload = line.slice(markerIndex + marker.length).trim();
|
|
30
37
|
try {
|
|
31
38
|
const event = JSON.parse(payload);
|
|
32
|
-
return event && typeof event ===
|
|
39
|
+
return event && typeof event === "object" && !Array.isArray(event)
|
|
40
|
+
? event
|
|
41
|
+
: null;
|
|
33
42
|
} catch {
|
|
34
43
|
return null;
|
|
35
44
|
}
|
|
36
45
|
}
|
|
37
46
|
|
|
47
|
+
function startupMeasurements(records) {
|
|
48
|
+
return records.map((record) => {
|
|
49
|
+
const processToStageMs = Number(
|
|
50
|
+
record.process_to_unlock_demand_ms ?? record.process_to_frame_ms,
|
|
51
|
+
);
|
|
52
|
+
const unlockToFrameMs = Number(record.unlock_to_frame_ms);
|
|
53
|
+
const monotonicMs = Number(record.monotonic_ms);
|
|
54
|
+
const appLaunchEpochMs = Number(record.app_launch_epoch_ms);
|
|
55
|
+
return {
|
|
56
|
+
stage: String(record.stage ?? "unknown"),
|
|
57
|
+
monotonicMs: Number.isFinite(monotonicMs) ? monotonicMs : null,
|
|
58
|
+
appLaunchEpochMs: Number.isFinite(appLaunchEpochMs)
|
|
59
|
+
? appLaunchEpochMs
|
|
60
|
+
: null,
|
|
61
|
+
processStartMonotonicMs:
|
|
62
|
+
Number.isFinite(monotonicMs) && Number.isFinite(processToStageMs)
|
|
63
|
+
? roundMs(monotonicMs - processToStageMs)
|
|
64
|
+
: null,
|
|
65
|
+
processToStageMs: Number.isFinite(processToStageMs)
|
|
66
|
+
? processToStageMs
|
|
67
|
+
: null,
|
|
68
|
+
unlockToFrameMs: Number.isFinite(unlockToFrameMs)
|
|
69
|
+
? unlockToFrameMs
|
|
70
|
+
: null,
|
|
71
|
+
};
|
|
72
|
+
});
|
|
73
|
+
}
|
|
74
|
+
|
|
38
75
|
function increment(counts, value) {
|
|
39
|
-
const key = value == null || value ===
|
|
76
|
+
const key = value == null || value === "" ? "unknown" : String(value);
|
|
40
77
|
counts[key] = (counts[key] ?? 0) + 1;
|
|
41
78
|
}
|
|
42
79
|
|
|
@@ -47,66 +84,227 @@ function roundMs(value) {
|
|
|
47
84
|
function contentVariantSatisfies(actual, required) {
|
|
48
85
|
if (actual === required) return true;
|
|
49
86
|
return (
|
|
50
|
-
actual ===
|
|
51
|
-
(required ===
|
|
87
|
+
actual === "positions_and_orders" &&
|
|
88
|
+
(required === "positions" || required === "orders")
|
|
52
89
|
);
|
|
53
90
|
}
|
|
54
91
|
|
|
55
92
|
function requiredFreshStreams(contentVariant, requiredVariants) {
|
|
56
|
-
if (requiredVariants.length === 0) return [
|
|
93
|
+
if (requiredVariants.length === 0) return ["positions", "orders"];
|
|
57
94
|
const streams = new Set();
|
|
58
95
|
for (const required of requiredVariants) {
|
|
59
96
|
if (!contentVariantSatisfies(contentVariant, required)) continue;
|
|
60
|
-
if (required ===
|
|
61
|
-
else if (required ===
|
|
62
|
-
else if (required ===
|
|
63
|
-
streams.add(
|
|
64
|
-
streams.add(
|
|
97
|
+
if (required === "positions") streams.add("positions");
|
|
98
|
+
else if (required === "orders") streams.add("orders");
|
|
99
|
+
else if (required === "positions_and_orders") {
|
|
100
|
+
streams.add("positions");
|
|
101
|
+
streams.add("orders");
|
|
102
|
+
} else if (required === "trending" || required === "pills") {
|
|
103
|
+
streams.add("positions");
|
|
104
|
+
streams.add("orders");
|
|
105
|
+
streams.add("markets");
|
|
65
106
|
} else {
|
|
66
|
-
// Empty
|
|
67
|
-
streams.add(
|
|
68
|
-
streams.add(
|
|
107
|
+
// Empty content is derived from both account collections.
|
|
108
|
+
streams.add("positions");
|
|
109
|
+
streams.add("orders");
|
|
69
110
|
}
|
|
70
111
|
}
|
|
71
112
|
return [...streams];
|
|
72
113
|
}
|
|
73
114
|
|
|
115
|
+
function frameTime(record) {
|
|
116
|
+
const value = Number(
|
|
117
|
+
record.frame_checkpoint_monotonic_ms ?? record.monotonic_ms,
|
|
118
|
+
);
|
|
119
|
+
return Number.isFinite(value) ? value : null;
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
function frameIsFresh(record) {
|
|
123
|
+
if (record.fresh_for_lifecycle === false) return false;
|
|
124
|
+
return (
|
|
125
|
+
FRESH_SOURCES.has(record.source) ||
|
|
126
|
+
((record.source === "resident_state" || record.source === "memory_cache") &&
|
|
127
|
+
FRESH_SOURCES.has(record.origin_source) &&
|
|
128
|
+
record.fresh_for_lifecycle === true)
|
|
129
|
+
);
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
function frameIsCached(record) {
|
|
133
|
+
return (
|
|
134
|
+
!frameIsFresh(record) &&
|
|
135
|
+
["disk_cache", "memory_cache", "resident_state"].includes(record.source)
|
|
136
|
+
);
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
function visibleFrameGroups(records) {
|
|
140
|
+
const demandLifecycles = new Map(
|
|
141
|
+
records
|
|
142
|
+
.filter((record) => record.stage === "viewport_demand")
|
|
143
|
+
.map((record) => [
|
|
144
|
+
String(record.demand_id ?? ""),
|
|
145
|
+
record.lifecycle ?? "unknown",
|
|
146
|
+
]),
|
|
147
|
+
);
|
|
148
|
+
const groups = new Map();
|
|
149
|
+
for (const record of records) {
|
|
150
|
+
if (record.stage !== "next_frame_checkpoint") continue;
|
|
151
|
+
const at = frameTime(record);
|
|
152
|
+
if (at === null) continue;
|
|
153
|
+
const demandId = String(record.demand_id ?? "");
|
|
154
|
+
const key = `${demandId}:${at}`;
|
|
155
|
+
const group = groups.get(key) ?? {
|
|
156
|
+
at,
|
|
157
|
+
demandId,
|
|
158
|
+
lifecycle:
|
|
159
|
+
record.lifecycle ?? demandLifecycles.get(demandId) ?? "unknown",
|
|
160
|
+
records: [],
|
|
161
|
+
};
|
|
162
|
+
group.records.push(record);
|
|
163
|
+
groups.set(key, group);
|
|
164
|
+
}
|
|
165
|
+
return [...groups.values()].sort((left, right) => left.at - right.at);
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
function frameGroupSatisfies(group, requiredVariants, predicate) {
|
|
169
|
+
const records = group.records.filter(predicate);
|
|
170
|
+
const variants = [
|
|
171
|
+
...new Set(
|
|
172
|
+
records
|
|
173
|
+
.map((record) => String(record.content_variant ?? ""))
|
|
174
|
+
.filter(Boolean),
|
|
175
|
+
),
|
|
176
|
+
];
|
|
177
|
+
const contentMatches =
|
|
178
|
+
requiredVariants.length === 0 ||
|
|
179
|
+
requiredVariants.every((required) =>
|
|
180
|
+
variants.some((actual) => contentVariantSatisfies(actual, required)),
|
|
181
|
+
);
|
|
182
|
+
if (!contentMatches) return false;
|
|
183
|
+
const requiredStreams = requiredFreshStreams(
|
|
184
|
+
variants[0] ?? "",
|
|
185
|
+
requiredVariants,
|
|
186
|
+
);
|
|
187
|
+
const streams = new Set(records.map((record) => String(record.stream ?? "")));
|
|
188
|
+
return (
|
|
189
|
+
requiredStreams.length > 0 &&
|
|
190
|
+
requiredStreams.every((stream) => streams.has(stream))
|
|
191
|
+
);
|
|
192
|
+
}
|
|
193
|
+
|
|
194
|
+
function cacheTakeoverMeasurement(records, requiredContentVariants = []) {
|
|
195
|
+
const groups = visibleFrameGroups(records);
|
|
196
|
+
const cached = groups.find((group) =>
|
|
197
|
+
frameGroupSatisfies(group, requiredContentVariants, frameIsCached),
|
|
198
|
+
);
|
|
199
|
+
const fresh = cached
|
|
200
|
+
? groups.find(
|
|
201
|
+
(group) =>
|
|
202
|
+
group.at > cached.at &&
|
|
203
|
+
frameGroupSatisfies(group, requiredContentVariants, frameIsFresh),
|
|
204
|
+
)
|
|
205
|
+
: null;
|
|
206
|
+
const cacheHydratedValues = records
|
|
207
|
+
.filter((record) => record.stage === "disk_cache_hydrated")
|
|
208
|
+
.map((record) => Number(record.monotonic_ms))
|
|
209
|
+
.filter(
|
|
210
|
+
(value) =>
|
|
211
|
+
Number.isFinite(value) && (cached === undefined || value <= cached.at),
|
|
212
|
+
);
|
|
213
|
+
const cacheHydratedAt =
|
|
214
|
+
cacheHydratedValues.length > 0 ? Math.max(...cacheHydratedValues) : null;
|
|
215
|
+
const failureReason = !cached
|
|
216
|
+
? "missing_cached_visible_frame"
|
|
217
|
+
: !fresh
|
|
218
|
+
? "missing_later_fresh_visible_frame"
|
|
219
|
+
: cacheHydratedAt === null
|
|
220
|
+
? "missing_disk_cache_hydrated"
|
|
221
|
+
: null;
|
|
222
|
+
return {
|
|
223
|
+
status: failureReason === null ? "pass" : "fail",
|
|
224
|
+
requiredContentVariants: requiredContentVariants,
|
|
225
|
+
cacheHydratedAtMonotonicMs: cacheHydratedAt,
|
|
226
|
+
cachedVisibleAtMonotonicMs: cached?.at ?? null,
|
|
227
|
+
cachedVisibleLifecycle: cached?.lifecycle ?? null,
|
|
228
|
+
cachedVisibleSources: cached
|
|
229
|
+
? [
|
|
230
|
+
...new Set(
|
|
231
|
+
cached.records
|
|
232
|
+
.filter(frameIsCached)
|
|
233
|
+
.map((record) => String(record.source)),
|
|
234
|
+
),
|
|
235
|
+
]
|
|
236
|
+
: [],
|
|
237
|
+
freshVisibleAtMonotonicMs: fresh?.at ?? null,
|
|
238
|
+
freshVisibleLifecycle: fresh?.lifecycle ?? null,
|
|
239
|
+
cacheToVisibleMs:
|
|
240
|
+
cached && cacheHydratedAt !== null
|
|
241
|
+
? roundMs(cached.at - cacheHydratedAt)
|
|
242
|
+
: null,
|
|
243
|
+
cachedToFreshVisibleMs:
|
|
244
|
+
cached && fresh ? roundMs(fresh.at - cached.at) : null,
|
|
245
|
+
failureReason,
|
|
246
|
+
};
|
|
247
|
+
}
|
|
248
|
+
|
|
74
249
|
function visibleMeasurements(records, requiredFreshContentVariants = []) {
|
|
75
|
-
const demands = records.filter(
|
|
250
|
+
const demands = records.filter(
|
|
251
|
+
(record) => record.stage === "viewport_demand",
|
|
252
|
+
);
|
|
76
253
|
return demands.map((demand) => {
|
|
77
|
-
const demandId = String(demand.demand_id ??
|
|
254
|
+
const demandId = String(demand.demand_id ?? "");
|
|
78
255
|
const startedAt = Number(demand.monotonic_ms);
|
|
256
|
+
const belongsToDemand = (record) =>
|
|
257
|
+
String(record.demand_id ?? "") === demandId &&
|
|
258
|
+
["account_generation", "context_generation"].every(
|
|
259
|
+
(key) =>
|
|
260
|
+
demand[key] === undefined ||
|
|
261
|
+
record[key] === undefined ||
|
|
262
|
+
String(record[key]) === String(demand[key]),
|
|
263
|
+
);
|
|
79
264
|
const frames = records.filter(
|
|
80
265
|
(record) =>
|
|
81
|
-
record.stage ===
|
|
82
|
-
|
|
266
|
+
record.stage === "next_frame_checkpoint" && belongsToDemand(record),
|
|
267
|
+
);
|
|
268
|
+
const firstVisibleBoundary = records.find(
|
|
269
|
+
(record) =>
|
|
270
|
+
record.stage === "first_visible_recorded" && belongsToDemand(record),
|
|
271
|
+
);
|
|
272
|
+
const freshVisibleBoundary = records.find(
|
|
273
|
+
(record) =>
|
|
274
|
+
record.stage === "fresh_visible_recorded" && belongsToDemand(record),
|
|
83
275
|
);
|
|
84
276
|
const frameTimes = frames
|
|
85
|
-
.map((record) =>
|
|
277
|
+
.map((record) =>
|
|
278
|
+
Number(record.frame_checkpoint_monotonic_ms ?? record.monotonic_ms),
|
|
279
|
+
)
|
|
86
280
|
.filter(Number.isFinite);
|
|
87
|
-
const
|
|
281
|
+
const recordedFirstVisibleAt = frameTime(firstVisibleBoundary ?? {});
|
|
282
|
+
const firstVisibleAt =
|
|
283
|
+
recordedFirstVisibleAt ??
|
|
284
|
+
(frameTimes.length > 0 ? Math.min(...frameTimes) : null);
|
|
88
285
|
const firstFreshByStream = {};
|
|
89
286
|
const freshStreamsSeen = new Set();
|
|
90
|
-
let freshVisibleAt =
|
|
91
|
-
let freshContentVariant = null;
|
|
287
|
+
let freshVisibleAt = frameTime(freshVisibleBoundary ?? {});
|
|
288
|
+
let freshContentVariant = freshVisibleBoundary?.content_variant ?? null;
|
|
92
289
|
const sortedFrames = [...frames].sort(
|
|
93
290
|
(left, right) =>
|
|
94
291
|
Number(left.frame_checkpoint_monotonic_ms ?? left.monotonic_ms) -
|
|
95
292
|
Number(right.frame_checkpoint_monotonic_ms ?? right.monotonic_ms),
|
|
96
293
|
);
|
|
97
294
|
for (const frame of sortedFrames) {
|
|
98
|
-
const freshVisible =
|
|
99
|
-
frame.source === 'fresh_socket' ||
|
|
100
|
-
(frame.source === 'resident_state' &&
|
|
101
|
-
frame.origin_source === 'fresh_socket' &&
|
|
102
|
-
frame.fresh_for_lifecycle === true);
|
|
295
|
+
const freshVisible = frameIsFresh(frame);
|
|
103
296
|
if (!freshVisible || !frame.stream) continue;
|
|
104
|
-
const frameAt = Number(
|
|
297
|
+
const frameAt = Number(
|
|
298
|
+
frame.frame_checkpoint_monotonic_ms ?? frame.monotonic_ms,
|
|
299
|
+
);
|
|
105
300
|
if (!Number.isFinite(frameAt)) continue;
|
|
106
301
|
const stream = String(frame.stream);
|
|
107
|
-
firstFreshByStream[stream] = Math.min(
|
|
302
|
+
firstFreshByStream[stream] = Math.min(
|
|
303
|
+
firstFreshByStream[stream] ?? frameAt,
|
|
304
|
+
frameAt,
|
|
305
|
+
);
|
|
108
306
|
freshStreamsSeen.add(stream);
|
|
109
|
-
const contentVariant = String(frame.content_variant ??
|
|
307
|
+
const contentVariant = String(frame.content_variant ?? "");
|
|
110
308
|
const contentMatches =
|
|
111
309
|
requiredFreshContentVariants.length === 0 ||
|
|
112
310
|
requiredFreshContentVariants.every((required) =>
|
|
@@ -134,84 +332,211 @@ function visibleMeasurements(records, requiredFreshContentVariants = []) {
|
|
|
134
332
|
firstFreshByStream.positions != null &&
|
|
135
333
|
firstFreshByStream.orders != null
|
|
136
334
|
) {
|
|
137
|
-
freshVisibleAt = Math.max(
|
|
335
|
+
freshVisibleAt = Math.max(
|
|
336
|
+
firstFreshByStream.positions,
|
|
337
|
+
firstFreshByStream.orders,
|
|
338
|
+
);
|
|
138
339
|
}
|
|
139
|
-
const dataAges = frames
|
|
340
|
+
const dataAges = frames
|
|
341
|
+
.map((record) => Number(record.data_age_ms))
|
|
342
|
+
.filter(Number.isFinite);
|
|
140
343
|
return {
|
|
141
344
|
demandId,
|
|
142
|
-
lifecycle: demand.lifecycle ??
|
|
345
|
+
lifecycle: demand.lifecycle ?? "unknown",
|
|
143
346
|
startedAtMonotonicMs: Number.isFinite(startedAt) ? startedAt : null,
|
|
144
347
|
firstVisibleAtMonotonicMs: firstVisibleAt,
|
|
145
|
-
ttcMs:
|
|
146
|
-
Number.
|
|
348
|
+
ttcMs: Number.isFinite(Number(firstVisibleBoundary?.duration_ms))
|
|
349
|
+
? roundMs(Number(firstVisibleBoundary.duration_ms))
|
|
350
|
+
: Number.isFinite(startedAt) && firstVisibleAt != null
|
|
147
351
|
? roundMs(firstVisibleAt - startedAt)
|
|
148
352
|
: null,
|
|
149
353
|
freshVisibleAtMonotonicMs: freshVisibleAt,
|
|
150
354
|
freshContentVariant,
|
|
151
355
|
dataReadyAtDemand: sortedFrames.some(
|
|
152
|
-
(frame) =>
|
|
153
|
-
frame.data_ready_at_demand === true &&
|
|
154
|
-
(frame.source === 'fresh_socket' ||
|
|
155
|
-
(frame.origin_source === 'fresh_socket' && frame.fresh_for_lifecycle === true)),
|
|
356
|
+
(frame) => frame.data_ready_at_demand === true && frameIsFresh(frame),
|
|
156
357
|
),
|
|
157
|
-
dfdMs:
|
|
158
|
-
Number.
|
|
358
|
+
dfdMs: Number.isFinite(Number(freshVisibleBoundary?.duration_ms))
|
|
359
|
+
? roundMs(Number(freshVisibleBoundary.duration_ms))
|
|
360
|
+
: Number.isFinite(startedAt) && freshVisibleAt != null
|
|
159
361
|
? roundMs(freshVisibleAt - startedAt)
|
|
160
362
|
: null,
|
|
161
|
-
deliverySources: [
|
|
162
|
-
|
|
363
|
+
deliverySources: [
|
|
364
|
+
...new Set(frames.map((record) => String(record.source ?? "unknown"))),
|
|
365
|
+
],
|
|
366
|
+
contentVariant:
|
|
367
|
+
frames.find((record) => record.content_variant)?.content_variant ??
|
|
368
|
+
null,
|
|
163
369
|
maxDataAgeMs: dataAges.length > 0 ? Math.max(...dataAges) : null,
|
|
164
370
|
};
|
|
165
371
|
});
|
|
166
372
|
}
|
|
167
373
|
|
|
374
|
+
function percentile(values, fraction) {
|
|
375
|
+
if (values.length === 0) return null;
|
|
376
|
+
const sorted = [...values].sort((left, right) => left - right);
|
|
377
|
+
return sorted[Math.max(0, Math.ceil(fraction * sorted.length) - 1)];
|
|
378
|
+
}
|
|
379
|
+
|
|
380
|
+
function measurementStats(measurements, field) {
|
|
381
|
+
const rawMs = measurements
|
|
382
|
+
.map((measurement) => measurement[field])
|
|
383
|
+
.filter((value) => Number.isFinite(value))
|
|
384
|
+
.sort((left, right) => left - right);
|
|
385
|
+
return {
|
|
386
|
+
successCount: rawMs.length,
|
|
387
|
+
failureCount: measurements.length - rawMs.length,
|
|
388
|
+
minMs: rawMs[0] ?? null,
|
|
389
|
+
p50Ms: percentile(rawMs, 0.5),
|
|
390
|
+
p75Ms: percentile(rawMs, 0.75),
|
|
391
|
+
p95Ms: percentile(rawMs, 0.95),
|
|
392
|
+
maxMs: rawMs.at(-1) ?? null,
|
|
393
|
+
rawMs,
|
|
394
|
+
};
|
|
395
|
+
}
|
|
396
|
+
|
|
397
|
+
function visibleMeasurementCohorts(measurements) {
|
|
398
|
+
const byLifecycle = new Map();
|
|
399
|
+
for (const measurement of measurements) {
|
|
400
|
+
const lifecycle = String(measurement.lifecycle ?? "unknown");
|
|
401
|
+
const cohort = byLifecycle.get(lifecycle) ?? [];
|
|
402
|
+
cohort.push(measurement);
|
|
403
|
+
byLifecycle.set(lifecycle, cohort);
|
|
404
|
+
}
|
|
405
|
+
return Object.fromEntries(
|
|
406
|
+
[...byLifecycle.entries()].map(([lifecycle, cohort]) => {
|
|
407
|
+
const sourceDistribution = {};
|
|
408
|
+
for (const measurement of cohort) {
|
|
409
|
+
for (const source of measurement.deliverySources) {
|
|
410
|
+
increment(sourceDistribution, source);
|
|
411
|
+
}
|
|
412
|
+
}
|
|
413
|
+
return [
|
|
414
|
+
lifecycle,
|
|
415
|
+
{
|
|
416
|
+
sampleCount: cohort.length,
|
|
417
|
+
ttc: measurementStats(cohort, "ttcMs"),
|
|
418
|
+
dfd: measurementStats(cohort, "dfdMs"),
|
|
419
|
+
sourceDistribution,
|
|
420
|
+
},
|
|
421
|
+
];
|
|
422
|
+
}),
|
|
423
|
+
);
|
|
424
|
+
}
|
|
425
|
+
|
|
168
426
|
function socketPipelineMeasurements(records) {
|
|
169
427
|
const sockets = records.filter(
|
|
170
|
-
(record) => record.stage ===
|
|
428
|
+
(record) => record.stage === "socket_received" && record.delivery_id,
|
|
171
429
|
);
|
|
172
430
|
return sockets.flatMap((socket) => {
|
|
173
431
|
const deliveryId = String(socket.delivery_id);
|
|
174
432
|
const socketAt = Number(socket.monotonic_ms);
|
|
175
433
|
const commit = records.find(
|
|
176
|
-
(record) =>
|
|
434
|
+
(record) =>
|
|
435
|
+
record.stage === "react_commit" &&
|
|
436
|
+
String(record.delivery_id ?? "") === deliveryId,
|
|
177
437
|
);
|
|
178
438
|
const frame = records.find(
|
|
179
|
-
(record) =>
|
|
439
|
+
(record) =>
|
|
440
|
+
record.stage === "next_frame_checkpoint" &&
|
|
441
|
+
String(record.delivery_id ?? "") === deliveryId,
|
|
180
442
|
);
|
|
181
443
|
if (!commit || !frame || !Number.isFinite(socketAt)) return [];
|
|
182
444
|
const commitAt = Number(commit.monotonic_ms);
|
|
183
|
-
const frameAt = Number(
|
|
445
|
+
const frameAt = Number(
|
|
446
|
+
frame.frame_checkpoint_monotonic_ms ?? frame.monotonic_ms,
|
|
447
|
+
);
|
|
184
448
|
const subscriberCandidates = records
|
|
185
449
|
.filter(
|
|
186
450
|
(record) =>
|
|
187
|
-
record.stage ===
|
|
188
|
-
String(record.delivery_id ??
|
|
451
|
+
record.stage === "subscriber_delivery" &&
|
|
452
|
+
String(record.delivery_id ?? "") === deliveryId &&
|
|
189
453
|
Number(record.monotonic_ms) <= commitAt,
|
|
190
454
|
)
|
|
191
455
|
.map((record) => Number(record.monotonic_ms))
|
|
192
456
|
.filter(Number.isFinite);
|
|
193
|
-
const subscriberAt =
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
457
|
+
const subscriberAt =
|
|
458
|
+
subscriberCandidates.length > 0
|
|
459
|
+
? Math.max(...subscriberCandidates)
|
|
460
|
+
: socketAt;
|
|
461
|
+
return [
|
|
462
|
+
{
|
|
463
|
+
deliveryId,
|
|
464
|
+
stream: socket.stream ?? null,
|
|
465
|
+
lifecycle: commit.lifecycle ?? socket.lifecycle ?? "unknown",
|
|
466
|
+
socketToVisibleMs: roundMs(frameAt - socketAt),
|
|
467
|
+
socketToSubscriberMs: roundMs(subscriberAt - socketAt),
|
|
468
|
+
subscriberToCommitMs: roundMs(commitAt - subscriberAt),
|
|
469
|
+
commitToFrameMs: roundMs(frameAt - commitAt),
|
|
470
|
+
},
|
|
471
|
+
];
|
|
472
|
+
});
|
|
473
|
+
}
|
|
474
|
+
|
|
475
|
+
function firstFreshDeliveryChecks(records, requirements) {
|
|
476
|
+
return requirements.map((requirement) => {
|
|
477
|
+
const stream = String(requirement.stream);
|
|
478
|
+
const requiredLifecycle =
|
|
479
|
+
requirement.lifecycle === undefined
|
|
480
|
+
? null
|
|
481
|
+
: String(requirement.lifecycle);
|
|
482
|
+
const subscriberThrottleMs = Number(requirement.subscriber_throttle_ms);
|
|
483
|
+
const maxSocketToSubscriberMs = Number(
|
|
484
|
+
requirement.max_socket_to_subscriber_ms,
|
|
485
|
+
);
|
|
486
|
+
const socket = records.find(
|
|
487
|
+
(record) =>
|
|
488
|
+
record.stage === "socket_received" &&
|
|
489
|
+
record.source === "fresh_socket" &&
|
|
490
|
+
record.stream === stream &&
|
|
491
|
+
(requiredLifecycle === null ||
|
|
492
|
+
record.lifecycle === requiredLifecycle) &&
|
|
493
|
+
record.delivery_id,
|
|
494
|
+
);
|
|
495
|
+
const delivery = socket
|
|
496
|
+
? records.find(
|
|
497
|
+
(record) =>
|
|
498
|
+
record.stage === "subscriber_delivery" &&
|
|
499
|
+
record.delivery_id === socket.delivery_id &&
|
|
500
|
+
record.stream === stream &&
|
|
501
|
+
record.lifecycle === socket.lifecycle &&
|
|
502
|
+
Number(record.throttle_ms ?? 0) === subscriberThrottleMs,
|
|
503
|
+
)
|
|
504
|
+
: null;
|
|
505
|
+
const socketAt = Number(socket?.monotonic_ms);
|
|
506
|
+
const subscriberAt = Number(delivery?.monotonic_ms);
|
|
507
|
+
const socketToSubscriberMs =
|
|
508
|
+
Number.isFinite(socketAt) && Number.isFinite(subscriberAt)
|
|
509
|
+
? roundMs(subscriberAt - socketAt)
|
|
510
|
+
: null;
|
|
511
|
+
return {
|
|
512
|
+
stream,
|
|
513
|
+
lifecycle: socket?.lifecycle ?? requiredLifecycle,
|
|
514
|
+
subscriberThrottleMs,
|
|
515
|
+
maxSocketToSubscriberMs,
|
|
516
|
+
deliveryId: socket?.delivery_id ?? null,
|
|
517
|
+
socketToSubscriberMs,
|
|
518
|
+
status:
|
|
519
|
+
socketToSubscriberMs !== null &&
|
|
520
|
+
socketToSubscriberMs <= maxSocketToSubscriberMs
|
|
521
|
+
? "pass"
|
|
522
|
+
: "fail",
|
|
523
|
+
};
|
|
205
524
|
});
|
|
206
525
|
}
|
|
207
526
|
|
|
208
527
|
function summarize(
|
|
209
528
|
records,
|
|
529
|
+
startupRecords,
|
|
210
530
|
sourcePath,
|
|
211
531
|
offset,
|
|
212
532
|
bytesScanned,
|
|
213
533
|
requiredFreshContentVariants = [],
|
|
534
|
+
firstFreshDeliveryRequirements = [],
|
|
214
535
|
) {
|
|
536
|
+
const measurements = visibleMeasurements(
|
|
537
|
+
records,
|
|
538
|
+
requiredFreshContentVariants,
|
|
539
|
+
);
|
|
215
540
|
const stages = {};
|
|
216
541
|
const lifecycles = {};
|
|
217
542
|
const streams = {};
|
|
@@ -225,6 +550,10 @@ function summarize(
|
|
|
225
550
|
const monotonicValues = records
|
|
226
551
|
.map((record) => Number(record.monotonic_ms))
|
|
227
552
|
.filter(Number.isFinite);
|
|
553
|
+
const startupStages = {};
|
|
554
|
+
for (const record of startupRecords) {
|
|
555
|
+
increment(startupStages, record.stage);
|
|
556
|
+
}
|
|
228
557
|
return {
|
|
229
558
|
schemaVersion: 1,
|
|
230
559
|
marker: MARKER,
|
|
@@ -232,84 +561,157 @@ function summarize(
|
|
|
232
561
|
offset,
|
|
233
562
|
bytesScanned,
|
|
234
563
|
recordCount: records.length,
|
|
564
|
+
startupMarker: STARTUP_MARKER,
|
|
565
|
+
startupRecordCount: startupRecords.length,
|
|
566
|
+
startupStages,
|
|
567
|
+
startupMeasurements: startupMeasurements(startupRecords),
|
|
235
568
|
stages,
|
|
236
569
|
lifecycles,
|
|
237
570
|
streams,
|
|
238
571
|
sources,
|
|
239
|
-
firstMonotonicMs:
|
|
240
|
-
|
|
241
|
-
|
|
572
|
+
firstMonotonicMs:
|
|
573
|
+
monotonicValues.length > 0 ? Math.min(...monotonicValues) : null,
|
|
574
|
+
lastMonotonicMs:
|
|
575
|
+
monotonicValues.length > 0 ? Math.max(...monotonicValues) : null,
|
|
576
|
+
visibleMeasurements: measurements,
|
|
577
|
+
visibleMeasurementCohorts: visibleMeasurementCohorts(measurements),
|
|
578
|
+
cacheTakeoverMeasurement: cacheTakeoverMeasurement(
|
|
579
|
+
records,
|
|
580
|
+
requiredFreshContentVariants,
|
|
581
|
+
),
|
|
242
582
|
socketPipelineMeasurements: socketPipelineMeasurements(records),
|
|
583
|
+
firstFreshDeliveryChecks: firstFreshDeliveryChecks(
|
|
584
|
+
records,
|
|
585
|
+
firstFreshDeliveryRequirements,
|
|
586
|
+
),
|
|
243
587
|
};
|
|
244
588
|
}
|
|
245
589
|
|
|
246
590
|
export async function captureHomepagePerformance(input) {
|
|
247
591
|
const node = input.node ?? {};
|
|
248
|
-
const phase = String(node.phase ??
|
|
249
|
-
if (phase !==
|
|
250
|
-
throw new Error(
|
|
592
|
+
const phase = String(node.phase ?? "").toLowerCase();
|
|
593
|
+
if (phase !== "start" && phase !== "end") {
|
|
594
|
+
throw new Error(
|
|
595
|
+
"metamask.perps.capture_performance requires phase=start or phase=end.",
|
|
596
|
+
);
|
|
251
597
|
}
|
|
252
598
|
|
|
253
599
|
const projectRoot = input.context?.projectRoot;
|
|
254
600
|
const artifactsDir = input.context?.artifactsDir;
|
|
255
601
|
if (!projectRoot || !artifactsDir) {
|
|
256
|
-
throw new Error(
|
|
602
|
+
throw new Error(
|
|
603
|
+
"metamask.perps.capture_performance requires projectRoot and artifactsDir.",
|
|
604
|
+
);
|
|
257
605
|
}
|
|
258
|
-
const sourcePath = String(
|
|
259
|
-
|
|
260
|
-
|
|
606
|
+
const sourcePath = String(
|
|
607
|
+
node.source_path ?? "temp/recipe/runtime/app-console.log",
|
|
608
|
+
);
|
|
609
|
+
const sourceFile = resolveWithin(projectRoot, sourcePath, "source_path");
|
|
610
|
+
const stateFile = resolveWithin(artifactsDir, STATE_FILE, "capture state");
|
|
261
611
|
await mkdir(artifactsDir, { recursive: true });
|
|
262
612
|
|
|
263
|
-
if (phase ===
|
|
613
|
+
if (phase === "start") {
|
|
264
614
|
const offset = await fileSize(sourceFile);
|
|
265
|
-
await writeFile(
|
|
615
|
+
await writeFile(
|
|
616
|
+
stateFile,
|
|
617
|
+
`${JSON.stringify({ sourcePath, offset }, null, 2)}\n`,
|
|
618
|
+
);
|
|
266
619
|
return { phase, sourcePath, offset };
|
|
267
620
|
}
|
|
268
621
|
|
|
269
622
|
let state;
|
|
270
623
|
try {
|
|
271
|
-
state = JSON.parse(await readFile(stateFile,
|
|
624
|
+
state = JSON.parse(await readFile(stateFile, "utf8"));
|
|
272
625
|
} catch (error) {
|
|
273
626
|
throw new Error(
|
|
274
|
-
|
|
627
|
+
"metamask.perps.capture_performance phase=end requires a successful phase=start in the same recipe run.",
|
|
275
628
|
{ cause: error },
|
|
276
629
|
);
|
|
277
630
|
}
|
|
278
|
-
if (
|
|
279
|
-
|
|
631
|
+
if (
|
|
632
|
+
state.sourcePath !== sourcePath ||
|
|
633
|
+
!Number.isInteger(state.offset) ||
|
|
634
|
+
state.offset < 0
|
|
635
|
+
) {
|
|
636
|
+
throw new Error(
|
|
637
|
+
"metamask.perps.capture_performance capture state is invalid or uses a different source_path.",
|
|
638
|
+
);
|
|
280
639
|
}
|
|
281
640
|
|
|
282
641
|
const content = await readFile(sourceFile);
|
|
283
642
|
const offset = state.offset <= content.length ? state.offset : 0;
|
|
284
|
-
const segment = content.subarray(offset).toString(
|
|
285
|
-
const lines = segment
|
|
286
|
-
|
|
643
|
+
const segment = content.subarray(offset).toString("utf8");
|
|
644
|
+
const lines = segment
|
|
645
|
+
.split(/\r?\n/u)
|
|
646
|
+
.filter((line) => line.includes(MARKER) || line.includes(STARTUP_MARKER));
|
|
647
|
+
const records = lines
|
|
648
|
+
.filter((line) => line.includes(MARKER))
|
|
649
|
+
.map((line) => parseRecord(line, MARKER))
|
|
650
|
+
.filter(Boolean);
|
|
651
|
+
const startupRecords = lines
|
|
652
|
+
.filter((line) => line.includes(STARTUP_MARKER))
|
|
653
|
+
.map((line) => parseRecord(line, STARTUP_MARKER))
|
|
654
|
+
.filter(Boolean);
|
|
287
655
|
const requireRecords = node.require_records !== false;
|
|
288
656
|
const requiredStages = Array.isArray(node.required_stages)
|
|
289
657
|
? node.required_stages.map(String)
|
|
290
658
|
: [];
|
|
291
|
-
const presentStages = new Set(
|
|
292
|
-
|
|
659
|
+
const presentStages = new Set(
|
|
660
|
+
records.map((record) => String(record.stage ?? "")),
|
|
661
|
+
);
|
|
662
|
+
const missingStages = requiredStages.filter(
|
|
663
|
+
(stage) => !presentStages.has(stage),
|
|
664
|
+
);
|
|
665
|
+
const requiredStartupStages = Array.isArray(node.required_startup_stages)
|
|
666
|
+
? node.required_startup_stages.map(String)
|
|
667
|
+
: [];
|
|
668
|
+
const presentStartupStages = new Set(
|
|
669
|
+
startupRecords.map((record) => String(record.stage ?? "")),
|
|
670
|
+
);
|
|
671
|
+
const missingStartupStages = requiredStartupStages.filter(
|
|
672
|
+
(stage) => !presentStartupStages.has(stage),
|
|
673
|
+
);
|
|
293
674
|
const requiredLifecycles = Array.isArray(node.required_lifecycles)
|
|
294
675
|
? node.required_lifecycles.map(String)
|
|
295
676
|
: [];
|
|
296
|
-
const presentLifecycles = new Set(
|
|
677
|
+
const presentLifecycles = new Set(
|
|
678
|
+
records.map((record) => String(record.lifecycle ?? "")),
|
|
679
|
+
);
|
|
297
680
|
const missingLifecycles = requiredLifecycles.filter(
|
|
298
681
|
(lifecycle) => !presentLifecycles.has(lifecycle),
|
|
299
682
|
);
|
|
300
683
|
const requiredSources = Array.isArray(node.required_sources)
|
|
301
684
|
? node.required_sources.map(String)
|
|
302
685
|
: [];
|
|
303
|
-
const presentSources = new Set(
|
|
304
|
-
|
|
305
|
-
|
|
686
|
+
const presentSources = new Set(
|
|
687
|
+
records.map((record) => String(record.source ?? "")),
|
|
688
|
+
);
|
|
689
|
+
const missingSources = requiredSources.filter(
|
|
690
|
+
(source) => !presentSources.has(source),
|
|
691
|
+
);
|
|
692
|
+
const requiredFreshContentVariants = Array.isArray(
|
|
693
|
+
node.required_fresh_content_variants,
|
|
694
|
+
)
|
|
306
695
|
? node.required_fresh_content_variants.map(String)
|
|
307
696
|
: [];
|
|
308
|
-
const measurements = visibleMeasurements(
|
|
697
|
+
const measurements = visibleMeasurements(
|
|
698
|
+
records,
|
|
699
|
+
requiredFreshContentVariants,
|
|
700
|
+
);
|
|
701
|
+
const requiredFreshLifecycles = Array.isArray(node.required_fresh_lifecycles)
|
|
702
|
+
? node.required_fresh_lifecycles.map(String)
|
|
703
|
+
: [];
|
|
704
|
+
const missingFreshLifecycles = requiredFreshLifecycles.filter(
|
|
705
|
+
(lifecycle) =>
|
|
706
|
+
!measurements.some(
|
|
707
|
+
(measurement) =>
|
|
708
|
+
measurement.lifecycle === lifecycle && measurement.dfdMs !== null,
|
|
709
|
+
),
|
|
710
|
+
);
|
|
309
711
|
const presentFreshContentVariants = new Set(
|
|
310
712
|
measurements
|
|
311
713
|
.filter((measurement) => measurement.dfdMs !== null)
|
|
312
|
-
.map((measurement) => String(measurement.freshContentVariant ??
|
|
714
|
+
.map((measurement) => String(measurement.freshContentVariant ?? "")),
|
|
313
715
|
);
|
|
314
716
|
const missingFreshContentVariants = requiredFreshContentVariants.filter(
|
|
315
717
|
(variant) =>
|
|
@@ -317,58 +719,124 @@ export async function captureHomepagePerformance(input) {
|
|
|
317
719
|
contentVariantSatisfies(actual, variant),
|
|
318
720
|
),
|
|
319
721
|
);
|
|
722
|
+
const firstFreshDeliveryRequirements = Array.isArray(
|
|
723
|
+
node.first_fresh_delivery_requirements,
|
|
724
|
+
)
|
|
725
|
+
? node.first_fresh_delivery_requirements
|
|
726
|
+
: [];
|
|
727
|
+
const deliveryChecks = firstFreshDeliveryChecks(
|
|
728
|
+
records,
|
|
729
|
+
firstFreshDeliveryRequirements,
|
|
730
|
+
);
|
|
731
|
+
const failedFirstFreshDeliveries = deliveryChecks.filter(
|
|
732
|
+
(check) => check.status === "fail",
|
|
733
|
+
);
|
|
734
|
+
const requireCacheBeforeFresh = node.require_cache_before_fresh === true;
|
|
735
|
+
const cacheTakeover = cacheTakeoverMeasurement(
|
|
736
|
+
records,
|
|
737
|
+
requiredFreshContentVariants,
|
|
738
|
+
);
|
|
320
739
|
|
|
321
|
-
const prefix = String(node.artifact_prefix ??
|
|
740
|
+
const prefix = String(node.artifact_prefix ?? "logs/homepage-performance");
|
|
322
741
|
const logPath = `${prefix}.log`;
|
|
323
742
|
const summaryPath = `${prefix}-summary.json`;
|
|
324
|
-
const logFile = resolveWithin(artifactsDir, logPath,
|
|
325
|
-
const summaryFile = resolveWithin(
|
|
743
|
+
const logFile = resolveWithin(artifactsDir, logPath, "artifact_prefix");
|
|
744
|
+
const summaryFile = resolveWithin(
|
|
745
|
+
artifactsDir,
|
|
746
|
+
summaryPath,
|
|
747
|
+
"artifact_prefix",
|
|
748
|
+
);
|
|
326
749
|
const validation = {
|
|
327
750
|
status:
|
|
328
751
|
(!requireRecords || records.length > 0) &&
|
|
329
752
|
missingStages.length === 0 &&
|
|
753
|
+
missingStartupStages.length === 0 &&
|
|
330
754
|
missingLifecycles.length === 0 &&
|
|
331
755
|
missingSources.length === 0 &&
|
|
332
|
-
|
|
333
|
-
|
|
334
|
-
|
|
756
|
+
missingFreshLifecycles.length === 0 &&
|
|
757
|
+
missingFreshContentVariants.length === 0 &&
|
|
758
|
+
failedFirstFreshDeliveries.length === 0 &&
|
|
759
|
+
(!requireCacheBeforeFresh || cacheTakeover.status === "pass")
|
|
760
|
+
? "pass"
|
|
761
|
+
: "fail",
|
|
335
762
|
requireRecords,
|
|
336
763
|
missingStages,
|
|
764
|
+
missingStartupStages,
|
|
337
765
|
missingLifecycles,
|
|
338
766
|
missingSources,
|
|
767
|
+
missingFreshLifecycles,
|
|
339
768
|
missingFreshContentVariants,
|
|
769
|
+
failedFirstFreshDeliveries,
|
|
770
|
+
cacheBeforeFresh: requireCacheBeforeFresh
|
|
771
|
+
? cacheTakeover.status
|
|
772
|
+
: "not_required",
|
|
340
773
|
};
|
|
341
774
|
const summary = {
|
|
342
775
|
...summarize(
|
|
343
776
|
records,
|
|
777
|
+
startupRecords,
|
|
344
778
|
sourcePath,
|
|
345
779
|
offset,
|
|
346
780
|
content.length - offset,
|
|
347
781
|
requiredFreshContentVariants,
|
|
782
|
+
firstFreshDeliveryRequirements,
|
|
348
783
|
),
|
|
349
784
|
validation,
|
|
350
785
|
};
|
|
351
|
-
const nodeId = String(input.context?.nodeId ??
|
|
786
|
+
const nodeId = String(input.context?.nodeId ?? "capture-performance");
|
|
352
787
|
await mkdir(path.dirname(logFile), { recursive: true });
|
|
353
|
-
await writeFile(logFile, lines.length > 0 ? `${lines.join(
|
|
788
|
+
await writeFile(logFile, lines.length > 0 ? `${lines.join("\n")}\n` : "");
|
|
354
789
|
await writeFile(summaryFile, `${JSON.stringify(summary, null, 2)}\n`);
|
|
355
790
|
await rm(stateFile, { force: true });
|
|
356
791
|
|
|
357
792
|
if (requireRecords && records.length === 0) {
|
|
358
|
-
throw new Error(
|
|
793
|
+
throw new Error(
|
|
794
|
+
`No ${MARKER} records were emitted after the capture started.`,
|
|
795
|
+
);
|
|
359
796
|
}
|
|
360
797
|
if (missingStages.length > 0) {
|
|
361
|
-
throw new Error(
|
|
798
|
+
throw new Error(
|
|
799
|
+
`Missing required ${MARKER} stages: ${missingStages.join(", ")}.`,
|
|
800
|
+
);
|
|
801
|
+
}
|
|
802
|
+
if (missingStartupStages.length > 0) {
|
|
803
|
+
throw new Error(
|
|
804
|
+
`Missing required ${STARTUP_MARKER} stages: ${missingStartupStages.join(", ")}.`,
|
|
805
|
+
);
|
|
362
806
|
}
|
|
363
807
|
if (missingLifecycles.length > 0) {
|
|
364
|
-
throw new Error(
|
|
808
|
+
throw new Error(
|
|
809
|
+
`Missing required ${MARKER} lifecycles: ${missingLifecycles.join(", ")}.`,
|
|
810
|
+
);
|
|
365
811
|
}
|
|
366
812
|
if (missingSources.length > 0) {
|
|
367
|
-
throw new Error(
|
|
813
|
+
throw new Error(
|
|
814
|
+
`Missing required ${MARKER} sources: ${missingSources.join(", ")}.`,
|
|
815
|
+
);
|
|
816
|
+
}
|
|
817
|
+
if (missingFreshLifecycles.length > 0) {
|
|
818
|
+
throw new Error(
|
|
819
|
+
`Missing required fresh ${MARKER} lifecycles: ${missingFreshLifecycles.join(", ")}.`,
|
|
820
|
+
);
|
|
368
821
|
}
|
|
369
822
|
if (missingFreshContentVariants.length > 0) {
|
|
370
823
|
throw new Error(
|
|
371
|
-
`Missing required fresh ${MARKER} content variants: ${missingFreshContentVariants.join(
|
|
824
|
+
`Missing required fresh ${MARKER} content variants: ${missingFreshContentVariants.join(", ")}.`,
|
|
825
|
+
);
|
|
826
|
+
}
|
|
827
|
+
if (failedFirstFreshDeliveries.length > 0) {
|
|
828
|
+
const failures = failedFirstFreshDeliveries.map(
|
|
829
|
+
(check) =>
|
|
830
|
+
`${check.stream}/${check.lifecycle ?? "any"} throttle=${check.subscriberThrottleMs}ms ` +
|
|
831
|
+
`measured=${check.socketToSubscriberMs ?? "missing"}ms max=${check.maxSocketToSubscriberMs}ms`,
|
|
832
|
+
);
|
|
833
|
+
throw new Error(
|
|
834
|
+
`First fresh ${MARKER} delivery requirements failed: ${failures.join("; ")}.`,
|
|
835
|
+
);
|
|
836
|
+
}
|
|
837
|
+
if (requireCacheBeforeFresh && cacheTakeover.status !== "pass") {
|
|
838
|
+
throw new Error(
|
|
839
|
+
`Cache-before-fresh ${MARKER} requirement failed: ${cacheTakeover.failureReason}.`,
|
|
372
840
|
);
|
|
373
841
|
}
|
|
374
842
|
|
|
@@ -376,8 +844,8 @@ export async function captureHomepagePerformance(input) {
|
|
|
376
844
|
phase,
|
|
377
845
|
...summary,
|
|
378
846
|
artifacts: [
|
|
379
|
-
{ path: logPath, type:
|
|
380
|
-
{ path: summaryPath, type:
|
|
847
|
+
{ path: logPath, type: "log", nodeId },
|
|
848
|
+
{ path: summaryPath, type: "report", nodeId },
|
|
381
849
|
],
|
|
382
850
|
};
|
|
383
851
|
}
|