Verify staged lazy loading and record Phase 3 measurements

This commit is contained in:
Jonathan Sykes
2026-10-08 04:19:02 +08:00
parent fab444b7b8
commit b84050578b
12 changed files with 5462 additions and 46 deletions

View File

@@ -209,6 +209,65 @@ inspect individual responses to distinguish those from app asset downloads.
Migration results are `phase2-migration-<profile>-2026-10-07.json`.
`ASSET_SYNC=0` selects the legacy path while preserving the persistent utility
cache. `ASSET_HASHING=0` also selects legacy sync because single-tag URLs cannot
pass per-file hash checks. Current assets.json groups all block; idle loading and
lazy parsing await Phase 3. Incremental mode now caches the manifested
pass per-file hash checks. In Phase 2 all assets.json groups blocked; Phase 3 selects core plus the active
layout and warms background groups at idle. Incremental mode now caches the manifested
`piano-engine.mjs` even though it remains absent from the legacy SHELL.
## Phase 3 lazy loading and staged apply
```bash
node perf/lazy.mjs --browser all
node perf/migration.mjs --browser all --profile lossy
node perf/migration.mjs --browser all --profile unthrottled
node perf/baseline.mjs --runs 1 --browser all --profile lte --scenario all \
--out perf/results/phase3-final-2026-10-08.json \
--compare perf/results/after-phase0-2026-10-07.json
node perf/baseline.mjs --runs 1 --browser all --profile lte --scenario cold \
--frontend-commit 7bc5b4f8b50d78ece3cca5100d54a97fece2748a \
--out perf/results/phase3-phase0-initial-2026-10-08.json
ASSET_SYNC=0 node perf/baseline.mjs --runs 1 --browser all --profile lte \
--scenario offline --out perf/results/phase3-sync-rollback-2026-10-08.json
ASSET_HASHING=0 node perf/baseline.mjs --runs 1 --browser all --profile lte \
--scenario offline --out perf/results/phase3-hashing-rollback-2026-10-08.json
```
`initialJsCss` sums compressed Resource Timing bodies for page JS/CSS initiated
before DOMContentLoaded, excluding SW precache and later idle feature parsing.
The saved Phase 0 results lack that metric: `--frontend-commit` reconstructs its
frontend in scratch using the current delivery backend for a matched initial-byte
comparison. Historical boot/FCP comparisons still use `after-phase0`. Reduced
single-run measurements are directional; Phase 5 supplies repeated medians.
Update scenarios now wait for every build-N group to finish idle caching before
starting the counter, excluding unfinished initial downloads. Waiting-worker
bytes represent blocking readiness; an inactive layout's changed CSS downloads
later at idle after activation.
The browser harness checks pre-body layout selection, immediate offline reload,
four distinct Car mode controls, optional Settings search, all extension panels,
zero network for warmed first use/layout switches, contract guards and pinned
instances. Known WebKit disconnected API transport errors are recorded separately;
script errors still fail. Save-Data prevents automatic warming.
The index contains inert JSON and an external head bootstrap under unchanged
CSP. Source JSON mirrors assets.json for native/static shells; the server replaces
it with hashed build-local URLs. No boot-time manifest network request is added.
Shared layout visibility defaults remain eager in layout-base.css.
Pure EQ settings metadata and DeviceDB remain eager to preserve synchronous
player defaults and offline file bookkeeping. P2P/direct parse on actual use or
the existing eight-second sharing startup. Searching Settings explicitly loads
optional panels so their original labels remain searchable.
| JS-requested assets | Phase 3 URL treatment |
|---|---|
| opfs-worker.js, hash-worker.js, direct-recv-worker.js, p2p-recv-worker.js | Lazy.worker uses embedded per-file URL |
| Workers' sha256.js, resume-core.js, sha256-wasm.js | First postMessage carries originating build URLs; importScripts uses them |
| piano-engine.mjs | Dynamic import uses Lazy.url; incremental cache warms it |
| vendor/framework7-swipe.min.js | Ordered layout group and fallback use Lazy.url |
| loudness-wasm.js | Remains an eager stamped script; embedded binary needs no separate URL |
| fonts/fonts.css and font preloads; webmanifest/icons | Plain URLs stay aligned with CSS/manifest references; all belong to manifested cache groups |
Legacy pages cannot report their selected layout. Their one-time migration blocks
all layouts to preserve immediate offline selection; modern pages block only the
reported layout and its shared controls. Executing groups never re-evaluate live.
See plans/phase3-staged-apply-decision.md: Phase 4 must add disposal/state handoff.

View File

@@ -25,7 +25,7 @@
* --compare <json-path> Compare results against a previous baseline JSON
*/
import { spawn, spawnSync } from 'node:child_process';
import { spawn, spawnSync, execFileSync } from 'node:child_process';
import { existsSync, mkdirSync, rmSync, cpSync, appendFileSync, statSync, writeFileSync, readFileSync, symlinkSync, unlinkSync } from 'node:fs';
import path from 'node:path';
import { fileURLToPath } from 'node:url';
@@ -58,6 +58,7 @@ function parseArgs() {
scenario: 'all', // 'cold', 'warm', 'offline', 'update-js', 'update-css', or 'all'
out: null,
compare: null,
frontendCommit: null,
};
for (let i = 0; i < args.length; i++) {
@@ -67,6 +68,7 @@ function parseArgs() {
else if (a === '--profile' && i + 1 < args.length) options.profile = args[++i];
else if (a === '--scenario' && i + 1 < args.length) options.scenario = args[++i];
else if (a === '--out' && i + 1 < args.length) options.out = args[++i];
else if (a === '--frontend-commit' && i + 1 < args.length) options.frontendCommit = args[++i];
else if (a === '--compare' && i + 1 < args.length) options.compare = args[++i];
}
@@ -365,6 +367,8 @@ async function runColdScenario({ browserType, server, proxy, profile }) {
const fcp = paint.find((p) => p.name === 'first-contentful-paint');
return {
initialJsCss: performance.getEntriesByType('resource').filter(entry=>/\.(?:js|css)(?:[?#]|$)/.test(entry.name)&&entry.startTime <= nav.domContentLoadedEventEnd&&['script','link','css'].includes(entry.initiatorType)).reduce((sum,entry)=>sum+entry.encodedBodySize,0),
initialAssetRequests: performance.getEntriesByType('resource').filter(entry=>/\.(?:js|css)(?:[?#]|$)/.test(entry.name)&&entry.startTime <= nav.domContentLoadedEventEnd&&['script','link','css'].includes(entry.initiatorType)).map(entry=>({url:entry.name,bytes:entry.encodedBodySize})),
fcp: fcp ? Math.round(fcp.startTime) : null,
lcp: window.__lcp ? Math.round(window.__lcp) : null,
domContentLoaded: nav ? Math.round(nav.domContentLoadedEventEnd - nav.startTime) : null,
@@ -393,6 +397,8 @@ async function runColdScenario({ browserType, server, proxy, profile }) {
bootDone: pageMetrics.bootDone,
longTaskTotal: pageMetrics.longTaskTotal,
requests: stats.requests,
initialJsCss: pageMetrics.initialJsCss,
initialAssetRequests: pageMetrics.initialAssetRequests,
swInstallDuration,
swInstallBytes: stats.totalWireBytes,
storageEstimate: storage,
@@ -465,6 +471,8 @@ async function runWarmScenario({ browserType, server, proxy, profile }) {
const fcp = paint.find((p) => p.name === 'first-contentful-paint');
return {
initialJsCss: performance.getEntriesByType('resource').filter(entry=>/\.(?:js|css)(?:[?#]|$)/.test(entry.name)&&entry.startTime <= nav.domContentLoadedEventEnd&&['script','link','css'].includes(entry.initiatorType)).reduce((sum,entry)=>sum+entry.encodedBodySize,0),
initialAssetRequests: performance.getEntriesByType('resource').filter(entry=>/\.(?:js|css)(?:[?#]|$)/.test(entry.name)&&entry.startTime <= nav.domContentLoadedEventEnd&&['script','link','css'].includes(entry.initiatorType)).map(entry=>({url:entry.name,bytes:entry.encodedBodySize})),
fcp: fcp ? Math.round(fcp.startTime) : null,
lcp: window.__lcp ? Math.round(window.__lcp) : null,
domContentLoaded: nav ? Math.round(nav.domContentLoadedEventEnd - nav.startTime) : null,
@@ -542,6 +550,8 @@ async function runOfflineScenario({ browserType, server, proxy, profile }) {
const fcp = paint.find((p) => p.name === 'first-contentful-paint');
return {
initialJsCss: performance.getEntriesByType('resource').filter(entry=>/\.(?:js|css)(?:[?#]|$)/.test(entry.name)&&entry.startTime <= nav.domContentLoadedEventEnd&&['script','link','css'].includes(entry.initiatorType)).reduce((sum,entry)=>sum+entry.encodedBodySize,0),
initialAssetRequests: performance.getEntriesByType('resource').filter(entry=>/\.(?:js|css)(?:[?#]|$)/.test(entry.name)&&entry.startTime <= nav.domContentLoadedEventEnd&&['script','link','css'].includes(entry.initiatorType)).map(entry=>({url:entry.name,bytes:entry.encodedBodySize})),
fcp: fcp ? Math.round(fcp.startTime) : null,
lcp: window.__lcp ? Math.round(window.__lcp) : null,
domContentLoaded: nav ? Math.round(nav.domContentLoadedEventEnd - nav.startTime) : null,
@@ -602,6 +612,27 @@ async function runUpdateScenario({ browserType, server, proxy, profile, updateKi
await sleep(50);
}
// 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);
}
}
// Switch server to Tree N+1 (app.js or theme-glass.css modification)
const targetTree = updateKind === 'js' ? trees.treeUpdateJs : trees.treeUpdateCss;
await server.restartWithPublic(targetTree);
@@ -666,9 +697,14 @@ async function main() {
const treeUpdateJs = path.join(tmpRoot, 'tree-update-js');
const treeUpdateCss = path.join(tmpRoot, 'tree-update-css');
cpSync(path.join(REPO_ROOT, 'frontend'), treeBase, { recursive: true });
cpSync(path.join(REPO_ROOT, 'frontend'), treeUpdateJs, { recursive: true });
cpSync(path.join(REPO_ROOT, 'frontend'), treeUpdateCss, { recursive: true });
let frontendSource=path.join(REPO_ROOT,'frontend');
if(options.frontendCommit) {
const archive=path.join(tmpRoot,'frontend.tar');
writeFileSync(archive,execFileSync('git',['archive',options.frontendCommit,'frontend'],{cwd:REPO_ROOT,maxBuffer:20*1024*1024}));
execFileSync('tar',['-xf',archive,'-C',tmpRoot]);
frontendSource=path.join(tmpRoot,'frontend');
}
for(const tree of [treeBase,treeUpdateJs,treeUpdateCss])cpSync(frontendSource,tree,{recursive:true});
// Append 1 comment line to app.js in treeUpdateJs
appendFileSync(path.join(treeUpdateJs, 'app.js'), '\n// baseline bench: 1-line app.js comment\n');
@@ -684,6 +720,7 @@ async function main() {
const results = {
date: new Date().toISOString(),
frontendCommit: options.frontendCommit,
commit: spawnSync('git', ['rev-parse', 'HEAD'], { cwd: REPO_ROOT }).stdout.toString().trim(),
runsConfigured: options.runs,
browsers: {},
@@ -729,6 +766,8 @@ async function main() {
autoplayBlocked: coldRuns.some((r) => r.autoplayBlocked),
wireBytesByType: coldRuns[0]?.wireBytesByType || {},
requests: coldRuns[0]?.requests || [],
initialJsCss: summarizeList(coldRuns.map(r=>r.initialJsCss)),
initialAssetRequests: coldRuns[0]?.initialAssetRequests || [],
};
}

135
perf/lazy.mjs Normal file
View File

@@ -0,0 +1,135 @@
#!/usr/bin/env node
// Phase 3 checks against the real Bun server, a same-origin restart and real workers.
import assert from 'node:assert/strict';
import { spawn } from 'node:child_process';
import { mkdtempSync, cpSync, mkdirSync, symlinkSync, readFileSync, writeFileSync, rmSync } from 'node:fs';
import { resolve, join } from 'node:path';
import { createServer } from 'node:net';
import { chromium, webkit } from 'playwright';
import { createThrottleProxy } from './proxy.mjs';
const repo=resolve(new URL('..',import.meta.url).pathname), scratch=mkdtempSync(join(repo,'perf/.tmp/lazy-'));
const sleep=ms=>new Promise(r=>setTimeout(r,ms));
async function wait(page,fn,arg,timeout=90000){const end=Date.now()+timeout;while(Date.now()<end){if(await page.evaluate(fn,arg))return;await sleep(100);}throw Error('Timed out: '+fn.toString());}
async function checkCssOrder(page){
const changes=await page.evaluate(async()=>{
await Lazy.load('layout:'+Lazy.layout);
const settle=()=>new Promise(resolve=>setTimeout(resolve,300));await settle();
const nodes=[...document.querySelectorAll('body,body *')],props=['display','position','color','background-color','font-family','font-size','padding','margin','border-radius','--bg','--text','--radius'];
const snapshot=()=>nodes.map(n=>props.map(p=>getComputedStyle(n).getPropertyValue(p)));
const links=[...document.querySelectorAll('link[data-lazy-css]')],positions=links.map(n=>[n,n.nextSibling]),before=snapshot();
const loaded=()=>new Promise((resolve,reject)=>{let tries=0;function poll(){if(links.every(n=>n.sheet))return resolve();if(tries++>120)return reject(Error('Reordered CSS did not load'));requestAnimationFrame(poll);}poll();});
links.forEach(n=>document.head.append(n));await loaded();await settle();const after=snapshot();
for(const [n,next] of positions.reverse())document.head.insertBefore(n,next);
await loaded();await settle();
return before.flatMap((values,i)=>values.flatMap((v,j)=>v===after[i][j]?[]:[{element:nodes[i].id||nodes[i].className,property:props[j],before:v,after:after[i][j]}]));
});
assert.deepEqual(changes,[],'requesting scoped layout CSS earlier preserves its computed appearance');
}
const args=process.argv.slice(2), requested=args.includes('--browser')?args[args.indexOf('--browser')+1]:'all';
const results=[];
async function run(name){
const cwd=join(scratch,name);mkdirSync(cwd);cpSync(join(repo,'frontend'),join(cwd,'public'),{recursive:true});
const portServer=createServer();await new Promise(r=>portServer.listen(0,'127.0.0.1',r));const port=portServer.address().port;await new Promise(r=>portServer.close(r));
let child,browser,proxy,log='';
async function stop(){if(!child)return;const p=child;child=null;if(p.exitCode===null){p.kill('SIGTERM');await new Promise(r=>p.once('exit',r));}}
async function start(){log='';child=spawn('bun',[join(repo,'server/server.js')],{cwd,env:{...process.env,PORT:String(port),ASSET_HASHING:'1',ASSET_SYNC:'1',BUILD_TAG:'',DB_PATH:join(cwd,'db.sqlite'),DATA_DIR:join(cwd,'data'),MEDIA_DIR:join(cwd,'media'),UPLOAD_DIR:join(cwd,'uploads')},stdio:['ignore','pipe','pipe']});for(const stream of [child.stdout,child.stderr])stream.on('data',c=>log=(log+c).slice(-15000));for(let i=0;i<200;i++){if(child.exitCode!==null)throw Error(log);try{if((await fetch('http://127.0.0.1:'+port+'/api/version')).ok)return;}catch{}await sleep(100);}throw Error(log);}
try {
await start();proxy=createThrottleProxy({targetPort:port,profile:'unthrottled'});const origin='http://127.0.0.1:'+await proxy.listen();proxy.startTracking();
browser=await ({chromium,webkit}[name]).launch({headless:true});const context=await browser.newContext();let disconnected=false;
await context.route('**/*',r=>r.request().url().startsWith(origin)?r.continue():r.abort());
await context.addInitScript(()=>{window.__saveData=localStorage.getItem('perf-save-data')==='1';const observer=new MutationObserver(()=>{if(document.body){window.__firstBodyLayout=document.documentElement.dataset.layout;observer.disconnect();}});observer.observe(document,{childList:true,subtree:true});Object.defineProperty(navigator,'onLine',{configurable:true,get:()=>localStorage.getItem('perf-offline')!=='1'});Object.defineProperty(navigator,'connection',{configurable:true,value:{get saveData(){return window.__saveData;}}});});
const page=await context.newPage(),errors=[],failedRequests=[];page.on('requestfailed',request=>{if(disconnected)failedRequests.push(request.url());});page.on('pageerror',e=>errors.push({message:e.message,disconnected}));page.on('console',m=>{if(m.type()==='error')console.log(name,'console:',m.text());});
console.log(name,'initial boot');await page.goto(origin);await page.waitForFunction(()=>typeof data!=='undefined'&&document.querySelector('#carModeBtn'));
await wait(page,async()=>!!(await navigator.serviceWorker.getRegistration())?.active);
await page.evaluate(()=>Lazy.warm());
await wait(page,async()=>{const c=await caches.open('ytplayer-assets'),r=await c.match('/__ytp_asset_state');if(!r)return false;const s=await r.json();return !(await Promise.all(Object.values(s.current.groups).flatMap(g=>g.files).map(async p=>!!await c.match(p+'?v='+s.current.files[p].h)))).includes(false);});
console.log(name,'warm complete');assert.equal(await page.evaluate(()=>Lazy.loaded('feature:car-mode')),false);
assert.equal(await page.evaluate(()=>[...document.querySelectorAll('.stg-overlay,.deck-chips,.vol-pct,.mode-name,.stg-bot,.ch-badge,.upnext-clear,.list-sub,.p2p-strip,.sleep-status .sl-ic,.sleep-status .sl-sub,.mc-tune')].every(n=>getComputedStyle(n).display==='none')),true,'classic retains shared visibility defaults without loading Glass CSS');
await checkCssOrder(page);
assert.equal(await page.evaluate(()=>Lazy.loaded('feature:piano')),false);
assert.equal(await page.evaluate(()=>[typeof newPlaylist,typeof renderSettings,typeof enterServiceMode,typeof Notes.load,typeof LyricsCore.fromText].every(type=>type==='function')),true,'playlist, settings, service mode and lyric core remain in RAM');
// Profile/imported settings use applyAppearance, not the Settings select handler.
await page.evaluate(()=>{data.settings.layout='glass-stage';applyAppearance();});
await wait(page,()=>document.documentElement.dataset.layout==='glass-stage'&&Lazy.layout==='glass-stage');
await page.evaluate(()=>{data.settings.layout='classic';applyAppearance();});
await wait(page,()=>document.documentElement.dataset.layout==='classic'&&Lazy.layout==='classic');
// Eager application panels stay usable without executing optional settings modules.
await page.click('[data-view="settings"]');await page.click('[data-section="appearance"]');await page.waitForSelector('#setLayout');
assert.equal(await page.evaluate(()=>Lazy.loaded('feature:piano')),false);
await page.selectOption('#setLayout','glass-stage');await page.waitForFunction(()=>document.documentElement.dataset.layout==='glass-stage');
await page.evaluate(()=>{window.__saveData=true;localStorage.setItem('perf-save-data','1');localStorage.setItem('perf-offline','1');});
await stop();disconnected=true;await proxy.setOffline(true);
console.log(name,'offline reload');await page.reload();await page.waitForFunction(()=>typeof data!=='undefined'&&document.querySelector('#carModeBtn'));
assert.equal(await page.evaluate(()=>document.documentElement.dataset.layout),'glass-stage');
assert.equal(await page.evaluate(()=>window.__firstBodyLayout),'glass-stage','selected layout is established before body/first paint');
await checkCssOrder(page);
const before=proxy.getStats().requestCount;
await page.getByRole('button',{name:'Car mode',exact:true}).click();await page.waitForSelector('.car-mode');
assert.equal(await page.locator('.car-controls button').count(),4,'all transport controls survive lazy setup');
assert.equal(await page.locator('.car-controls #voiceSearchBtn').count(),0,'voice search stays outside transport');
await page.getByRole('button',{name:'Exit Car mode'}).click();
assert.equal(proxy.getStats().requestCount,before,'warmed feature first use reaches no network');
await page.click('[data-view="settings"]');await page.click('[data-section="appearance"]');await page.selectOption('#setLayout','bento-hub');await page.waitForFunction(()=>document.documentElement.dataset.layout==='bento-hub');
await page.selectOption('#setLayout','f7-swipe');await page.waitForFunction(()=>document.documentElement.dataset.layout==='f7-swipe'&&!!window.Framework7);
await page.selectOption('#setLayout','classic');await page.waitForFunction(()=>document.documentElement.dataset.layout==='classic');
assert.equal(proxy.getStats().requestCount,before,'offline layout changes reach no network');
await page.getByRole('searchbox',{name:'Search settings and controls'}).fill('Enable piano transcription');
await page.locator('.settings-results button').first().waitFor();
await page.getByRole('searchbox',{name:'Search settings and controls'}).fill('');
// Every extension panel renders from disk without executing other panels eagerly.
for(const id of ['piano','midi','floating-lyrics','vocal-reducer','equalizer','setlist-import','broadcast']) {
await page.evaluate(id=>document.querySelector('[data-section="'+id+'"]').click(),id);
await wait(page,async id=>{const section=SettingsSections.get(id);return !section.lazyGroup;},id);
}
await page.evaluate(async()=>{await Lazy.load('feature:export');await Lazy.load('feature:video-edit');});
assert.equal(proxy.getStats().requestCount,before,'all warmed extension assets stay offline');
await start();disconnected=false;await proxy.setOffline(false);await page.evaluate(()=>localStorage.removeItem('perf-offline'));
// Keep an old tab whose core and group contracts remain those of build N.
const old=await context.newPage();old.on('pageerror',e=>errors.push({message:e.message,disconnected}));await old.goto(origin);await old.waitForSelector('#carModeBtn');
await old.evaluate(async()=>{window.__saveData=true;await Lazy.load('feature:setlist-import');window.__pinned=window.SetlistImport;});
await page.evaluate(()=>window.__saveData=true);
const previous=await page.evaluate(()=>Lazy.manifest);
await stop();
for(const file of ['video-edit.js','setlist-import.js','car-mode.js'])writeFileSync(join(cwd,'public',file),readFileSync(join(cwd,'public',file),'utf8')+'\n// phase3 new file\n');
const defs=JSON.parse(readFileSync(join(cwd,'public/assets.json')));defs.groups['feature:car-mode'].contract=2;writeFileSync(join(cwd,'public/assets.json'),JSON.stringify(defs));
// Production stamping replaces the source-only native map with these updated groups.
await start();const target=await(await fetch(origin+'/api/manifest')).json();
await page.evaluate(async()=>{window.__saveData=true;const r=await navigator.serviceWorker.getRegistration();await r.update();});
await wait(page,async target=>{const r=await navigator.serviceWorker.getRegistration();if(!r?.waiting)return false;return(await SwUpdate.askCacheStatus(r.waiting))?.version===target;},target.buildTag);
// No automatic navigation; a real playing flag blocks the real apply flow.
let navs=0;page.on('framenavigated',f=>{if(f===page.mainFrame())navs++;});
await sleep(1000);assert.equal(navs,0);
await page.evaluate(()=>Player._wantsPlaying=true);
page.once('dialog',d=>d.dismiss());
const guard=await page.evaluate(async()=>{try{await SwUpdate.applyUpdate({getRegistration:()=>navigator.serviceWorker.getRegistration(),askStatus:SwUpdate.askCacheStatus,reload:()=>location.reload()});return false;}catch{return true;}});
assert.equal(guard,true);assert.equal(navs,0);
await page.evaluate(()=>Player._wantsPlaying=false);
// Explicit worker message, then explicit navigation into new stamped core.
await page.evaluate(async()=>{const r=await navigator.serviceWorker.getRegistration();r.waiting.postMessage({type:'SKIP_WAITING'});});
await wait(page,async tag=>{const c=await caches.open('ytplayer-assets');return(await(await c.match('/__ytp_asset_state')).json()).current.buildTag===tag;},target.buildTag);
await page.reload();await page.waitForSelector('#carModeBtn');assert.equal(await page.evaluate(()=>Lazy.manifest.buildTag),target.buildTag);
// No warm: the new page gets a same-contract N-1 file immediately from disk.
const cacheKey='/video-edit.js?v='+target.files['/video-edit.js'].h;
assert.equal(await page.evaluate(async key=>!!await(await caches.open('ytplayer-assets')).match(key),cacheKey),false);
const stale=await page.evaluate(async key=>{const r=await fetch(key);return {h:r.headers.get('X-Asset-Hash'),body:await r.text()};},cacheKey);
assert.equal(stale.h,previous.files['/video-edit.js'].h);assert.ok(!stale.body.includes('// phase3 new file'));
await page.evaluate(()=>Lazy.load('feature:video-edit'));const pinned=await page.evaluate(()=>{window.__videoPinned=VideoEdit;return Lazy.loaded('feature:video-edit');});assert.equal(pinned,true);
// Different contract is already downloaded before commit, but the old core must keep N-1.
const oldRequests=[];old.on('request',r=>oldRequests.push(r.url()));
await old.evaluate(()=>Lazy.load('feature:car-mode'));assert.equal(await old.evaluate(()=>Lazy.reloadRequired),true);
await old.getByRole('button',{name:'Refresh UI',exact:true}).waitFor();
assert.ok(oldRequests.some(u=>u.includes('/car-mode.js?v='+previous.files['/car-mode.js'].h)));
await old.evaluate(()=>Lazy.load('feature:setlist-import'));assert.equal(await old.evaluate(()=>window.SetlistImport===window.__pinned),true);
// Warm new files explicitly; compatible, unexecuted old-page group selects new cached URL.
await page.evaluate(async()=>{const r=await navigator.serviceWorker.getRegistration();r.active.postMessage({type:'WARM_ASSETS',saveData:false});});
await wait(page,async key=>!!await(await caches.open('ytplayer-assets')).match(key),cacheKey);
await old.evaluate(()=>Lazy.load('feature:video-edit'));assert.ok(oldRequests.some(u=>u.includes(cacheKey)));
await page.evaluate(()=>Lazy.load('feature:video-edit'));assert.equal(await page.evaluate(()=>window.VideoEdit===window.__videoPinned),true);
const offlineTransportErrors=errors.filter(error=>name==='webkit'&&error.disconnected&&(error.message==='TypeError: Load failed'||/\/api\/(?:recommendations\?[^\s]*|version)\.$/.test(error.message)));
assert.deepEqual(errors.filter(error=>!offlineTransportErrors.includes(error)),[]);
if(offlineTransportErrors.length)assert.ok(failedRequests.some(url=>/\/api\/(?:recommendations|version)/.test(url)));
results.push({browser:name,sourceBuildTag:previous.buildTag,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});
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});}

View File

@@ -153,6 +153,9 @@ async function run(name) {
assert.equal(banners, 1, 'no banner after applying the current build');
assert.equal(await oldTab.evaluate(async()=>await (await fetch('/app.js')).text()),oldBytes,'open legacy tab keeps N-1 plain assets');
await oldTab.close();
// Phase 3 commits core + layout first; idle warming must fill the retained full shell before offline feature checks.
await page.evaluate(()=>Lazy.warm());
await waitAsync(page,async files=>{const cache=await caches.open('ytplayer-assets');return (await Promise.all(Object.keys(files).map(async path=>{const response=await cache.match(path+'?v='+files[path].h);return !!response&&response.headers.get('X-Asset-Hash')===files[path].h;}))).every(Boolean);},Object.fromEntries(shell.map(path=>[path==='/'?'/index.html':path,manifest.files[path==='/'?'/index.html':path]])));
const cacheResult = await page.evaluate(async ({ tag, shell, files }) => {
const cache = await caches.open('ytplayer-assets');
const missing = [], incorrect = [];
@@ -231,5 +234,5 @@ async function run(name) {
try {
const results = [];
for (const name of browserName === 'all' ? ['chromium', 'webkit'] : [browserName]) results.push(await run(name));
writeFileSync(join(repo, `perf/results/phase2-migration-${profile}-2026-10-07.json`), JSON.stringify({ commit, results }, null, 2) + '\n');
writeFileSync(join(repo, `perf/results/phase3-migration-${profile}-2026-10-08.json`), JSON.stringify({ commit, results }, null, 2) + '\n');
} finally { rmSync(scratch, { recursive: true, force: true }); }

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,132 @@
{
"date": "2026-10-07T20:10:25.561Z",
"frontendCommit": null,
"commit": "46ec9ab2ca0f1b5365b5b05b08052df967d6fcfc",
"runsConfigured": 1,
"browsers": {
"chromium": {
"lte": {
"offline": {
"requestCount": {
"median": 0,
"min": 0,
"max": 0,
"values": [
0
]
},
"wireBytes": {
"median": 0,
"min": 0,
"max": 0,
"values": [
0
]
},
"fcp": {
"median": 48,
"min": 48,
"max": 48,
"values": [
48
]
},
"lcp": {
"median": 48,
"min": 48,
"max": 48,
"values": [
48
]
},
"domContentLoaded": {
"median": 165,
"min": 165,
"max": 165,
"values": [
165
]
},
"bootDone": {
"median": 170,
"min": 170,
"max": 170,
"values": [
170
]
},
"reloadDuration": {
"median": 187,
"min": 187,
"max": 187,
"values": [
187
]
}
}
}
},
"webkit": {
"lte": {
"offline": {
"requestCount": {
"median": 0,
"min": 0,
"max": 0,
"values": [
0
]
},
"wireBytes": {
"median": 0,
"min": 0,
"max": 0,
"values": [
0
]
},
"fcp": {
"median": 80,
"min": 80,
"max": 80,
"values": [
80
]
},
"lcp": {
"median": 136,
"min": 136,
"max": 136,
"values": [
136
]
},
"domContentLoaded": {
"median": 134,
"min": 134,
"max": 134,
"values": [
134
]
},
"bootDone": {
"median": 140,
"min": 140,
"max": 140,
"values": [
140
]
},
"reloadDuration": {
"median": 163,
"min": 163,
"max": 163,
"values": [
163
]
}
}
}
}
}
}

View File

@@ -0,0 +1,79 @@
{
"results": [
{
"browser": "chromium",
"sourceBuildTag": "bea1f63eba85",
"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": "bea1f63eba85",
"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:38845/api/version.",
"disconnected": true
},
{
"message": "TypeError: Load failed",
"disconnected": true
},
{
"message": "/127.0.0.1:38845/api/recommendations?fp=163577c02a53bc40.",
"disconnected": true
},
{
"message": "TypeError: Load failed",
"disconnected": true
},
{
"message": "/127.0.0.1:38845/api/version.",
"disconnected": true
}
]
}
]
}

View File

@@ -0,0 +1,41 @@
{
"commit": "b77938a",
"results": [
{
"browser": "chromium",
"oldTag": "1175f1a1d2c1",
"newTag": "bea1f63eba85",
"banners": 1,
"updateReloads": 1,
"cachedFiles": 114,
"durationMs": 24356,
"wireBytes": 607853,
"offline": true,
"legacyURLs": 21,
"profile": "lossy",
"userInitiatedOnly": true,
"playbackGuard": true,
"evictionRepair": true,
"incrementalRefresh": true,
"offlineTransportErrors": 0
},
{
"browser": "webkit",
"oldTag": "1175f1a1d2c1",
"newTag": "bea1f63eba85",
"banners": 1,
"updateReloads": 1,
"cachedFiles": 114,
"durationMs": 20402,
"wireBytes": 729652,
"offline": true,
"legacyURLs": 21,
"profile": "lossy",
"userInitiatedOnly": true,
"playbackGuard": true,
"evictionRepair": true,
"incrementalRefresh": true,
"offlineTransportErrors": 4
}
]
}

View File

@@ -0,0 +1,41 @@
{
"commit": "b77938a",
"results": [
{
"browser": "chromium",
"oldTag": "1175f1a1d2c1",
"newTag": "bea1f63eba85",
"banners": 1,
"updateReloads": 1,
"cachedFiles": 114,
"durationMs": 10864,
"wireBytes": 625216,
"offline": true,
"legacyURLs": 21,
"profile": "unthrottled",
"userInitiatedOnly": true,
"playbackGuard": true,
"evictionRepair": true,
"incrementalRefresh": true,
"offlineTransportErrors": 0
},
{
"browser": "webkit",
"oldTag": "1175f1a1d2c1",
"newTag": "bea1f63eba85",
"banners": 1,
"updateReloads": 1,
"cachedFiles": 114,
"durationMs": 9584,
"wireBytes": 743322,
"offline": true,
"legacyURLs": 21,
"profile": "unthrottled",
"userInitiatedOnly": true,
"playbackGuard": true,
"evictionRepair": true,
"incrementalRefresh": true,
"offlineTransportErrors": 4
}
]
}

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,132 @@
{
"date": "2026-10-07T20:10:14.094Z",
"frontendCommit": null,
"commit": "46ec9ab2ca0f1b5365b5b05b08052df967d6fcfc",
"runsConfigured": 1,
"browsers": {
"chromium": {
"lte": {
"offline": {
"requestCount": {
"median": 0,
"min": 0,
"max": 0,
"values": [
0
]
},
"wireBytes": {
"median": 0,
"min": 0,
"max": 0,
"values": [
0
]
},
"fcp": {
"median": 44,
"min": 44,
"max": 44,
"values": [
44
]
},
"lcp": {
"median": 44,
"min": 44,
"max": 44,
"values": [
44
]
},
"domContentLoaded": {
"median": 135,
"min": 135,
"max": 135,
"values": [
135
]
},
"bootDone": {
"median": 140,
"min": 140,
"max": 140,
"values": [
140
]
},
"reloadDuration": {
"median": 158,
"min": 158,
"max": 158,
"values": [
158
]
}
}
}
},
"webkit": {
"lte": {
"offline": {
"requestCount": {
"median": 0,
"min": 0,
"max": 0,
"values": [
0
]
},
"wireBytes": {
"median": 0,
"min": 0,
"max": 0,
"values": [
0
]
},
"fcp": {
"median": 64,
"min": 64,
"max": 64,
"values": [
64
]
},
"lcp": {
"median": 105,
"min": 105,
"max": 105,
"values": [
105
]
},
"domContentLoaded": {
"median": 103,
"min": 103,
"max": 103,
"values": [
103
]
},
"bootDone": {
"median": 107,
"min": 107,
"max": 107,
"values": [
107
]
},
"reloadDuration": {
"median": 124,
"min": 124,
"max": 124,
"values": [
124
]
}
}
}
}
}
}