Merge Phase 6 and reconcile offline completion, response capture and contract tests

This commit is contained in:
Jonathan Sykes
2026-10-09 15:12:14 +08:00
62 changed files with 229652 additions and 43 deletions

View File

@@ -0,0 +1,137 @@
# Phase 6 investigation
All comparisons use the current instrumented harness with matching frontend and
server from detached worktrees beneath `perf/.tmp`. No production requests.
Command template (three fresh contexts):
```
node perf/baseline.mjs --runs 3 --browser webkit --profile lte --scenario cold --source-root perf/.tmp/phase6-<variant> --out perf/results/phase6-<variant>.json
```
| Source | Commit | LTE FCP median ms | Wire bytes | app.js positive responses/sample |
|---|---|---:|---:|---:|
| Phase 2 | 4950d3e | 1044 | 1191063 | 2 |
| Immediately before lazy layouts | d98d444 | 1032 | 1191686 | 2 |
| First lazy-layout cut | 5d6a154 | 1615 | 1106796 | 2 |
| Phase 3 tip | cd4a25a | 1646 | 1108191 | 2 |
| Phase 5 tip (same product as Phase 4) | 21c1273 | 1736 | 1097007 | 2 |
| Phase 5 + worker force-cache | scratch | 1745 | 1097006 | 2 |
| Phase 5 + parser-owned classic CSS | scratch | 898 | 1097051 | 2 |
| Phase 5 + classic preload | scratch | 909 | 1097032 | 2 |
The adjacent-commit measurements isolate `5d6a154`, not the Phase 4 extraction.
All three FCP samples, complete proxy waterfalls, and page Resource Timing are
stored in each result. The first sample often includes startup compression cost;
it is retained in every median, not discarded.
Phase 3 sample 2: common styles finish around 880 ms. The parser-written classic
stylesheet is discovered at 416 ms but reaches the proxy only at 1434 ms and
finishes at 1607 ms. FCP is 1646 ms. Phase 5 sample 2: classic reaches the proxy
at 1540 ms, finishes at 1711 ms, FCP is 1736 ms. Speculatively discovered body
scripts already occupy the request queue when the selected stylesheet arrives.
Phase 2 discovers its declarative styles together around 231 ms, FCP 1044 ms.
The first service-worker request is at 2458 ms in Phase 3 and 2337 ms in Phase 5,
AFTER paint. Worker install competition does not explain this FCP regression.
Font preload URLs match the font CSS; late layout CSS is the final rendering
barrier. Making classic CSS declarative again removes the delay without changing
CSS bytes or cascade order. This scratch variant unconditionally applies classic
and is diagnostic only; the shipping fix must retain selected-layout behavior.
The modern synchronizer already uses default fetch caching. Changing it to
force-cache has no effect: every WebKit sample still transfers app.js twice.
The page and worker cannot reuse these HTTP-cache responses in this harness.
The next hypothesis was a same-build page handoff reusing the page HTTP cache.
The prototype checked both the X-Asset-Hash header and decoded-body SHA-256, and
stripped compressed transport headers before constructing the cached Response.
Updates and legacy migration must continue through their existing worker path.
The shipping CSS fix is a preload, not an unconditional stylesheet. Its three-run
FCP is 909 ms; the first classic request reaches the proxy at 250 ms and completes
at 423 ms. Lazy still applies only the saved layout, in the original cascade order.
Other selected layouts may speculatively download the small classic stylesheet;
it never applies or blocks their rendering. No inline script or CSP change.
The page-to-worker prototype successfully delivered bodies, but still transferred
app.js twice. `phase6-handoff-mode.json` records the two requests: identical URL,
`Accept-Encoding: gzip, deflate`, no-cors mode and immutable response, but script
versus empty request destinations. Force-cache in the page did not reuse the
script body's cache either. This prototype was discarded, not shipped.
A fetch preload plus a later fetch also failed the one-body assertion. The
shipping fix instead starts one cache-first fetch promise in the head loader.
The final eager app-bootstrap consumes that response, verifies header AND SHA-256,
and caches it before executing unchanged app.js as a classic script. A specific
server-generated CSP hash authorizes only those exact bytes; unsafe-inline remains
absent. DOMContentLoaded boot runs exactly once; the temporary script node and
response promise are released. Native, insecure HTTP and rollback modes retain
external classic execution. Only server asset-manifest rendering changed; no
frontend imports, worker sync algorithm, update flow or playback source changed.
The initial owned-response trial passed with one app.js body, FCP 1040 ms and
981468 wire bytes (one fresh sample; not a final median). Five-run acceptance and
regression results will be recorded separately. Historical diagnostics retained
all samples. Full-harness configurations now reset to the base tree before each
browser/profile, preventing a preceding synthetic update from contaminating the
next cold build. The historical invalid media fixture remains unchanged: home
launch only, not autoplay/audio latency.
Self-review added explicit N-1 app boot after eviction: verify the retained body
and use its exact classic URL through the controlling worker. It never fills the
new key with old bytes. Stale core execution additionally requires every group
contract to match the running shell's expectations; a core or feature contract
change rejects it. Both-engine lazy smoke now changes app bytes, evicts the new
entry, boots N-1 offline, then repairs the current entry before the existing
feature pinning/contract/playback checks. No source in app.js changed.
The all-engine inline prototype's completed five-run Chromium/LTE profile had
cold boot 6748 ms, warm boot 458 ms (Phase 5: 410 ms), offline boot 204 ms, core
update 870 ms. This was a real warm regression, so the full sweep was interrupted
before accepting its results. Raw output: perf/.tmp/phase6-inline-chromium-profile.log.
The native-script controlled variant measured warm 414 ms over five samples.
Shipping capture is therefore restricted to WebKit (including iOS CriOS); other
engines preload and retain their native classic-script/code-cache path. Normal
native cache hits avoid decoding/rehashing; retained core fallback still enforces
all group contracts. The new worker marks stale responses no-store and strips
decoded transport headers, preventing browser resource-cache poisoning under a
new URL. CacheStorage's exact keys and previous entries stay unchanged.
The native-window contract lookup trial regressed Chromium warm boot (~698 ms).
It is rejected. The worker now owns the all-group stale app contract guard
(pure planner and actual worker VM tests first); native boot has no window-side
CacheStorage or hashing work. WebKit's byte-verified owned response path is
unchanged. The native fixture models the actual worker fallback decision instead
of a second page-side decision. All 221 frontend tests and 184 server tests pass;
both-engine lazy/offline/eviction/pinning/contracts/playback smoke passes.
The WebKit warm prototype also paid repeated handoff/cache/hash work (warm
~98 ms versus Phase 5 83 ms). Capture now applies only to uncontrolled WebKit
visits. Controlled warm/offline/update pages use the worker's native exact-URL
script path, including its strengthened contract guard. A failing head-loader
test precedes this change; all 222 frontend and 184 server tests and both-engine
lazy smoke pass. Cold capture and both rollback flags are unchanged.
The final native-path review caught a new Chromium paint regression: five-run
FCP 1616/2328 ms (LTE/lossy), despite faster boot. A contemporary Phase 5 LTE
control measured 1360 ms over three runs. Lowering the new app preload priority
reduced FCP to 1396 ms (three runs). Native parser execution restores app's
original DOMContentLoaded boot boundary. A rejected no-preload controlled-page
trial worsened WebKit warm boot to 98 ms; native cache preloading remains.
The standalone helper adds a CacheStorage script read to native startup. Packing
the loader into the existing final core script, section-rail.js, measured Chrome
warm 425 ms versus contemporary Phase 5 426 ms (five runs); WebKit measured
95 ms versus 88 ms, so zero warm overhead is not yet established. The merged
three-run Chrome LTE cold trial measured FCP 1384 ms and boot 6512 ms. Core's
contract becomes 2 because an old section script lacks this loader and cannot
safely substitute for it. No Phase 5 SHELL file is removed; the experimental
Phase 6 standalone helper is removed. Native-order, exact final-script packing
and contract tests precede implementation. All 223 frontend and 184 server tests
and both-engine lazy/offline/eviction/pinning/contracts/playback smoke pass.
The intermediate native-auto-priority full dataset is retained as a rejected
trial; it is not the final acceptance result. Source directories named in trial
JSONs are dirty diagnostic worktrees of 9408ee0 with exactly the changes above.

11
plans/phase6-plan.md Normal file
View File

@@ -0,0 +1,11 @@
# Phase 6 plan
Start from Phase 5 21c1273. Fix cold first-visit delivery only; preserve playback, banner, migration, exact hashes, contracts/pinning, offline and both rollback flags.
1. Extend perf/baseline.mjs/proxy.mjs with page Resource Timing and complete request waterfalls, and a source-worktree option. Create detached historical worktrees beneath perf/.tmp for Phase 2, 3 and 5 tips. Run isolated WebKit/LTE cold measurements, >=3 runs each. Bisect Phase 3 commits with the same instrumented harness. Test head-loader/CSS discovery, fonts, worker start relative to FCP and immutable HTTP-cache behavior with controlled scratch variants. Record actual evidence in plans/phase6-investigation.md.
2. Add failing tests before each root fix. Likely files: frontend/index.html, lazy.js or a tiny layout bootstrap; frontend/sw.js and asset-sync-core.js only as evidence warrants. Keep the legacy install path and update all-or-nothing behavior intact; cache completed immutable responses safely without any unverifiable body. Preserve selected layout before paint and existing CSS cascade order.
3. Commit each fix separately with frontend and isolated Bun server suites green. Revert individual fixes for rollback; ASSET_HASHING=0/ASSET_SYNC=0 stay supported. Do not deploy or contact production.
4. Run final five-run WebKit cold LTE and lossy medians: FCP <= measured Phase 2 (targets ~1,300/3,800 ms); app.js positive-body responses exactly one; cold wire clearly below baseline. Measure Chromium and warm/offline/core-JS/CSS/feature updates against Phase 5, with repeated medians where regression risk exists. Rerun migration both engines with and without stalls, hidden timers, lazy/seam offline checks, rollback paths and all tests.
5. Self-review master sections 3.2/3.3/6/6b and full diff. Commit <=350-word plans/phase6-report.md with actual before/after table and explicit unmet targets; overwrite DONE06 signal and end.
Risks: CSS cascade/no-flash preservation, duplicate requests crossing browser/worker HTTP-cache partitions, race between page handoff and install, timeout fallbacks delaying cache readiness, CPU contention invalidating timing. Historical worktrees/scratch data stay inside wt-assets; run benchmarks sequentially and preserve the invalid historical media fixture only for comparable home-launch metrics, not playback claims.

46
plans/phase6-report.md Normal file
View File

@@ -0,0 +1,46 @@
# Phase 6 report
Tested `4b1f520`, from Phase 5 `21c1273`. Adjacent-commit measurements isolate
`5d6a154`: WebKit/LTE FCP 1032→1615 ms. Its selected classic CSS arrived late
(1540–1711 ms); worker installation started after paint. Fonts matched their
preloads. Default/force-cache and HTTP-cache handoff trials still transferred
app.js twice.
Shipped early classic style discovery (`e7a7cdc`), a single verified page-owned
WebKit response and specific CSP hash (`a86fa1c`, refined through `9408ee0`),
worker-owned stale-core contract protection (`97eb31e`), and native parser boot
with low-priority app preloading (`4b1f520`). The loader shares the existing final
core script; core contract 2 prevents unsafe old packing fallback. app.js,
playback and sw-update.js are unchanged.
Five-run final medians:
| Engine/profile | Phase 5→6 FCP ms | Phase 5→6 cold bytes | app bodies |
|---|---:|---:|---:|
| WebKit/LTE | 1739→905 | 1097048→983326 | 2→1 |
| WebKit/lossy | 4303→2748 | 1105402→990152 | 2→1 |
| Chromium/LTE | 1356→1388 | 651012→653267 | 1→1 |
| Chromium/lossy | 2052→1976 | 651096→653267 | 1→1 |
WebKit paint, one-body and below-baseline wire targets pass (−16.6%/−16.1%
versus original wire baseline). Phase 2 measured 1044/4045 ms over three runs.
Strict zero regression is **not achieved**: Chrome/LTE FCP +32 ms, cold bytes
+0.35%, long tasks 278→304 ms; warm boots Chrome 410/403→418/442 and WebKit
83/85→88/89 ms. Metadata adds ~0.4 KB per core update. Added loader/security
work has a cost; complete timing attribution remains unproved. Contemporary
Phase 5 controls and all offline/update deltas appear in
[phase6-verification.md](phase6-verification.md), with actual command excerpts.
Chromium cold boot improves 7043/11342→6478/10314 ms.
Migration passes both engines with/without stalls; banner once, explicit legacy
refresh, old URLs, N-1, offline and playback guards. Lazy/layout/feature seams,
hidden timers, both rollback modes, 223 frontend/184 server/3 measurement tests
pass. No fonts/icons re-downloaded. ASSET_HASHING=0 and ASSET_SYNC=0 remain.
Real iPhone install/update, quota pressure and 30-minute locked-screen audio
require owner checks. The historical fixture measures home launch, not autoplay.
Post-crash verification retained in `6f0235f`: both-engine migrations, offline
layouts/features, rollback and cold smoke pass. WebKit smoke FCP 1253/2881 ms
(LTE/lossy), one app response each. Durable five-run results remain authoritative.

View File

@@ -0,0 +1,219 @@
# Phase 6 verification
Final product under test: `4b1f520` (Phase 5 parent `21c1273`). No production requests.
All historical source worktrees contain matching server and frontend revisions.
The complete diagnostic waterfalls and five-run acceptance samples are committed
under `perf/results/phase6-*.json`. KB in these tables means 1,000 bytes.
## Cause and controlled trials
See [phase6-investigation.md](phase6-investigation.md) for the three-run tip
comparisons, adjacent-commit bisect, rejected cache/handoff prototypes and fixes.
The regressing commit is `5d6a154`: LTE FCP 1,032 → 1,615 ms immediately across
that cut. Phase 2/3/5 medians are 1,044/1,646/1,736 ms. Phase 4 extraction did
not introduce the paint regression.
Representative WebKit/LTE sample 2, proxy times relative to tracking start:
phase6-before-phase5.json versus phase6-webkit-cold-2026-10-08.json (9408ee0
owned-response diagnostic, before final loader packing). Final medians follow.
| Request | Phase 5 start/end ms | Phase 6 start/end ms |
|---|---:|---:|
| lazy.js | 250/417 | 250/418 |
| styles.css | 251/570 | 251/570 |
| theme-classic.css | 1540/1711 | 250/421 |
| page app.js | 1524/2273 | 1552/2297 |
| first sw.js | 2337/2533 | 2367/2564 |
| worker app.js | 2903/3651 | absent |
| FCP | 1736 | 905 |
The stylesheet becomes available before the parser applies it. The worker starts
after paint. Default fetch caching was already in the modern synchronizer;
force-cache and a page-to-worker HTTP-cache handoff both still transferred two
app bodies in WebKit. A single page-owned response, verified and cached before
boot registers the worker, removes the redundant app transfer. Controlled pages
and other engines retain native external scripts; no window cache/hash lookup
is added to those paths. Each retained stale app requires all shell contracts
to match, enforced by the worker.
## Final measurements
Exact final command (120 samples: both engines, both profiles, six scenarios,
five fresh samples each):
```sh
node perf/baseline.mjs --runs 5 --browser all --profile all --scenario all --assert-cold --out perf/results/phase6-final-2026-10-08.json
```
Final app.js positive-byte responses are exactly one in all 20 cold samples.
The earlier separate cold/regression datasets used 9408ee0 and are diagnostics,
not the final acceptance build. `phase6-native-auto-priority-2026-10-08.json`
combines that rejected intermediate sweep. Final core contract is 2: the final
section-rail script now owns the loader; a stale pre-packing section script would
not load app.js. No Phase 5 SHELL file was deleted. app.js and sw-update.js remain
byte-for-byte unchanged.
Actual final command output excerpts:
```text
Chromium/LTE: run 4/5... done (653267 bytes, 115 reqs, fcp=1396ms, boot=6478ms)
Chromium/lossy: run 2/5... done (653267 bytes, 115 reqs, fcp=1972ms, boot=10292ms)
WebKit/LTE: run 2/5... done (983326 bytes, 158 reqs, fcp=905ms, boot=2335ms)
WebKit/lossy: run 2/5... done (990152 bytes, 161 reqs, fcp=2748ms, boot=5046ms)
```
WebKit FCP targets (≤1300/3800 ms) pass. Contemporary Phase 2 three-run medians:
1044/4045 ms; its lossy samples are 4045/3218/5297, all retained. Original baseline
WebKit cold wire is 1,179,624 bytes. Final LTE/lossy wire is 983,326/990,152 bytes
(16.6%/16.1% lower). The final cold set transfers app.js once in both engines.
## Complete final comparison
Five-run medians. Each cell is Phase 5 → Phase 6; bytes are exact body wire bytes.
| Browser/profile | Cold requests | Cold bytes | FCP ms | LCP ms | Boot ms | Long tasks ms |
|---|---:|---:|---:|---:|---:|---:|
| chromium/lte | 115 → 115 | 651012 → 653267 | 1356 → 1388 | 1356 → 1388 | 7043 → 6478 | 278 → 304 |
| chromium/lossy | 115 → 115 | 651096 → 653267 | 2052 → 1976 | 2052 → 1976 | 11342 → 10314 | 295 → 297 |
| webkit/lte | 159 → 158 | 1097048 → 983326 | 1739 → 905 | 1810 → 982 | 2315 → 2335 | 0 → 0 |
| webkit/lossy | 162 → 161 | 1105402 → 990152 | 4303 → 2748 | 4371 → 2821 | 5573 → 5046 | 0 → 0 |
WebKit does not expose the Long Tasks API; its zero fields mean unmeasured.
| Browser/profile | Warm boot ms | Offline boot ms | Offline reload ms | Core update ms/bytes | CSS update ms/bytes | Feature update ms/bytes |
|---|---:|---:|---:|---:|---:|---:|
| chromium/lte | 410 → 418 | 185 → 188 | 202 → 206 | 745/125795 → 872/126186 | 1380/35363 → 1388/35760 | 1378/33408 → 1392/33762 |
| chromium/lossy | 403 → 442 | 189 → 194 | 204 → 207 | 747/125795 → 805/126186 | 1377/35363 → 1383/35760 | 1375/33408 → 1386/33762 |
| webkit/lte | 83 → 88 | 86 → 88 | 100 → 102 | 1890/147259 → 2015/147626 | 1455/45659 → 1467/46254 | 1455/43318 → 1380/41860 |
| webkit/lossy | 85 → 89 | 81 → 85 | 95 → 98 | 4192/147259 → 2934/147626 | 3647/43705 → 3687/44211 | 3644/41364 → 3693/41860 |
## Limits and exact unmet parity targets
Strict zero regression versus the recorded Phase 5 run is NOT achieved.
Chromium/LTE FCP is 1388 versus 1356 ms (+32); cold bytes 653267 versus 651012
(+2255, 0.35%); long tasks 304 versus 278 ms. The verified loader, CSP permission,
contract checks and metadata add code/bytes; parser insertion also changes task
boundaries. The whole timing delta has not been causally isolated. Three-run
contemporary Phase 5 LTE medians were FCP 1368, boot 7090 and long tasks 294 ms.
Final Chrome boot improves to 6478/10314 ms (Phase 5 7043/11342).
Warm/offline/update deltas in the complete table remain explicit. Contemporary
five-run Phase 5 warm boots were Chrome 426/446 and WebKit 88/88 ms; final values
418/442 and 88/89 ms. Contemporary Phase 5 core-update times were Chrome 869/799
and WebKit 1955/2776 ms; final 872/805 and 2015/2934 ms. Metadata adds roughly
0.4 KB to core updates. Periodic lossy stalls depend on request order; timing
comparisons cannot establish exact parity. The extra CPU/cache work is small,
but zero cost was not proved, and the positive timing deltas are not dismissed.
Every final update passed assertChangedAssets: only app.js, theme-glass.css or
presenter.js is downloaded as application code, with index/API/worker metadata;
fonts/icons are not re-downloaded. Whole CSS-update wire remains above 30 KB,
a pre-existing Phase 5 gap. Readiness and completion remain separately measured.
Real iPhone Safari/PWA code-cache behavior, OPFS quota pressure, forced cache
eviction and uninterrupted locked-screen/background audio for 30 minutes were
not verified. The historical media fixture is invalid for autoplay: these are
home-launch metrics, not media-start measurements. No playback code changed.
Other selected layouts may prefetch 4079 unused classic CSS bytes; that preload
never applies, and the selected stylesheet retains its original cascade order.
## Compatibility verification on the final product
All commands below ran on 4b1f520, build 31617662d2f0:
```sh
node perf/migration.mjs --browser all --profile unthrottled --out perf/results/phase6-migration-unthrottled-2026-10-08.json
node perf/migration.mjs --browser all --profile lossy --out perf/results/phase6-migration-lossy-2026-10-08.json
node perf/hidden-timers.mjs
node perf/lazy.mjs --browser all --out perf/results/phase6-lazy-2026-10-08.json
node perf/seams.mjs --browser all
ASSET_SYNC=0 node perf/baseline.mjs --runs 1 --browser all --profile lte --scenario offline --out perf/results/phase6-sync-rollback-2026-10-08.json
ASSET_HASHING=0 node perf/baseline.mjs --runs 1 --browser all --profile lte --scenario offline --out perf/results/phase6-hashing-rollback-2026-10-08.json
node --test frontend/*.test.js
node --test perf/measurement.test.mjs
(cd server && bun install && PATH="$PWD/../perf/.tmp/test-venv/bin:$PATH" bun run test)
```
Actual output excerpts:
```text
Migration unthrottled: chromium banners=1 updateReloads=1 cachedFiles=122 userInitiatedOnly=true offline=true playbackGuard=true evictionRepair=true
Migration unthrottled: webkit banners=1 updateReloads=1 cachedFiles=122 userInitiatedOnly=true offline=true playbackGuard=true evictionRepair=true
Migration lossy: chromium durationMs=25233 banners=1 updateReloads=1 legacyURLs=21 userInitiatedOnly=true offline=true
Migration lossy: webkit durationMs=22860 banners=1 updateReloads=1 legacyURLs=21 userInitiatedOnly=true offline=true
Hidden timers: Page errors: none; Result: ALL ASSERTIONS PASSED
Lazy: chromium PASS; webkit PASS
Seams: chromium checked=[shell,presenter,remote,party,share,stats] offlineNetworkRequests=0 errors=[]
Seams: webkit checked=[shell,presenter,remote,party,share,stats] offlineNetworkRequests=0 errors=[]
Frontend: tests 223; pass 223; fail 0; skipped 0
Measurement: tests 3; pass 3; fail 0
Server: sum of per-file output: 184 pass; 0 fail
```
Both rollback modes launched offline with zero network in Chromium and WebKit.
The archived b77938a fixture was rebuilt by the migration script each time.
Legacy reload remains user-initiated; its pre-existing Refresh UI timer exception
is unchanged. New-page playback guards, old-client readiness, one banner, old
plain/?v=oldTag/?__ytpfresh URLs, already-open old tabs and cached offline reload
remain covered. Four expected WebKit disconnected API transport errors per
migration were recorded and matched to failed API requests; no script errors.
The seam harness's historical Phase 4 artifact was restored after saving the
new output as phase6-seams-2026-10-08.json.
Self-review re-read the complete product diff and master §§3.2/3.3/6/6b.
No frontend import was added to server code; no original shell file was removed;
no auto skipWaiting, live stateful re-evaluation, playback edit, push or deploy.
Cache responses are exact-key checked; stale bodies under a new URL are no-store
and carry no decoded transport encoding. Core contract 2 blocks loader-free N-1
packing, while same-contract app fallback and executed feature pinning remain
covered by both-engine smoke. Failed quota writes never delete saved music.
## Post-crash completion — 2026-10-09
Resumed from ff3f934 on codex/phase6. Commit 71608c2 clarifies the diagnostic
waterfall provenance; product code and served build 31617662d2f0 are unchanged.
The durable five-run/120-sample final measurements above were preserved.
Reinstalled Playwright browsers passed fresh compatibility checks:
```sh
node perf/migration.mjs --browser all --profile unthrottled --out perf/results/phase6-migration-unthrottled-2026-10-09.json
node perf/migration.mjs --browser all --profile lossy --out perf/results/phase6-migration-lossy-2026-10-09.json
node perf/hidden-timers.mjs
node perf/lazy.mjs --browser all --out perf/results/phase6-lazy-2026-10-09.json
node perf/seams.mjs --browser all
ASSET_SYNC=0 node perf/baseline.mjs --runs 1 --browser all --profile lte --scenario offline --out perf/results/phase6-sync-rollback-2026-10-09.json
ASSET_HASHING=0 node perf/baseline.mjs --runs 1 --browser all --profile lte --scenario offline --out perf/results/phase6-hashing-rollback-2026-10-09.json
node --test frontend/*.test.js
node --test perf/measurement.test.mjs
(cd server && bun install && PATH="$PWD/../perf/.tmp/test-venv/bin:$PATH" bun run test)
```
Fresh output excerpts (all commands exited successfully):
```text
Migration unthrottled: chromium durationMs=13290; webkit durationMs=11711
Migration lossy: chromium durationMs=27355; webkit durationMs=24426
All four: banners=1 updateReloads=1 cachedFiles=122 legacyURLs=21
All four: userInitiatedOnly=true offline=true playbackGuard=true evictionRepair=true incrementalRefresh=true
Hidden timers: Page errors: none; Result: ALL ASSERTIONS PASSED
Lazy: chromium PASS; webkit PASS
Seams: chromium and webkit checked=[shell,presenter,remote,party,share,stats] offlineNetworkRequests=0 errors=[]
Both rollback flags, both engines: requestCount=0 wireBytes=0
Frontend: tests 223; pass 223; fail 0; skipped 0
Measurement: tests 3; pass 3; fail 0
Server: per-file totals 184 pass; 0 fail
```
The seam output is saved as phase6-seams-2026-10-09.json; the historical Phase 4
result was restored. Offline API transport errors remain distinguished from
script failures. No playback or sw-update.js changes were added. bun.lock is
excluded from commits.
Post-crash cold smoke command:
```sh
node perf/baseline.mjs --runs 1 --browser webkit --profile all --scenario cold --assert-cold --out perf/results/phase6-cold-smoke-2026-10-09.json
```
LTE/lossy FCP was 1253/2881 ms, wire 983326/990152 bytes; both samples passed exactly one positive-byte app.js response. These single-run rechecks do not replace the durable five-run medians.