@deeeed/metamask-harness 0.43.0 → 0.44.1
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 +32 -0
- package/adapters/mobile/bridge-runtime/cdp-bridge.cjs +312 -35
- package/adapters/mobile/bridge-runtime/console-forwarder.cjs +20 -8
- package/adapters/mobile/bridge-runtime/lib/cdp-broker.cjs +102 -0
- package/adapters/mobile/bridge-runtime/lib/target-discovery.cjs +4 -2
- package/adapters/mobile/reload-app.mjs +99 -1
- package/dist/adapters/mobile/prepare.js +12 -0
- package/dist/adapters.js +22 -3
- package/dist/commands/call.js +40 -2
- package/dist/commands/run-engine.js +247 -139
- package/dist/commands/run-report.js +68 -0
- package/dist/commands/run.js +47 -2
- package/dist/execution-provenance.js +342 -0
- package/dist/run-diagnostics.js +36 -11
- package/dist/runner.js +44 -13
- package/library/actions/mobile/perps/performance-capture.mjs +577 -189
- package/library/actions/mobile/perps/perps.mjs +122 -0
- package/library/actions/mobile/platform/bridge.mjs +40 -8
- package/library/actions/mobile/platform/native-session.mjs +1 -1
- package/library/actions/mobile/wallet/ensure_unlocked.mjs +22 -4
- package/library/actions/mobile/wallet/lock.mjs +1 -4
- package/library/actions/mobile/wallet/select_account.mjs +129 -17
- package/library/manifests/mobile.action-manifest.json +28 -3
- package/library/recipes/mobile/perps/performance.recipe.json +73 -47
- package/package.json +1 -1
|
@@ -1,4 +1,5 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import { createHash } from "node:crypto";
|
|
2
|
+
import { mkdir, open, readFile, rm, stat, writeFile } from "node:fs/promises";
|
|
2
3
|
import path from "node:path";
|
|
3
4
|
|
|
4
5
|
const MARKER = "[PerpsPerf]";
|
|
@@ -7,12 +8,39 @@ const DETAILED_MARKERS = [MARKER, LEGACY_MARKER];
|
|
|
7
8
|
const STARTUP_MARKER = "[StartupPerf]";
|
|
8
9
|
const LOAD_PROOF_MARKER = "[PerpsLoadProof]";
|
|
9
10
|
const STATE_FILE = ".homepage-performance-capture.json";
|
|
11
|
+
const CAPTURE_BOUNDARY_BYTES = 4096;
|
|
12
|
+
const BOOTSTRAP_LOOKBACK_BYTES = 512 * 1024;
|
|
10
13
|
const FRESH_SOURCES = new Set([
|
|
11
14
|
"fresh_socket",
|
|
12
15
|
"provider_snapshot",
|
|
13
16
|
"terminal_global_snapshot_v2",
|
|
17
|
+
"terminal_v2",
|
|
14
18
|
"provider",
|
|
15
19
|
]);
|
|
20
|
+
const SURFACE_STAGE_ORDER = [
|
|
21
|
+
"surface_demand",
|
|
22
|
+
"surface_initial_ui_recorded",
|
|
23
|
+
"surface_resolved_recorded",
|
|
24
|
+
"surface_live_recorded",
|
|
25
|
+
];
|
|
26
|
+
const SURFACE_STAGES = new Set(SURFACE_STAGE_ORDER);
|
|
27
|
+
const PERFORMANCE_IDENTITY_FIELDS = [
|
|
28
|
+
"demand_id",
|
|
29
|
+
"perps_session_id",
|
|
30
|
+
"lifecycle",
|
|
31
|
+
"account_generation",
|
|
32
|
+
"context_generation",
|
|
33
|
+
];
|
|
34
|
+
const LOAD_PROOF_BASE_IDENTITY_FIELDS = PERFORMANCE_IDENTITY_FIELDS.slice(1);
|
|
35
|
+
const LOAD_PROOF_IDENTITY_FIELDS = [
|
|
36
|
+
...LOAD_PROOF_BASE_IDENTITY_FIELDS,
|
|
37
|
+
"connection_generation",
|
|
38
|
+
];
|
|
39
|
+
const GENERATION_IDENTITY_FIELDS = new Set([
|
|
40
|
+
"account_generation",
|
|
41
|
+
"context_generation",
|
|
42
|
+
"connection_generation",
|
|
43
|
+
]);
|
|
16
44
|
|
|
17
45
|
function resolveWithin(root, relativePath, label) {
|
|
18
46
|
const absoluteRoot = path.resolve(root);
|
|
@@ -24,15 +52,83 @@ function resolveWithin(root, relativePath, label) {
|
|
|
24
52
|
return absolutePath;
|
|
25
53
|
}
|
|
26
54
|
|
|
27
|
-
async function
|
|
55
|
+
async function fileState(file) {
|
|
28
56
|
try {
|
|
29
|
-
|
|
57
|
+
const value = await stat(file);
|
|
58
|
+
return { size: value.size, device: value.dev, inode: value.ino };
|
|
30
59
|
} catch (error) {
|
|
31
|
-
if (error?.code === "ENOENT") return
|
|
60
|
+
if (error?.code === "ENOENT") return null;
|
|
32
61
|
throw error;
|
|
33
62
|
}
|
|
34
63
|
}
|
|
35
64
|
|
|
65
|
+
async function readFileRange(file, start, length) {
|
|
66
|
+
if (length === 0) return Buffer.alloc(0);
|
|
67
|
+
const handle = await open(file, "r");
|
|
68
|
+
try {
|
|
69
|
+
const buffer = Buffer.alloc(length);
|
|
70
|
+
const { bytesRead } = await handle.read(buffer, 0, length, start);
|
|
71
|
+
return buffer.subarray(0, bytesRead);
|
|
72
|
+
} finally {
|
|
73
|
+
await handle.close();
|
|
74
|
+
}
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
function captureBoundary(content, offset) {
|
|
78
|
+
const start = Math.max(0, offset - CAPTURE_BOUNDARY_BYTES);
|
|
79
|
+
const bytes = content.subarray(start, offset);
|
|
80
|
+
return {
|
|
81
|
+
start,
|
|
82
|
+
length: bytes.length,
|
|
83
|
+
sha256: createHash("sha256").update(bytes).digest("hex"),
|
|
84
|
+
};
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
function captureBoundaryBytes(start, bytes) {
|
|
88
|
+
return {
|
|
89
|
+
start,
|
|
90
|
+
length: bytes.length,
|
|
91
|
+
sha256: createHash("sha256").update(bytes).digest("hex"),
|
|
92
|
+
};
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
async function readCaptureSource(sourceFile, state) {
|
|
96
|
+
const current = await fileState(sourceFile);
|
|
97
|
+
if (!current) {
|
|
98
|
+
if (state.offset === 0 && state.sourceIdentity === null) {
|
|
99
|
+
return Buffer.alloc(0);
|
|
100
|
+
}
|
|
101
|
+
throw new Error("Perps performance capture source disappeared after phase=start.");
|
|
102
|
+
}
|
|
103
|
+
if (state.sourceIdentity === null) {
|
|
104
|
+
state.sourceIdentity = current;
|
|
105
|
+
}
|
|
106
|
+
if (
|
|
107
|
+
state.sourceIdentity &&
|
|
108
|
+
(current.device !== state.sourceIdentity.device ||
|
|
109
|
+
current.inode !== state.sourceIdentity.inode)
|
|
110
|
+
) {
|
|
111
|
+
throw new Error("Perps performance capture source was replaced after phase=start.");
|
|
112
|
+
}
|
|
113
|
+
if (current.size < state.offset) {
|
|
114
|
+
throw new Error("Perps performance capture source shrank after phase=start.");
|
|
115
|
+
}
|
|
116
|
+
const content = await readFile(sourceFile);
|
|
117
|
+
const boundary = captureBoundary(content, state.offset);
|
|
118
|
+
if (state.sourceBoundary === null) {
|
|
119
|
+
state.sourceBoundary = boundary;
|
|
120
|
+
} else if (
|
|
121
|
+
boundary.start !== state.sourceBoundary.start ||
|
|
122
|
+
boundary.length !== state.sourceBoundary.length ||
|
|
123
|
+
boundary.sha256 !== state.sourceBoundary.sha256
|
|
124
|
+
) {
|
|
125
|
+
throw new Error(
|
|
126
|
+
"Perps performance capture source was truncated or rewritten after phase=start.",
|
|
127
|
+
);
|
|
128
|
+
}
|
|
129
|
+
return content;
|
|
130
|
+
}
|
|
131
|
+
|
|
36
132
|
function parseRecord(line, marker) {
|
|
37
133
|
const markerIndex = line.indexOf(marker);
|
|
38
134
|
if (markerIndex < 0) return null;
|
|
@@ -56,83 +152,279 @@ function parseDetailedRecord(line) {
|
|
|
56
152
|
return marker ? parseRecord(line, marker) : null;
|
|
57
153
|
}
|
|
58
154
|
|
|
59
|
-
function
|
|
60
|
-
|
|
61
|
-
.
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
.
|
|
65
|
-
|
|
155
|
+
function identityField(record, field) {
|
|
156
|
+
if (field === "perps_session_id") {
|
|
157
|
+
if (Object.hasOwn(record, "perps_session_id")) {
|
|
158
|
+
return { present: true, value: record.perps_session_id };
|
|
159
|
+
}
|
|
160
|
+
if (Object.hasOwn(record, "session_id")) {
|
|
161
|
+
return { present: true, value: record.session_id };
|
|
162
|
+
}
|
|
163
|
+
return { present: false, value: undefined };
|
|
164
|
+
}
|
|
165
|
+
return Object.hasOwn(record, field)
|
|
166
|
+
? { present: true, value: record[field] }
|
|
167
|
+
: { present: false, value: undefined };
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
function hasValidIdentityField(record, field) {
|
|
171
|
+
const identity = identityField(record, field);
|
|
172
|
+
if (
|
|
173
|
+
!identity.present ||
|
|
174
|
+
identity.value === null ||
|
|
175
|
+
identity.value === undefined
|
|
176
|
+
) {
|
|
177
|
+
return false;
|
|
178
|
+
}
|
|
179
|
+
if (GENERATION_IDENTITY_FIELDS.has(field)) {
|
|
180
|
+
return (
|
|
181
|
+
typeof identity.value === "number" &&
|
|
182
|
+
Number.isInteger(identity.value) &&
|
|
183
|
+
identity.value >= 0
|
|
184
|
+
);
|
|
185
|
+
}
|
|
186
|
+
return String(identity.value).trim().length > 0;
|
|
187
|
+
}
|
|
188
|
+
|
|
189
|
+
function hasCompleteIdentity(record, fields = PERFORMANCE_IDENTITY_FIELDS) {
|
|
190
|
+
return fields.every((field) => hasValidIdentityField(record, field));
|
|
66
191
|
}
|
|
67
192
|
|
|
68
|
-
function
|
|
69
|
-
const match = line.match(/\b(\d{2}):(\d{2}):(\d{2})\.(\d{3})\b/u);
|
|
70
|
-
if (!match) return null;
|
|
193
|
+
function sameIdentity(left, right, fields = PERFORMANCE_IDENTITY_FIELDS) {
|
|
71
194
|
return (
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
195
|
+
hasCompleteIdentity(left, fields) &&
|
|
196
|
+
hasCompleteIdentity(right, fields) &&
|
|
197
|
+
fields.every((field) => {
|
|
198
|
+
const leftField = identityField(left, field);
|
|
199
|
+
const rightField = identityField(right, field);
|
|
200
|
+
return String(leftField.value) === String(rightField.value);
|
|
201
|
+
})
|
|
76
202
|
);
|
|
77
203
|
}
|
|
78
204
|
|
|
79
|
-
function
|
|
80
|
-
|
|
81
|
-
|
|
205
|
+
function identityKey(record, fields = PERFORMANCE_IDENTITY_FIELDS) {
|
|
206
|
+
return JSON.stringify(
|
|
207
|
+
fields.map((field) => {
|
|
208
|
+
const value = identityField(record, field);
|
|
209
|
+
return value.present ? [field, String(value.value)] : [field, "<missing>"];
|
|
210
|
+
}),
|
|
211
|
+
);
|
|
212
|
+
}
|
|
213
|
+
|
|
214
|
+
function performanceRecordTime(record) {
|
|
215
|
+
const value = Number(
|
|
216
|
+
record.frame_checkpoint_monotonic_ms ?? record.monotonic_ms,
|
|
217
|
+
);
|
|
218
|
+
return Number.isFinite(value) ? value : null;
|
|
219
|
+
}
|
|
220
|
+
|
|
221
|
+
function isAccountContentVariant(value) {
|
|
222
|
+
return ["positions", "orders", "positions_and_orders"].includes(
|
|
223
|
+
String(value ?? ""),
|
|
224
|
+
);
|
|
225
|
+
}
|
|
226
|
+
|
|
227
|
+
function orderedSurfaceSequence(records, demand, requiredStages) {
|
|
228
|
+
const stages = SURFACE_STAGE_ORDER.filter((stage) =>
|
|
229
|
+
requiredStages.includes(stage),
|
|
230
|
+
);
|
|
231
|
+
if (stages.length === 0) return [demand];
|
|
232
|
+
const demandAt = performanceRecordTime(demand);
|
|
233
|
+
if (demandAt === null) return null;
|
|
234
|
+
const sequence = [];
|
|
235
|
+
let previousAt = demandAt;
|
|
236
|
+
for (const stage of stages) {
|
|
237
|
+
if (stage === "surface_demand") {
|
|
238
|
+
sequence.push(demand);
|
|
239
|
+
continue;
|
|
240
|
+
}
|
|
241
|
+
const matches = records
|
|
242
|
+
.filter(
|
|
243
|
+
(record) =>
|
|
244
|
+
record.stage === stage &&
|
|
245
|
+
sameIdentity(record, demand),
|
|
246
|
+
)
|
|
247
|
+
.sort(
|
|
248
|
+
(left, right) =>
|
|
249
|
+
performanceRecordTime(left) - performanceRecordTime(right),
|
|
250
|
+
);
|
|
251
|
+
if (matches.length !== 1) return null;
|
|
252
|
+
const match = matches[0];
|
|
253
|
+
const matchAt = performanceRecordTime(match);
|
|
82
254
|
if (
|
|
83
|
-
|
|
84
|
-
|
|
255
|
+
matchAt === null ||
|
|
256
|
+
matchAt < previousAt ||
|
|
257
|
+
!Number.isFinite(Number(match.duration_ms)) ||
|
|
258
|
+
Number(match.duration_ms) < 0 ||
|
|
259
|
+
(stage === "surface_live_recorded" &&
|
|
260
|
+
(match.fresh_for_lifecycle !== true || !frameIsFresh(match)))
|
|
85
261
|
) {
|
|
86
|
-
|
|
262
|
+
return null;
|
|
87
263
|
}
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
const
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
264
|
+
sequence.push(match);
|
|
265
|
+
previousAt = matchAt;
|
|
266
|
+
}
|
|
267
|
+
return sequence;
|
|
268
|
+
}
|
|
269
|
+
|
|
270
|
+
function coherentPerformanceProof(
|
|
271
|
+
records,
|
|
272
|
+
loadProofRecords,
|
|
273
|
+
requiredStages,
|
|
274
|
+
requiredLiveStreams,
|
|
275
|
+
expectedLifecycle = null,
|
|
276
|
+
expectedContentVariant = null,
|
|
277
|
+
) {
|
|
278
|
+
const requiredSurfaceStages = requiredStages.filter((stage) =>
|
|
279
|
+
SURFACE_STAGES.has(stage),
|
|
280
|
+
);
|
|
281
|
+
const required =
|
|
282
|
+
requiredSurfaceStages.length > 0 || requiredLiveStreams.length > 0;
|
|
283
|
+
if (!required) {
|
|
284
|
+
return {
|
|
285
|
+
status: "not_required",
|
|
286
|
+
demand: null,
|
|
287
|
+
identity: null,
|
|
288
|
+
missingLiveStreams: [],
|
|
289
|
+
sequence: [],
|
|
290
|
+
};
|
|
291
|
+
}
|
|
292
|
+
const demands = records.filter((record) => record.stage === "surface_demand");
|
|
293
|
+
let best = null;
|
|
294
|
+
for (const demand of demands) {
|
|
295
|
+
if (!hasCompleteIdentity(demand)) continue;
|
|
296
|
+
if (
|
|
297
|
+
records.filter(
|
|
298
|
+
(record) =>
|
|
299
|
+
record.stage === "surface_demand" && sameIdentity(record, demand),
|
|
300
|
+
).length !== 1
|
|
301
|
+
) {
|
|
302
|
+
continue;
|
|
114
303
|
}
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
304
|
+
if (
|
|
305
|
+
expectedLifecycle !== null &&
|
|
306
|
+
String(demand.lifecycle ?? "") !== expectedLifecycle
|
|
307
|
+
) {
|
|
308
|
+
continue;
|
|
309
|
+
}
|
|
310
|
+
const sequence = orderedSurfaceSequence(
|
|
311
|
+
records,
|
|
312
|
+
demand,
|
|
313
|
+
requiredSurfaceStages,
|
|
314
|
+
);
|
|
315
|
+
if (!sequence) continue;
|
|
316
|
+
if (
|
|
317
|
+
expectedContentVariant !== null &&
|
|
318
|
+
sequence
|
|
319
|
+
.filter((record) => record.stage !== "surface_demand")
|
|
320
|
+
.some(
|
|
321
|
+
(record) =>
|
|
322
|
+
!contentVariantSatisfies(
|
|
323
|
+
String(record.content_variant ?? ""),
|
|
324
|
+
expectedContentVariant,
|
|
325
|
+
),
|
|
326
|
+
)
|
|
327
|
+
) {
|
|
328
|
+
continue;
|
|
329
|
+
}
|
|
330
|
+
const liveRecords = loadProofRecords.filter(
|
|
331
|
+
(record) =>
|
|
332
|
+
record.stage === "values_ready" &&
|
|
333
|
+
record.source === "fresh_socket" &&
|
|
334
|
+
hasCompleteIdentity(record, LOAD_PROOF_IDENTITY_FIELDS) &&
|
|
335
|
+
sameIdentity(record, demand, LOAD_PROOF_BASE_IDENTITY_FIELDS),
|
|
336
|
+
);
|
|
337
|
+
const connectionGenerations = new Set(
|
|
338
|
+
liveRecords.map((record) => record.connection_generation),
|
|
339
|
+
);
|
|
340
|
+
const liveRecordConnectionGeneration =
|
|
341
|
+
connectionGenerations.size === 1
|
|
342
|
+
? [...connectionGenerations][0]
|
|
343
|
+
: null;
|
|
344
|
+
const liveSurface = sequence.find(
|
|
345
|
+
(record) => record.stage === "surface_live_recorded",
|
|
346
|
+
);
|
|
347
|
+
const accountLiveSurface =
|
|
348
|
+
liveSurface && isAccountContentVariant(liveSurface.content_variant)
|
|
349
|
+
? liveSurface
|
|
350
|
+
: null;
|
|
351
|
+
const surfaceConnectionGeneration =
|
|
352
|
+
accountLiveSurface &&
|
|
353
|
+
hasValidIdentityField(accountLiveSurface, "connection_generation")
|
|
354
|
+
? accountLiveSurface.connection_generation
|
|
355
|
+
: null;
|
|
356
|
+
const identityCoherent =
|
|
357
|
+
connectionGenerations.size <= 1 &&
|
|
358
|
+
(!accountLiveSurface ||
|
|
359
|
+
(surfaceConnectionGeneration !== null &&
|
|
360
|
+
(liveRecordConnectionGeneration === null ||
|
|
361
|
+
surfaceConnectionGeneration === liveRecordConnectionGeneration)));
|
|
362
|
+
const coherentConnectionGeneration =
|
|
363
|
+
liveRecordConnectionGeneration ?? surfaceConnectionGeneration;
|
|
364
|
+
const liveStreams = new Set(
|
|
365
|
+
liveRecordConnectionGeneration === null
|
|
366
|
+
? []
|
|
367
|
+
: liveRecords.map((record) => String(record.stream ?? "")),
|
|
368
|
+
);
|
|
369
|
+
const missingLiveStreams = requiredLiveStreams.filter(
|
|
370
|
+
(stream) => !liveStreams.has(stream),
|
|
371
|
+
);
|
|
372
|
+
const candidate = {
|
|
373
|
+
demand,
|
|
374
|
+
sequence,
|
|
375
|
+
missingLiveStreams,
|
|
376
|
+
identityCoherent,
|
|
377
|
+
connectionGeneration: coherentConnectionGeneration,
|
|
378
|
+
};
|
|
379
|
+
if (
|
|
380
|
+
!best ||
|
|
381
|
+
Number(identityCoherent) > Number(best.identityCoherent) ||
|
|
382
|
+
(identityCoherent === best.identityCoherent &&
|
|
383
|
+
missingLiveStreams.length < best.missingLiveStreams.length)
|
|
384
|
+
) {
|
|
385
|
+
best = candidate;
|
|
386
|
+
}
|
|
387
|
+
if (identityCoherent && missingLiveStreams.length === 0) break;
|
|
128
388
|
}
|
|
129
|
-
return
|
|
389
|
+
return {
|
|
390
|
+
status:
|
|
391
|
+
best?.identityCoherent && best.missingLiveStreams.length === 0
|
|
392
|
+
? "pass"
|
|
393
|
+
: "fail",
|
|
394
|
+
demand: best?.demand ?? null,
|
|
395
|
+
identity: best
|
|
396
|
+
? {
|
|
397
|
+
...Object.fromEntries(
|
|
398
|
+
PERFORMANCE_IDENTITY_FIELDS.map((field) => {
|
|
399
|
+
const value = identityField(best.demand, field);
|
|
400
|
+
return [field, value.present ? value.value : null];
|
|
401
|
+
}),
|
|
402
|
+
),
|
|
403
|
+
connection_generation: best.connectionGeneration,
|
|
404
|
+
}
|
|
405
|
+
: null,
|
|
406
|
+
missingLiveStreams: best?.missingLiveStreams ?? requiredLiveStreams,
|
|
407
|
+
sequence: best?.sequence.map((record) => record.stage) ?? [],
|
|
408
|
+
};
|
|
409
|
+
}
|
|
410
|
+
|
|
411
|
+
function latestPerpsBootstrapStartRecord(content) {
|
|
412
|
+
return content
|
|
413
|
+
.split(/\r?\n/u)
|
|
414
|
+
.filter((line) => line.includes(LOAD_PROOF_MARKER))
|
|
415
|
+
.map((line) => parseRecord(line, LOAD_PROOF_MARKER))
|
|
416
|
+
.filter((record) => record?.stage === "perps_bootstrap_start")
|
|
417
|
+
.at(-1) ?? null;
|
|
130
418
|
}
|
|
131
419
|
|
|
132
420
|
function mergeLoadProofRecords(records) {
|
|
133
421
|
const byStageStreamSource = new Map();
|
|
134
422
|
for (const record of records) {
|
|
135
|
-
const
|
|
423
|
+
const identityFields =
|
|
424
|
+
record.stage === "values_ready"
|
|
425
|
+
? LOAD_PROOF_IDENTITY_FIELDS
|
|
426
|
+
: LOAD_PROOF_BASE_IDENTITY_FIELDS;
|
|
427
|
+
const key = `${record.stage ?? "unknown"}:${record.stream ?? ""}:${record.source ?? ""}:${identityKey(record, identityFields)}`;
|
|
136
428
|
const current = byStageStreamSource.get(key);
|
|
137
429
|
if (record.stage === "perps_bootstrap_start") {
|
|
138
430
|
if (
|
|
@@ -143,14 +435,9 @@ function mergeLoadProofRecords(records) {
|
|
|
143
435
|
}
|
|
144
436
|
continue;
|
|
145
437
|
}
|
|
146
|
-
const recordIsFallback =
|
|
147
|
-
record.evidence === "existing_sentry_ws_marker";
|
|
148
|
-
const currentIsFallback =
|
|
149
|
-
current?.evidence === "existing_sentry_ws_marker";
|
|
150
438
|
const recordElapsedMs = Number(record.elapsed_ms);
|
|
151
439
|
const currentElapsedMs = Number(current?.elapsed_ms);
|
|
152
|
-
const
|
|
153
|
-
recordIsFallback === currentIsFallback &&
|
|
440
|
+
const earlier =
|
|
154
441
|
record.elapsed_ms !== null &&
|
|
155
442
|
record.elapsed_ms !== undefined &&
|
|
156
443
|
Number.isFinite(recordElapsedMs) &&
|
|
@@ -158,18 +445,14 @@ function mergeLoadProofRecords(records) {
|
|
|
158
445
|
current?.elapsed_ms !== undefined &&
|
|
159
446
|
Number.isFinite(currentElapsedMs) &&
|
|
160
447
|
recordElapsedMs < currentElapsedMs;
|
|
161
|
-
if (
|
|
162
|
-
!current ||
|
|
163
|
-
(currentIsFallback && !recordIsFallback) ||
|
|
164
|
-
sameEvidenceClassEarlier
|
|
165
|
-
) {
|
|
448
|
+
if (!current || earlier) {
|
|
166
449
|
byStageStreamSource.set(key, record);
|
|
167
450
|
}
|
|
168
451
|
}
|
|
169
452
|
return [...byStageStreamSource.values()];
|
|
170
453
|
}
|
|
171
454
|
|
|
172
|
-
async function waitForPerformanceRecord({ node, sourceFile,
|
|
455
|
+
async function waitForPerformanceRecord({ node, sourceFile, state }) {
|
|
173
456
|
const stage = node.wait_for_stage;
|
|
174
457
|
const requiredLiveStreams = Array.isArray(node.required_live_streams)
|
|
175
458
|
? node.required_live_streams.map(String)
|
|
@@ -187,6 +470,9 @@ async function waitForPerformanceRecord({ node, sourceFile, offset }) {
|
|
|
187
470
|
...(node.wait_for_lifecycle !== undefined
|
|
188
471
|
? { lifecycle: String(node.wait_for_lifecycle) }
|
|
189
472
|
: {}),
|
|
473
|
+
...(node.wait_for_content_variant !== undefined
|
|
474
|
+
? { content_variant: String(node.wait_for_content_variant) }
|
|
475
|
+
: {}),
|
|
190
476
|
};
|
|
191
477
|
const timeoutMs = Math.max(1, Number(node.wait_timeout_ms ?? 60_000));
|
|
192
478
|
const pollIntervalMs = Math.max(
|
|
@@ -196,8 +482,8 @@ async function waitForPerformanceRecord({ node, sourceFile, offset }) {
|
|
|
196
482
|
const deadline = Date.now() + timeoutMs;
|
|
197
483
|
|
|
198
484
|
while (true) {
|
|
199
|
-
const content = await
|
|
200
|
-
const start =
|
|
485
|
+
const content = await readCaptureSource(sourceFile, state);
|
|
486
|
+
const start = state.offset;
|
|
201
487
|
const segmentLines = content
|
|
202
488
|
.subarray(start)
|
|
203
489
|
.toString("utf8")
|
|
@@ -206,33 +492,42 @@ async function waitForPerformanceRecord({ node, sourceFile, offset }) {
|
|
|
206
492
|
.filter(detailedMarker)
|
|
207
493
|
.map(parseDetailedRecord)
|
|
208
494
|
.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
495
|
const markerLoadProofRecords = segmentLines
|
|
217
496
|
.filter((line) => line.includes(LOAD_PROOF_MARKER))
|
|
218
497
|
.map((line) => parseRecord(line, LOAD_PROOF_MARKER))
|
|
219
498
|
.filter(Boolean);
|
|
220
|
-
const
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
]
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
const liveStreamsMatched = requiredLiveStreams.every((stream) =>
|
|
233
|
-
liveStreams.has(stream),
|
|
499
|
+
const loadProofRecords = mergeLoadProofRecords(markerLoadProofRecords);
|
|
500
|
+
const coherentProof = coherentPerformanceProof(
|
|
501
|
+
performanceRecords,
|
|
502
|
+
loadProofRecords,
|
|
503
|
+
expected && SURFACE_STAGES.has(expected.stage) ? [expected.stage] : [],
|
|
504
|
+
requiredLiveStreams,
|
|
505
|
+
node.wait_for_lifecycle === undefined
|
|
506
|
+
? null
|
|
507
|
+
: String(node.wait_for_lifecycle),
|
|
508
|
+
node.wait_for_content_variant === undefined
|
|
509
|
+
? null
|
|
510
|
+
: String(node.wait_for_content_variant),
|
|
234
511
|
);
|
|
235
|
-
|
|
512
|
+
const stageMatched =
|
|
513
|
+
expected === null ||
|
|
514
|
+
performanceRecords.some(
|
|
515
|
+
(record) =>
|
|
516
|
+
Object.entries(expected).every(
|
|
517
|
+
([key, value]) =>
|
|
518
|
+
key === "content_variant"
|
|
519
|
+
? contentVariantSatisfies(
|
|
520
|
+
String(record.content_variant ?? ""),
|
|
521
|
+
value,
|
|
522
|
+
)
|
|
523
|
+
: String(record[key] ?? "") === value,
|
|
524
|
+
) &&
|
|
525
|
+
(coherentProof.demand === null ||
|
|
526
|
+
sameIdentity(record, coherentProof.demand)),
|
|
527
|
+
);
|
|
528
|
+
const coherenceMatched =
|
|
529
|
+
coherentProof.status === "not_required" || coherentProof.status === "pass";
|
|
530
|
+
if (stageMatched && coherenceMatched) return;
|
|
236
531
|
if (Date.now() >= deadline) {
|
|
237
532
|
throw new Error(
|
|
238
533
|
`Timed out waiting ${timeoutMs}ms for ${MARKER} stage ${JSON.stringify(expected)} and live streams ${JSON.stringify(requiredLiveStreams)}.`,
|
|
@@ -271,13 +566,19 @@ function startupMeasurements(records) {
|
|
|
271
566
|
}
|
|
272
567
|
|
|
273
568
|
function perpsBootstrapBoundary(homepageRecords, loadProofRecords) {
|
|
274
|
-
const
|
|
569
|
+
const demands = homepageRecords
|
|
275
570
|
.filter((record) => record.stage === "surface_demand")
|
|
276
|
-
.
|
|
277
|
-
.
|
|
278
|
-
const
|
|
571
|
+
.filter((record) => Number.isFinite(Number(record.monotonic_ms)))
|
|
572
|
+
.sort((left, right) => Number(left.monotonic_ms) - Number(right.monotonic_ms));
|
|
573
|
+
const demand = demands[0] ?? null;
|
|
574
|
+
const firstDemandAt = demand === null ? null : Number(demand.monotonic_ms);
|
|
279
575
|
const bootstrapTimes = loadProofRecords
|
|
280
|
-
.filter(
|
|
576
|
+
.filter(
|
|
577
|
+
(record) =>
|
|
578
|
+
record.stage === "perps_bootstrap_start" &&
|
|
579
|
+
demand !== null &&
|
|
580
|
+
sameIdentity(record, demand, LOAD_PROOF_BASE_IDENTITY_FIELDS),
|
|
581
|
+
)
|
|
281
582
|
.map((record) => Number(record.monotonic_ms))
|
|
282
583
|
.filter(
|
|
283
584
|
(value) =>
|
|
@@ -379,6 +680,13 @@ function frameTime(record) {
|
|
|
379
680
|
|
|
380
681
|
function frameIsFresh(record) {
|
|
381
682
|
if (record.fresh_for_lifecycle === false) return false;
|
|
683
|
+
if (record.source === "retained_market_context") {
|
|
684
|
+
return (
|
|
685
|
+
record.lifecycle === "account_switch" &&
|
|
686
|
+
!isAccountContentVariant(record.content_variant) &&
|
|
687
|
+
record.fresh_for_lifecycle === true
|
|
688
|
+
);
|
|
689
|
+
}
|
|
382
690
|
return (
|
|
383
691
|
FRESH_SOURCES.has(record.source) ||
|
|
384
692
|
((record.source === "resident_state" || record.source === "memory_cache") &&
|
|
@@ -395,13 +703,8 @@ function frameIsCached(record) {
|
|
|
395
703
|
}
|
|
396
704
|
|
|
397
705
|
function visibleFrameGroups(records) {
|
|
398
|
-
const
|
|
399
|
-
|
|
400
|
-
.filter((record) => record.stage === "surface_demand")
|
|
401
|
-
.map((record) => [
|
|
402
|
-
String(record.demand_id ?? ""),
|
|
403
|
-
record.lifecycle ?? "unknown",
|
|
404
|
-
]),
|
|
706
|
+
const demands = records.filter(
|
|
707
|
+
(record) => record.stage === "surface_demand",
|
|
405
708
|
);
|
|
406
709
|
const groups = new Map();
|
|
407
710
|
for (const record of records) {
|
|
@@ -409,12 +712,13 @@ function visibleFrameGroups(records) {
|
|
|
409
712
|
const at = frameTime(record);
|
|
410
713
|
if (at === null) continue;
|
|
411
714
|
const demandId = String(record.demand_id ?? "");
|
|
412
|
-
const key = `${
|
|
715
|
+
const key = `${identityKey(record)}:${at}`;
|
|
716
|
+
const demand = demands.find((candidate) => sameIdentity(candidate, record));
|
|
413
717
|
const group = groups.get(key) ?? {
|
|
414
718
|
at,
|
|
415
719
|
demandId,
|
|
416
|
-
|
|
417
|
-
|
|
720
|
+
demand,
|
|
721
|
+
lifecycle: record.lifecycle ?? demand?.lifecycle ?? "unknown",
|
|
418
722
|
records: [],
|
|
419
723
|
};
|
|
420
724
|
group.records.push(record);
|
|
@@ -451,23 +755,29 @@ function frameGroupSatisfies(group, requiredVariants, predicate) {
|
|
|
451
755
|
|
|
452
756
|
function cacheTakeoverMeasurement(records, requiredContentVariants = []) {
|
|
453
757
|
const groups = visibleFrameGroups(records);
|
|
454
|
-
const cached = groups.find(
|
|
455
|
-
|
|
758
|
+
const cached = groups.find(
|
|
759
|
+
(group) =>
|
|
760
|
+
group.demand &&
|
|
761
|
+
frameGroupSatisfies(group, requiredContentVariants, frameIsCached),
|
|
456
762
|
);
|
|
457
763
|
const fresh = cached
|
|
458
764
|
? groups.find(
|
|
459
765
|
(group) =>
|
|
460
766
|
group.at > cached.at &&
|
|
767
|
+
group.demand &&
|
|
768
|
+
sameIdentity(group.demand, cached.demand) &&
|
|
461
769
|
frameGroupSatisfies(group, requiredContentVariants, frameIsFresh),
|
|
462
770
|
)
|
|
463
771
|
: null;
|
|
464
772
|
const cacheHydratedValues = records
|
|
465
|
-
.filter((record) => record.stage === "disk_cache_hydrated")
|
|
466
|
-
.map((record) => Number(record.monotonic_ms))
|
|
467
773
|
.filter(
|
|
468
|
-
(
|
|
469
|
-
|
|
470
|
-
|
|
774
|
+
(record) =>
|
|
775
|
+
record.stage === "disk_cache_hydrated" &&
|
|
776
|
+
cached !== undefined &&
|
|
777
|
+
sameIdentity(record, cached.demand),
|
|
778
|
+
)
|
|
779
|
+
.map((record) => Number(record.monotonic_ms))
|
|
780
|
+
.filter((value) => Number.isFinite(value) && value <= cached.at);
|
|
471
781
|
const cacheHydratedAt =
|
|
472
782
|
cacheHydratedValues.length > 0 ? Math.max(...cacheHydratedValues) : null;
|
|
473
783
|
const failureReason = !cached
|
|
@@ -511,29 +821,42 @@ function visibleMeasurements(records, requiredFreshContentVariants = []) {
|
|
|
511
821
|
return demands.map((demand) => {
|
|
512
822
|
const demandId = String(demand.demand_id ?? "");
|
|
513
823
|
const startedAt = Number(demand.monotonic_ms);
|
|
514
|
-
const belongsToDemand = (record) =>
|
|
515
|
-
|
|
516
|
-
|
|
517
|
-
|
|
518
|
-
|
|
519
|
-
record[key] === undefined ||
|
|
520
|
-
String(record[key]) === String(demand[key]),
|
|
521
|
-
);
|
|
824
|
+
const belongsToDemand = (record) => sameIdentity(record, demand);
|
|
825
|
+
const afterDemand = (record) => {
|
|
826
|
+
const at = performanceRecordTime(record);
|
|
827
|
+
return at !== null && Number.isFinite(startedAt) && at >= startedAt;
|
|
828
|
+
};
|
|
522
829
|
const frames = records.filter(
|
|
523
830
|
(record) =>
|
|
524
|
-
record.stage === "next_frame_checkpoint" &&
|
|
831
|
+
record.stage === "next_frame_checkpoint" &&
|
|
832
|
+
belongsToDemand(record) &&
|
|
833
|
+
afterDemand(record),
|
|
525
834
|
);
|
|
526
835
|
const commits = records.filter(
|
|
527
|
-
(record) => record.stage === "react_commit" && belongsToDemand(record),
|
|
528
|
-
);
|
|
529
|
-
const firstVisibleBoundary = records.find(
|
|
530
|
-
(record) =>
|
|
531
|
-
record.stage === "surface_resolved_recorded" && belongsToDemand(record),
|
|
532
|
-
);
|
|
533
|
-
const freshVisibleBoundary = records.find(
|
|
534
836
|
(record) =>
|
|
535
|
-
record.stage === "
|
|
837
|
+
record.stage === "react_commit" &&
|
|
838
|
+
belongsToDemand(record) &&
|
|
839
|
+
afterDemand(record),
|
|
536
840
|
);
|
|
841
|
+
const firstVisibleBoundary = records
|
|
842
|
+
.filter(
|
|
843
|
+
(record) =>
|
|
844
|
+
record.stage === "surface_resolved_recorded" &&
|
|
845
|
+
belongsToDemand(record) &&
|
|
846
|
+
afterDemand(record),
|
|
847
|
+
)
|
|
848
|
+
.sort((left, right) => frameTime(left) - frameTime(right))[0];
|
|
849
|
+
const firstVisibleBoundaryAt = frameTime(firstVisibleBoundary ?? {});
|
|
850
|
+
const freshVisibleBoundary = records
|
|
851
|
+
.filter(
|
|
852
|
+
(record) =>
|
|
853
|
+
record.stage === "surface_live_recorded" &&
|
|
854
|
+
belongsToDemand(record) &&
|
|
855
|
+
afterDemand(record) &&
|
|
856
|
+
(firstVisibleBoundaryAt === null ||
|
|
857
|
+
frameTime(record) >= firstVisibleBoundaryAt),
|
|
858
|
+
)
|
|
859
|
+
.sort((left, right) => frameTime(left) - frameTime(right))[0];
|
|
537
860
|
const frameTimes = frames
|
|
538
861
|
.map((record) =>
|
|
539
862
|
Number(record.frame_checkpoint_monotonic_ms ?? record.monotonic_ms),
|
|
@@ -601,6 +924,11 @@ function visibleMeasurements(records, requiredFreshContentVariants = []) {
|
|
|
601
924
|
const dataAges = frames
|
|
602
925
|
.map((record) => Number(record.data_age_ms))
|
|
603
926
|
.filter(Number.isFinite);
|
|
927
|
+
const deliveryRecords = [
|
|
928
|
+
...frames,
|
|
929
|
+
firstVisibleBoundary,
|
|
930
|
+
freshVisibleBoundary,
|
|
931
|
+
].filter(Boolean);
|
|
604
932
|
return {
|
|
605
933
|
demandId,
|
|
606
934
|
lifecycle: demand.lifecycle ?? "unknown",
|
|
@@ -625,10 +953,15 @@ function visibleMeasurements(records, requiredFreshContentVariants = []) {
|
|
|
625
953
|
? roundMs(freshVisibleAt - startedAt)
|
|
626
954
|
: null,
|
|
627
955
|
deliverySources: [
|
|
628
|
-
...new Set(
|
|
956
|
+
...new Set(
|
|
957
|
+
deliveryRecords
|
|
958
|
+
.map((record) => String(record.source ?? ""))
|
|
959
|
+
.filter(Boolean),
|
|
960
|
+
),
|
|
629
961
|
],
|
|
630
962
|
contentVariant:
|
|
631
|
-
|
|
963
|
+
deliveryRecords.find((record) => record.content_variant)
|
|
964
|
+
?.content_variant ??
|
|
632
965
|
null,
|
|
633
966
|
maxDataAgeMs: dataAges.length > 0 ? Math.max(...dataAges) : null,
|
|
634
967
|
};
|
|
@@ -694,18 +1027,31 @@ function socketPipelineMeasurements(records) {
|
|
|
694
1027
|
return sockets.flatMap((socket) => {
|
|
695
1028
|
const deliveryId = String(socket.delivery_id);
|
|
696
1029
|
const socketAt = Number(socket.monotonic_ms);
|
|
697
|
-
|
|
698
|
-
|
|
699
|
-
|
|
700
|
-
|
|
701
|
-
|
|
702
|
-
|
|
703
|
-
|
|
704
|
-
|
|
705
|
-
|
|
706
|
-
|
|
707
|
-
|
|
1030
|
+
if (!Number.isFinite(socketAt)) return [];
|
|
1031
|
+
const commit = records
|
|
1032
|
+
.filter(
|
|
1033
|
+
(record) =>
|
|
1034
|
+
record.stage === "react_commit" &&
|
|
1035
|
+
String(record.delivery_id ?? "") === deliveryId &&
|
|
1036
|
+
sameIdentity(record, socket) &&
|
|
1037
|
+
Number(record.monotonic_ms) >= socketAt,
|
|
1038
|
+
)
|
|
1039
|
+
.sort(
|
|
1040
|
+
(left, right) => Number(left.monotonic_ms) - Number(right.monotonic_ms),
|
|
1041
|
+
)[0];
|
|
1042
|
+
if (!commit) return [];
|
|
708
1043
|
const commitAt = Number(commit.monotonic_ms);
|
|
1044
|
+
const frame = records
|
|
1045
|
+
.filter(
|
|
1046
|
+
(record) =>
|
|
1047
|
+
record.stage === "next_frame_checkpoint" &&
|
|
1048
|
+
String(record.delivery_id ?? "") === deliveryId &&
|
|
1049
|
+
sameIdentity(record, socket) &&
|
|
1050
|
+
frameTime(record) !== null &&
|
|
1051
|
+
frameTime(record) >= commitAt,
|
|
1052
|
+
)
|
|
1053
|
+
.sort((left, right) => frameTime(left) - frameTime(right))[0];
|
|
1054
|
+
if (!frame) return [];
|
|
709
1055
|
const frameAt = Number(
|
|
710
1056
|
frame.frame_checkpoint_monotonic_ms ?? frame.monotonic_ms,
|
|
711
1057
|
);
|
|
@@ -714,14 +1060,14 @@ function socketPipelineMeasurements(records) {
|
|
|
714
1060
|
(record) =>
|
|
715
1061
|
record.stage === "subscriber_delivery" &&
|
|
716
1062
|
String(record.delivery_id ?? "") === deliveryId &&
|
|
1063
|
+
sameIdentity(record, socket) &&
|
|
1064
|
+
Number(record.monotonic_ms) >= socketAt &&
|
|
717
1065
|
Number(record.monotonic_ms) <= commitAt,
|
|
718
1066
|
)
|
|
719
1067
|
.map((record) => Number(record.monotonic_ms))
|
|
720
1068
|
.filter(Number.isFinite);
|
|
721
|
-
|
|
722
|
-
|
|
723
|
-
? Math.max(...subscriberCandidates)
|
|
724
|
-
: socketAt;
|
|
1069
|
+
if (subscriberCandidates.length === 0) return [];
|
|
1070
|
+
const subscriberAt = Math.max(...subscriberCandidates);
|
|
725
1071
|
return [
|
|
726
1072
|
{
|
|
727
1073
|
deliveryId,
|
|
@@ -757,14 +1103,20 @@ function firstFreshDeliveryChecks(records, requirements) {
|
|
|
757
1103
|
record.delivery_id,
|
|
758
1104
|
);
|
|
759
1105
|
const delivery = socket
|
|
760
|
-
? records
|
|
761
|
-
(
|
|
762
|
-
record
|
|
763
|
-
|
|
764
|
-
|
|
765
|
-
|
|
766
|
-
|
|
767
|
-
|
|
1106
|
+
? records
|
|
1107
|
+
.filter(
|
|
1108
|
+
(record) =>
|
|
1109
|
+
record.stage === "subscriber_delivery" &&
|
|
1110
|
+
record.delivery_id === socket.delivery_id &&
|
|
1111
|
+
record.stream === stream &&
|
|
1112
|
+
sameIdentity(record, socket) &&
|
|
1113
|
+
Number(record.monotonic_ms) >= Number(socket.monotonic_ms) &&
|
|
1114
|
+
Number(record.throttle_ms ?? 0) === subscriberThrottleMs,
|
|
1115
|
+
)
|
|
1116
|
+
.sort(
|
|
1117
|
+
(left, right) =>
|
|
1118
|
+
Number(left.monotonic_ms) - Number(right.monotonic_ms),
|
|
1119
|
+
)[0]
|
|
768
1120
|
: null;
|
|
769
1121
|
const socketAt = Number(socket?.monotonic_ms);
|
|
770
1122
|
const subscriberAt = Number(delivery?.monotonic_ms);
|
|
@@ -797,6 +1149,7 @@ function summarize(
|
|
|
797
1149
|
bytesScanned,
|
|
798
1150
|
requiredFreshContentVariants = [],
|
|
799
1151
|
firstFreshDeliveryRequirements = [],
|
|
1152
|
+
includeCacheTakeover = false,
|
|
800
1153
|
) {
|
|
801
1154
|
const measurements = visibleMeasurements(
|
|
802
1155
|
records,
|
|
@@ -849,10 +1202,9 @@ function summarize(
|
|
|
849
1202
|
monotonicValues.length > 0 ? Math.max(...monotonicValues) : null,
|
|
850
1203
|
visibleMeasurements: measurements,
|
|
851
1204
|
visibleMeasurementCohorts: visibleMeasurementCohorts(measurements),
|
|
852
|
-
cacheTakeoverMeasurement:
|
|
853
|
-
records,
|
|
854
|
-
|
|
855
|
-
),
|
|
1205
|
+
cacheTakeoverMeasurement: includeCacheTakeover
|
|
1206
|
+
? cacheTakeoverMeasurement(records, requiredFreshContentVariants)
|
|
1207
|
+
: { status: "not_required" },
|
|
856
1208
|
socketPipelineMeasurements: socketPipelineMeasurements(records),
|
|
857
1209
|
firstFreshDeliveryChecks: firstFreshDeliveryChecks(
|
|
858
1210
|
records,
|
|
@@ -885,15 +1237,29 @@ export async function capturePerpsPerformance(input) {
|
|
|
885
1237
|
await mkdir(artifactsDir, { recursive: true });
|
|
886
1238
|
|
|
887
1239
|
if (phase === "start") {
|
|
888
|
-
const
|
|
889
|
-
const
|
|
890
|
-
|
|
1240
|
+
const sourceIdentity = await fileState(sourceFile);
|
|
1241
|
+
const offset = sourceIdentity?.size ?? 0;
|
|
1242
|
+
const lookbackStart = Math.max(0, offset - BOOTSTRAP_LOOKBACK_BYTES);
|
|
1243
|
+
const existingContent = sourceIdentity
|
|
1244
|
+
? (
|
|
1245
|
+
await readFileRange(sourceFile, lookbackStart, offset - lookbackStart)
|
|
1246
|
+
).toString("utf8")
|
|
1247
|
+
: "";
|
|
1248
|
+
const boundaryStart = Math.max(0, offset - CAPTURE_BOUNDARY_BYTES);
|
|
1249
|
+
const sourceBoundary = sourceIdentity
|
|
1250
|
+
? captureBoundaryBytes(
|
|
1251
|
+
boundaryStart,
|
|
1252
|
+
await readFileRange(sourceFile, boundaryStart, offset - boundaryStart),
|
|
1253
|
+
)
|
|
1254
|
+
: null;
|
|
891
1255
|
await writeFile(
|
|
892
1256
|
stateFile,
|
|
893
1257
|
`${JSON.stringify(
|
|
894
1258
|
{
|
|
895
1259
|
sourcePath,
|
|
896
1260
|
offset,
|
|
1261
|
+
sourceIdentity,
|
|
1262
|
+
sourceBoundary,
|
|
897
1263
|
captureStartedAtEpochMs: Date.now(),
|
|
898
1264
|
perpsBootstrapStartRecord:
|
|
899
1265
|
latestPerpsBootstrapStartRecord(existingContent),
|
|
@@ -918,6 +1284,19 @@ export async function capturePerpsPerformance(input) {
|
|
|
918
1284
|
state.sourcePath !== sourcePath ||
|
|
919
1285
|
!Number.isInteger(state.offset) ||
|
|
920
1286
|
state.offset < 0 ||
|
|
1287
|
+
!(
|
|
1288
|
+
state.sourceIdentity === null ||
|
|
1289
|
+
(state.sourceIdentity &&
|
|
1290
|
+
Number.isInteger(state.sourceIdentity.device) &&
|
|
1291
|
+
Number.isInteger(state.sourceIdentity.inode))
|
|
1292
|
+
) ||
|
|
1293
|
+
!(
|
|
1294
|
+
state.sourceBoundary === null ||
|
|
1295
|
+
(state.sourceBoundary &&
|
|
1296
|
+
Number.isInteger(state.sourceBoundary.start) &&
|
|
1297
|
+
Number.isInteger(state.sourceBoundary.length) &&
|
|
1298
|
+
typeof state.sourceBoundary.sha256 === "string")
|
|
1299
|
+
) ||
|
|
921
1300
|
!Number.isFinite(state.captureStartedAtEpochMs)
|
|
922
1301
|
) {
|
|
923
1302
|
throw new Error(
|
|
@@ -928,11 +1307,11 @@ export async function capturePerpsPerformance(input) {
|
|
|
928
1307
|
await waitForPerformanceRecord({
|
|
929
1308
|
node,
|
|
930
1309
|
sourceFile,
|
|
931
|
-
|
|
1310
|
+
state,
|
|
932
1311
|
});
|
|
933
1312
|
|
|
934
|
-
const content = await
|
|
935
|
-
const offset = state.offset
|
|
1313
|
+
const content = await readCaptureSource(sourceFile, state);
|
|
1314
|
+
const offset = state.offset;
|
|
936
1315
|
const segment = content.subarray(offset).toString("utf8");
|
|
937
1316
|
const segmentLines = segment.split(/\r?\n/u);
|
|
938
1317
|
const lines = segmentLines
|
|
@@ -959,10 +1338,7 @@ export async function capturePerpsPerformance(input) {
|
|
|
959
1338
|
.map((line) => parseRecord(line, LOAD_PROOF_MARKER))
|
|
960
1339
|
.filter(Boolean),
|
|
961
1340
|
];
|
|
962
|
-
const loadProofRecords = mergeLoadProofRecords(
|
|
963
|
-
...markerLoadProofRecords,
|
|
964
|
-
...existingLiveStreamRecords(segmentLines, markerLoadProofRecords),
|
|
965
|
-
]);
|
|
1341
|
+
const loadProofRecords = mergeLoadProofRecords(markerLoadProofRecords);
|
|
966
1342
|
const requireRecords = node.require_records !== false;
|
|
967
1343
|
const requiredStages = Array.isArray(node.required_stages)
|
|
968
1344
|
? node.required_stages.map(String)
|
|
@@ -1048,17 +1424,19 @@ export async function capturePerpsPerformance(input) {
|
|
|
1048
1424
|
const requiredLiveStreams = Array.isArray(node.required_live_streams)
|
|
1049
1425
|
? node.required_live_streams.map(String)
|
|
1050
1426
|
: [];
|
|
1051
|
-
const
|
|
1052
|
-
|
|
1053
|
-
|
|
1054
|
-
|
|
1055
|
-
|
|
1056
|
-
|
|
1057
|
-
|
|
1058
|
-
|
|
1059
|
-
|
|
1060
|
-
|
|
1427
|
+
const coherentProof = coherentPerformanceProof(
|
|
1428
|
+
records,
|
|
1429
|
+
loadProofRecords,
|
|
1430
|
+
requiredStages,
|
|
1431
|
+
requiredLiveStreams,
|
|
1432
|
+
node.wait_for_lifecycle === undefined
|
|
1433
|
+
? null
|
|
1434
|
+
: String(node.wait_for_lifecycle),
|
|
1435
|
+
node.wait_for_content_variant === undefined
|
|
1436
|
+
? null
|
|
1437
|
+
: String(node.wait_for_content_variant),
|
|
1061
1438
|
);
|
|
1439
|
+
const missingLiveStreams = coherentProof.missingLiveStreams;
|
|
1062
1440
|
const measuredPerpsBootstrapBoundary = perpsBootstrapBoundary(
|
|
1063
1441
|
records,
|
|
1064
1442
|
loadProofRecords,
|
|
@@ -1088,6 +1466,7 @@ export async function capturePerpsPerformance(input) {
|
|
|
1088
1466
|
missingFreshContentVariants.length === 0 &&
|
|
1089
1467
|
failedFirstFreshDeliveries.length === 0 &&
|
|
1090
1468
|
missingLiveStreams.length === 0 &&
|
|
1469
|
+
coherentProof.status !== "fail" &&
|
|
1091
1470
|
(!requirePerpsBootstrapStartBeforeDemand ||
|
|
1092
1471
|
measuredPerpsBootstrapBoundary.status === "pass") &&
|
|
1093
1472
|
(!requireCacheBeforeFresh || cacheTakeover.status === "pass")
|
|
@@ -1102,6 +1481,9 @@ export async function capturePerpsPerformance(input) {
|
|
|
1102
1481
|
missingFreshContentVariants,
|
|
1103
1482
|
failedFirstFreshDeliveries,
|
|
1104
1483
|
missingLiveStreams,
|
|
1484
|
+
coherentSurfaceAndLive: coherentProof.status,
|
|
1485
|
+
coherentIdentity: coherentProof.identity,
|
|
1486
|
+
coherentSurfaceSequence: coherentProof.sequence,
|
|
1105
1487
|
perpsBootstrapStartBeforeDemand: requirePerpsBootstrapStartBeforeDemand
|
|
1106
1488
|
? measuredPerpsBootstrapBoundary.status
|
|
1107
1489
|
: "not_required",
|
|
@@ -1120,6 +1502,7 @@ export async function capturePerpsPerformance(input) {
|
|
|
1120
1502
|
content.length - offset,
|
|
1121
1503
|
requiredFreshContentVariants,
|
|
1122
1504
|
firstFreshDeliveryRequirements,
|
|
1505
|
+
requireCacheBeforeFresh,
|
|
1123
1506
|
),
|
|
1124
1507
|
captureWindow: {
|
|
1125
1508
|
startedAtEpochMs: state.captureStartedAtEpochMs,
|
|
@@ -1149,6 +1532,11 @@ export async function capturePerpsPerformance(input) {
|
|
|
1149
1532
|
`Missing required ${STARTUP_MARKER} stages: ${missingStartupStages.join(", ")}.`,
|
|
1150
1533
|
);
|
|
1151
1534
|
}
|
|
1535
|
+
if (coherentProof.status === "fail") {
|
|
1536
|
+
throw new Error(
|
|
1537
|
+
`Required ${MARKER} surface stages and live streams did not form one ordered identity-coherent demand tuple.`,
|
|
1538
|
+
);
|
|
1539
|
+
}
|
|
1152
1540
|
if (
|
|
1153
1541
|
requirePerpsBootstrapStartBeforeDemand &&
|
|
1154
1542
|
measuredPerpsBootstrapBoundary.status !== "pass"
|