Files
ytplayer/plans/perf-final.md

18 KiB
Raw Permalink Blame History

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

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:

=== 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:

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.