Setting the file. One moment.
Sandbox SSR · Sandbox Bench · vercel/next.js · Skills Docs
ContentsBack to the top of the page 24.4
Bench Common
scripts/ sandbox-ssr.mjs
JavaScript · 655 lines · 21 KB
15 // Common: [--runs 2] [--fixture-ref main] [--no-profile] [--keep]
16 // [--allow-ungated] [--dry-run]
17 import fs from 'fs'
18 import os from 'os'
19 import path from 'path'
20 import { analyzeE2eRows } from './bench-stats.mjs'
21 import { openDb, importRun, loadRows, verify as verifyDb } from './bench-db.mjs'
22 import {
23 execFileP,
24 CONFIG,
25 REACT_REPO_LAZY,
26 CACHE,
27 SETUP_VERSION,
28 REACT_GH_REPO,
29 status,
30 writeStatus,
31 sb,
32 sbExec,
33 rmVm,
34 runDetached,
35 resolvePrArms,
36 assertCiGreen,
37 commitTitle,
38 printRunContext,
39 makeLive,
40 sha256,
41 snapshotIdFor,
42 takeSnapshot,
43 ensureRefArm,
44 } from './bench-common.mjs'
45
46 const FIXTURE_DIR = 'fixtures/flight-ssr-bench'
47 // Provenance: the Flight server/client files the fixture actually
48 // executes — Node and Edge entry points, so changes touching only one
49 // stream flavor still move the fingerprint — plus the shared
50 // react-server runtime (hooks/cache), which none of the layer files
51 // reflect.
52 const FP_FILES = [
53 'react-server-dom-webpack/cjs/react-server-dom-webpack-server.node.production.js' ,
54 'react-server-dom-webpack/cjs/react-server-dom-webpack-server.edge.production.js' ,
55 'react-server-dom-webpack/cjs/react-server-dom-webpack-client.edge.production.js' ,
56 'react-dom/cjs/react-dom-server.node.production.js' ,
57 'react-dom/cjs/react-dom-server.edge.production.js' ,
58 'react/cjs/react.react-server.production.js' ,
59 ]
60
61 function parseArgs () {
62 const a = process.argv. slice ( 2 )
63 const get = ( name , dflt ) => {
64 const i = a. indexOf (name)
65 return i >= 0 ? a[i + 1 ] : dflt
66 }
67 const arms = get ( '--arms' , '' )
68 . split ( ',' )
69 . filter (Boolean)
70 . map (( s ) => {
71 const [ name , src ] = s. split ( '=' )
72 if ( ! name || ! src)
73 throw new Error ( `bad arm "${ s }" in --arms, want name=<ref>` )
74 return { name, ref: src }
75 })
76 const pr = get ( '--pr' , undefined )
77 if ((pr ? 1 : 0 ) + (arms. length ? 1 : 0 ) !== 1 ) {
78 throw new Error ( 'need exactly one of: --pr, --arms' )
79 }
80 if (arms. length && arms. length !== 2 )
81 throw new Error ( '--arms needs exactly two arms (base first)' )
82 return {
83 arms,
84 pr,
85 dryRun: a. includes ( '--dry-run' ),
86 allowUngated: a. includes ( '--allow-ungated' ),
87 // The workload: one fixture tree shared by both arms.
88 fixtureRef: get ( '--fixture-ref' , 'main' ),
89 runs: Number ( get ( '--runs' , '2' )),
90 vms: Number ( get ( '--vms' , '16' )),
91 keep: a. includes ( '--keep' ),
92 profile: ! a. includes ( '--no-profile' ),
93 label: get ( '--label' , 'ssr' ),
94 }
95 }
96
97 // Resolve a react ref: shas and already-fetched refs locally, branch
98 // names against the remote (so "main" is today's main), pinned once
99 // per run.
100 const reactShaMemo = new Map ()
101 async function reactShaFor ( ref ) {
102 if (reactShaMemo. has (ref)) return reactShaMemo. get (ref)
103 const repo = REACT_REPO_LAZY ()
104 let sha
105 if ( / ^ [0-9a-f] {7,40}$ / i . test (ref)) {
106 try {
107 sha = (
108 await execFileP ( 'git' , [
109 '-C' ,
110 repo,
111 'rev-parse' ,
112 '--verify' ,
113 `${ ref }^{commit}` ,
114 ])
115 ).stdout. trim ()
116 } catch {}
117 }
118 if ( ! sha) {
119 const dst = `refs/bench-tmp/${ process . pid }/ssr-${ reactShaMemo . size }`
120 for ( let attempt = 1 ; ; attempt ++ ) {
121 try {
122 await execFileP ( 'git' , [
123 '-C' ,
124 repo,
125 'fetch' ,
126 '-q' ,
127 CONFIG .reactRepoUrl,
128 `+${ ref }:${ dst }` ,
129 ])
130 break
131 } catch (e) {
132 if (attempt >= 3 ) throw e
133 await new Promise (( r ) => setTimeout (r, 2000 * attempt))
134 }
135 }
136 sha = (
137 await execFileP ( 'git' , [ '-C' , repo, 'rev-parse' , `${ dst }^{commit}` ])
138 ).stdout. trim ()
139 }
140 reactShaMemo. set (ref, sha)
141 return sha
142 }
143
144 async function resolveArms ( cfg ) {
145 const arms = cfg.pr
146 ? await resolvePrArms (
147 cfg.pr,
148 REACT_REPO_LAZY (),
149 CONFIG .reactRepoUrl,
150 'main'
151 )
152 : cfg.arms
153 for ( const arm of arms) {
154 arm.sha = await reactShaFor (arm.ref)
155 }
156 cfg.fixtureSha = await reactShaFor (cfg.fixtureRef)
157 console. error (
158 `fixture: ${ FIXTURE_DIR } @ ${ cfg . fixtureRef } (${ cfg . fixtureSha . slice ( 0 , 12 ) })`
159 )
160 // The fixture must emit machine-readable results; parsing its human
161 // tables would silently break as it evolves.
162 const benchJs = (
163 await execFileP (
164 'git' ,
165 [
166 '-C' ,
167 REACT_REPO_LAZY (),
168 'show' ,
169 `${ cfg . fixtureSha }:${ FIXTURE_DIR }/bench.js` ,
170 ],
171 { maxBuffer: 1 << 24 }
172 )
173 ).stdout
174 if ( ! benchJs. includes ( '--json-out' )) {
175 throw new Error (
176 `the fixture at ${ cfg . fixtureRef } does not support --json-out; ` +
177 'pass --fixture-ref <ref that does> (see SKILL.md)'
178 )
179 }
180 // The workload is pinned; if the PR itself changes the fixture, this
181 // run will NOT measure those changes. Say so rather than silently
182 // benching something else.
183 const touched = (
184 await execFileP ( 'git' , [
185 '-C' ,
186 REACT_REPO_LAZY (),
187 'diff' ,
188 '--name-only' ,
189 `${ arms [ 0 ]. sha }..${ arms [ 1 ]. sha }` ,
190 '--' ,
191 FIXTURE_DIR ,
192 ])
193 ).stdout. trim ()
194 if (touched) {
195 cfg.fixtureTouchedByPr = true
196 console. error (
197 `NOTE: the candidate changes ${ FIXTURE_DIR } itself; the benchmark ` +
198 `uses the pinned fixture (${ cfg . fixtureSha . slice ( 0 , 12 ) }) and does ` +
199 `not measure those fixture changes: \n ${ touched }`
200 )
201 }
202 return arms
203 }
204
205 // Snapshot: fixture installed + both arms' builds staged, so run VMs
206 // boot straight into measurement.
207 async function ensureSsrSnapshot ( cfg ) {
208 const key = await sha256 (
209 SETUP_VERSION +
210 'ssr1' +
211 cfg.fixtureSha +
212 cfg.arms. map (( a ) => `${ a . name }=${ a . sha }` ). join ()
213 )
214 let id = await snapshotIdFor ( CACHE , key)
215 if (id) return id
216 const vm = `sbench-ssrsnap-${ Date . now (). toString ( 36 ) }`
217 console. error (
218 `creating ssr snapshot (one-time for fixture=${ cfg . fixtureSha . slice ( 0 , 12 ) } ` +
219 `arms=${ cfg . arms . map (( a ) => a . sha . slice ( 0 , 12 )). join ( ',' ) })...`
220 )
221 await sb ([
222 'create' ,
223 '--name' ,
224 vm,
225 '--runtime' ,
226 'node24' ,
227 '--vcpus' ,
228 '8' ,
229 '--timeout' ,
230 '45m' ,
231 '--non-persistent' ,
232 '--network-policy' ,
233 'allow-all' ,
234 '--tag' ,
235 'purpose=sandbox-bench' ,
236 '--silent' ,
237 ])
238 try {
239 const fixtureTgz = path. join (
240 os. tmpdir (),
241 `ssr-fixture-${ cfg . fixtureSha . slice ( 0 , 12 ) }.tgz`
242 )
243 await execFileP ( 'bash' , [
244 '-c' ,
245 `git -C ${ REACT_REPO_LAZY () } archive ${ cfg . fixtureSha } ${ FIXTURE_DIR } | gzip -1 > ${ fixtureTgz }` ,
246 ])
247 await sb ([ 'cp' , fixtureTgz, `${ vm }:/vercel/sandbox/fixture.tgz` ])
248 fs. rmSync (fixtureTgz, { force: true })
249 for ( const arm of cfg.arms) {
250 await sb ([ 'cp' , arm.tgz, `${ vm }:/vercel/sandbox/arm-${ arm . name }.tgz` ])
251 }
252 const extractArms = cfg.arms
253 . map (
254 ( a ) =>
255 `mkdir -p /vercel/sandbox/arm-${ a . name } && tar -xzf /vercel/sandbox/arm-${ a . name }.tgz -C /vercel/sandbox/arm-${ a . name }`
256 )
257 . join ( ' \n ' )
258 // Smoke: one full bench pass on the base arm proves the fixture
259 // installs, builds its bundle, and emits parseable JSON before 16
260 // VMs boot from this snapshot.
261 await sbExec (
262 vm,
263 '35m' ,
264 `set -e
265 npm i -g yarn >/dev/null 2>&1
266 mkdir -p /vercel/sandbox/fixture
267 tar -xzf /vercel/sandbox/fixture.tgz --strip-components=2 -C /vercel/sandbox/fixture
268 ${ extractArms }
269 cd /vercel/sandbox/fixture
270 echo "PHASE install $(date +%s)"
271 yarn install --ignore-engines >/tmp/install.log 2>&1 || (tail -10 /tmp/install.log; exit 1)
272 echo "PHASE smoke $(date +%s)"
273 for p in /vercel/sandbox/arm-${ cfg . arms [ 0 ]. name }/build/oss-experimental/*; do rm -rf node_modules/$(basename $p); done
274 cp -r /vercel/sandbox/arm-${ cfg . arms [ 0 ]. name }/build/oss-experimental/* node_modules/
275 NODE_ENV=production node --expose-gc bench.js --json-out=/tmp/smoke.json >/tmp/smoke.log 2>&1 || (tail -20 /tmp/smoke.log; exit 1)
276 node -e 'const j=require("/tmp/smoke.json"); if (!Array.isArray(j.results) || j.results.length < 4) { console.error("smoke json bad"); process.exit(1); }'
277 rm -f /tmp/smoke.json /vercel/sandbox/fixture.tgz /vercel/sandbox/arm-*.tgz
278 echo "PHASE done $(date +%s)"
279 echo ssr env ready` ,
280 'ssrsnap'
281 )
282 return await takeSnapshot (vm, CACHE , key)
283 } finally {
284 await rmVm (vm)
285 }
286 }
287
288 // ------------------------------------------------------------------ run
289
290 async function runVm ( index , cfg , snap , outDir ) {
291 const vm = `sbench-${ cfg . label }-${ index }-${ Date . now (). toString ( 36 ) }`
292 const tag = `vm${ index }`
293 console. error ( `${ tag }: creating ${ vm } from ssr snapshot` )
294 writeStatus ({
295 vms: { ... status.state.vms, [vm]: { state: 'booting' , rows: 0 } },
296 })
297 await sb ([
298 'create' ,
299 '--name' ,
300 vm,
301 '--snapshot' ,
302 snap,
303 '--vcpus' ,
304 '8' ,
305 '--timeout' ,
306 '5h' ,
307 '--non-persistent' ,
308 '--network-policy' ,
309 'allow-all' ,
310 '--tag' ,
311 'purpose=sandbox-bench' ,
312 '--silent' ,
313 ])
314 try {
315 const [ base , cand ] = cfg.arms. map (( a ) => a.name)
316 // Row emitter: fixture JSON -> one row per (variant, phase), variant
317 // names normalized to the server's kebab keys ("Flight + Fizz
318 // (Edge, async)" -> flight-edge-async). Metrics are OMITTED when
319 // absent — a zero would pair against a real value as a fabricated
320 // -100% claim.
321 const emit = `
322 const [,run,arm,fp,ver,cpu]=process.argv;
323 const key=(n)=>n.toLowerCase().replace("flight + fizz","flight")
324 .replace(/[(),]/g,"").trim().replace(/ \\ s+/g,"-");
325 const rows=[];
326 const inj=require("/tmp/inject.json");
327 for (const r of inj.results) {
328 const row={block:+run,arm,run:1,fp,ver,cpu,route:key(r.name),phase:"inject",
329 mean:r.mean,median:r.median,p95:r.p95};
330 if (r.gcTotalMs>0) row.gcMs=r.gcTotalMs/r.iterations;
331 if (r.heapAfter>0) row.heapMb=r.heapAfter/1048576;
332 rows.push(row);
333 }
334 const srv=require("/tmp/server.json");
335 for (const r of srv.results) {
336 const row={block:+run,arm,run:1,fp,ver,cpu,route:key(r.name),
337 phase:"server-c"+r.concurrency,rps:r.reqPerSec,median:r.latencyMedian};
338 if (r.latencyP99>0) row.p99=r.latencyP99;
339 if (r.errors>0) row.errors=r.errors;
340 rows.push(row);
341 }
342 console.log(rows.map(r=>JSON.stringify(r)).join(" \\ n"));
343 `
344 const loop = `set -e
345 VMINDEX=${ index }
346 CPU=$(grep -m1 'model name' /proc/cpuinfo | cut -d: -f2- | sed 's/^ //')
347 : > /vercel/sandbox/results.jsonl
348 cd /vercel/sandbox/fixture
349 for arm in ${ base } ${ cand }; do
350 V=$(node -e "console.log(require('/vercel/sandbox/arm-$arm/build/oss-experimental/react/package.json').version)")
351 F=$(cat ${ FP_FILES . map (( f ) => `/vercel/sandbox/arm-$arm/build/oss-experimental/${ f }` ). join ( ' ' ) } | sha256sum | cut -c1-12)
352 echo "arm $arm ver=$V fp=$F"
353 eval "VER_$arm=$V; FP_$arm=$F"
354 done
355 # Identical fingerprints can be legitimate (arms differing only in
356 # files outside FP_FILES), so warn, not fail.
357 if [ "$FP_${ base }" = "$FP_${ cand }" ]; then
358 echo "WARNING: arms fingerprint identically ($FP_${ base }) — the hashed React builds are byte-identical; verify the arms differ where intended"
359 fi
360 for run in $(seq 1 ${ cfg . runs }); do
361 # Alternate within the boot AND stagger by VM index so no arm owns
362 # the cold first slot across the fleet.
363 if [ $(((run + VMINDEX) % 2)) = 1 ]; then ORDER="${ cand } ${ base }"; else ORDER="${ base } ${ cand }"; fi
364 for arm in $ORDER; do
365 for p in /vercel/sandbox/arm-$arm/build/oss-experimental/*; do rm -rf node_modules/$(basename $p); done
366 cp -r /vercel/sandbox/arm-$arm/build/oss-experimental/* node_modules/
367 NODE_ENV=production node --expose-gc bench.js --json-out=/tmp/inject.json >/tmp/bench.log 2>&1 \
368 || (tail -20 /tmp/bench.log; exit 1)
369 NODE_ENV=production node bench-server.js --bench --json-out=/tmp/server.json >/tmp/server.log 2>&1 \
370 || (tail -20 /tmp/server.log; exit 1)
371 eval "FP= \\ $FP_$arm; VER= \\ $VER_$arm"
372 node -e '${ emit }' "$run" "$arm" "$FP" "$VER" "$CPU" > /tmp/rows.txt
373 cat /tmp/rows.txt >> /vercel/sandbox/results.jsonl
374 sed 's/^/ROW /' /tmp/rows.txt
375 echo "run $run $arm done"
376 done
377 done
378 wc -l /vercel/sandbox/results.jsonl`
379 let vmRows = 0
380 const out = await runDetached (
381 vm,
382 tag,
383 loop,
384 ( row ) => {
385 vmRows ++
386 writeStatus ({
387 vms: {
388 ... status.state.vms,
389 [vm]: { state: 'measuring' , rows: vmRows },
390 },
391 })
392 cfg. live (index, row)
393 },
394 160
395 )
396 writeStatus ({
397 vms: {
398 ... status.state.vms,
399 [vm]: { ... status.state.vms[vm], state: 'collecting' },
400 },
401 })
402 const local = path. join (outDir, `results-vm${ index }.jsonl` )
403 await sb ([ 'cp' , `${ vm }:/vercel/sandbox/results.jsonl` , local])
404 const remoteCount = Number (
405 out. match ( /( \d + ) \/ vercel \/ sandbox \/ results \. jsonl/ )?.[ 1 ] ?? NaN
406 )
407 const localCount = fs
408 . readFileSync (local, 'utf8' )
409 . trim ()
410 . split ( ' \n ' )
411 . filter (Boolean). length
412 if ( ! Number. isFinite (remoteCount) || localCount !== remoteCount) {
413 throw new Error (
414 `${ tag }: downloaded ${ localCount } rows, remote reported ${ remoteCount } — truncated transfer`
415 )
416 }
417 if (cfg.profile) {
418 writeStatus ({
419 vms: {
420 ... status.state.vms,
421 [vm]: { ... status.state.vms[vm], state: 'profiling' },
422 },
423 })
424 // Strictly AFTER the timed runs — profiling never touches the
425 // numbers. Best-effort: a failed pass must not kill collection.
426 // Second-arm-runs-warmer drift cancels across VMs (see
427 // sandbox-e2e.mjs).
428 const profOrder = index % 2 === 1 ? `${ cand } ${ base }` : `${ base } ${ cand }`
429 const prof = `set -e
430 cd /vercel/sandbox/fixture
431 for arm in ${ profOrder }; do
432 for p in /vercel/sandbox/arm-$arm/build/oss-experimental/*; do rm -rf node_modules/$(basename $p); done
433 cp -r /vercel/sandbox/arm-$arm/build/oss-experimental/* node_modules/
434 NODE_ENV=production node --expose-gc bench.js --profile >/tmp/prof.log 2>&1 || (tail -10 /tmp/prof.log; exit 1)
435 mkdir -p /vercel/sandbox/prof-$arm && mv build/profiles/* /vercel/sandbox/prof-$arm/
436 echo "profiled $arm"
437 done
438 # Lets profile analysis split by capture order (see sandbox-e2e.mjs).
439 echo "${ profOrder }" > /vercel/sandbox/prof-order.txt
440 cd /vercel/sandbox && tar -czf profiles.tgz prof-*`
441 try {
442 await sbExec (vm, '40m' , prof, `${ tag }:prof` )
443 const profTgz = path. join (outDir, `profiles-vm${ index }.tgz` )
444 await sb ([ 'cp' , `${ vm }:/vercel/sandbox/profiles.tgz` , profTgz])
445 if ( ! fs. existsSync (profTgz) || fs. statSync (profTgz).size === 0 ) {
446 throw new Error ( 'profile tarball missing or empty after cp' )
447 }
448 const vmProfDir = path. join (outDir, `prof-vm${ index }` )
449 fs. mkdirSync (vmProfDir, { recursive: true })
450 await execFileP ( 'tar' , [ '-xzf' , profTgz, '-C' , vmProfDir])
451 console. error ( `${ tag }: profiles in ${ vmProfDir }` )
452 } catch (profErr) {
453 console. error (
454 `${ tag }: profile capture failed (timed results unaffected): ${ profErr . message }`
455 )
456 }
457 }
458 writeStatus ({
459 vms: {
460 ... status.state.vms,
461 [vm]: { ... status.state.vms[vm], state: 'done' },
462 },
463 })
464 return local
465 } finally {
466 if ( ! cfg.keep) await rmVm (vm)
467 else console. error ( `${ tag }: kept ${ vm }` )
468 }
469 }
470
471 export const SSR_METRICS = [
472 'rps' ,
473 'mean' ,
474 'median' ,
475 'p95' ,
476 'p99' ,
477 'gcMs' ,
478 'heapMb' ,
479 ]
480
481 function analyze ( cfg , outDir ) {
482 const db = openDb ()
483 const { runId , samples , artifacts } = importRun (db, outDir)
484 const { failures , notes } = verifyDb (db, runId)
485 for ( const n of notes) console. log ( `note: ${ n }` )
486 if (failures. length ) {
487 throw new Error ( `results db verify FAILED: \n ${ failures . join ( ' \n ' ) }` )
488 }
489 console. error (
490 `results db: ${ samples } samples, ${ artifacts } artifacts as ${ runId } (verify ok)`
491 )
492 const rows = loadRows (db, runId)
493 const [ base , cand ] = cfg.arms. map (( a ) => a.name)
494 printRunContext (( m ) => console. log (m), cfg.runContext)
495 console. log ( `fixture: ${ cfg . fixtureSha . slice ( 0 , 12 ) } (${ cfg . fixtureRef })` )
496 analyzeE2eRows (rows, base, cand, SSR_METRICS )
497 }
498
499 // ----------------------------------------------------------------- main
500
501 async function plan ( cfg ) {
502 const lines = []
503 lines. push (
504 `mode: ${ cfg . pr ? `react PR ${ cfg . pr } (ssr fixture suite)` : 'react A/B (ssr fixture suite)'}`
505 )
506 lines. push (
507 `scope: team=${ CONFIG . team ?? '<UNSET — ask user, then: node config.mjs set team=... project=...>'} project=${ CONFIG . project ?? '<UNSET>'}`
508 )
509 try {
510 const arms = await resolveArms (cfg)
511 for ( const arm of arms) {
512 const cached = fs
513 . readdirSync ( CACHE )
514 . some (( f ) => f. startsWith ( `arm-${ arm . sha . slice ( 0 , 12 ) }` ))
515 lines. push (
516 `arm ${ arm . name }: react=${ arm . sha . slice ( 0 , 12 ) } (${ cached ? 'build cached' : 'would build remotely ~15m'})`
517 )
518 lines. push (
519 ` CI gate: react commit must be CI-green (or --allow-ungated after sandbox-gate.mjs)`
520 )
521 }
522 lines. push (
523 `fixture: ${ FIXTURE_DIR } @ ${ cfg . fixtureSha . slice ( 0 , 12 ) } (pinned, both arms)`
524 )
525 } catch (e) {
526 lines. push (
527 `arms: unresolved in dry-run (${ e . message . split ( ' \n ' )[ 0 ]. slice ( 0 , 120 ) })`
528 )
529 }
530 lines. push (
531 `then: ssr snapshot (cached by content key; ~20m if cold) -> ` +
532 `${ cfg . vms } VMs x ${ cfg . runs } paired ABBA runs, 8 variants x (inject + server c=1/c=10)`
533 )
534 lines. push (
535 `then: boot-level analysis (n=${ cfg . vms } boots) -> claims at p<0.01 on A/A-validated infra`
536 )
537 console. log (lines. map (( l ) => `[dry-run] ${ l }` ). join ( ' \n ' ))
538 }
539
540 const cfg = parseArgs ()
541 if (cfg.dryRun) {
542 await plan (cfg)
543 process. exit ( 0 )
544 }
545 fs. mkdirSync ( CACHE , { recursive: true })
546 const outDir = path. join ( CACHE , `run-${ cfg . label }-${ Date . now (). toString ( 36 ) }` )
547 fs. mkdirSync (outDir, { recursive: true })
548 console. log ( `run dir: ${ outDir }` )
549 status.file = path. join (outDir, 'status.json' )
550 const digest = setInterval (() => {
551 const vms = Object. values (status.state.vms ?? {})
552 const rows = vms. reduce (( a , v ) => a + (v.rows ?? 0 ), 0 )
553 const states = {}
554 for ( const v of vms) states[v.state] = (states[v.state] ?? 0 ) + 1
555 const vmSummary = Object. entries (states)
556 . map (([ k , n ]) => `${ n } ${ k }` )
557 . join ( ', ' )
558 console. log (
559 `progress: ${ status . state . phase }` +
560 (status.state.rowsExpected
561 ? ` — rows ${ rows }/${ status . state . rowsExpected }`
562 : '' ) +
563 (vmSummary ? ` (${ vmSummary })` : '' )
564 )
565 }, 120_000 )
566 digest. unref ?.()
567 writeStatus ({
568 label: cfg.label,
569 phase: 'resolving arms' ,
570 pid: process.pid,
571 startedAt: new Date (). toISOString (),
572 vms: {},
573 rowsExpected: null ,
574 })
575 try {
576 cfg.arms = await resolveArms (cfg)
577 // Human context for the analysis header and meta.json.
578 const num = String (cfg.pr ?? '' ). match ( /( \d + ) \/ ?$ / )?.[ 1 ]
579 let pr
580 if (num) {
581 pr = { url: `https://github.com/${ REACT_GH_REPO }/pull/${ num }` }
582 try {
583 pr. title = (
584 await execFileP ( 'gh' , [
585 'api' ,
586 `repos/${ REACT_GH_REPO }/pulls/${ num }` ,
587 '--jq' ,
588 '.title' ,
589 ])
590 ).stdout. trim ()
591 } catch {}
592 }
593 cfg.runContext = {
594 pr,
595 arms: [],
596 }
597 for ( const a of cfg.arms) {
598 cfg.runContext.arms. push ({
599 name: a.name,
600 title: await commitTitle ( REACT_REPO_LAZY (), a.sha),
601 })
602 }
603 printRunContext (( m ) => console. error (m), cfg.runContext)
604 for ( const arm of cfg.arms) {
605 await assertCiGreen (arm.sha, arm.name, cfg.allowUngated)
606 await ensureRefArm (arm)
607 }
608 fs. writeFileSync (
609 path. join (outDir, 'meta.json' ),
610 JSON . stringify (
611 {
612 base: cfg.arms[ 0 ].name,
613 cand: cfg.arms[ 1 ].name,
614 label: cfg.label,
615 suite: 'ssr' ,
616 fixtureRef: cfg.fixtureRef,
617 fixtureSha: cfg.fixtureSha,
618 fixtureTouchedByPr: cfg.fixtureTouchedByPr || undefined ,
619 vms: cfg.vms,
620 blocks: 1 ,
621 runs: cfg.runs,
622 pr: cfg.runContext.pr,
623 arms: cfg.runContext.arms,
624 },
625 null ,
626 2
627 )
628 )
629 cfg.live = makeLive (
630 cfg.arms[ 0 ].name,
631 [ 'median' , 'rps' ],
632 ( r ) => `${ r . route } ${ r . phase }`
633 )
634 writeStatus ({
635 phase: 'building ssr snapshot' ,
636 arms: cfg.arms. map (( a ) => `${ a . name }=${ a . sha . slice ( 0 , 12 ) }` ),
637 })
638 const snap = await ensureSsrSnapshot (cfg)
639 writeStatus ({ phase: 'measuring' , snap })
640 await Promise . all (
641 Array. from ({ length: cfg.vms }, ( _ , i ) => runVm (i, cfg, snap, outDir))
642 )
643 writeStatus ({ phase: 'analyzing' })
644 console. error ( `results in ${ outDir }` )
645 analyze (cfg, outDir)
646 writeStatus ({ phase: 'done' })
647 } catch (err) {
648 writeStatus ({
649 phase: 'failed' ,
650 error: String ((err && err.message) || err). slice ( 0 , 500 ),
651 })
652 throw err
653 } finally {
654 clearInterval (digest)
655 }