Guard stale app execution against feature contract changes

This commit is contained in:
Jonathan Sykes
2026-10-08 09:18:51 +08:00
parent 3505dc3c0e
commit c4a512bea6
7 changed files with 143 additions and 8 deletions

View File

@@ -350,3 +350,7 @@ then use the page FCP to locate the rendering dependency.
Pass `--assert-cold` on new builds to require exactly one positive-byte app.js
response in every cold sample, including worker installation. Omit this assertion
when diagnosing historical regressions.
Phase 6 counts its fetch-owned app.js response in initial JS/CSS bytes. Since the
bootstrap consumes that response asynchronously, DOMContentLoaded no longer means
app execution is complete; compare the existing actual boot-done landmark.

View File

@@ -377,8 +377,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,start:entry.startTime,responseStart:entry.responseStart,end:entry.responseEnd,initiator:entry.initiatorType})),
initialJsCss: performance.getEntriesByType('resource').filter(entry=>/\.(?:js|css)(?:[?#]|$)/.test(entry.name)&&entry.startTime <= nav.domContentLoadedEventEnd&&(['script','link','css'].includes(entry.initiatorType)||(entry.initiatorType==='fetch'&&new URL(entry.name).pathname==='/app.js'))).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)||(entry.initiatorType==='fetch'&&new URL(entry.name).pathname==='/app.js'))).map(entry=>({url:entry.name,bytes:entry.encodedBodySize,start:entry.startTime,responseStart:entry.responseStart,end:entry.responseEnd,initiator:entry.initiatorType})),
fcp: fcp ? Math.round(fcp.startTime) : null,
lcp: window.__lcp ? Math.round(window.__lcp) : null,
domContentLoaded: nav ? Math.round(nav.domContentLoadedEventEnd - nav.startTime) : null,
@@ -486,8 +486,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,start:entry.startTime,responseStart:entry.responseStart,end:entry.responseEnd,initiator:entry.initiatorType})),
initialJsCss: performance.getEntriesByType('resource').filter(entry=>/\.(?:js|css)(?:[?#]|$)/.test(entry.name)&&entry.startTime <= nav.domContentLoadedEventEnd&&(['script','link','css'].includes(entry.initiatorType)||(entry.initiatorType==='fetch'&&new URL(entry.name).pathname==='/app.js'))).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)||(entry.initiatorType==='fetch'&&new URL(entry.name).pathname==='/app.js'))).map(entry=>({url:entry.name,bytes:entry.encodedBodySize,start:entry.startTime,responseStart:entry.responseStart,end:entry.responseEnd,initiator:entry.initiatorType})),
fcp: fcp ? Math.round(fcp.startTime) : null,
lcp: window.__lcp ? Math.round(window.__lcp) : null,
domContentLoaded: nav ? Math.round(nav.domContentLoadedEventEnd - nav.startTime) : null,
@@ -567,8 +567,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,start:entry.startTime,responseStart:entry.responseStart,end:entry.responseEnd,initiator:entry.initiatorType})),
initialJsCss: performance.getEntriesByType('resource').filter(entry=>/\.(?:js|css)(?:[?#]|$)/.test(entry.name)&&entry.startTime <= nav.domContentLoadedEventEnd&&(['script','link','css'].includes(entry.initiatorType)||(entry.initiatorType==='fetch'&&new URL(entry.name).pathname==='/app.js'))).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)||(entry.initiatorType==='fetch'&&new URL(entry.name).pathname==='/app.js'))).map(entry=>({url:entry.name,bytes:entry.encodedBodySize,start:entry.startTime,responseStart:entry.responseStart,end:entry.responseEnd,initiator:entry.initiatorType})),
fcp: fcp ? Math.round(fcp.startTime) : null,
lcp: window.__lcp ? Math.round(window.__lcp) : null,
domContentLoaded: nav ? Math.round(nav.domContentLoadedEventEnd - nav.startTime) : null,

View File

@@ -84,6 +84,23 @@ async function run(name){
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'));
// Change only app bytes, then prove loss of its current cache entry can boot
// the compatible N-1 core offline without weakening the inline CSP policy.
await stop();writeFileSync(join(cwd,'public/app.js'),readFileSync(join(cwd,'public/app.js'),'utf8')+'\n// phase6 app eviction probe\n');await start();
const appTarget=await(await fetch(origin+'/api/manifest')).json();
await page.evaluate(async()=>{const r=await navigator.serviceWorker.getRegistration();await r.update();});
await wait(page,async tag=>{const r=await navigator.serviceWorker.getRegistration();return r?.waiting&&(await SwUpdate.askCacheStatus(r.waiting))?.version===tag;},appTarget.buildTag);
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;},appTarget.buildTag);
const appKey='/app.js?v='+appTarget.files['/app.js'].h;
await page.evaluate(async key=>{await(await caches.open('ytplayer-assets')).delete(key);},appKey);
await stop();disconnected=true;await proxy.setOffline(true);
const probe=await context.newPage();probe.on('pageerror',e=>errors.push({message:e.message,disconnected}));
const offlineBefore=proxy.getStats().requestCount;await probe.goto(origin);await probe.waitForFunction(()=>typeof data!=='undefined'&&typeof Player!=='undefined'&&document.querySelector('#carModeBtn'));
assert.equal(await probe.evaluate(async key=>!!await(await caches.open('ytplayer-assets')).match(key),appKey),false,'N-1 never poisons the current app key');
assert.equal(proxy.getStats().requestCount,offlineBefore,'N-1 app boot is entirely offline');
await probe.close();await start();disconnected=false;await proxy.setOffline(false);
await page.evaluate(async key=>{const r=await fetch(key+'&__ytpfresh=1');if(r.headers.get('X-Asset-Hash')!==key.split('v=')[1])throw Error('Wrong repair body');await(await caches.open('ytplayer-assets')).put(key,r);},appKey);
// 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;});
@@ -128,7 +145,7 @@ async function run(name){
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});
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,appEvictionN1Offline:true,playbackGuard:true,prePaintLayout:true,offlineSettingsPanels:true,scriptErrors:[],offlineTransportErrors});
console.log(name,'PASS');
} finally {await browser?.close();await proxy?.close();await stop();}
}

View File

@@ -0,0 +1,97 @@
{
"results": [
{
"browser": "chromium",
"sourceBuildTag": "41a7ba2c29a3",
"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,
"appEvictionN1Offline": true,
"playbackGuard": true,
"prePaintLayout": true,
"offlineSettingsPanels": true,
"scriptErrors": [],
"offlineTransportErrors": []
},
{
"browser": "webkit",
"sourceBuildTag": "41a7ba2c29a3",
"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,
"appEvictionN1Offline": true,
"playbackGuard": true,
"prePaintLayout": true,
"offlineSettingsPanels": true,
"scriptErrors": [],
"offlineTransportErrors": [
{
"message": "TypeError: Load failed",
"disconnected": true
},
{
"message": "/127.0.0.1:36117/api/version.",
"disconnected": true
},
{
"message": "TypeError: Load failed",
"disconnected": true
},
{
"message": "/127.0.0.1:36117/api/recommendations?fp=163577c02a53bc40.",
"disconnected": true
},
{
"message": "TypeError: Load failed",
"disconnected": true
},
{
"message": "/127.0.0.1:36117/api/version.",
"disconnected": true
},
{
"message": "TypeError: Load failed",
"disconnected": true
},
{
"message": "/127.0.0.1:36117/api/recommendations?fp=163577c02a53bc40.",
"disconnected": true
},
{
"message": "TypeError: Load failed",
"disconnected": true
},
{
"message": "/127.0.0.1:36117/api/version.",
"disconnected": true
}
]
}
]
}