diff --git a/perf/README.md b/perf/README.md index 24b9a32..e9fecd5 100644 --- a/perf/README.md +++ b/perf/README.md @@ -130,3 +130,14 @@ To verify performance gains and detect regressions in later phases: node perf/baseline.mjs --compare perf/results/baseline-2026-10-07.json ``` Prints an ASCII/Markdown delta comparison table highlighting percentage changes across wire bytes, boot duration, and update overhead. + +--- + +## 6. Hidden Timers Verification (`perf/hidden-timers.mjs`) + +Verifies that non-playback UI intervals (`chips`, `reanchor`, `rotateChips`, `flush`, `checkBuildTag`) suspend while the document is hidden and restore upon visibility, while essential background intervals (`maybeCheck`, 120s resume check) remain active: +```bash +node perf/hidden-timers.mjs +``` +Exits with code 0 on success, or code 1 if any interval fails to pause, fails to resume, or if page errors occur. + diff --git a/plans/perf-baseline.md b/plans/perf-baseline.md index 1b120c7..ae540ed 100644 --- a/plans/perf-baseline.md +++ b/plans/perf-baseline.md @@ -173,3 +173,77 @@ Because automated headless WebKit cannot fully replicate iOS Safari's mobile bac 5. **Log Findings**: - Record iPhone Model, iOS Version, Battery Drain (% per 30m), Dropouts Count (0 = pass), and Resumption Status (Pass/Fail). + +--- + +## 7. After Phase 0 Benchmark (Step S5 Verification) + +**Date**: 2026-10-07 +**Commit**: `eb6aae7` +**Dataset**: `perf/results/after-phase0-2026-10-07.json` (compared against `perf/results/baseline-2026-10-07.json`) + +### Headline Before vs. After Comparison Table (Median of 5 Runs) + +| Browser / Profile | Scenario | Requests (Before → After) | Wire Bytes (Before → After) | FCP / LCP (ms) | Boot Done (ms) | SW Dur / Reload (ms) | +|---|---|---|---|---|---|---| +| **Chromium LTE** | Cold First Load | 164 → 166 (+1.2%) | 1,015.0 KB → 1,015.8 KB (+0.1%) | 1540/1896 → 1572/1916 | 10,163 → 10,480 (+3.1%) | 38,446 → 38,364 | +| | Warm Reload (SW) | 4 → 4 (0.0%) | 5.2 KB → 5.2 KB (0.0%) | 180/684 → 184/680 | 675 → 692 (+2.5%) | - | +| | Offline Reload | 0 → 0 (0.0%) | 0 B → 0 B (0.0%) | 72/108 → 76/112 | 219 → 259 | 240 → 282 | +| | Update (1-line JS) | 88 → 89 (+1.1%) | 596.6 KB → 597.0 KB (+0.1%) | - | - | 2,013 → 2,155 (+7.1%) | +| | Update (1-line CSS) | 88 → 89 (+1.1%) | 596.7 KB → 597.1 KB (+0.1%) | - | - | 1,970 → 2,107 (+7.0%) | +| **Chromium Lossy** | Cold First Load | 164 → 166 (+1.2%) | 1,015.0 KB → 1,015.8 KB (+0.1%) | 2344/2912 → 2372/2940 | 16,015 → 16,408 (+2.5%) | 42,518 → 42,031 | +| | Warm Reload (SW) | 4 → 4 (0.0%) | 5.2 KB → 5.2 KB (0.0%) | 184/644 → 188/648 | 651 → 768 (+18.0%) | - | +| | Offline Reload | 0 → 0 (0.0%) | 0 B → 0 B (0.0%) | 80/92 → 84/96 | 237 → 241 | 253 → 262 | +| | Update (1-line JS) | 88 → 89 (+1.1%) | 596.6 KB → 597.0 KB (+0.1%) | - | - | 2,031 → 2,115 (+4.1%) | +| | Update (1-line CSS) | 88 → 89 (+1.1%) | 596.7 KB → 597.1 KB (+0.1%) | - | - | 2,004 → 2,116 (+5.6%) | +| **WebKit LTE** | Cold First Load | 162 → 164 (+1.2%) | 1.12 MB → 1.13 MB (+0.1%) | 1088/1287 → 1100/1303 | 3,201 → 3,290 (+2.8%) | 30,508 → 31,026 | +| | Warm Reload (SW) | 0 → 0 (0.0%) | 0 B → 0 B (0.0%) | 110/110 → 112/112 | 131 → 139 (+6.1%) | - | +| | Offline Reload | 0 → 0 (0.0%) | 0 B → 0 B (0.0%) | 107/107 → 110/110 | 128 → 137 | 135 → 148 | +| | Update (1-line JS) | 86 → 87 (+1.2%) | 664.3 KB → 664.9 KB (+0.1%) | - | - | 4,457 → 4,584 (+2.8%) | +| | Update (1-line CSS) | 86 → 87 (+1.2%) | 664.3 KB → 664.9 KB (+0.1%) | - | - | 4,510 → 4,550 (+0.9%) | +| **WebKit Lossy** | Cold First Load | 162 → 164 (+1.2%) | 1.12 MB → 1.13 MB (+0.1%) | 3557/3735 → 3738/3905 | 7,303 → 7,305 (+0.0%) | 35,449 → 35,101 | +| | Warm Reload (SW) | 0 → 0 (0.0%) | 0 B → 0 B (0.0%) | 111/112 → 112/114 | 131 → 128 (-2.3%) | - | +| | Offline Reload | 0 → 0 (0.0%) | 0 B → 0 B (0.0%) | 109/119 → 111/120 | 129 → 132 | 141 → 139 | +| | Update (1-line JS) | 86 → 87 (+1.2%) | 664.3 KB → 664.9 KB (+0.1%) | - | - | 8,135 → 10,017 (+23.1%) | +| | Update (1-line CSS) | 87 → 87 (0.0%) | 664.4 KB → 664.9 KB (+0.1%) | - | - | 8,269 → 10,013 (+21.1%) | + +### Hidden Timers Verification Result + +Executed via `node perf/hidden-timers.mjs`: +``` +=== Hidden Timers Verification === +Initial visible intervals (8): anon1000, anon120000, checkBuildTag, chips, flush, maybeCheck, reanchor, rotateChips +Hidden intervals (3): anon1000, anon120000, maybeCheck +Restored visible intervals (9): anon1000, anon120000, anon50000, checkBuildTag, chips, flush, maybeCheck, reanchor, rotateChips +Page errors: none + +Result: ALL ASSERTIONS PASSED +``` +All five non-playback UI timers (`chips`, `reanchor`, `rotateChips`, `flush`, `checkBuildTag`) suspend immediately when `document.hidden` becomes true and resume immediately upon returning to visible, while essential background loops (`maybeCheck` inbox poll and `anon120000` 120s OPFS download resume check) remain continuously active. + +### Variance & Noise Observed + +Across the 5 iterations per scenario: +- **Transfer Volumes & Request Counts**: Completely deterministic (0.0% variance across all runs). +- **Boot Milestones & Paint Timings**: Stable within ±1–3% under LTE emulation on both Chromium and WebKit. +- **Update Duration under Lossy WebKit**: Exhibited expected network noise (~8.1 s vs ~10.0 s, ~21–23% delta) due to the random 1,500 ms connection stall injection pattern in `perf/proxy.mjs`. +- **Chromium Update Duration**: Clustered closely around ~2,100 ms with occasional ~100 ms fast runs when the browser resolves registration updates asynchronously. + +### Findings + +Introducing `frontend/visible-timer.js` successfully eliminates unnecessary CPU wakeups and timer execution during hidden tab states while adding virtually zero runtime transfer overhead (+1 request and +373–471 wire bytes on service worker updates; +2 requests and +843 wire bytes on initial cold visit). Background audio playback and essential network recovery handlers are preserved without regressing paint, boot, or offline capabilities. + +### Structural Transfer Analysis: Fonts & Icons vs. Code, and Cold Double-Download + +Detailed breakdown of network transfer patterns revealed two major inefficiencies in the current asset delivery architecture: + +1. **Static Fonts and Icons vs. JS/CSS**: + - **Cold First Visit**: Total wire transfer is 1,040.2 KB. Web fonts (`PlusJakartaSans`, `BricolageGrotesque`, `HankenGrotesk`, `JetBrainsMono`, and `fonts.css`) consume **397.1 KB (38.2%)** and PNG icons consume **6.3 KB (0.6%)**, totaling **403.4 KB (38.8%)** of immutable media. Application JavaScript consumes **478.1 KB (46.0%)** and CSS consumes **130.7 KB (12.6%)**, totaling **608.8 KB (58.5%)**. + - **Update N -> N+1 Wire Cost**: On any single-line change to `app.js` or `theme-glass.css`, `sw.js` invalidates the entire shell cache and re-downloads all 89 `SHELL` items. Out of 611.3 KB transferred, **~259.5 KB (~42.5%)** is spent re-downloading static web fonts and icons that never changed, compared to **~321.9 KB (52.7%)** for JS/CSS. +2. **Cold First-Visit Shell Double-Download**: + - The initial visit downloads every app shell resource **twice over the wire**: + - First, the browser's HTML parser requests `index.html`, stylesheets, scripts, and fonts to render the document (83 requests, ~520 KB). + - Second, once `boot()` registers `/sw.js`, the service worker `install` event triggers `precacheShell()`. Line 165 of `frontend/sw.js` executes `fetch(new Request(url, { cache: 'reload' }))`. + - The explicit `{ cache: 'reload' }` directive bypasses the browser's HTTP cache entirely, forcing a redundant full re-download of all 89 shell assets from the server. + - As measured by the harness request log, cold first visit request count is **166 requests** (exactly double the shell count) and wire data transfer is **1,040 KB** (twice the unprimed payload). +