Compare commits
3 Commits
f133e1bb9f
...
21c12733d3
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
21c12733d3 | ||
|
|
a08d363f2d | ||
|
|
ce5a159d4a |
@@ -311,3 +311,27 @@ plus the source frontend build tag. WebKit does not expose the long-task API.
|
||||
Migration supports `--out` to preserve earlier phase evidence. Existing lazy
|
||||
harness results are copied to `phase4-lazy-2026-10-08.json` after each Phase 4 run
|
||||
and the Phase 3 result is restored. No production requests or deploys are needed.
|
||||
|
||||
## Phase 5 complete measurements
|
||||
|
||||
```bash
|
||||
node --test perf/measurement.test.mjs
|
||||
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
|
||||
node perf/lazy.mjs --browser all --out perf/results/phase5-lazy-2026-10-08.json
|
||||
```
|
||||
|
||||
Warm reloads now wait for verified idle asset caching before counters reset and
|
||||
assert that no application asset body transfers. CSS update totals now include
|
||||
activation and the changed inactive stylesheet's verified background cache fill,
|
||||
matching the feature-update boundary. `readyWireBytes`/`readyDuration` retain the
|
||||
blocking readiness snapshot separately. Core updates stop at blocking readiness
|
||||
because app.js is blocking. Every update asserts that the only application asset
|
||||
payload is the edited file; worker script checks/imports and index/manifest count
|
||||
as overhead in the total. Historical Phase 3/4 CSS numbers were readiness only.
|
||||
No product behavior changes are involved in these measurement corrections.
|
||||
|
||||
Keep browser/build/test processes idle during full timing measurements. WebKit's
|
||||
zero long-task field means unavailable, not zero work. Autoplay-blocked/media-ready
|
||||
null results do not establish playback performance or iPhone audio continuity.
|
||||
|
||||
@@ -26,6 +26,7 @@
|
||||
*/
|
||||
|
||||
import assert from 'node:assert/strict';
|
||||
import { assertChangedAssets, assertWarmAssets, waitForIdleAssets } from './measurement.mjs';
|
||||
import { createHash } from 'node:crypto';
|
||||
import { spawn, spawnSync, execFileSync } from 'node:child_process';
|
||||
import { existsSync, mkdirSync, rmSync, cpSync, appendFileSync, statSync, writeFileSync, readFileSync, symlinkSync, unlinkSync } from 'node:fs';
|
||||
@@ -461,6 +462,8 @@ async function runWarmScenario({ browserType, server, proxy, profile }) {
|
||||
});
|
||||
});
|
||||
|
||||
await waitForIdleAssets(page);
|
||||
|
||||
// Now measure warm re-navigation
|
||||
proxy.resetStats();
|
||||
proxy.startTracking();
|
||||
@@ -470,6 +473,7 @@ async function runWarmScenario({ browserType, server, proxy, profile }) {
|
||||
|
||||
proxy.stopTracking();
|
||||
const stats = proxy.getStats();
|
||||
assertWarmAssets(stats.requests);
|
||||
|
||||
const pageMetrics = await page.evaluate(() => {
|
||||
const nav = performance.getEntriesByType('navigation')[0];
|
||||
@@ -622,24 +626,7 @@ async function runUpdateScenario({ browserType, server, proxy, profile, updateKi
|
||||
|
||||
// Phase 3 activation precedes idle feature caching. Measure updates from a
|
||||
// genuinely warm build, rather than counting unfinished first-install bytes.
|
||||
if (await page.evaluate(()=>!!window.Lazy && !!window.Lazy.warm)) {
|
||||
await page.evaluate(()=>Lazy.warm());
|
||||
const end=Date.now()+90000;
|
||||
while (true) {
|
||||
const ready=await page.evaluate(async()=>{
|
||||
const c=await caches.open('ytplayer-assets'),r=await c.match('/__ytp_asset_state');
|
||||
if(!r)return false;
|
||||
const m=(await r.json()).current;
|
||||
return (await Promise.all(Object.values(m.groups).flatMap(g=>g.files).map(async p=>{
|
||||
const response=await c.match(p+'?v='+m.files[p].h);
|
||||
return response?.headers.get('X-Asset-Hash')===m.files[p].h;
|
||||
}))).every(Boolean);
|
||||
});
|
||||
if(ready)break;
|
||||
if(Date.now()>end)throw Error('Idle warm did not complete before update benchmark');
|
||||
await sleep(100);
|
||||
}
|
||||
}
|
||||
await waitForIdleAssets(page);
|
||||
|
||||
// Switch server to Tree N+1 (app.js or theme-glass.css modification)
|
||||
const targetTree = updateKind === 'feature' ? trees.treeUpdateFeature : updateKind === 'js' ? trees.treeUpdateJs : trees.treeUpdateCss;
|
||||
@@ -662,20 +649,23 @@ async function runUpdateScenario({ browserType, server, proxy, profile, updateKi
|
||||
await sleep(50);
|
||||
}
|
||||
|
||||
if (updateKind === 'feature') {
|
||||
const readyDuration = Date.now() - t0;
|
||||
const readyWireBytes = proxy.getStats().totalWireBytes;
|
||||
if (updateKind === 'feature' || updateKind === 'css') {
|
||||
const changedPath=updateKind==='feature'?'/presenter.js':'/theme-glass.css';
|
||||
// Readiness excludes background chunks. Include activation and the changed
|
||||
// feature's actual idle transfer rather than reporting readiness as its cost.
|
||||
const expected = createHash('sha256').update(readFileSync(path.join(targetTree,'presenter.js'))).digest('hex').slice(0,10);
|
||||
const expected = createHash('sha256').update(readFileSync(path.join(targetTree,changedPath.slice(1)))).digest('hex').slice(0,10);
|
||||
await page.evaluate(async()=>{const r=await navigator.serviceWorker.getRegistration();r.waiting.postMessage({type:'SKIP_WAITING'});});
|
||||
const end=Date.now()+90000;
|
||||
while(true) {
|
||||
const complete=await page.evaluate(async h=>{
|
||||
const complete=await page.evaluate(async ({h,p})=>{
|
||||
const r=await navigator.serviceWorker.getRegistration();
|
||||
if(r.waiting || r.installing)return false;
|
||||
r.active?.postMessage({type:'WARM_ASSETS',saveData:false});
|
||||
const c=await caches.open('ytplayer-assets'),entry=await c.match('/presenter.js?v='+h);
|
||||
const c=await caches.open('ytplayer-assets'),entry=await c.match(p+'?v='+h);
|
||||
return entry?.headers.get('X-Asset-Hash')===h;
|
||||
},expected);
|
||||
},{h:expected,p:changedPath});
|
||||
if(complete)break;
|
||||
if(Date.now()>end)throw Error('Changed feature was not cached after activation');
|
||||
await sleep(100);
|
||||
@@ -684,14 +674,13 @@ async function runUpdateScenario({ browserType, server, proxy, profile, updateKi
|
||||
const duration = Date.now() - t0;
|
||||
proxy.stopTracking();
|
||||
const stats = proxy.getStats();
|
||||
if(updateKind==='feature') {
|
||||
const assetDownloads=stats.requests.filter(r=>r.bytes>0).map(r=>new URL(r.url,navUrl).pathname).filter(p=>/\.(?:js|css|woff2?|png|svg)$/.test(p)&&!['/sw.js','/asset-sync-core.js'].includes(p));
|
||||
assert.deepEqual([...new Set(assetDownloads)],['/presenter.js'],'feature edit transfers only its chunk; no app/core/font/icon payload');
|
||||
}
|
||||
assertChangedAssets(stats.requests, updateKind==='feature'?'/presenter.js':updateKind==='css'?'/theme-glass.css':'/app.js');
|
||||
await browser.close();
|
||||
|
||||
return {
|
||||
updateKind,
|
||||
readyDuration,
|
||||
readyWireBytes,
|
||||
requests: stats.requests,
|
||||
duration,
|
||||
requestCount: stats.requestCount,
|
||||
@@ -895,6 +884,8 @@ async function main() {
|
||||
}
|
||||
|
||||
results.browsers[bName][prof].updateCss = {
|
||||
readyDuration: summarizeList(updCssRuns.map(r=>r.readyDuration)),
|
||||
readyWireBytes: summarizeList(updCssRuns.map(r=>r.readyWireBytes)),
|
||||
requestCount: summarizeList(updCssRuns.map((r) => r.requestCount)),
|
||||
wireBytes: summarizeList(updCssRuns.map((r) => r.wireBytes)),
|
||||
duration: summarizeList(updCssRuns.map((r) => r.duration)),
|
||||
|
||||
@@ -132,4 +132,4 @@ async function run(name){
|
||||
console.log(name,'PASS');
|
||||
} finally {await browser?.close();await proxy?.close();await stop();}
|
||||
}
|
||||
try{for(const name of requested==='all'?['chromium','webkit']:[requested])await run(name);writeFileSync(join(repo,'perf/results/phase3-lazy-2026-10-08.json'),JSON.stringify({results},null,2)+'\n');}finally{rmSync(scratch,{recursive:true,force:true});}
|
||||
try{for(const name of requested==='all'?['chromium','webkit']:[requested])await run(name);writeFileSync(args.includes('--out')?resolve(args[args.indexOf('--out')+1]):join(repo,'perf/results/phase3-lazy-2026-10-08.json'),JSON.stringify({results},null,2)+'\n');}finally{rmSync(scratch,{recursive:true,force:true});}
|
||||
|
||||
29
perf/measurement.mjs
Normal file
29
perf/measurement.mjs
Normal file
@@ -0,0 +1,29 @@
|
||||
import assert from 'node:assert/strict';
|
||||
export function assertChangedAssets(requests, expected) {
|
||||
const paths=requests.filter(r=>r.bytes>0).map(r=>new URL(r.url,'http://localhost').pathname).filter(p=>/\.(?:js|css|woff2?|png|svg)$/.test(p)&&!['/sw.js','/asset-sync-core.js'].includes(p));
|
||||
assert.deepEqual([...new Set(paths)],[expected],'only the changed application asset transfers; worker checks and derived index/manifest are separate overhead');
|
||||
}
|
||||
export async function waitForIdleAssets(page) {
|
||||
if(!await page.evaluate(()=>!!window.Lazy?.warm))return;
|
||||
await page.evaluate(()=>Lazy.warm());
|
||||
const end=Date.now()+120000;
|
||||
while(true) {
|
||||
const ready=await page.evaluate(async()=>{
|
||||
const c=await caches.open('ytplayer-assets'),r=await c.match('/__ytp_asset_state');
|
||||
// Legacy rollback has no manifest state and installs its complete shell instead.
|
||||
if(!r)return (await caches.keys()).some(k=>/^ytplayer-(?!assets|thumbs|fonts)/.test(k));
|
||||
const m=(await r.json()).current;
|
||||
return (await Promise.all(Object.values(m.groups).flatMap(g=>g.files).map(async p=>{
|
||||
const response=await c.match(p+'?v='+m.files[p].h);
|
||||
return response?.headers.get('X-Asset-Hash')===m.files[p].h;
|
||||
}))).every(Boolean);
|
||||
});
|
||||
if(ready)return;
|
||||
if(Date.now()>end)throw Error('Idle warm did not complete before benchmark');
|
||||
await new Promise(r=>setTimeout(r,100));
|
||||
}
|
||||
}
|
||||
export function assertWarmAssets(requests) {
|
||||
const assets=requests.filter(r=>r.bytes>0&&/\.(?:js|css|woff2?|png|svg)(?:[?#]|$)/.test(r.url)&&!new URL(r.url,'http://localhost').pathname.endsWith('/sw.js'));
|
||||
assert.deepEqual(assets,[],'warm reload does not transfer unfinished shell assets');
|
||||
}
|
||||
16
perf/measurement.test.mjs
Normal file
16
perf/measurement.test.mjs
Normal file
@@ -0,0 +1,16 @@
|
||||
import { test } from 'node:test';
|
||||
import assert from 'node:assert/strict';
|
||||
import { assertChangedAssets, assertWarmAssets } from './measurement.mjs';
|
||||
test('changed CSS and feature measurements reject unrelated application transfers',()=>{
|
||||
const requests=[{url:'/sw.js',bytes:7000},{url:'/asset-sync-core.js',bytes:2000},{url:'/index.html',bytes:12000},{url:'/api/manifest',bytes:3000},{url:'/theme-glass.css?v=new',bytes:5000},{url:'/fonts/a.woff2',bytes:0}];
|
||||
assertChangedAssets(requests,'/theme-glass.css');
|
||||
assert.throws(()=>assertChangedAssets([...requests,{url:'/app.js?v=new',bytes:10000}],'/theme-glass.css'));
|
||||
assert.throws(()=>assertChangedAssets(requests.filter(r=>!r.url.includes('theme-glass')),'/theme-glass.css'));
|
||||
assertChangedAssets([{url:'/presenter.js?v=new',bytes:3200}],'/presenter.js');
|
||||
assert.throws(()=>assertChangedAssets([{url:'/presenter.js?v=new',bytes:3200},{url:'/icons/192.png',bytes:1000}],'/presenter.js'));
|
||||
});
|
||||
|
||||
test('warm measurements reject unfinished shell downloads but allow API checks',()=>{
|
||||
assertWarmAssets([{url:'/api/version',bytes:62},{url:'/app.js?v=current',bytes:0}]);
|
||||
assert.throws(()=>assertWarmAssets([{url:'/theme-glass.css?v=current',bytes:5000}]));
|
||||
});
|
||||
6055
perf/results/final-2026-10-08.json
Normal file
6055
perf/results/final-2026-10-08.json
Normal file
File diff suppressed because it is too large
Load Diff
205
perf/results/phase5-css-check-2026-10-08.json
Normal file
205
perf/results/phase5-css-check-2026-10-08.json
Normal file
@@ -0,0 +1,205 @@
|
||||
{
|
||||
"date": "2026-10-07T21:09:51.732Z",
|
||||
"frontendCommit": null,
|
||||
"frontendBuildTag": "c3dbdd7039db",
|
||||
"commit": "ce5a159d4a960c76c1a8ead10500758c95442918",
|
||||
"runsConfigured": 1,
|
||||
"browsers": {
|
||||
"chromium": {
|
||||
"lte": {
|
||||
"updateCss": {
|
||||
"readyDuration": {
|
||||
"median": 224,
|
||||
"min": 224,
|
||||
"max": 224,
|
||||
"values": [
|
||||
224
|
||||
]
|
||||
},
|
||||
"readyWireBytes": {
|
||||
"median": 30263,
|
||||
"min": 30263,
|
||||
"max": 30263,
|
||||
"values": [
|
||||
30263
|
||||
]
|
||||
},
|
||||
"requestCount": {
|
||||
"median": 8,
|
||||
"min": 8,
|
||||
"max": 8,
|
||||
"values": [
|
||||
8
|
||||
]
|
||||
},
|
||||
"wireBytes": {
|
||||
"median": 35363,
|
||||
"min": 35363,
|
||||
"max": 35363,
|
||||
"values": [
|
||||
35363
|
||||
]
|
||||
},
|
||||
"duration": {
|
||||
"median": 1387,
|
||||
"min": 1387,
|
||||
"max": 1387,
|
||||
"values": [
|
||||
1387
|
||||
]
|
||||
},
|
||||
"wireBytesByType": {
|
||||
"html": 11439,
|
||||
"js": 15540,
|
||||
"css": 5100,
|
||||
"fonts": 0,
|
||||
"images": 0,
|
||||
"media": 0,
|
||||
"api": 3284,
|
||||
"other": 0
|
||||
},
|
||||
"requests": [
|
||||
{
|
||||
"url": "http://127.0.0.1:4435/sw.js",
|
||||
"status": 200,
|
||||
"bytes": 7770
|
||||
},
|
||||
{
|
||||
"url": "http://127.0.0.1:4435/asset-sync-core.js?v=155be9339b",
|
||||
"status": 304,
|
||||
"bytes": 0
|
||||
},
|
||||
{
|
||||
"url": "http://127.0.0.1:4435/api/manifest",
|
||||
"status": 200,
|
||||
"bytes": 3222
|
||||
},
|
||||
{
|
||||
"url": "http://127.0.0.1:4435/index.html?v=997252e5f7",
|
||||
"status": 200,
|
||||
"bytes": 11439
|
||||
},
|
||||
{
|
||||
"url": "http://127.0.0.1:4435/api/version",
|
||||
"status": 200,
|
||||
"bytes": 62
|
||||
},
|
||||
{
|
||||
"url": "http://127.0.0.1:4435/sw.js",
|
||||
"status": 200,
|
||||
"bytes": 7770
|
||||
},
|
||||
{
|
||||
"url": "http://127.0.0.1:4435/asset-sync-core.js?v=155be9339b",
|
||||
"status": 304,
|
||||
"bytes": 0
|
||||
},
|
||||
{
|
||||
"url": "http://127.0.0.1:4435/theme-glass.css?v=253e56cfb3",
|
||||
"status": 200,
|
||||
"bytes": 5100
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
},
|
||||
"webkit": {
|
||||
"lte": {
|
||||
"updateCss": {
|
||||
"readyDuration": {
|
||||
"median": 1002,
|
||||
"min": 1002,
|
||||
"max": 1002,
|
||||
"values": [
|
||||
1002
|
||||
]
|
||||
},
|
||||
"readyWireBytes": {
|
||||
"median": 28713,
|
||||
"min": 28713,
|
||||
"max": 28713,
|
||||
"values": [
|
||||
28713
|
||||
]
|
||||
},
|
||||
"requestCount": {
|
||||
"median": 8,
|
||||
"min": 8,
|
||||
"max": 8,
|
||||
"values": [
|
||||
8
|
||||
]
|
||||
},
|
||||
"wireBytes": {
|
||||
"median": 45659,
|
||||
"min": 45659,
|
||||
"max": 45659,
|
||||
"values": [
|
||||
45659
|
||||
]
|
||||
},
|
||||
"duration": {
|
||||
"median": 1537,
|
||||
"min": 1537,
|
||||
"max": 1537,
|
||||
"values": [
|
||||
1537
|
||||
]
|
||||
},
|
||||
"wireBytesByType": {
|
||||
"html": 14030,
|
||||
"js": 21890,
|
||||
"css": 5939,
|
||||
"fonts": 0,
|
||||
"images": 0,
|
||||
"media": 0,
|
||||
"api": 3800,
|
||||
"other": 0
|
||||
},
|
||||
"requests": [
|
||||
{
|
||||
"url": "http://127.0.0.1:4435/sw.js",
|
||||
"status": 200,
|
||||
"bytes": 8991
|
||||
},
|
||||
{
|
||||
"url": "http://127.0.0.1:4435/asset-sync-core.js?v=155be9339b",
|
||||
"status": 200,
|
||||
"bytes": 1954
|
||||
},
|
||||
{
|
||||
"url": "http://127.0.0.1:4435/api/manifest",
|
||||
"status": 200,
|
||||
"bytes": 3738
|
||||
},
|
||||
{
|
||||
"url": "http://127.0.0.1:4435/index.html?v=997252e5f7",
|
||||
"status": 200,
|
||||
"bytes": 14030
|
||||
},
|
||||
{
|
||||
"url": "http://127.0.0.1:4435/api/version",
|
||||
"status": 200,
|
||||
"bytes": 62
|
||||
},
|
||||
{
|
||||
"url": "http://127.0.0.1:4435/sw.js",
|
||||
"status": 200,
|
||||
"bytes": 8991
|
||||
},
|
||||
{
|
||||
"url": "http://127.0.0.1:4435/theme-glass.css?v=253e56cfb3",
|
||||
"status": 200,
|
||||
"bytes": 5939
|
||||
},
|
||||
{
|
||||
"url": "http://127.0.0.1:4435/asset-sync-core.js?v=155be9339b",
|
||||
"status": 200,
|
||||
"bytes": 1954
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
79
perf/results/phase5-lazy-2026-10-08.json
Normal file
79
perf/results/phase5-lazy-2026-10-08.json
Normal file
@@ -0,0 +1,79 @@
|
||||
{
|
||||
"results": [
|
||||
{
|
||||
"browser": "chromium",
|
||||
"sourceBuildTag": "c3dbdd7039db",
|
||||
"classicSharedDefaults": true,
|
||||
"cssOrderPreserved": true,
|
||||
"settingsSearch": true,
|
||||
"carControls": 4,
|
||||
"offlineFeature": true,
|
||||
"firstUseNetwork": 0,
|
||||
"layoutsOffline": [
|
||||
"glass-stage",
|
||||
"bento-hub",
|
||||
"f7-swipe",
|
||||
"classic"
|
||||
],
|
||||
"pinned": true,
|
||||
"compatibleUnexecutedNewURL": true,
|
||||
"incompatibleN1ReloadRequired": true,
|
||||
"stagedN1": true,
|
||||
"playbackGuard": true,
|
||||
"prePaintLayout": true,
|
||||
"offlineSettingsPanels": true,
|
||||
"scriptErrors": [],
|
||||
"offlineTransportErrors": []
|
||||
},
|
||||
{
|
||||
"browser": "webkit",
|
||||
"sourceBuildTag": "c3dbdd7039db",
|
||||
"classicSharedDefaults": true,
|
||||
"cssOrderPreserved": true,
|
||||
"settingsSearch": true,
|
||||
"carControls": 4,
|
||||
"offlineFeature": true,
|
||||
"firstUseNetwork": 0,
|
||||
"layoutsOffline": [
|
||||
"glass-stage",
|
||||
"bento-hub",
|
||||
"f7-swipe",
|
||||
"classic"
|
||||
],
|
||||
"pinned": true,
|
||||
"compatibleUnexecutedNewURL": true,
|
||||
"incompatibleN1ReloadRequired": true,
|
||||
"stagedN1": true,
|
||||
"playbackGuard": true,
|
||||
"prePaintLayout": true,
|
||||
"offlineSettingsPanels": true,
|
||||
"scriptErrors": [],
|
||||
"offlineTransportErrors": [
|
||||
{
|
||||
"message": "TypeError: Load failed",
|
||||
"disconnected": true
|
||||
},
|
||||
{
|
||||
"message": "/127.0.0.1:44473/api/version.",
|
||||
"disconnected": true
|
||||
},
|
||||
{
|
||||
"message": "TypeError: Load failed",
|
||||
"disconnected": true
|
||||
},
|
||||
{
|
||||
"message": "/127.0.0.1:44473/api/recommendations?fp=163577c02a53bc40.",
|
||||
"disconnected": true
|
||||
},
|
||||
{
|
||||
"message": "TypeError: Load failed",
|
||||
"disconnected": true
|
||||
},
|
||||
{
|
||||
"message": "/127.0.0.1:44473/api/version.",
|
||||
"disconnected": true
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
}
|
||||
41
perf/results/phase5-migration-lossy-2026-10-08.json
Normal file
41
perf/results/phase5-migration-lossy-2026-10-08.json
Normal file
@@ -0,0 +1,41 @@
|
||||
{
|
||||
"commit": "b77938a",
|
||||
"results": [
|
||||
{
|
||||
"browser": "chromium",
|
||||
"oldTag": "1175f1a1d2c1",
|
||||
"newTag": "c3dbdd7039db",
|
||||
"banners": 1,
|
||||
"updateReloads": 1,
|
||||
"cachedFiles": 122,
|
||||
"durationMs": 25453,
|
||||
"wireBytes": 628797,
|
||||
"offline": true,
|
||||
"legacyURLs": 21,
|
||||
"profile": "lossy",
|
||||
"userInitiatedOnly": true,
|
||||
"playbackGuard": true,
|
||||
"evictionRepair": true,
|
||||
"incrementalRefresh": true,
|
||||
"offlineTransportErrors": 0
|
||||
},
|
||||
{
|
||||
"browser": "webkit",
|
||||
"oldTag": "1175f1a1d2c1",
|
||||
"newTag": "c3dbdd7039db",
|
||||
"banners": 1,
|
||||
"updateReloads": 1,
|
||||
"cachedFiles": 122,
|
||||
"durationMs": 23132,
|
||||
"wireBytes": 737972,
|
||||
"offline": true,
|
||||
"legacyURLs": 21,
|
||||
"profile": "lossy",
|
||||
"userInitiatedOnly": true,
|
||||
"playbackGuard": true,
|
||||
"evictionRepair": true,
|
||||
"incrementalRefresh": true,
|
||||
"offlineTransportErrors": 4
|
||||
}
|
||||
]
|
||||
}
|
||||
41
perf/results/phase5-migration-unthrottled-2026-10-08.json
Normal file
41
perf/results/phase5-migration-unthrottled-2026-10-08.json
Normal file
@@ -0,0 +1,41 @@
|
||||
{
|
||||
"commit": "b77938a",
|
||||
"results": [
|
||||
{
|
||||
"browser": "chromium",
|
||||
"oldTag": "1175f1a1d2c1",
|
||||
"newTag": "c3dbdd7039db",
|
||||
"banners": 1,
|
||||
"updateReloads": 1,
|
||||
"cachedFiles": 122,
|
||||
"durationMs": 10954,
|
||||
"wireBytes": 646234,
|
||||
"offline": true,
|
||||
"legacyURLs": 21,
|
||||
"profile": "unthrottled",
|
||||
"userInitiatedOnly": true,
|
||||
"playbackGuard": true,
|
||||
"evictionRepair": true,
|
||||
"incrementalRefresh": true,
|
||||
"offlineTransportErrors": 0
|
||||
},
|
||||
{
|
||||
"browser": "webkit",
|
||||
"oldTag": "1175f1a1d2c1",
|
||||
"newTag": "c3dbdd7039db",
|
||||
"banners": 1,
|
||||
"updateReloads": 1,
|
||||
"cachedFiles": 122,
|
||||
"durationMs": 10056,
|
||||
"wireBytes": 751943,
|
||||
"offline": true,
|
||||
"legacyURLs": 21,
|
||||
"profile": "unthrottled",
|
||||
"userInitiatedOnly": true,
|
||||
"playbackGuard": true,
|
||||
"evictionRepair": true,
|
||||
"incrementalRefresh": true,
|
||||
"offlineTransportErrors": 4
|
||||
}
|
||||
]
|
||||
}
|
||||
28
perf/results/phase5-seams-2026-10-08.json
Normal file
28
perf/results/phase5-seams-2026-10-08.json
Normal file
@@ -0,0 +1,28 @@
|
||||
[
|
||||
{
|
||||
"browser": "chromium",
|
||||
"checked": [
|
||||
"shell",
|
||||
"presenter",
|
||||
"remote",
|
||||
"party",
|
||||
"share",
|
||||
"stats"
|
||||
],
|
||||
"offlineNetworkRequests": 0,
|
||||
"errors": []
|
||||
},
|
||||
{
|
||||
"browser": "webkit",
|
||||
"checked": [
|
||||
"shell",
|
||||
"presenter",
|
||||
"remote",
|
||||
"party",
|
||||
"share",
|
||||
"stats"
|
||||
],
|
||||
"offlineNetworkRequests": 0,
|
||||
"errors": []
|
||||
}
|
||||
]
|
||||
133
perf/results/phase5-warm-check-2026-10-08.json
Normal file
133
perf/results/phase5-warm-check-2026-10-08.json
Normal file
@@ -0,0 +1,133 @@
|
||||
{
|
||||
"date": "2026-10-07T21:11:58.283Z",
|
||||
"frontendCommit": null,
|
||||
"frontendBuildTag": "c3dbdd7039db",
|
||||
"commit": "ce5a159d4a960c76c1a8ead10500758c95442918",
|
||||
"runsConfigured": 1,
|
||||
"browsers": {
|
||||
"chromium": {
|
||||
"lte": {
|
||||
"warm": {
|
||||
"requestCount": {
|
||||
"median": 4,
|
||||
"min": 4,
|
||||
"max": 4,
|
||||
"values": [
|
||||
4
|
||||
]
|
||||
},
|
||||
"wireBytes": {
|
||||
"median": 7863,
|
||||
"min": 7863,
|
||||
"max": 7863,
|
||||
"values": [
|
||||
7863
|
||||
]
|
||||
},
|
||||
"fcp": {
|
||||
"median": 152,
|
||||
"min": 152,
|
||||
"max": 152,
|
||||
"values": [
|
||||
152
|
||||
]
|
||||
},
|
||||
"lcp": {
|
||||
"median": 152,
|
||||
"min": 152,
|
||||
"max": 152,
|
||||
"values": [
|
||||
152
|
||||
]
|
||||
},
|
||||
"domContentLoaded": {
|
||||
"median": 542,
|
||||
"min": 542,
|
||||
"max": 542,
|
||||
"values": [
|
||||
542
|
||||
]
|
||||
},
|
||||
"bootDone": {
|
||||
"median": 567,
|
||||
"min": 567,
|
||||
"max": 567,
|
||||
"values": [
|
||||
567
|
||||
]
|
||||
},
|
||||
"longTaskTotal": {
|
||||
"median": 128,
|
||||
"min": 128,
|
||||
"max": 128,
|
||||
"values": [
|
||||
128
|
||||
]
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"webkit": {
|
||||
"lte": {
|
||||
"warm": {
|
||||
"requestCount": {
|
||||
"median": 0,
|
||||
"min": 0,
|
||||
"max": 0,
|
||||
"values": [
|
||||
0
|
||||
]
|
||||
},
|
||||
"wireBytes": {
|
||||
"median": 0,
|
||||
"min": 0,
|
||||
"max": 0,
|
||||
"values": [
|
||||
0
|
||||
]
|
||||
},
|
||||
"fcp": {
|
||||
"median": 87,
|
||||
"min": 87,
|
||||
"max": 87,
|
||||
"values": [
|
||||
87
|
||||
]
|
||||
},
|
||||
"lcp": {
|
||||
"median": 130,
|
||||
"min": 130,
|
||||
"max": 130,
|
||||
"values": [
|
||||
130
|
||||
]
|
||||
},
|
||||
"domContentLoaded": {
|
||||
"median": 128,
|
||||
"min": 128,
|
||||
"max": 128,
|
||||
"values": [
|
||||
128
|
||||
]
|
||||
},
|
||||
"bootDone": {
|
||||
"median": 132,
|
||||
"min": 132,
|
||||
"max": 132,
|
||||
"values": [
|
||||
132
|
||||
]
|
||||
},
|
||||
"longTaskTotal": {
|
||||
"median": 0,
|
||||
"min": 0,
|
||||
"max": 0,
|
||||
"values": [
|
||||
0
|
||||
]
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
158
plans/perf-final.md
Normal file
158
plans/perf-final.md
Normal file
@@ -0,0 +1,158 @@
|
||||
# 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.
|
||||
11
plans/phase5-plan.md
Normal file
11
plans/phase5-plan.md
Normal file
@@ -0,0 +1,11 @@
|
||||
# Phase 5 plan
|
||||
|
||||
Start at Phase 4 f133e1b on codex/phase5. No new product features.
|
||||
|
||||
1. Audit perf/baseline.mjs against the original five-run configuration; retain all browser/profile combinations and cold/warm/offline/core-JS/CSS scenarios, plus the existing presenter-chunk update. Measure a genuinely warmed reload and complete CSS background download; preserve readiness measurements separately and document changed boundaries. Extend harness assertions before changing measurement behavior.
|
||||
2. Run frontend and isolated server suites, hidden-timers, Chromium/WebKit lazy and seam smoke tests, and b77938a migration with unthrottled and lossy/stalling proxies. Preserve previous phase results. Fix only demonstrated product regressions, individually committed with tests.
|
||||
3. Run the isolated full harness: --runs 5 --browser all --profile all --scenario all --out perf/results/final-2026-10-08.json. Compare actual requests, compressed wire bytes, FCP/LCP, boot and Chromium long tasks with baseline and after-phase0. Target CSS update <30 KB; only changed application payload for feature/core updates; report unmet targets plainly.
|
||||
4. Write plans/perf-final.md with actual command excerpts, measurement limitations, phase contributions and remaining render/thumbnail/SPA hot spots. Write plans/rollout.md with separate Phase 1 then 2 then 3 then 4 deployment order, checks and rollback instructions. No deployment or production requests here.
|
||||
5. Review the entire phase diff against master sections 5/6/6b, commit a <=350-word phase5-report.md, and overwrite the queue signal with DONE05 and the final commit.
|
||||
|
||||
Risks: browser CPU noise, incomplete idle warm contaminating counters, historical readiness vs complete-download boundaries, Linux WebKit differing from iPhone, and missing real-device battery/audio evidence. Do not alter playback or the banner rule. Rollback product changes by reverting their individual commits; operational ASSET_HASHING=0 and ASSET_SYNC=0 require server restart and a user-initiated update. No generic lazy-feature flag exists; revert the applicable phase for loader rollback.
|
||||
13
plans/phase5-report.md
Normal file
13
plans/phase5-report.md
Normal file
@@ -0,0 +1,13 @@
|
||||
# Phase 5 report
|
||||
|
||||
Measured Phase 4 f133e1b on codex/phase5. Plan ce5a159; measurement correction a08d363. Product frontend/server bytes are unchanged (served base build c3dbdd7039db).
|
||||
|
||||
The full harness completed 120 runs: five samples × six scenarios × Chromium/WebKit × LTE/lossy. Evidence: perf/results/final-2026-10-08.json; full before/after tables and command excerpts in plans/perf-final.md. Warm counters now start after verified idle caching; CSS totals include the changed background stylesheet, with blocking readiness retained separately. Assertions reject unrelated application/font/icon downloads.
|
||||
|
||||
Chromium LTE vs after-phase0: cold 1,040,158→651,012 bytes, 166→115 requests; boot 10,480→7,043 ms; long tasks 454→278 ms. WebKit LTE boot 3,290→2,315 ms, but FCP regressed 1,100→1,739 ms. Core edits transfer 125,795/147,259 bytes (Chromium/WebKit); presenter edits 33,408/43,318 bytes on LTE, including overhead. Complete CSS updates cost 35,363/45,659 bytes: <30 KB fails. WebKit still downloads app.js twice on first install. These targets remain unmet.
|
||||
|
||||
Verification: 198 frontend, 183 server and two measurement tests pass; hidden timers, both-engine lazy/seam offline smoke, playback guard and b77938a migration with/without stalling pass. Migration: one user-initiated reload/banner, 122 shell entries, 21 legacy URL checks, open-old-tab and offline compatibility. No product regression fix was committed.
|
||||
|
||||
Self-review against master §6/6b preserved banner/hash rules, migration, pinned contracts, caches and playback. No push/deploy/production request. Rollout order and rollback commands are in plans/rollout.md; generic lazy feature flags do not exist.
|
||||
|
||||
Limitations: the historical 12-character benchmedia01 fixture is rejected as a media ID, so these runs prove home launch, not playing-ready. Nested SPA relative assets remain a pre-existing defect. Large-list rendering and the 25,000-thumbnail quota risk remain. Real-iPhone installed/offline/update behavior, 30-minute locked audio, battery, interruptions and OPFS quota checks are still required. Numerical acceptance is not fully met; report failures before rollout.
|
||||
86
plans/rollout.md
Normal file
86
plans/rollout.md
Normal file
@@ -0,0 +1,86 @@
|
||||
# Incremental-assets rollout
|
||||
|
||||
This is a deployment runbook, not a record of a deployment. Production was unavailable during this work; no production request, push or deploy was performed. The owner must review the reports and complete the iPhone gate before rollout. Deploy matching server/frontend artifacts: Docker serves frontend from server/public, and the server snapshots assets at boot.
|
||||
|
||||
## Exact order
|
||||
|
||||
1. **Phase 1 alone: codex/phase1.** Ship the per-file-hash server and descriptive groups while retaining the legacy worker/page update path. Verify the old production fixture migration locally, then live headers and version stamping below. Let existing clients complete their user-initiated Refresh UI. Do not combine this deployment with Phase 2.
|
||||
2. **Phase 2 alone: codex/phase2.** Ship incremental worker/cache migration. Run both migration profiles and live version/cache checks, then confirm old pages reach the ready fast path and an already-open old tab still works. Confirm no install/activate causes a reload without a user action.
|
||||
3. **Phase 3: codex/phase3.** Ship the loader, selected-layout boot, idle warm and contract safeguards. Verify offline layouts/features, Save-Data and playback guard on a real installed iPhone. Confirm the selected layout survives a force-close and offline launch.
|
||||
4. **Phase 4: codex/phase4 (then the Phase 5 validation/docs tip codex/phase5).** Ship the six app seams after the previous gates pass. Verify each cached feature and synchronous player facade. Phase 5 adds measurement/runbook files; any future regression fix must be reviewed independently.
|
||||
|
||||
Use the actual branch tips, not the Phase 1–4 intermediate seam commits. Do not deploy phases concurrently. After each stage, retain the previous image and its environment configuration until the next stage passes. Preserve the data volume and all OPFS/client caches; rollback must not ask users to clear storage.
|
||||
|
||||
## Local verification commands at each branch
|
||||
|
||||
Run in the existing isolated worktree, or a separate review checkout; do not switch a dirty deployment checkout. Install server dependencies first. Python yt-dlp-worker tests need an importable yt_dlp in PATH; report skips explicitly.
|
||||
|
||||
```bash
|
||||
node --test frontend/*.test.js
|
||||
(cd server && bun install && bun run test)
|
||||
perf/make-shell-fixture.sh b77938a 1175f1a1d2c1
|
||||
```
|
||||
|
||||
Phase 1: `node perf/migration.mjs --browser all` (its default profile is the stalling proxy). Phase 2 onward:
|
||||
|
||||
```bash
|
||||
node perf/migration.mjs --browser all --profile unthrottled
|
||||
node perf/migration.mjs --browser all --profile lossy
|
||||
node perf/hidden-timers.mjs
|
||||
```
|
||||
|
||||
Phase 3 onward: `node perf/lazy.mjs --browser all` (offline launch/layout switching, warmed feature first use, contract guards, pinned groups and playback guard). Phase 4 onward: `node perf/seams.mjs --browser all` (Presenter, Remote, Watch party, Share/External, Stats and shell with the server stopped). On Phase 5 use `--out perf/results/<new-name>.json` for migration/lazy to preserve historical results; seams retains its historical default, so copy the new result then restore the older tracked result if running in a review worktree.
|
||||
|
||||
Final reproduction:
|
||||
|
||||
```bash
|
||||
node --test perf/measurement.test.mjs
|
||||
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 harness includes cold/warm/offline and app.js, theme-glass.css and presenter.js edits. Run it without competing browsers/builds/tests. Re-running an old phase's own harness measures its own readiness boundary; use the Phase 5 harness for complete background-update comparisons.
|
||||
|
||||
## Live checks after production is restored (owner executes)
|
||||
|
||||
Set `origin` to the deployed URL; use curl only for public endpoints. Do not copy credentials into these commands.
|
||||
|
||||
```bash
|
||||
origin=https://worship.hesed.sbs
|
||||
curl -fsS "$origin/api/version"
|
||||
curl -fsS "$origin/" > /tmp/ytp-index.html
|
||||
curl -fsS "$origin/index.html" > /tmp/ytp-index-route.html
|
||||
curl -fsS "$origin/search" > /tmp/ytp-search.html
|
||||
curl -fsS "$origin/playlist/x" > /tmp/ytp-playlist.html
|
||||
cmp /tmp/ytp-index.html /tmp/ytp-index-route.html
|
||||
cmp /tmp/ytp-index.html /tmp/ytp-search.html
|
||||
cmp /tmp/ytp-index.html /tmp/ytp-playlist.html
|
||||
curl -fsS "$origin/api/manifest" > /tmp/ytp-manifest.json
|
||||
curl -fsS "$origin/sw.js" > /tmp/ytp-sw.js
|
||||
```
|
||||
|
||||
Read the manifest's buildTag and /app.js h. Confirm buildTag equals /api/version, the index meta and injected worker BUILD_TAG; each index asset uses its own h. Request `/api/manifest` with `If-None-Match: "<buildTag>"`: expect 304 and no body; ordinary response must be no-store. Request `/app.js?v=<current h>`: expect 200, X-Asset-Hash equal h and `public, max-age=31536000, immutable`; request stale `?v=<oldTag>`: expect 200 with current body, correct X-Asset-Hash and no-store. Plain `/app.js` and `?__ytpfresh=probe` must still be 200. `/sw.js` must be no-store. Run these checks against the origin and public proxy to catch intermediary caching errors.
|
||||
|
||||
Matching stamped SPA bodies alone does not prove nested-route asset resolution: open /playlist/x in a browser and inspect relative URLs/MIME types. This remains a documented hotspot, not a release success inferred from cmp.
|
||||
|
||||
In a browser inspect CacheStorage: Phase 2+ uses ytplayer-assets with exact `/path?v=<hash>` entries and /__ytp_asset_state. Wait for verified blocking readiness before applying; background groups finish later. Keep a second old tab open, apply from the first while paused and confirm the second still opens its cached features. After one further update confirm only changed N-1 versions remain; do not manually delete caches to simulate success.
|
||||
|
||||
## Rollback levers
|
||||
|
||||
- `ASSET_HASHING=0`: set in the server/container environment and restart/recreate the service using the normal owner-approved deployment process. Restores legacy single-tag stamping/cache headers and automatically selects legacy sync. Verify index URLs use the single buildTag; per-file immutable/header expectations above no longer apply. This is available from Phase 1.
|
||||
- `ASSET_SYNC=0`: available from Phase 2. Set and restart the server; hashing can stay on, but the served worker uses its legacy install/update path. Existing ytplayer-assets is a preserved utility cache. Legacy SHELL does not include piano-engine.mjs, so this emergency path does not promise every optional feature offline; verify the feature you need before disconnecting. With hashing enabled the changed sync setting changes buildTag; verify the new worker bytes and /api/version before applying the update from a paused page.
|
||||
- Restore defaults by removing either variable or setting it to `1`, restart, verify the new build and apply through Refresh UI. Test either lever locally with `ASSET_SYNC=0 node perf/baseline.mjs --runs 1 --browser all --profile lte --scenario offline --out perf/results/rollback-sync.json` (substitute ASSET_HASHING=0 for the other lever).
|
||||
- **No generic lazy/chunk feature flag was implemented.** Roll back a Phase 3/4 regression by reverting the specific commit or restoring the last validated phase image with its matching frontend/server. Existing user feature settings are not loader rollback switches. A hashing/sync flag cannot undo a lazy-load interface change.
|
||||
|
||||
Server flags do not mutate already-running JavaScript. They take effect on a newly installed worker/page through the normal user-initiated, playback-guarded update. No cache wipe, forced reload, auto-skipWaiting or stateful chunk re-evaluation is allowed. Executing feature instances stay pinned until session end or a guarded reload; live replacement requires disposal/state handoff before it can be enabled.
|
||||
|
||||
## Manual iPhone gate (record model/iOS/network, build tags and observations)
|
||||
|
||||
- Install from Safari; open the Home Screen app, verify the current meta/version and selected classic/nonclassic layout without a flash.
|
||||
- Update from b77938a via its own Refresh UI; assert one banner and one reload after the tap, no automatic install/activate reload. Legacy pages do not report PLAYING and retain their existing post-tap timer limitation. Pause playback first on those pages.
|
||||
- On a new page press Refresh UI during playback: see the explicit playing guard, no reload. Pause then apply; separately exercise the explicit override only when intentionally accepting playback interruption. Test the worker defers a reported-playing page's SKIP_WAITING.
|
||||
- Allow idle warm on Wi-Fi and cellular (Save-Data off); open each uncommon feature the first time offline. Repeat with Save-Data on and confirm automatic warming is skipped; unprimed optional features are not promised offline.
|
||||
- Pick Glass, Bento or F7, finish idle warm, enable Airplane Mode, force-close/reopen and reload; selected layout and saved audio/lyrics remain. Switch layouts offline, including classic.
|
||||
- Play a saved playlist, record battery %, lock 30 minutes and listen for dropouts. Record drain, track transitions, lock-screen controls, sleep timer, interruption/Siri/call recovery and resume position. Verify audio-only and video playback. Linux WebKit cannot certify this.
|
||||
- Check real-device storage estimate alongside existing OPFS music before/after updates; verify N-1 retention and quota pressure do not evict saved music. Repeat background audio with cellular/lossy transport and a pending update. Record failures rather than clearing storage to hide them.
|
||||
Reference in New Issue
Block a user