@deeeed/metamask-harness 0.34.1 → 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 +13 -0
- package/adapters/mobile/bridge-runtime/cdp-bridge.cjs +222 -0
- package/adapters/mobile/bridge-runtime/console-forwarder.cjs +22 -1
- package/adapters/mobile/bridge-runtime/lib/ws-client.cjs +18 -0
- package/adapters/mobile/open-device.sh +6 -4
- package/dist/adapters/mobile/prepare.js +1 -4
- package/dist/adapters.js +134 -6
- package/dist/commands/launch/mobile.js +16 -2
- package/dist/live-adapter-contract.js +6 -6
- 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 +506 -120
- 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 +38 -5
- package/library/actions/mobile/wallet/ensure_unlocked.mjs +5 -1
- package/library/manifests/mobile.action-manifest.json +226 -512
- 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
|
@@ -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,74 +332,143 @@ 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
|
+
];
|
|
205
472
|
});
|
|
206
473
|
}
|
|
207
474
|
|
|
@@ -218,8 +485,8 @@ function firstFreshDeliveryChecks(records, requirements) {
|
|
|
218
485
|
);
|
|
219
486
|
const socket = records.find(
|
|
220
487
|
(record) =>
|
|
221
|
-
record.stage ===
|
|
222
|
-
record.source ===
|
|
488
|
+
record.stage === "socket_received" &&
|
|
489
|
+
record.source === "fresh_socket" &&
|
|
223
490
|
record.stream === stream &&
|
|
224
491
|
(requiredLifecycle === null ||
|
|
225
492
|
record.lifecycle === requiredLifecycle) &&
|
|
@@ -228,7 +495,7 @@ function firstFreshDeliveryChecks(records, requirements) {
|
|
|
228
495
|
const delivery = socket
|
|
229
496
|
? records.find(
|
|
230
497
|
(record) =>
|
|
231
|
-
record.stage ===
|
|
498
|
+
record.stage === "subscriber_delivery" &&
|
|
232
499
|
record.delivery_id === socket.delivery_id &&
|
|
233
500
|
record.stream === stream &&
|
|
234
501
|
record.lifecycle === socket.lifecycle &&
|
|
@@ -251,20 +518,25 @@ function firstFreshDeliveryChecks(records, requirements) {
|
|
|
251
518
|
status:
|
|
252
519
|
socketToSubscriberMs !== null &&
|
|
253
520
|
socketToSubscriberMs <= maxSocketToSubscriberMs
|
|
254
|
-
?
|
|
255
|
-
:
|
|
521
|
+
? "pass"
|
|
522
|
+
: "fail",
|
|
256
523
|
};
|
|
257
524
|
});
|
|
258
525
|
}
|
|
259
526
|
|
|
260
527
|
function summarize(
|
|
261
528
|
records,
|
|
529
|
+
startupRecords,
|
|
262
530
|
sourcePath,
|
|
263
531
|
offset,
|
|
264
532
|
bytesScanned,
|
|
265
533
|
requiredFreshContentVariants = [],
|
|
266
534
|
firstFreshDeliveryRequirements = [],
|
|
267
535
|
) {
|
|
536
|
+
const measurements = visibleMeasurements(
|
|
537
|
+
records,
|
|
538
|
+
requiredFreshContentVariants,
|
|
539
|
+
);
|
|
268
540
|
const stages = {};
|
|
269
541
|
const lifecycles = {};
|
|
270
542
|
const streams = {};
|
|
@@ -278,6 +550,10 @@ function summarize(
|
|
|
278
550
|
const monotonicValues = records
|
|
279
551
|
.map((record) => Number(record.monotonic_ms))
|
|
280
552
|
.filter(Number.isFinite);
|
|
553
|
+
const startupStages = {};
|
|
554
|
+
for (const record of startupRecords) {
|
|
555
|
+
increment(startupStages, record.stage);
|
|
556
|
+
}
|
|
281
557
|
return {
|
|
282
558
|
schemaVersion: 1,
|
|
283
559
|
marker: MARKER,
|
|
@@ -285,13 +561,24 @@ function summarize(
|
|
|
285
561
|
offset,
|
|
286
562
|
bytesScanned,
|
|
287
563
|
recordCount: records.length,
|
|
564
|
+
startupMarker: STARTUP_MARKER,
|
|
565
|
+
startupRecordCount: startupRecords.length,
|
|
566
|
+
startupStages,
|
|
567
|
+
startupMeasurements: startupMeasurements(startupRecords),
|
|
288
568
|
stages,
|
|
289
569
|
lifecycles,
|
|
290
570
|
streams,
|
|
291
571
|
sources,
|
|
292
|
-
firstMonotonicMs:
|
|
293
|
-
|
|
294
|
-
|
|
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
|
+
),
|
|
295
582
|
socketPipelineMeasurements: socketPipelineMeasurements(records),
|
|
296
583
|
firstFreshDeliveryChecks: firstFreshDeliveryChecks(
|
|
297
584
|
records,
|
|
@@ -302,71 +589,129 @@ function summarize(
|
|
|
302
589
|
|
|
303
590
|
export async function captureHomepagePerformance(input) {
|
|
304
591
|
const node = input.node ?? {};
|
|
305
|
-
const phase = String(node.phase ??
|
|
306
|
-
if (phase !==
|
|
307
|
-
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
|
+
);
|
|
308
597
|
}
|
|
309
598
|
|
|
310
599
|
const projectRoot = input.context?.projectRoot;
|
|
311
600
|
const artifactsDir = input.context?.artifactsDir;
|
|
312
601
|
if (!projectRoot || !artifactsDir) {
|
|
313
|
-
throw new Error(
|
|
602
|
+
throw new Error(
|
|
603
|
+
"metamask.perps.capture_performance requires projectRoot and artifactsDir.",
|
|
604
|
+
);
|
|
314
605
|
}
|
|
315
|
-
const sourcePath = String(
|
|
316
|
-
|
|
317
|
-
|
|
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");
|
|
318
611
|
await mkdir(artifactsDir, { recursive: true });
|
|
319
612
|
|
|
320
|
-
if (phase ===
|
|
613
|
+
if (phase === "start") {
|
|
321
614
|
const offset = await fileSize(sourceFile);
|
|
322
|
-
await writeFile(
|
|
615
|
+
await writeFile(
|
|
616
|
+
stateFile,
|
|
617
|
+
`${JSON.stringify({ sourcePath, offset }, null, 2)}\n`,
|
|
618
|
+
);
|
|
323
619
|
return { phase, sourcePath, offset };
|
|
324
620
|
}
|
|
325
621
|
|
|
326
622
|
let state;
|
|
327
623
|
try {
|
|
328
|
-
state = JSON.parse(await readFile(stateFile,
|
|
624
|
+
state = JSON.parse(await readFile(stateFile, "utf8"));
|
|
329
625
|
} catch (error) {
|
|
330
626
|
throw new Error(
|
|
331
|
-
|
|
627
|
+
"metamask.perps.capture_performance phase=end requires a successful phase=start in the same recipe run.",
|
|
332
628
|
{ cause: error },
|
|
333
629
|
);
|
|
334
630
|
}
|
|
335
|
-
if (
|
|
336
|
-
|
|
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
|
+
);
|
|
337
639
|
}
|
|
338
640
|
|
|
339
641
|
const content = await readFile(sourceFile);
|
|
340
642
|
const offset = state.offset <= content.length ? state.offset : 0;
|
|
341
|
-
const segment = content.subarray(offset).toString(
|
|
342
|
-
const lines = segment
|
|
343
|
-
|
|
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);
|
|
344
655
|
const requireRecords = node.require_records !== false;
|
|
345
656
|
const requiredStages = Array.isArray(node.required_stages)
|
|
346
657
|
? node.required_stages.map(String)
|
|
347
658
|
: [];
|
|
348
|
-
const presentStages = new Set(
|
|
349
|
-
|
|
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
|
+
);
|
|
350
674
|
const requiredLifecycles = Array.isArray(node.required_lifecycles)
|
|
351
675
|
? node.required_lifecycles.map(String)
|
|
352
676
|
: [];
|
|
353
|
-
const presentLifecycles = new Set(
|
|
677
|
+
const presentLifecycles = new Set(
|
|
678
|
+
records.map((record) => String(record.lifecycle ?? "")),
|
|
679
|
+
);
|
|
354
680
|
const missingLifecycles = requiredLifecycles.filter(
|
|
355
681
|
(lifecycle) => !presentLifecycles.has(lifecycle),
|
|
356
682
|
);
|
|
357
683
|
const requiredSources = Array.isArray(node.required_sources)
|
|
358
684
|
? node.required_sources.map(String)
|
|
359
685
|
: [];
|
|
360
|
-
const presentSources = new Set(
|
|
361
|
-
|
|
362
|
-
|
|
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
|
+
)
|
|
363
695
|
? node.required_fresh_content_variants.map(String)
|
|
364
696
|
: [];
|
|
365
|
-
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
|
+
);
|
|
366
711
|
const presentFreshContentVariants = new Set(
|
|
367
712
|
measurements
|
|
368
713
|
.filter((measurement) => measurement.dfdMs !== null)
|
|
369
|
-
.map((measurement) => String(measurement.freshContentVariant ??
|
|
714
|
+
.map((measurement) => String(measurement.freshContentVariant ?? "")),
|
|
370
715
|
);
|
|
371
716
|
const missingFreshContentVariants = requiredFreshContentVariants.filter(
|
|
372
717
|
(variant) =>
|
|
@@ -384,34 +729,52 @@ export async function captureHomepagePerformance(input) {
|
|
|
384
729
|
firstFreshDeliveryRequirements,
|
|
385
730
|
);
|
|
386
731
|
const failedFirstFreshDeliveries = deliveryChecks.filter(
|
|
387
|
-
(check) => check.status ===
|
|
732
|
+
(check) => check.status === "fail",
|
|
733
|
+
);
|
|
734
|
+
const requireCacheBeforeFresh = node.require_cache_before_fresh === true;
|
|
735
|
+
const cacheTakeover = cacheTakeoverMeasurement(
|
|
736
|
+
records,
|
|
737
|
+
requiredFreshContentVariants,
|
|
388
738
|
);
|
|
389
739
|
|
|
390
|
-
const prefix = String(node.artifact_prefix ??
|
|
740
|
+
const prefix = String(node.artifact_prefix ?? "logs/homepage-performance");
|
|
391
741
|
const logPath = `${prefix}.log`;
|
|
392
742
|
const summaryPath = `${prefix}-summary.json`;
|
|
393
|
-
const logFile = resolveWithin(artifactsDir, logPath,
|
|
394
|
-
const summaryFile = resolveWithin(
|
|
743
|
+
const logFile = resolveWithin(artifactsDir, logPath, "artifact_prefix");
|
|
744
|
+
const summaryFile = resolveWithin(
|
|
745
|
+
artifactsDir,
|
|
746
|
+
summaryPath,
|
|
747
|
+
"artifact_prefix",
|
|
748
|
+
);
|
|
395
749
|
const validation = {
|
|
396
750
|
status:
|
|
397
751
|
(!requireRecords || records.length > 0) &&
|
|
398
752
|
missingStages.length === 0 &&
|
|
753
|
+
missingStartupStages.length === 0 &&
|
|
399
754
|
missingLifecycles.length === 0 &&
|
|
400
755
|
missingSources.length === 0 &&
|
|
756
|
+
missingFreshLifecycles.length === 0 &&
|
|
401
757
|
missingFreshContentVariants.length === 0 &&
|
|
402
|
-
failedFirstFreshDeliveries.length === 0
|
|
403
|
-
|
|
404
|
-
|
|
758
|
+
failedFirstFreshDeliveries.length === 0 &&
|
|
759
|
+
(!requireCacheBeforeFresh || cacheTakeover.status === "pass")
|
|
760
|
+
? "pass"
|
|
761
|
+
: "fail",
|
|
405
762
|
requireRecords,
|
|
406
763
|
missingStages,
|
|
764
|
+
missingStartupStages,
|
|
407
765
|
missingLifecycles,
|
|
408
766
|
missingSources,
|
|
767
|
+
missingFreshLifecycles,
|
|
409
768
|
missingFreshContentVariants,
|
|
410
769
|
failedFirstFreshDeliveries,
|
|
770
|
+
cacheBeforeFresh: requireCacheBeforeFresh
|
|
771
|
+
? cacheTakeover.status
|
|
772
|
+
: "not_required",
|
|
411
773
|
};
|
|
412
774
|
const summary = {
|
|
413
775
|
...summarize(
|
|
414
776
|
records,
|
|
777
|
+
startupRecords,
|
|
415
778
|
sourcePath,
|
|
416
779
|
offset,
|
|
417
780
|
content.length - offset,
|
|
@@ -420,37 +783,60 @@ export async function captureHomepagePerformance(input) {
|
|
|
420
783
|
),
|
|
421
784
|
validation,
|
|
422
785
|
};
|
|
423
|
-
const nodeId = String(input.context?.nodeId ??
|
|
786
|
+
const nodeId = String(input.context?.nodeId ?? "capture-performance");
|
|
424
787
|
await mkdir(path.dirname(logFile), { recursive: true });
|
|
425
|
-
await writeFile(logFile, lines.length > 0 ? `${lines.join(
|
|
788
|
+
await writeFile(logFile, lines.length > 0 ? `${lines.join("\n")}\n` : "");
|
|
426
789
|
await writeFile(summaryFile, `${JSON.stringify(summary, null, 2)}\n`);
|
|
427
790
|
await rm(stateFile, { force: true });
|
|
428
791
|
|
|
429
792
|
if (requireRecords && records.length === 0) {
|
|
430
|
-
throw new Error(
|
|
793
|
+
throw new Error(
|
|
794
|
+
`No ${MARKER} records were emitted after the capture started.`,
|
|
795
|
+
);
|
|
431
796
|
}
|
|
432
797
|
if (missingStages.length > 0) {
|
|
433
|
-
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
|
+
);
|
|
434
806
|
}
|
|
435
807
|
if (missingLifecycles.length > 0) {
|
|
436
|
-
throw new Error(
|
|
808
|
+
throw new Error(
|
|
809
|
+
`Missing required ${MARKER} lifecycles: ${missingLifecycles.join(", ")}.`,
|
|
810
|
+
);
|
|
437
811
|
}
|
|
438
812
|
if (missingSources.length > 0) {
|
|
439
|
-
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
|
+
);
|
|
440
821
|
}
|
|
441
822
|
if (missingFreshContentVariants.length > 0) {
|
|
442
823
|
throw new Error(
|
|
443
|
-
`Missing required fresh ${MARKER} content variants: ${missingFreshContentVariants.join(
|
|
824
|
+
`Missing required fresh ${MARKER} content variants: ${missingFreshContentVariants.join(", ")}.`,
|
|
444
825
|
);
|
|
445
826
|
}
|
|
446
827
|
if (failedFirstFreshDeliveries.length > 0) {
|
|
447
828
|
const failures = failedFirstFreshDeliveries.map(
|
|
448
829
|
(check) =>
|
|
449
|
-
`${check.stream}/${check.lifecycle ??
|
|
450
|
-
`measured=${check.socketToSubscriberMs ??
|
|
830
|
+
`${check.stream}/${check.lifecycle ?? "any"} throttle=${check.subscriberThrottleMs}ms ` +
|
|
831
|
+
`measured=${check.socketToSubscriberMs ?? "missing"}ms max=${check.maxSocketToSubscriberMs}ms`,
|
|
451
832
|
);
|
|
452
833
|
throw new Error(
|
|
453
|
-
`First fresh ${MARKER} delivery requirements failed: ${failures.join(
|
|
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}.`,
|
|
454
840
|
);
|
|
455
841
|
}
|
|
456
842
|
|
|
@@ -458,8 +844,8 @@ export async function captureHomepagePerformance(input) {
|
|
|
458
844
|
phase,
|
|
459
845
|
...summary,
|
|
460
846
|
artifacts: [
|
|
461
|
-
{ path: logPath, type:
|
|
462
|
-
{ path: summaryPath, type:
|
|
847
|
+
{ path: logPath, type: "log", nodeId },
|
|
848
|
+
{ path: summaryPath, type: "report", nodeId },
|
|
463
849
|
],
|
|
464
850
|
};
|
|
465
851
|
}
|