Files
ytplayer/plans/perf-final.md

159 lines
18 KiB
Markdown
Raw Permalink Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

# Final performance and regression sweep
Phase 5 starts from Phase 4 f133e1b. The full run uses measurement commit a08d363 and served base build c3dbdd7039db. Product frontend/server bytes are unchanged in this phase. The final JSON records its actual source commit and UTC start time; the filename uses the local 2026-10-08 date.
Sources: perf/results/baseline-2026-10-07.json, after-phase0-2026-10-07.json and final-2026-10-08.json. The first two contain five runs for Chromium/WebKit × LTE/lossy. Their recorded commits are 64f8eae and 7bc5b4f, respectively (the older prose baseline labels b77938a; this report uses the JSON metadata). Numbers below are medians of actual runs, compressed HTTP response-body bytes, not raw source sizes or header-inclusive packet captures. WebKit generally receives gzip; Chromium receives Brotli. Do not compare their absolute byte totals as equivalent compression.
## Commands and measurement boundaries
```bash
node --test frontend/*.test.js
(cd server && bun install && PATH="$PWD/../perf/.tmp/test-venv/bin:$PATH" bun run test)
node --test perf/measurement.test.mjs
node perf/hidden-timers.mjs
node perf/lazy.mjs --browser all --out perf/results/phase5-lazy-2026-10-08.json
node perf/seams.mjs --browser all
node perf/migration.mjs --browser all --profile unthrottled \
--out perf/results/phase5-migration-unthrottled-2026-10-08.json
node perf/migration.mjs --browser all --profile lossy \
--out perf/results/phase5-migration-lossy-2026-10-08.json
node perf/baseline.mjs --runs 5 --browser all --profile all --scenario all \
--out perf/results/final-2026-10-08.json \
--compare perf/results/baseline-2026-10-07.json
```
The full measurement runs alone, after other browser/build/test processes finish. Chromium uses 4× CPU slowdown for launch scenarios and CDP network shaping; WebKit uses a paced proxy and stalls every seventh lossy request. Update scenarios do not use the launch CPU throttle. Chromium CDP shaping targets the page; the harness does not separately attach CDP to the service worker, so its update durations must not be treated as a calibrated mobile-worker throughput measurement. WebKit proxy shaping covers worker responses as well. Initial navigation is `/?v=benchmedia01`. Static inspection and a Node regex check confirm this ID has 12 characters and fails the existing 11-character media-ID guard in both b77938a and the final build. The query is ignored; these are home-launch measurements and Share/External is not loaded by this invalid deep link. `bootDone` is the first cards view, not a validated playing-ready milestone.
Two measurement corrections are deliberately included. Warm reloads wait for all verified idle assets before resetting counters; Phase 3/4 warm runs counted unfinished first-install downloads. CSS updates now include activation and the inactive Glass stylesheet's verified idle download; historical Phase 3/4 CSS totals stopped at blocking readiness. Final `readyWireBytes` and `readyDuration` preserve the separate readiness snapshot. Feature updates use the same complete-cache boundary. Core app.js is blocking, so its waiting-readiness boundary already includes the changed payload. Auto-update checks can begin during the server restart before the explicit registration.update timestamp; very short duration outliers (including a 45 ms Chromium LTE core run) are retained, and medians should not be interpreted as complete deploy-to-user latency. Each update asserts that only the edited application file transfers; worker checks/imports and derived index/manifest remain counted overhead. No fonts/icons may be re-downloaded.
The harness creates synthetic N+1 comment/rule trees. Later profile cold scenarios inherit the last synthetic update tree, as in the original harness; executable behavior is identical, but its derived tag and a comment in presenter.js differ from the reported base tag. The run records one response log per scenario alongside all five aggregate samples. Long-task totals cover the entire cold observation window, not only boot. The media timeout options are historically passed as the predicate argument, so the nominal eight-second check uses Playwright's default timeout; this boundary was retained for historical comparison. Media-ready null/autoplayBlocked does not establish playback performance: the invalid fixture ID prevents the intended playback attempt, and the flag cannot distinguish that from an autoplay rejection. This pre-existing harness flaw was retained for an honest comparison with the stored baseline; a separate valid-ID playback experiment is still required.
## Five-run medians: baseline → after Phase 0 → final
All byte columns are bytes; all timings are ms. WebKit long-task timing is unavailable.
| Browser/profile | Scenario | Requests | Wire bytes | FCP | LCP | Boot-done | Long tasks |
|---|---|---:|---:|---:|---:|---:|---:|
| chromium/lte | cold | 164 → 166 → 115 | 1,039,315 → 1,040,158 → 651,012 | 1,540 → 1,572 → 1,356 | 1,896 → 1,916 → 1,356 | 10,163 → 10,480 → 7,043 | 397 → 454 → 278 |
| chromium/lte | warm | 4 → 4 → 4 | 5,321 → 5,331 → 7,863 | 180 → 192 → 120 | 684 → 704 → 140 | 675 → 692 → 410 | 156 → 169 → 58 |
| chromium/lte | offline | 0 → 0 → 0 | 0 → 0 → 0 | 72 → 80 → 52 | 108 → 80 → 52 | 219 → 259 → 185 | — → — → — |
| chromium/lossy | cold | 164 → 166 → 115 | 1,039,309 → 1,040,225 → 651,096 | 2,344 → 2,372 → 2,052 | 2,912 → 2,940 → 2,052 | 16,015 → 16,408 → 11,342 | 432 → 480 → 295 |
| chromium/lossy | warm | 4 → 4 → 3 | 5,326 → 5,332 → 7,858 | 184 → 192 → 136 | 644 → 780 → 136 | 651 → 768 → 403 | 155 → 195 → 59 |
| chromium/lossy | offline | 0 → 0 → 0 | 0 → 0 → 0 | 80 → 76 → 60 | 92 → 76 → 60 | 237 → 241 → 189 | — → — → — |
| webkit/lte | cold | 162 → 164 → 159 | 1,179,624 → 1,180,749 → 1,097,048 | 1,088 → 1,100 → 1,739 | 1,287 → 1,303 → 1,810 | 3,201 → 3,290 → 2,315 | unavailable |
| webkit/lte | warm | 0 → 0 → 0 | 0 → 0 → 0 | 110 → 117 → 78 | 110 → 146 → 78 | 131 → 139 → 83 | unavailable |
| webkit/lte | offline | 0 → 0 → 0 | 0 → 0 → 0 | 107 → 113 → 56 | 107 → 144 → 84 | 128 → 137 → 86 | unavailable |
| webkit/lossy | cold | 162 → 164 → 162 | 1,179,624 → 1,180,749 → 1,105,402 | 3,557 → 3,738 → 4,303 | 3,735 → 3,905 → 4,371 | 7,303 → 7,305 → 5,573 | unavailable |
| webkit/lossy | warm | 0 → 0 → 0 | 0 → 0 → 0 | 111 → 110 → 54 | 112 → 110 → 83 | 131 → 128 → 85 | unavailable |
| webkit/lossy | offline | 0 → 0 → 0 | 0 → 0 → 0 | 109 → 113 → 75 | 119 → 113 → 78 | 129 → 132 → 81 | unavailable |
## Update medians: baseline → after Phase 0 → final
Feature/CSS final totals include the changed background file after explicit activation. Historical full-shell updates included every file before waiting readiness.
| Browser/profile | Edited file | Requests | Complete wire bytes | Duration | Final CSS blocking-ready bytes |
|---|---|---:|---:|---:|---:|
| chromium/lte | app.js | 88 → 89 → 8 | 610,965 → 611,338 → 125,795 | 2,013 → 2,155 → 745 | — |
| chromium/lte | theme-glass.css | 88 → 89 → 8 | 611,005 → 611,476 → 35,363 | 1,970 → 2,107 → 1,380 | 30,263 |
| chromium/lte | presenter.js | — → — → 8 | — → — → 33,408 | — → — → 1,378 | — |
| chromium/lossy | app.js | 88 → 89 → 8 | 610,965 → 611,338 → 125,795 | 2,031 → 2,115 → 747 | — |
| chromium/lossy | theme-glass.css | 88 → 89 → 8 | 611,005 → 611,476 → 35,363 | 2,004 → 2,116 → 1,377 | 30,263 |
| chromium/lossy | presenter.js | — → — → 8 | — → — → 33,408 | — → — → 1,375 | — |
| webkit/lte | app.js | 86 → 87 → 5 | 680,250 → 680,822 → 147,259 | 4,457 → 4,584 → 1,890 | — |
| webkit/lte | theme-glass.css | 86 → 87 → 8 | 680,265 → 680,831 → 45,659 | 4,510 → 4,550 → 1,455 | 28,713 |
| webkit/lte | presenter.js | — → — → 8 | — → — → 43,318 | — → — → 1,455 | — |
| webkit/lossy | app.js | 86 → 87 → 5 | 680,250 → 680,822 → 147,259 | 8,135 → 10,017 → 4,192 | — |
| webkit/lossy | theme-glass.css | 87 → 87 → 7 | 680,327 → 680,831 → 43,705 | 8,269 → 10,013 → 3,647 | 28,713 |
| webkit/lossy | presenter.js | — → — → 7 | — → — → 41,364 | — → — → 3,644 | — |
## Targets and response evidence
| Browser/profile | Cold initial JS+CSS | Cold app.js positive-byte responses | Core-edit app.js payload | CSS-edit stylesheet payload | Feature-edit presenter payload |
|---|---:|---:|---:|---:|---:|
| chromium/lte | 210,419 | 1 | 95,503 | 5,100 | 3,118 |
| chromium/lossy | 210,419 | 1 | 95,503 | 5,100 | 3,118 |
| webkit/lte | 255,200 | 2 | 118,544 | 5,939 | 3,587 |
| webkit/lossy | 255,200 | 2 | 118,544 | 5,939 | 3,587 |
Actual full-harness output excerpt:
```text
=== YTPlayer Performance Baseline Harness ===
Runs per scenario: 5
Browser target: all
Network profile: all
Scenario target: all
Bun server running on ephemeral port 5089
>>> Running Benchmark: Browser=[chromium] Profile=[lte]
>>> Running Benchmark: Browser=[chromium] Profile=[lossy]
>>> Running Benchmark: Browser=[webkit] Profile=[lte]
>>> Running Benchmark: Browser=[webkit] Profile=[lossy]
Baseline results saved to: perf/results/final-2026-10-08.json
| Update N->N+1 (1-line JS) | 8 | 122.8 KB | - | 745ms |
| Update N->N+1 (1-line CSS)| 8 | 34.5 KB | - | 1380ms |
| Update N->N+1 (1-line JS) | 8 | 122.8 KB | - | 747ms |
| Update N->N+1 (1-line CSS)| 8 | 34.5 KB | - | 1377ms |
| Update N->N+1 (1-line JS) | 5 | 143.8 KB | - | 1890ms |
| Update N->N+1 (1-line CSS)| 8 | 44.6 KB | - | 1455ms |
| Update N->N+1 (1-line JS) | 5 | 143.8 KB | - | 4192ms |
| Update N->N+1 (1-line CSS)| 7 | 42.7 KB | - | 3647ms |
```
chromium/lte: cold wire bytes -37.4% vs after Phase 0; boot -32.8%; FCP -13.7%. Complete CSS update 35,363 bytes: FAIL against <30,000-byte target.
chromium/lossy: cold wire bytes -37.4% vs after Phase 0; boot -30.9%; FCP -13.5%. Complete CSS update 35,363 bytes: FAIL against <30,000-byte target.
webkit/lte: cold wire bytes -7.1% vs after Phase 0; boot -29.6%; FCP +58.1%. Complete CSS update 45,659 bytes: FAIL against <30,000-byte target.
webkit/lossy: cold wire bytes -6.4% vs after Phase 0; boot -23.7%; FCP +15.1%. Complete CSS update 43,705 bytes: FAIL against <30,000-byte target.
The earlier matched-delivery Phase 0 frontend reconstruction (perf/results/phase3-phase0-initial-2026-10-08.json, one run per browser) measured 276,414 Chromium / 338,940 WebKit initial JS+CSS bytes. Final five-run medians are 210,419 / 255,200: reductions of 23.9% / 24.7%. The historical five-run Phase 0 JSON did not collect this initial-only metric, so this comparison is explicitly a single-run reconstruction against repeated final measurements.
## What the phases contributed
- Phase 0 added characterization tests and suspended five hidden UI timers. It did not change shell/update behavior; its small launch timing changes are measurement noise rather than a claimed speed gain.
- Phase 1 supplied file hashes, a deterministic manifest, per-file stamps, hash headers and current/stale cache rules while retaining the old client update path. Its compatibility checks are prerequisites for Phase 2; alone it does not stop the legacy worker's complete shell download.
- Phase 2 made downloads resumable and incremental with hash validation, N-1 retention, atomic blocking commit and playback-guarded application. Fonts/icons no longer transfer on a code-only update. Legacy pages still reload only after their own user-initiated Refresh UI and retain that timer limitation.
- Phase 3 deferred inactive layouts and uncommon feature parsing, reads persisted layout before body paint, warms all groups at idle unless Save-Data, and permits same-contract N-1 fallback. Eager playlist/settings/service/editor stay in RAM. Executing groups remain pinned; incompatible new groups stay on N-1 and require a guarded reload.
- Phase 4 reduced app.js from 587,029 to 408,473 raw bytes: 178,556 bytes (30.4169%). Shell/shared views/Settings/rail remain eager; Presenter, Remote, Watch party, Share/External and Stats view are lazy chunks. The protected player, queue, persistence, boot, sleep/drift/transition bodies stayed intact. A presenter edit now transfers its chunk plus update overhead, rather than app.js or the shell.
- Phase 5 corrected misleading measurement boundaries, re-ran regression verification and supplied rollout instructions. It introduces no product features or changes to playback/banner behavior.
## Regression sweep evidence
Actual command-output excerpts:
```text
node --test frontend/*.test.js:
pass 198; fail 0; skipped 0
isolated Bun server suites:
183 pass; 0 fail; 0 skip
node --test perf/measurement.test.mjs:
pass 2; fail 0
node perf/hidden-timers.mjs:
Hidden intervals (2): anon120000, maybeCheck
Restored visible intervals (7): anon120000, checkBuildTag, chips, flush, maybeCheck, reanchor, rotateChips
Page errors: none
Result: ALL ASSERTIONS PASSED
node perf/seams.mjs --browser all:
chromium {"checked":["shell","presenter","remote","party","share","stats"],"offlineNetworkRequests":0,"errors":[]}
webkit {"checked":["shell","presenter","remote","party","share","stats"],"offlineNetworkRequests":0,"errors":[]}
```
Both lazy-browser results verify pre-body layout selection; immediate offline reload; warmed feature use without network; Glass/Bento/F7/classic offline switching; eager Settings and optional search panels; stale fallback; pinned executing groups; compatible unexecuted new URL; incompatible N-1 with reload-required; and update-during-playback guard. Seam tests stop the server and block the proxy. They test cached feature UI/local actions, not successful offline network relay/remote-party transport.
Both migration profiles use actual b77938a frontend/server at origin N, then the current server at the same origin. Each browser reports one banner, one user-initiated reload, 122 cached legacy-shell entries, 21 correct-body old plain/stale/fresh URL checks, open-old-tab compatibility, repair after eviction and offline launch. Unthrottled durations were Chromium 10,954 ms/WebKit 10,056 ms; stalling durations 25,453/23,132 ms. WebKit recorded four disconnected API transport errors per migration and no script errors; matching failed version/recommendation requests are required before these errors are classified as expected. No automatic install/activate reload is allowed. See the tracked phase5-migration JSON files for full output.
The hashing/sync rollback paths were verified in Phase 4 on both browsers with zero-request offline reloads; Phase 5 documents their exact use and preserves the corresponding server tests. No additional product regression was fixed during this sweep. CSS/FCP/double-download gaps below are not represented as passing acceptance targets.
## Remaining gaps and hot spots
The final tables above show that Chromium launch/long-task and incremental-payload goals passed, but every CSS total exceeds the budget, WebKit first paint regresses, and WebKit still double-downloads app.js at first install. These numerical acceptance requirements are not all met; do not promote the rollout as fully validated. The CSS <30 KB budget includes worker/index/manifest overhead; counting only the changed stylesheet or only blocking readiness would conceal the actual update total. Linux WebKit still does not share page/SW first-install bytes as effectively as Chromium; check the duplicate app.js bodies in its cold response log. The earlier Phase 3 WebKit first-paint regression remains an explicit release concern rather than a claimed fix.
Navigation still rebuilds sidebar/list DOM; splitting its definitions does not make rendering incremental. Measure large libraries before changing it. The thumbnail cap is still 25,000 entries, trimmed with cache.keys every 100 puts; CacheStorage shares quota with OPFS saved audio, and opaque fallback entries can have large quota padding. No cap change was authorized in these phases. Manifest/version/SW checks remain update overhead, and inactive layouts/features still download at idle on mobile data unless Save-Data.
SPA `/`, `/index.html`, `/search`, `/playlist/x` return the same stamped index (server tests pass). Static inspection confirms that its relative app.js resolves to /playlist/app.js on /playlist/x (Node URL-resolution output: `app.js on /playlist/x resolves to /playlist/app.js`). That unmatched URL follows the HTML fallback, so direct nested navigation has an asset-resolution defect even though stamped-body tests pass; a browser route audit is still required. This is a pre-existing hotspot; it was not silently changed during the regression-only phase.
No real iPhone was available. Linux WebKit cannot verify installed-PWA process eviction, iOS HTTP-cache sharing, layout flash on actual hardware, Safari storage/quota eviction with a large saved-music library, 30-minute locked audio/battery drain, interruption/resume and lock-screen controls, cellular behavior, safe-area rendering, PiP, native sharing or relay/WebRTC functionality. The deterministic fixture reports autoplay/media-ready limitations rather than proving audio playback. Production was down; live origin/proxy headers, deployment and on-device old-to-new migration remain unverified. Follow plans/rollout.md and record failures before promoting the next phase.