Merge Phase 6 and reconcile offline completion, response capture and contract tests
This commit is contained in:
84
frontend/app-bootstrap.test.js
Normal file
84
frontend/app-bootstrap.test.js
Normal file
@@ -0,0 +1,84 @@
|
|||||||
|
const {test}=require('node:test');
|
||||||
|
const assert=require('node:assert/strict');
|
||||||
|
const {webcrypto,createHash}=require('node:crypto');
|
||||||
|
const vm=require('node:vm');
|
||||||
|
const {readFileSync}=require('node:fs');
|
||||||
|
function fixture({cached=false,tampered=false,enabled=true,loading=false,quota=false,insecure=false,previous=false,contract=1,capture=true,controller=true,parser=false}={}){
|
||||||
|
const source='window.appRuns=(window.appRuns||0)+1; window.boot=()=>window.bootRuns=(window.bootRuns||0)+1; document.addEventListener("DOMContentLoaded",boot);',hash=createHash('sha256').update(source).digest('hex'), key='/app.js?v='+hash.slice(0,10);
|
||||||
|
const oldSource='window.oldApp=true;'+source, oldHash=createHash('sha256').update(oldSource).digest('hex').slice(0,10), oldKey='/app.js?v='+oldHash;
|
||||||
|
const held=new Map(), appended=[],writes=[],listeners={};let calls=0;
|
||||||
|
const response=()=>new Response(tampered?'bad':source,{headers:{'X-Asset-Hash':hash.slice(0,10)}});
|
||||||
|
if(cached)held.set(key,response());
|
||||||
|
if(previous){held.set(oldKey,new Response(oldSource,{headers:{'X-Asset-Hash':oldHash}}));held.set('/__ytp_asset_state',Response.json({previous:{files:{'/app.js':{h:oldHash}},groups:{core:{contract,files:['/app.js']}}}}));}
|
||||||
|
const root={crypto:insecure?undefined:webcrypto,TextDecoder,Uint8Array,btoa,console,AssetSyncCore:require('./asset-sync-core'),navigator:{serviceWorker:{controller:controller?{}:null}},Lazy:{captureApp:capture,manifest:{groups:{core:{contract:1,files:['/app.js']}},appCache:enabled,files:{'/app.js':{h:hash.slice(0,10)}}},url:()=>key},fetch:async()=>{calls++;return previous?new Response(oldSource,{headers:{'X-Asset-Hash':oldHash}}):response();},caches:{open:async()=>({match:async k=>held.get(k)?.clone(),put:async(k,r)=>{if(quota)throw Error('quota');held.set(k,r.clone());}})}};
|
||||||
|
const doc=root.document={readyState:loading?'loading':'complete',currentScript:parser?{}:null,write:value=>{writes.push(value);vm.runInContext(source,context);},querySelector:()=>({content:"script-src 'self' 'sha256-"+Buffer.from(hash,'hex').toString('base64')+"'"}),addEventListener:(name,fn)=>(listeners[name] ||= []).push(fn),createElement:()=>({remove(){this.removed=true;}}),head:{append(node){appended.push(node);if(node.textContent)vm.runInContext(node.textContent,context);else {queueMicrotask(async()=>{if(previous && node.src===key){const state=await held.get('/__ytp_asset_state').clone().json();if(!root.AssetSyncCore.fallback(root.Lazy.manifest,state.previous,'/app.js'))return node.onerror();vm.runInContext(oldSource,context);}else vm.runInContext(node.src===oldKey?oldSource:source,context);node.onload();});}}}};
|
||||||
|
root.window=root;const context=vm.createContext(root);
|
||||||
|
vm.runInContext(readFileSync(require.resolve('./section-rail.js'),'utf8'),context);
|
||||||
|
return {root,held,appended,writes,listeners,key,calls:()=>calls,doc,response,oldKey};
|
||||||
|
}
|
||||||
|
test('cold boot caches verified app before execution so worker skips its download',async()=>{
|
||||||
|
const f=fixture();await f.root.AppBootstrap.ready;assert.equal(f.calls(),1);assert.ok(f.held.has(f.key));assert.equal(f.root.appRuns,1);assert.equal(f.root.bootRuns,1);assert.equal(f.appended[0].src,undefined);assert.equal(f.appended[0].removed,true);
|
||||||
|
});
|
||||||
|
test('offline boot reads the exact cached app without network or re-evaluation',async()=>{
|
||||||
|
const f=fixture({cached:true});await f.root.AppBootstrap.ready;assert.equal(f.calls(),0);assert.equal(f.root.appRuns,1);assert.equal(f.root.bootRuns,1);
|
||||||
|
});
|
||||||
|
test('tampered cached bytes are rejected before execution',async()=>{
|
||||||
|
const f=fixture({cached:true,tampered:true});await assert.rejects(f.root.AppBootstrap.ready,/hash/);assert.equal(f.root.appRuns,undefined);
|
||||||
|
});
|
||||||
|
test('native and rollback boot use the original external classic script',async()=>{
|
||||||
|
const f=fixture({enabled:false});await f.root.AppBootstrap.ready;assert.equal(f.calls(),0);assert.equal(f.appended[0].src,f.key);assert.equal(f.root.bootRuns,1);
|
||||||
|
});
|
||||||
|
test('boot waits for the normal DOM event when the parser has not finished',async()=>{
|
||||||
|
const f=fixture({loading:true});await f.root.AppBootstrap.ready;assert.equal(f.root.bootRuns,undefined);for(const fn of f.listeners.DOMContentLoaded)fn();assert.equal(f.root.appRuns,1);assert.equal(f.root.bootRuns,1);
|
||||||
|
});
|
||||||
|
test('quota failure still executes the verified response without deleting saved data',async()=>{
|
||||||
|
const f=fixture({quota:true});await f.root.AppBootstrap.ready;assert.equal(f.calls(),1);assert.equal(f.root.appRuns,1);assert.equal(f.root.bootRuns,1);
|
||||||
|
});
|
||||||
|
|
||||||
|
|
||||||
|
test('the early head response is consumed without a second page fetch',async()=>{
|
||||||
|
const f=fixture();f.root.Lazy.appResponse=Promise.resolve(f.response());await f.root.AppBootstrap.ready;
|
||||||
|
assert.equal(f.calls(),0);assert.ok(f.held.has(f.key));assert.equal(f.root.appRuns,1);
|
||||||
|
});
|
||||||
|
|
||||||
|
|
||||||
|
test('insecure local HTTP boot retains external execution without WebCrypto',async()=>{
|
||||||
|
const f=fixture({insecure:true});await f.root.AppBootstrap.ready;assert.equal(f.calls(),0);assert.equal(f.appended[0].src,f.key);assert.equal(f.root.bootRuns,1);
|
||||||
|
});
|
||||||
|
|
||||||
|
|
||||||
|
test('missing current app uses verified same-contract N-1 offline through the controlling worker',async()=>{
|
||||||
|
const f=fixture({previous:true});await f.root.AppBootstrap.ready;
|
||||||
|
assert.equal(f.appended[0].src,f.oldKey);assert.equal(f.root.oldApp,true);assert.equal(f.root.bootRuns,1);assert.equal(f.held.has(f.key),false);
|
||||||
|
});
|
||||||
|
test('an incompatible previous core never executes against the new shell',async()=>{
|
||||||
|
const f=fixture({previous:true,contract:2});await assert.rejects(f.root.AppBootstrap.ready,/hash/);assert.equal(f.root.appRuns,undefined);
|
||||||
|
});
|
||||||
|
|
||||||
|
|
||||||
|
test('a feature contract change also prevents stale core execution',async()=>{
|
||||||
|
const f=fixture({previous:true});f.root.Lazy.manifest.groups['feature:test']={contract:2,files:[]};
|
||||||
|
const state=await f.held.get('/__ytp_asset_state').json();state.previous.groups['feature:test']={contract:1,files:[]};f.held.set('/__ytp_asset_state',Response.json(state));
|
||||||
|
await assert.rejects(f.root.AppBootstrap.ready,/hash/);assert.equal(f.root.appRuns,undefined);
|
||||||
|
});
|
||||||
|
|
||||||
|
|
||||||
|
test('native-cache engines use their external script and keep normal code-cache behavior',async()=>{
|
||||||
|
const f=fixture({capture:false,controller:false});await f.root.AppBootstrap.ready;assert.equal(f.calls(),0);assert.equal(f.appended[0].src,f.key);assert.equal(f.root.bootRuns,1);
|
||||||
|
});
|
||||||
|
|
||||||
|
|
||||||
|
test('native-cache engines still reject an incompatible retained core',async()=>{
|
||||||
|
const f=fixture({capture:false,previous:true,contract:2});await assert.rejects(f.root.AppBootstrap.ready,/Unable to load player/);assert.equal(f.root.appRuns,undefined);
|
||||||
|
});
|
||||||
|
test('native-cache engines use the validated current cache entry without decoding its body',async()=>{
|
||||||
|
const f=fixture({capture:false,cached:true});await f.root.AppBootstrap.ready;assert.equal(f.calls(),0);assert.equal(f.appended[0].src,f.key);assert.equal(f.root.bootRuns,1);
|
||||||
|
});
|
||||||
|
|
||||||
|
test('native parser boot preserves classic evaluation before the original DOM event',async()=>{
|
||||||
|
const f=fixture({capture:false,loading:true,parser:true});
|
||||||
|
assert.equal(f.root.appRuns,1);assert.equal(f.root.bootRuns,undefined);
|
||||||
|
assert.deepEqual(f.writes,['<script src="'+f.key+'"></script>']);assert.equal(f.appended.length,0);
|
||||||
|
for(const fn of f.listeners.DOMContentLoaded)fn();await f.root.AppBootstrap.ready;
|
||||||
|
assert.equal(f.root.bootRuns,1);assert.equal(f.calls(),0);
|
||||||
|
});
|
||||||
@@ -30,8 +30,8 @@ test('Phase 4 preserves the exact player, continuity, tracking and queue bodies'
|
|||||||
test('eager shell and Settings definitions load before app state and defer reading it',()=>{
|
test('eager shell and Settings definitions load before app state and defer reading it',()=>{
|
||||||
const html=readFileSync(join(__dirname,'index.html'),'utf8');
|
const html=readFileSync(join(__dirname,'index.html'),'utf8');
|
||||||
for(const file of ['shell-core.js','views-core.js','section-rail.js']) {
|
for(const file of ['shell-core.js','views-core.js','section-rail.js']) {
|
||||||
assert.ok(html.indexOf('src="'+file+'"')<html.indexOf('src="app.js"'));
|
assert.ok(html.indexOf('src="'+file+'"')<=html.indexOf('src="section-rail.js"'));
|
||||||
const vm=require('node:vm');const context=vm.createContext({window:{},document:{}});
|
const vm=require('node:vm');const doc={readyState:'complete',addEventListener(){},createElement:()=>({}),head:{append(){}}};const globals={document:doc,Lazy:{manifest:{appCache:false},url:()=>'/app.js'},console};globals.window=globals;const context=vm.createContext(globals);
|
||||||
// Evaluation before any data/$/els/Player declaration must be safe.
|
// Evaluation before any data/$/els/Player declaration must be safe.
|
||||||
vm.runInContext(readFileSync(join(__dirname,file),'utf8'),context);
|
vm.runInContext(readFileSync(join(__dirname,file),'utf8'),context);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -41,6 +41,9 @@
|
|||||||
const group = Object.keys(manifest.groups).find(name => manifest.groups[name].files.includes(path));
|
const group = Object.keys(manifest.groups).find(name => manifest.groups[name].files.includes(path));
|
||||||
if (!group || !previous.groups[group]?.files.includes(path) ||
|
if (!group || !previous.groups[group]?.files.includes(path) ||
|
||||||
manifest.groups[group].contract !== previous.groups[group].contract) return null;
|
manifest.groups[group].contract !== previous.groups[group].contract) return null;
|
||||||
|
// A stale core must understand every group contract in the new shell.
|
||||||
|
if (path === "/app.js" && Object.entries(manifest.groups).some(([name, entry]) =>
|
||||||
|
previous.groups[name]?.contract !== entry.contract)) return null;
|
||||||
return url(path, previous.files[path]);
|
return url(path, previous.files[path]);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -96,3 +96,9 @@ test('metadata probes release response streams while stored asset bodies remain
|
|||||||
assert.equal((await core.status(m,c)).offlineReady,true);assert.ok(probes.length>0);assert.ok(probes.every(r=>r.bodyUsed));
|
assert.equal((await core.status(m,c)).offlineReady,true);assert.ok(probes.length>0);assert.ok(probes.every(r=>r.bodyUsed));
|
||||||
assert.equal(await(await match('/a.js?v=a')).text(),'a');assert.equal(await(await match('/index.html?v=b')).text(),'b');
|
assert.equal(await(await match('/a.js?v=a')).text(),'a');assert.equal(await(await match('/index.html?v=b')).text(),'b');
|
||||||
});
|
});
|
||||||
|
|
||||||
|
test('stale app core requires every feature contract to match the new shell',()=>{
|
||||||
|
const old={files:{'/app.js':{h:'old'}},groups:{core:{contract:1,files:['/app.js']},extra:{contract:1,files:[]}}};
|
||||||
|
const next=structuredClone(old);next.files['/app.js'].h='new';next.groups.extra.contract=2;
|
||||||
|
assert.equal(core.fallback(next,old,'/app.js'),null);next.groups.extra.contract=1;assert.equal(core.fallback(next,old,'/app.js'),'/app.js?v=old');
|
||||||
|
});
|
||||||
|
|||||||
@@ -15,11 +15,11 @@ test('staged same-contract responses use verified N-1 without poisoning the new
|
|||||||
const e=environment(); const cache=await e.storage.open(core.CACHE);
|
const e=environment(); const cache=await e.storage.open(core.CACHE);
|
||||||
const old={buildTag:'old',files:{'/extra.js':{h:'old'}},groups:{core:{files:[],contract:1},'feature:extra':{files:['/extra.js'],contract:1,background:true}}};
|
const old={buildTag:'old',files:{'/extra.js':{h:'old'}},groups:{core:{files:[],contract:1},'feature:extra':{files:['/extra.js'],contract:1,background:true}}};
|
||||||
e.m.groups.core.contract=1; e.m.groups['feature:extra']={files:['/extra.js'],contract:1,background:true}; e.m.files['/extra.js']={h:'new'};
|
e.m.groups.core.contract=1; e.m.groups['feature:extra']={files:['/extra.js'],contract:1,background:true}; e.m.files['/extra.js']={h:'new'};
|
||||||
await cache.put(core.STATE,Response.json({current:old})); await cache.put('/extra.js?v=old',new Response('old body',{headers:{'X-Asset-Hash':'old'}}));
|
await cache.put(core.STATE,Response.json({current:old})); await cache.put('/extra.js?v=old',new Response('old body',{headers:{'X-Asset-Hash':'old','Cache-Control':'public, max-age=31536000, immutable','Content-Encoding':'gzip','Content-Length':'9'}}));
|
||||||
await e.dispatch('install'); await e.dispatch('activate');await e.complete();
|
await e.dispatch('install'); await e.dispatch('activate');await e.complete();
|
||||||
assert.ok(await cache.match('/extra.js?v=new'));await cache.delete('/extra.js?v=new');
|
assert.ok(await cache.match('/extra.js?v=new'));await cache.delete('/extra.js?v=new');
|
||||||
assert.equal(await cache.match('/extra.js?v=new'),undefined);
|
assert.equal(await cache.match('/extra.js?v=new'),undefined);
|
||||||
const stale=await e.request('/extra.js?v=new'); assert.equal(await stale.text(),'old body');assert.equal(stale.headers.get('X-Asset-Hash'),'old');assert.equal(await cache.match('/extra.js?v=new'),undefined);
|
const stale=await e.request('/extra.js?v=new'); assert.equal(await stale.text(),'old body');assert.equal(stale.headers.get('X-Asset-Hash'),'old');assert.equal(stale.headers.get('Cache-Control'),'no-store');assert.equal(stale.headers.get('Content-Encoding'),null);assert.equal(stale.headers.get('Content-Length'),null);assert.equal(await cache.match('/extra.js?v=new'),undefined);
|
||||||
await e.dispatch('message',{data:{type:'WARM_ASSETS',saveData:true}}); assert.ok(await cache.match('/extra.js?v=new'));
|
await e.dispatch('message',{data:{type:'WARM_ASSETS',saveData:true}}); assert.ok(await cache.match('/extra.js?v=new'));
|
||||||
await e.dispatch('message',{data:{type:'WARM_ASSETS',saveData:false}}); assert.equal((await cache.match('/extra.js?v=new')).headers.get('X-Asset-Hash'),'new');
|
await e.dispatch('message',{data:{type:'WARM_ASSETS',saveData:false}}); assert.equal((await cache.match('/extra.js?v=new')).headers.get('X-Asset-Hash'),'new');
|
||||||
assert.equal((await e.request('/extra.js?v=new')).headers.get('X-Asset-Hash'),'new');
|
assert.equal((await e.request('/extra.js?v=new')).headers.get('X-Asset-Hash'),'new');
|
||||||
@@ -56,3 +56,22 @@ test('waiting status does not start completion and activation delegates it to it
|
|||||||
const job=e.complete();await begin;assert.equal(await cache.match('/extra.js?v=extra'),undefined);release();await job;
|
const job=e.complete();await begin;assert.equal(await cache.match('/extra.js?v=extra'),undefined);release();await job;
|
||||||
assert.equal((await core.status(e.m,cache)).offlineReady,true);
|
assert.equal((await core.status(e.m,cache)).offlineReady,true);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
test('page-captured app is reused on first install without a worker download',async()=>{
|
||||||
|
const e=environment(),cache=await e.storage.open(core.CACHE);
|
||||||
|
await cache.put('/app.js?v=app',new Response('page captured app',{headers:{'X-Asset-Hash':'app'}}));
|
||||||
|
await e.dispatch('install');assert.equal(e.fetches.includes('/app.js?v=app'),false);
|
||||||
|
assert.equal(await(await cache.match('/app.js?v=app')).text(),'page captured app');
|
||||||
|
assert.equal(e.skips.length,0);assert.equal(await core.state(cache),null);
|
||||||
|
});
|
||||||
|
|
||||||
|
|
||||||
|
test('worker never supplies stale app to a native script when a feature contract changed',async()=>{
|
||||||
|
const e=environment(),cache=await e.storage.open(core.CACHE);
|
||||||
|
const old={buildTag:'old',files:{'/app.js':{h:'old'}},groups:{core:{contract:1,files:['/app.js']},extra:{contract:1,files:[]}}};
|
||||||
|
e.m.groups.core.contract=1;e.m.groups.extra={contract:2,background:true,files:[]};
|
||||||
|
await cache.put(core.STATE,Response.json({current:old}));await cache.put('/app.js?v=old',new Response('old app',{headers:{'X-Asset-Hash':'old'}}));
|
||||||
|
await e.dispatch('install');await e.dispatch('activate');await cache.delete('/app.js?v=app');
|
||||||
|
e.sandbox.fetch=async()=>{throw Error('offline')};
|
||||||
|
const response=await e.request('/app.js?v=app');assert.equal(response.status,503);assert.equal(await response.text(),'Offline');
|
||||||
|
});
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
{
|
{
|
||||||
"groups": {
|
"groups": {
|
||||||
"core": {
|
"core": {
|
||||||
"contract": 1,
|
"contract": 2,
|
||||||
"eager": true,
|
"eager": true,
|
||||||
"files": [
|
"files": [
|
||||||
"/index.html",
|
"/index.html",
|
||||||
|
|||||||
File diff suppressed because one or more lines are too long
@@ -123,6 +123,24 @@
|
|||||||
for (const type of ['pointerenter', 'focus', 'touchstart']) element.addEventListener(type, () => load(name).catch(() => {}), { once: true, passive: true });
|
for (const type of ['pointerenter', 'focus', 'touchstart']) element.addEventListener(type, () => load(name).catch(() => {}), { once: true, passive: true });
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
|
// Own this response once. Classic-script and fetch preloads do not share
|
||||||
|
// their cached bodies in WebKit; the later bootstrap consumes this promise.
|
||||||
|
const agent=root.navigator.userAgent || '';
|
||||||
|
api.captureApp=!!(manifest.appCache && !root.navigator.serviceWorker?.controller && root.crypto?.subtle && /AppleWebKit\//.test(agent) && !/(?:Chrome|Chromium|Edg|OPR)\//.test(agent));
|
||||||
|
if(api.captureApp) {
|
||||||
|
api.appResponse=(async()=>{
|
||||||
|
const key=url('/app.js');
|
||||||
|
try {
|
||||||
|
const cache=await root.caches?.open('ytplayer-assets'), held=await cache?.match(key);
|
||||||
|
if(held?.headers.get('X-Asset-Hash')===manifest.files['/app.js']?.h && held)return held;
|
||||||
|
} catch {}
|
||||||
|
return root.fetch(key,{credentials:'same-origin'});
|
||||||
|
})();
|
||||||
|
api.appResponse.catch(()=>{});
|
||||||
|
} else if(manifest.appCache) {
|
||||||
|
// Preserve native script/code-cache reuse in engines without the WebKit gap.
|
||||||
|
const preload=doc.createElement('link');preload.rel='preload';preload.as='script';preload.fetchPriority='low';preload.href=url('/app.js');doc.head.append(preload);
|
||||||
|
}
|
||||||
root.Lazy = api;
|
root.Lazy = api;
|
||||||
root.navigator.serviceWorker?.addEventListener('message',event=>{
|
root.navigator.serviceWorker?.addEventListener('message',event=>{
|
||||||
if(event.data?.type==='ASSET_LAYOUT_REQUEST') event.source?.postMessage({type:'LAYOUT',value:api.layout});
|
if(event.data?.type==='ASSET_LAYOUT_REQUEST') event.source?.postMessage({type:'LAYOUT',value:api.layout});
|
||||||
|
|||||||
@@ -2,16 +2,16 @@ const { test } = require('node:test');
|
|||||||
const assert = require('node:assert/strict');
|
const assert = require('node:assert/strict');
|
||||||
const { readFileSync } = require('node:fs');
|
const { readFileSync } = require('node:fs');
|
||||||
const vm = require('node:vm');
|
const vm = require('node:vm');
|
||||||
function fixture(current, held = []) {
|
function fixture(current, held = [], appCache = false, userAgent = 'AppleWebKit/605.1 Safari/605.1', controlled = false) {
|
||||||
const inserted = [], listeners = {}, writes = [];
|
const inserted = [], listeners = {}, writes = [], fetched = [];
|
||||||
const manifest = { buildTag: 'own', groups: { core: { files: [] }, 'layout:classic': { files: ['/classic.css'] }, 'layout:glass-stage': { files: ['/glass.css', '/one.js', '/two.js'] }, 'feature:test': { contract:1, files: ['/one.js', '/two.js'] } }, files: { '/classic.css': {h:'c'}, '/glass.css':{h:'g'}, '/one.js':{h:'1'}, '/two.js':{h:'2'} } };
|
const manifest = { buildTag: 'own', appCache, groups: { core: { files: [] }, 'layout:classic': { files: ['/classic.css'] }, 'layout:glass-stage': { files: ['/glass.css', '/one.js', '/two.js'] }, 'feature:test': { contract:1, files: ['/one.js', '/two.js'] } }, files: { '/app.js':{h:'a'}, '/classic.css': {h:'c'}, '/glass.css':{h:'g'}, '/one.js':{h:'1'}, '/two.js':{h:'2'} } };
|
||||||
const doc = { readyState:'loading', documentElement: { dataset:{} }, getElementById: () => ({ textContent:JSON.stringify(manifest) }), querySelectorAll: () => [], createElement: tag => ({ tagName:tag.toUpperCase() }), write: value => writes.push(value), addEventListener: (t,f) => listeners[t]=f };
|
const doc = { readyState:'loading', documentElement: { dataset:{} }, getElementById: () => ({ textContent:JSON.stringify(manifest) }), querySelectorAll: () => [], createElement: tag => ({ tagName:tag.toUpperCase() }), write: value => writes.push(value), addEventListener: (t,f) => listeners[t]=f };
|
||||||
doc.head = { append: node => { inserted.push(node); queueMicrotask(() => node.onload?.()); } };
|
doc.head = { append: node => { inserted.push(node); queueMicrotask(() => node.onload?.()); } };
|
||||||
const root = { document:doc, localStorage:{ getItem:() => '{"settings":{"layout":"glass-stage"}}' }, console, Promise, URL, setTimeout, clearTimeout, navigator:{}, addEventListener(){} };
|
const root = { document:doc, localStorage:{ getItem:() => '{"settings":{"layout":"glass-stage"}}' }, console, Promise, URL, crypto:{subtle:{}}, fetch:async key=>{fetched.push(key);return new Response('app');}, setTimeout, clearTimeout, navigator:{userAgent,serviceWorker:{ready:Promise.resolve({}),controller:controlled?{}:null,addEventListener(){}}}, addEventListener(){} };
|
||||||
if (current) root.caches = { open: async () => ({ match: async key => key === '/__ytp_asset_state' ? new Response(JSON.stringify({current})) : held.includes(key) ? new Response('', {headers:{'X-Asset-Hash':key.split('=')[1]}}) : undefined }) };
|
if (current) root.caches = { open: async () => ({ match: async key => key === '/__ytp_asset_state' ? new Response(JSON.stringify({current})) : held.includes(key) ? new Response('', {headers:{'X-Asset-Hash':key.split('=')[1]}}) : undefined }) };
|
||||||
root.window=root; root.globalThis=root;
|
root.window=root; root.globalThis=root;
|
||||||
vm.runInNewContext(readFileSync(require.resolve('./lazy.js'),'utf8'), root);
|
vm.runInNewContext(readFileSync(require.resolve('./lazy.js'),'utf8'), root);
|
||||||
return { root, inserted, writes, listeners, manifest };
|
return { root, inserted, writes, listeners, manifest, fetched };
|
||||||
}
|
}
|
||||||
test('external head bootstrap selects remembered CSS before paint without fetching a manifest', () => {
|
test('external head bootstrap selects remembered CSS before paint without fetching a manifest', () => {
|
||||||
const {root,writes}=fixture();
|
const {root,writes}=fixture();
|
||||||
@@ -95,3 +95,31 @@ test('online launches request worker completion even with Save-Data and skip off
|
|||||||
assert.deepEqual(sent.map(m=>m.type),['COMPLETE_ASSETS','COMPLETE_ASSETS']);
|
assert.deepEqual(sent.map(m=>m.type),['COMPLETE_ASSETS','COMPLETE_ASSETS']);
|
||||||
f.root.navigator.onLine=false;f.root.Lazy.warm();await Promise.resolve();assert.equal(sent.length,2);
|
f.root.navigator.onLine=false;f.root.Lazy.warm();await Promise.resolve();assert.equal(sent.length,2);
|
||||||
});
|
});
|
||||||
|
test('server-enabled head capture owns exactly one app response; native and rollback do not fetch',async()=>{
|
||||||
|
const modern=fixture(null,[],true);await modern.root.Lazy.appResponse;
|
||||||
|
assert.deepEqual(modern.fetched,['/app.js?v=a']);
|
||||||
|
const legacy=fixture();assert.deepEqual(legacy.fetched,[]);assert.equal(legacy.root.Lazy.appResponse,undefined);
|
||||||
|
});
|
||||||
|
|
||||||
|
|
||||||
|
test('head capture is cache-first even before a worker controls the page',async()=>{
|
||||||
|
const f=fixture(fixture().manifest,['/app.js?v=a'],true);const response=await f.root.Lazy.appResponse;
|
||||||
|
assert.equal(response.headers.get('X-Asset-Hash'),'a');assert.deepEqual(f.fetched,[]);
|
||||||
|
});
|
||||||
|
|
||||||
|
|
||||||
|
test('Chromium retains native script loading and only preloads the current app URL',async()=>{
|
||||||
|
const f=fixture(null,[],true,'AppleWebKit/537.36 Chrome/140.0 Safari/537.36');
|
||||||
|
assert.equal(f.root.Lazy.captureApp,false);assert.equal(f.root.Lazy.appResponse,undefined);assert.deepEqual(f.fetched,[]);
|
||||||
|
assert.equal(f.inserted.length,1);assert.equal(f.inserted[0].rel,'preload');assert.equal(f.inserted[0].as,'script');assert.equal(f.inserted[0].href,'/app.js?v=a');assert.equal(f.inserted[0].fetchPriority,'low');
|
||||||
|
});
|
||||||
|
test('iOS Chromium-branded browsers still use their WebKit response capture',async()=>{
|
||||||
|
const f=fixture(null,[],true,'AppleWebKit/605.1 CriOS/140.0 Mobile Safari/605.1');await f.root.Lazy.appResponse;
|
||||||
|
assert.equal(f.root.Lazy.captureApp,true);assert.deepEqual(f.fetched,['/app.js?v=a']);
|
||||||
|
});
|
||||||
|
|
||||||
|
test('controlled WebKit visits use worker-cached native app scripts without a response handoff',async()=>{
|
||||||
|
const f=fixture(null,[],true,'AppleWebKit/605.1 Safari/605.1',true);
|
||||||
|
assert.equal(f.root.Lazy.captureApp,false);assert.equal(f.root.Lazy.appResponse,undefined);assert.deepEqual(f.fetched,[]);
|
||||||
|
assert.equal(f.inserted[0].as,'script');assert.equal(f.inserted[0].href,'/app.js?v=a');assert.equal(f.inserted[0].fetchPriority,'low');
|
||||||
|
});
|
||||||
|
|||||||
@@ -245,3 +245,77 @@ const SectionRail = (() => {
|
|||||||
|
|
||||||
return { boot, refresh, setFolded, isFolded };
|
return { boot, refresh, setFolded, isFolded };
|
||||||
})();
|
})();
|
||||||
|
|
||||||
|
/* Cache the single page-owned app response before it registers the worker. */
|
||||||
|
(function (root) {
|
||||||
|
'use strict';
|
||||||
|
const doc=root.document, manifest=root.Lazy.manifest, key=root.Lazy.url('/app.js');
|
||||||
|
let domReady=doc.readyState==='complete', appSawDom=false;
|
||||||
|
doc.addEventListener('DOMContentLoaded',()=>{domReady=true;appSawDom=typeof root.boot==='function';},{once:true});
|
||||||
|
function execute(node) {
|
||||||
|
doc.head.append(node);
|
||||||
|
node.remove();
|
||||||
|
delete root.Lazy.appResponse;
|
||||||
|
// app.js keeps its original DOMContentLoaded listener. Async response
|
||||||
|
// consumption can finish after that event; boot exactly once in that case.
|
||||||
|
if(domReady)root.boot();
|
||||||
|
}
|
||||||
|
async function external(url) {
|
||||||
|
await new Promise((resolve,reject)=>{
|
||||||
|
const node=doc.createElement('script');node.src=url;
|
||||||
|
node.onload=()=>{if(domReady&&!appSawDom)root.boot();resolve();};node.onerror=()=>reject(Error('Unable to load player'));
|
||||||
|
doc.head.append(node);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
function native() {
|
||||||
|
if(doc.readyState!=='loading' || !doc.currentScript)return external(key);
|
||||||
|
return new Promise((resolve,reject)=>{
|
||||||
|
doc.addEventListener('DOMContentLoaded',()=>typeof root.boot==='function'?resolve():reject(Error('Unable to load player')),{once:true});
|
||||||
|
const escaped=key.replaceAll('&','&').replaceAll('"','"').replaceAll('<','<');
|
||||||
|
doc.write('<script src="'+escaped+'"></script>');
|
||||||
|
});
|
||||||
|
}
|
||||||
|
async function verified(response, expected) {
|
||||||
|
const bytes=await response.clone().arrayBuffer();
|
||||||
|
const digest=new Uint8Array(await root.crypto.subtle.digest('SHA-256',bytes));
|
||||||
|
const hex=Array.from(digest,b=>b.toString(16).padStart(2,'0')).join('');
|
||||||
|
if(hex.slice(0,10)!==expected)throw Error('Player body hash mismatch');
|
||||||
|
return {bytes,digest};
|
||||||
|
}
|
||||||
|
async function start() {
|
||||||
|
if(!manifest.appCache || !root.crypto?.subtle || !root.Lazy.captureApp)return native();
|
||||||
|
const expected=manifest.files['/app.js'].h;
|
||||||
|
let cache;
|
||||||
|
try { cache=await root.caches?.open('ytplayer-assets'); } catch {}
|
||||||
|
let response=await cache?.match(key);
|
||||||
|
if(!response || response.headers.get('X-Asset-Hash')!==expected) {
|
||||||
|
try { response=await (root.Lazy.appResponse || root.fetch(key,{credentials:'same-origin'})); } catch {}
|
||||||
|
}
|
||||||
|
if(!response?.ok || response.headers.get('X-Asset-Hash')!==expected) {
|
||||||
|
// Preserve staged N-1 boot after an individual cache eviction. Only the
|
||||||
|
// controller can serve this old URL safely; never fetch stale URLs bare.
|
||||||
|
const state=cache && await root.AssetSyncCore.state(cache);
|
||||||
|
const previousKey=state && root.AssetSyncCore.fallback(manifest,state.previous,'/app.js');
|
||||||
|
const previous=previousKey && await cache.match(previousKey);
|
||||||
|
const previousHash=state?.previous?.files['/app.js']?.h;
|
||||||
|
const contractsMatch=state?.previous && Object.entries(manifest.groups).every(([name,group])=>state.previous.groups[name]?.contract===group.contract);
|
||||||
|
if(contractsMatch && root.navigator?.serviceWorker?.controller && previous?.ok && previous.headers.get('X-Asset-Hash')===previousHash) {
|
||||||
|
await verified(previous,previousHash);
|
||||||
|
delete root.Lazy.appResponse;
|
||||||
|
return external(previousKey);
|
||||||
|
}
|
||||||
|
throw Error('Player asset hash mismatch');
|
||||||
|
}
|
||||||
|
const {bytes,digest}=await verified(response,expected);
|
||||||
|
const permission="'sha256-"+root.btoa(String.fromCharCode(...digest))+"'";
|
||||||
|
if(!doc.querySelector('meta[http-equiv="Content-Security-Policy"]').content.includes(permission))throw Error('Player body hash mismatch');
|
||||||
|
// CacheStorage may share a full quota with saved music. Never remove data
|
||||||
|
// to make room; boot from these verified bytes even if caching is refused.
|
||||||
|
try { await cache?.put(key,response); } catch(error) { root.console.warn('[app-cache]',error.message); }
|
||||||
|
const node=doc.createElement('script');node.textContent=new root.TextDecoder().decode(bytes);
|
||||||
|
execute(node);
|
||||||
|
}
|
||||||
|
const ready=start();
|
||||||
|
root.AppBootstrap={ready};
|
||||||
|
ready.catch(error=>root.console.error('[app-cache]',error.message));
|
||||||
|
})(typeof window!=='undefined'?window:globalThis);
|
||||||
|
|||||||
@@ -32,7 +32,7 @@ test('assets.json describes the complete existing shell exactly once with eager
|
|||||||
const { groups } = JSON.parse(readFileSync(join(__dirname, 'assets.json'), 'utf8'));
|
const { groups } = JSON.parse(readFileSync(join(__dirname, 'assets.json'), 'utf8'));
|
||||||
const described = new Set();
|
const described = new Set();
|
||||||
for (const [name, group] of Object.entries(groups)) {
|
for (const [name, group] of Object.entries(groups)) {
|
||||||
assert.equal(group.contract, 1, `${name} initial contract`);
|
assert.equal(group.contract, name === 'core' ? 2 : 1, `${name} contract (core boot packing changed)`);
|
||||||
assert.equal(typeof group.eager, 'boolean');
|
assert.equal(typeof group.eager, 'boolean');
|
||||||
for (const asset of group.files) {
|
for (const asset of group.files) {
|
||||||
assert.ok(!described.has(asset), `${asset} has one group`);
|
assert.ok(!described.has(asset), `${asset} has one group`);
|
||||||
@@ -70,6 +70,8 @@ test('only core scripts and styles remain eager; native fallback and active layo
|
|||||||
const {groups}=JSON.parse(readFileSync(join(__dirname,'assets.json'),'utf8'));
|
const {groups}=JSON.parse(readFileSync(join(__dirname,'assets.json'),'utf8'));
|
||||||
const background=new Set(Object.values(groups).filter(g=>g.background).flatMap(g=>g.files));
|
const background=new Set(Object.values(groups).filter(g=>g.background).flatMap(g=>g.files));
|
||||||
for(const match of html.matchAll(/<(?:script\b[^>]*\bsrc|link\b[^>]*\bhref)="([^"]+)"/gi)) {
|
for(const match of html.matchAll(/<(?:script\b[^>]*\bsrc|link\b[^>]*\bhref)="([^"]+)"/gi)) {
|
||||||
|
// A style preload discovers bytes without applying or blocking another layout.
|
||||||
|
if(match[0].includes('rel="preload"'))continue;
|
||||||
const path='/'+match[1].replace(/^\//,'').split(/[?#]/)[0];
|
const path='/'+match[1].replace(/^\//,'').split(/[?#]/)[0];
|
||||||
assert.ok(!background.has(path),`${path} is not eager`);
|
assert.ok(!background.has(path),`${path} is not eager`);
|
||||||
}
|
}
|
||||||
@@ -88,3 +90,25 @@ test('layout-independent visibility and transport defaults stay in eager CSS',()
|
|||||||
for(const selector of ['.stg-overlay','.deck-chips','.np-btn.done .i-save','.sleep-status .sl-ic','.ab-group','.mc-tune'])assert.ok(shared.includes(selector));
|
for(const selector of ['.stg-overlay','.deck-chips','.np-btn.done .i-save','.sleep-status .sl-ic','.ab-group','.mc-tune'])assert.ok(shared.includes(selector));
|
||||||
assert.ok(html.includes('href="layout-base.css"'));
|
assert.ok(html.includes('href="layout-base.css"'));
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|
||||||
|
test('classic first-visit style preload is discovered before the head loader without applying another layout',()=>{
|
||||||
|
const preload=html.match(/<link\b[^>]*rel="preload"[^>]*href="theme-classic.css"[^>]*as="style"[^>]*>/);
|
||||||
|
assert.ok(preload,'classic stylesheet has an early preload');
|
||||||
|
assert.ok(html.indexOf(preload[0])<html.indexOf('src="lazy.js"'));
|
||||||
|
assert.ok(!/<link\b[^>]*rel="stylesheet"[^>]*href="theme-classic.css"/.test(html),'the selected layout remains parser-applied by the loader');
|
||||||
|
});
|
||||||
|
|
||||||
|
|
||||||
|
test('the last core script owns boot without an extra request or unsafe stale packing',()=>{
|
||||||
|
const {groups}=JSON.parse(readFileSync(join(__dirname,'assets.json'),'utf8'));
|
||||||
|
assert.equal(groups.core.contract,2,'old core packing cannot fall back without the loader');
|
||||||
|
assert.ok(groups.core.files.includes('/app.js'));
|
||||||
|
assert.ok(groups.core.files.includes('/section-rail.js'));
|
||||||
|
assert.equal(groups.core.files.includes('/app-bootstrap.js'),false);
|
||||||
|
assert.equal(listed.has('/app-bootstrap.js'),false);
|
||||||
|
const scripts=[...html.matchAll(/<script src="([^"]+)"/g)].map(m=>m[1]);
|
||||||
|
assert.equal(scripts.at(-1),'section-rail.js');
|
||||||
|
assert.ok(html.includes("'sha256-__APP_SCRIPT_HASH__'"));
|
||||||
|
assert.ok(readFileSync(join(__dirname,'section-rail.js'),'utf8').includes('root.AppBootstrap={ready}'));
|
||||||
|
});
|
||||||
|
|||||||
@@ -600,7 +600,13 @@ async function assetFetch(request, clientId) {
|
|||||||
if(state.current.files[path]?.h === h) {
|
if(state.current.files[path]?.h === h) {
|
||||||
const previousKey=AssetSyncCore.fallback(state.current,state.previous,path);
|
const previousKey=AssetSyncCore.fallback(state.current,state.previous,path);
|
||||||
const previousResponse=previousKey && await cache.match(previousKey);
|
const previousResponse=previousKey && await cache.match(previousKey);
|
||||||
if(previousResponse && previousResponse.headers.get('X-Asset-Hash')===state.previous.files[path].h) return previousResponse;
|
if(previousResponse && previousResponse.headers.get('X-Asset-Hash')===state.previous.files[path].h) {
|
||||||
|
// A stale body returned under a new URL must not poison the browser's
|
||||||
|
// resource cache either. Its decoded stream has no transport encoding.
|
||||||
|
const headers=new Headers(previousResponse.headers);headers.set('Cache-Control','no-store');
|
||||||
|
for(const name of ['Content-Encoding','Content-Length','Transfer-Encoding'])headers.delete(name);
|
||||||
|
return new Response(previousResponse.body,{status:previousResponse.status,headers});
|
||||||
|
}
|
||||||
}
|
}
|
||||||
try { const r = await fetch(key); if(r.ok && r.headers.get('X-Asset-Hash') !== h) return new Response('Asset version unavailable',{status:409}); if(r.ok && request.method !== 'HEAD') await cache.put(key,r.clone()); return r; } catch { return new Response('Offline',{status:503}); }
|
try { const r = await fetch(key); if(r.ok && r.headers.get('X-Asset-Hash') !== h) return new Response('Asset version unavailable',{status:409}); if(r.ok && request.method !== 'HEAD') await cache.put(key,r.clone()); return r; } catch { return new Response('Offline',{status:503}); }
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -347,3 +347,25 @@ node --test frontend/media-icons.test.js
|
|||||||
Both harnesses run Chromium and WebKit at mobile dimensions with stubbed SpeechRecognition and local Saved/Queue metadata. They serve only the frontend, block service workers and external requests, and never download media. Geometry tests cold and switched layouts, six widths (320–430), mic visible/hidden and update icon visible/hidden: center spread ≤1px, touch targets ≥40px, Saved image/text gap ≥8px, no horizontal overflow or clipped car control. `--quick` limits widths and speech cases; `--browser` selects an engine; `--frontend` selects historical fixtures. `--measure-only --label before` records known broken geometry without exiting nonzero.
|
Both harnesses run Chromium and WebKit at mobile dimensions with stubbed SpeechRecognition and local Saved/Queue metadata. They serve only the frontend, block service workers and external requests, and never download media. Geometry tests cold and switched layouts, six widths (320–430), mic visible/hidden and update icon visible/hidden: center spread ≤1px, touch targets ≥40px, Saved image/text gap ≥8px, no horizontal overflow or clipped car control. `--quick` limits widths and speech cases; `--browser` selects an engine; `--frontend` selects historical fixtures. `--measure-only --label before` records known broken geometry without exiting nonzero.
|
||||||
|
|
||||||
Icon checks switch the same page through four layouts × dark/light/contrast, assert SVG definitions, inherited colour, stroke family and save/progress/offline state, and save Queue/Now Playing/navigation shots. Glass Stage intentionally hides bottom navigation, so its full page is recorded instead. Any assertion failure exits nonzero. Shots and raw measurements live in git-ignored plans/ui-shots/. Full production inventory: plans/ui-icon-inventory.md. Native speech recognition, installed-PWA safe-area insets and iPhone rendering still need device verification.
|
Icon checks switch the same page through four layouts × dark/light/contrast, assert SVG definitions, inherited colour, stroke family and save/progress/offline state, and save Queue/Now Playing/navigation shots. Glass Stage intentionally hides bottom navigation, so its full page is recorded instead. Any assertion failure exits nonzero. Shots and raw measurements live in git-ignored plans/ui-shots/. Full production inventory: plans/ui-icon-inventory.md. Native speech recognition, installed-PWA safe-area insets and iPhone rendering still need device verification.
|
||||||
|
|
||||||
|
### Cold waterfall diagnosis
|
||||||
|
|
||||||
|
`--source-root perf/.tmp/<historical-worktree>` runs that worktree's matching
|
||||||
|
frontend **and server**; `--frontend-commit` instead combines archived frontend
|
||||||
|
with the current backend. Run historical comparisons sequentially to avoid CPU
|
||||||
|
contention. The result records `sourceCommit`, `sourceRoot` and the served build.
|
||||||
|
Every cold sample has `waterfalls`: proxy request order/start/headers/end times
|
||||||
|
and page Resource Timing discovery/start/end times. Proxy times start when
|
||||||
|
tracking begins; page times start at navigation. Compare within each clock,
|
||||||
|
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.
|
||||||
|
|
||||||
|
`--scenario warm,offline,update-js,update-css,update-feature` runs a regression
|
||||||
|
sweep without repeating an already-completed cold acceptance run.
|
||||||
|
|||||||
@@ -26,7 +26,7 @@
|
|||||||
*/
|
*/
|
||||||
|
|
||||||
import assert from 'node:assert/strict';
|
import assert from 'node:assert/strict';
|
||||||
import { assertChangedAssets, assertWarmAssets, waitForIdleAssets } from './measurement.mjs';
|
import { assertColdAssets, assertChangedAssets, assertWarmAssets, waitForIdleAssets } from './measurement.mjs';
|
||||||
import { createHash } from 'node:crypto';
|
import { createHash } from 'node:crypto';
|
||||||
import { spawn, spawnSync, execFileSync } 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 { existsSync, mkdirSync, rmSync, cpSync, appendFileSync, statSync, writeFileSync, readFileSync, symlinkSync, unlinkSync } from 'node:fs';
|
||||||
@@ -62,6 +62,8 @@ function parseArgs() {
|
|||||||
out: null,
|
out: null,
|
||||||
compare: null,
|
compare: null,
|
||||||
frontendCommit: null,
|
frontendCommit: null,
|
||||||
|
sourceRoot: null,
|
||||||
|
assertCold: false,
|
||||||
};
|
};
|
||||||
|
|
||||||
for (let i = 0; i < args.length; i++) {
|
for (let i = 0; i < args.length; i++) {
|
||||||
@@ -71,6 +73,8 @@ function parseArgs() {
|
|||||||
else if (a === '--profile' && i + 1 < args.length) options.profile = args[++i];
|
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 === '--scenario' && i + 1 < args.length) options.scenario = args[++i];
|
||||||
else if (a === '--out' && i + 1 < args.length) options.out = args[++i];
|
else if (a === '--out' && i + 1 < args.length) options.out = args[++i];
|
||||||
|
else if (a === '--assert-cold') options.assertCold = true;
|
||||||
|
else if (a === '--source-root' && i + 1 < args.length) options.sourceRoot = path.resolve(args[++i]);
|
||||||
else if (a === '--frontend-commit' && i + 1 < args.length) options.frontendCommit = 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];
|
else if (a === '--compare' && i + 1 < args.length) options.compare = args[++i];
|
||||||
}
|
}
|
||||||
@@ -151,7 +155,8 @@ async function seedDatabase(dbPath, mediaFiles) {
|
|||||||
* Bun Server Manager
|
* Bun Server Manager
|
||||||
*/
|
*/
|
||||||
class ServerManager {
|
class ServerManager {
|
||||||
constructor(scratchDir, publicDir) {
|
constructor(scratchDir, publicDir, sourceRoot = REPO_ROOT) {
|
||||||
|
this.sourceRoot = sourceRoot;
|
||||||
this.scratchDir = scratchDir;
|
this.scratchDir = scratchDir;
|
||||||
this.publicDir = publicDir;
|
this.publicDir = publicDir;
|
||||||
this.dataDir = path.join(scratchDir, 'data');
|
this.dataDir = path.join(scratchDir, 'data');
|
||||||
@@ -184,7 +189,7 @@ class ServerManager {
|
|||||||
DB_PATH: this.dbPath,
|
DB_PATH: this.dbPath,
|
||||||
};
|
};
|
||||||
|
|
||||||
const serverScript = path.join(REPO_ROOT, 'server', 'server.js');
|
const serverScript = path.join(this.sourceRoot, 'server', 'server.js');
|
||||||
this.serverProcess = spawn('bun', [serverScript], {
|
this.serverProcess = spawn('bun', [serverScript], {
|
||||||
cwd: srvCwd,
|
cwd: srvCwd,
|
||||||
env,
|
env,
|
||||||
@@ -372,8 +377,8 @@ async function runColdScenario({ browserType, server, proxy, profile }) {
|
|||||||
const fcp = paint.find((p) => p.name === 'first-contentful-paint');
|
const fcp = paint.find((p) => p.name === 'first-contentful-paint');
|
||||||
|
|
||||||
return {
|
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),
|
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)).map(entry=>({url:entry.name,bytes:entry.encodedBodySize})),
|
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,
|
fcp: fcp ? Math.round(fcp.startTime) : null,
|
||||||
lcp: window.__lcp ? Math.round(window.__lcp) : null,
|
lcp: window.__lcp ? Math.round(window.__lcp) : null,
|
||||||
domContentLoaded: nav ? Math.round(nav.domContentLoadedEventEnd - nav.startTime) : null,
|
domContentLoaded: nav ? Math.round(nav.domContentLoadedEventEnd - nav.startTime) : null,
|
||||||
@@ -481,8 +486,8 @@ async function runWarmScenario({ browserType, server, proxy, profile }) {
|
|||||||
const fcp = paint.find((p) => p.name === 'first-contentful-paint');
|
const fcp = paint.find((p) => p.name === 'first-contentful-paint');
|
||||||
|
|
||||||
return {
|
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),
|
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)).map(entry=>({url:entry.name,bytes:entry.encodedBodySize})),
|
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,
|
fcp: fcp ? Math.round(fcp.startTime) : null,
|
||||||
lcp: window.__lcp ? Math.round(window.__lcp) : null,
|
lcp: window.__lcp ? Math.round(window.__lcp) : null,
|
||||||
domContentLoaded: nav ? Math.round(nav.domContentLoadedEventEnd - nav.startTime) : null,
|
domContentLoaded: nav ? Math.round(nav.domContentLoadedEventEnd - nav.startTime) : null,
|
||||||
@@ -562,8 +567,8 @@ async function runOfflineScenario({ browserType, server, proxy, profile }) {
|
|||||||
const fcp = paint.find((p) => p.name === 'first-contentful-paint');
|
const fcp = paint.find((p) => p.name === 'first-contentful-paint');
|
||||||
|
|
||||||
return {
|
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),
|
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)).map(entry=>({url:entry.name,bytes:entry.encodedBodySize})),
|
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,
|
fcp: fcp ? Math.round(fcp.startTime) : null,
|
||||||
lcp: window.__lcp ? Math.round(window.__lcp) : null,
|
lcp: window.__lcp ? Math.round(window.__lcp) : null,
|
||||||
domContentLoaded: nav ? Math.round(nav.domContentLoadedEventEnd - nav.startTime) : null,
|
domContentLoaded: nav ? Math.round(nav.domContentLoadedEventEnd - nav.startTime) : null,
|
||||||
@@ -704,6 +709,8 @@ async function main() {
|
|||||||
if (options.browser === 'all' || options.browser === 'chromium') browsers.push(pw.chromium);
|
if (options.browser === 'all' || options.browser === 'chromium') browsers.push(pw.chromium);
|
||||||
if (options.browser === 'all' || options.browser === 'webkit') browsers.push(pw.webkit);
|
if (options.browser === 'all' || options.browser === 'webkit') browsers.push(pw.webkit);
|
||||||
|
|
||||||
|
const requestedScenarios=new Set(options.scenario.split(','));
|
||||||
|
const wants=name=>requestedScenarios.has('all')||requestedScenarios.has(name);
|
||||||
const profiles = [];
|
const profiles = [];
|
||||||
if (options.profile === 'all' || options.profile === 'lte') profiles.push('lte');
|
if (options.profile === 'all' || options.profile === 'lte') profiles.push('lte');
|
||||||
if (options.profile === 'all' || options.profile === 'lossy') profiles.push('lossy');
|
if (options.profile === 'all' || options.profile === 'lossy') profiles.push('lossy');
|
||||||
@@ -717,7 +724,7 @@ async function main() {
|
|||||||
const treeUpdateCss = path.join(tmpRoot, 'tree-update-css');
|
const treeUpdateCss = path.join(tmpRoot, 'tree-update-css');
|
||||||
const treeUpdateFeature = path.join(tmpRoot, 'tree-update-feature');
|
const treeUpdateFeature = path.join(tmpRoot, 'tree-update-feature');
|
||||||
|
|
||||||
let frontendSource=path.join(REPO_ROOT,'frontend');
|
let frontendSource=path.join(options.sourceRoot || REPO_ROOT,'frontend');
|
||||||
if(options.frontendCommit) {
|
if(options.frontendCommit) {
|
||||||
const archive=path.join(tmpRoot,'frontend.tar');
|
const archive=path.join(tmpRoot,'frontend.tar');
|
||||||
writeFileSync(archive,execFileSync('git',['archive',options.frontendCommit,'frontend'],{cwd:REPO_ROOT,maxBuffer:20*1024*1024}));
|
writeFileSync(archive,execFileSync('git',['archive',options.frontendCommit,'frontend'],{cwd:REPO_ROOT,maxBuffer:20*1024*1024}));
|
||||||
@@ -735,13 +742,15 @@ async function main() {
|
|||||||
const trees = { treeBase, treeUpdateJs, treeUpdateCss, treeUpdateFeature };
|
const trees = { treeBase, treeUpdateJs, treeUpdateCss, treeUpdateFeature };
|
||||||
|
|
||||||
const serverScratch = path.join(tmpRoot, 'server-run');
|
const serverScratch = path.join(tmpRoot, 'server-run');
|
||||||
const server = new ServerManager(serverScratch, treeBase);
|
const server = new ServerManager(serverScratch, treeBase, options.sourceRoot || REPO_ROOT);
|
||||||
await server.start();
|
await server.start();
|
||||||
console.log(`Bun server running on ephemeral port ${server.port}`);
|
console.log(`Bun server running on ephemeral port ${server.port}`);
|
||||||
|
|
||||||
const results = {
|
const results = {
|
||||||
date: new Date().toISOString(),
|
date: new Date().toISOString(),
|
||||||
frontendCommit: options.frontendCommit,
|
frontendCommit: options.frontendCommit,
|
||||||
|
sourceRoot: options.sourceRoot,
|
||||||
|
sourceCommit: spawnSync('git', ['rev-parse','HEAD'], { cwd: options.sourceRoot || REPO_ROOT }).stdout.toString().trim(),
|
||||||
frontendBuildTag: (await (await fetch('http://127.0.0.1:'+server.port+'/api/version')).json()).buildTag,
|
frontendBuildTag: (await (await fetch('http://127.0.0.1:'+server.port+'/api/version')).json()).buildTag,
|
||||||
commit: spawnSync('git', ['rev-parse', 'HEAD'], { cwd: REPO_ROOT }).stdout.toString().trim(),
|
commit: spawnSync('git', ['rev-parse', 'HEAD'], { cwd: REPO_ROOT }).stdout.toString().trim(),
|
||||||
runsConfigured: options.runs,
|
runsConfigured: options.runs,
|
||||||
@@ -754,6 +763,9 @@ async function main() {
|
|||||||
results.browsers[bName] = {};
|
results.browsers[bName] = {};
|
||||||
|
|
||||||
for (const prof of profiles) {
|
for (const prof of profiles) {
|
||||||
|
// The preceding profile can end on a synthetic feature-update tree.
|
||||||
|
// Always measure the actual base build on the next cold visit.
|
||||||
|
await server.restartWithPublic(treeBase);
|
||||||
console.log(`\n>>> Running Benchmark: Browser=[${bName}] Profile=[${prof}]`);
|
console.log(`\n>>> Running Benchmark: Browser=[${bName}] Profile=[${prof}]`);
|
||||||
results.browsers[bName][prof] = {};
|
results.browsers[bName][prof] = {};
|
||||||
|
|
||||||
@@ -763,14 +775,15 @@ async function main() {
|
|||||||
console.log(`Proxy listening on port ${proxy.port} (mode: ${proxyProfile})`);
|
console.log(`Proxy listening on port ${proxy.port} (mode: ${proxyProfile})`);
|
||||||
|
|
||||||
// 1. Cold Scenario
|
// 1. Cold Scenario
|
||||||
if (options.scenario === 'all' || options.scenario === 'cold') {
|
if (wants('cold')) {
|
||||||
console.log(` [Scenario: Cold Load] running ${options.runs} iterations...`);
|
console.log(` [Scenario: Cold Load] running ${options.runs} iterations...`);
|
||||||
const coldRuns = [];
|
const coldRuns = [];
|
||||||
for (let r = 0; r < options.runs; r++) {
|
for (let r = 0; r < options.runs; r++) {
|
||||||
process.stdout.write(` run ${r + 1}/${options.runs}... `);
|
process.stdout.write(` run ${r + 1}/${options.runs}... `);
|
||||||
const res = await runColdScenario({ browserType: bType, server, proxy, profile: prof });
|
const res = await runColdScenario({ browserType: bType, server, proxy, profile: prof });
|
||||||
|
if(options.assertCold)assertColdAssets(res.requests);
|
||||||
coldRuns.push(res);
|
coldRuns.push(res);
|
||||||
console.log(`done (${res.wireBytes} bytes, ${res.requestCount} reqs, boot=${res.bootDone}ms)`);
|
console.log(`done (${res.wireBytes} bytes, ${res.requestCount} reqs, fcp=${res.fcp}ms, boot=${res.bootDone}ms)`);
|
||||||
}
|
}
|
||||||
|
|
||||||
results.browsers[bName][prof].cold = {
|
results.browsers[bName][prof].cold = {
|
||||||
@@ -791,11 +804,12 @@ async function main() {
|
|||||||
requests: coldRuns[0]?.requests || [],
|
requests: coldRuns[0]?.requests || [],
|
||||||
initialJsCss: summarizeList(coldRuns.map(r=>r.initialJsCss)),
|
initialJsCss: summarizeList(coldRuns.map(r=>r.initialJsCss)),
|
||||||
initialAssetRequests: coldRuns[0]?.initialAssetRequests || [],
|
initialAssetRequests: coldRuns[0]?.initialAssetRequests || [],
|
||||||
|
waterfalls: coldRuns.map(r=>({fcp:r.fcp,requests:r.requests,page:r.initialAssetRequests})),
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
// 2. Warm Scenario
|
// 2. Warm Scenario
|
||||||
if (options.scenario === 'all' || options.scenario === 'warm') {
|
if (wants('warm')) {
|
||||||
console.log(` [Scenario: Warm Load] running ${options.runs} iterations...`);
|
console.log(` [Scenario: Warm Load] running ${options.runs} iterations...`);
|
||||||
const warmRuns = [];
|
const warmRuns = [];
|
||||||
for (let r = 0; r < options.runs; r++) {
|
for (let r = 0; r < options.runs; r++) {
|
||||||
@@ -817,7 +831,7 @@ async function main() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// 3. Offline Scenario
|
// 3. Offline Scenario
|
||||||
if (options.scenario === 'all' || options.scenario === 'offline') {
|
if (wants('offline')) {
|
||||||
console.log(` [Scenario: Offline Reload] running ${options.runs} iterations...`);
|
console.log(` [Scenario: Offline Reload] running ${options.runs} iterations...`);
|
||||||
const offRuns = [];
|
const offRuns = [];
|
||||||
for (let r = 0; r < options.runs; r++) {
|
for (let r = 0; r < options.runs; r++) {
|
||||||
@@ -839,7 +853,7 @@ async function main() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// 4. Update N -> N+1 (JS)
|
// 4. Update N -> N+1 (JS)
|
||||||
if (options.scenario === 'all' || options.scenario === 'update-js') {
|
if (wants('update-js')) {
|
||||||
console.log(` [Scenario: Update N->N+1 (1-line app.js)] running ${options.runs} iterations...`);
|
console.log(` [Scenario: Update N->N+1 (1-line app.js)] running ${options.runs} iterations...`);
|
||||||
const updJsRuns = [];
|
const updJsRuns = [];
|
||||||
for (let r = 0; r < options.runs; r++) {
|
for (let r = 0; r < options.runs; r++) {
|
||||||
@@ -866,7 +880,7 @@ async function main() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// 5. Update N -> N+1 (CSS)
|
// 5. Update N -> N+1 (CSS)
|
||||||
if (options.scenario === 'all' || options.scenario === 'update-css') {
|
if (wants('update-css')) {
|
||||||
console.log(` [Scenario: Update N->N+1 (1-line theme-glass.css)] running ${options.runs} iterations...`);
|
console.log(` [Scenario: Update N->N+1 (1-line theme-glass.css)] running ${options.runs} iterations...`);
|
||||||
const updCssRuns = [];
|
const updCssRuns = [];
|
||||||
for (let r = 0; r < options.runs; r++) {
|
for (let r = 0; r < options.runs; r++) {
|
||||||
@@ -894,7 +908,7 @@ async function main() {
|
|||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
if ((options.scenario === 'all' || options.scenario === 'update-feature') && existsSync(path.join(treeBase,'presenter.js'))) {
|
if ((wants('update-feature')) && existsSync(path.join(treeBase,'presenter.js'))) {
|
||||||
console.log(` [Scenario: Update split-out presenter, including idle cache fill] running ${options.runs} iterations...`);
|
console.log(` [Scenario: Update split-out presenter, including idle cache fill] running ${options.runs} iterations...`);
|
||||||
const runs=[];
|
const runs=[];
|
||||||
for(let r=0;r<options.runs;r++) {
|
for(let r=0;r<options.runs;r++) {
|
||||||
|
|||||||
@@ -84,6 +84,23 @@ async function run(name){
|
|||||||
await page.evaluate(async()=>{await Lazy.load('feature:export');await Lazy.load('feature:video-edit');});
|
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');
|
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'));
|
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.
|
// 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');
|
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 old.evaluate(async()=>{window.__saveData=true;await Lazy.load('feature:setlist-import');window.__pinned=window.SetlistImport;});
|
||||||
@@ -133,7 +150,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)));
|
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)),[]);
|
assert.deepEqual(errors.filter(error=>!offlineTransportErrors.includes(error)),[]);
|
||||||
if(offlineTransportErrors.length)assert.ok(failedRequests.some(url=>/\/api\/(?:recommendations|version)/.test(url)));
|
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');
|
console.log(name,'PASS');
|
||||||
} catch(error){console.error(name,error.stack);throw error;} finally {await browser?.close();proxy?.server.closeAllConnections();await proxy?.close();await stop();}
|
} catch(error){console.error(name,error.stack);throw error;} finally {await browser?.close();proxy?.server.closeAllConnections();await proxy?.close();await stop();}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -27,3 +27,8 @@ 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'));
|
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');
|
assert.deepEqual(assets,[],'warm reload does not transfer unfinished shell assets');
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export function assertColdAssets(requests) {
|
||||||
|
const transferred=requests.filter(r=>r.bytes>0&&new URL(r.url,'http://localhost').pathname==='/app.js');
|
||||||
|
assert.equal(transferred.length,1,'cold app.js transfers its body exactly once across page and worker');
|
||||||
|
}
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
import { test } from 'node:test';
|
import { test } from 'node:test';
|
||||||
import assert from 'node:assert/strict';
|
import assert from 'node:assert/strict';
|
||||||
import { assertChangedAssets, assertWarmAssets } from './measurement.mjs';
|
import { assertChangedAssets, assertWarmAssets, assertColdAssets } from './measurement.mjs';
|
||||||
test('changed CSS and feature measurements reject unrelated application transfers',()=>{
|
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}];
|
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');
|
assertChangedAssets(requests,'/theme-glass.css');
|
||||||
@@ -14,3 +14,10 @@ test('warm measurements reject unfinished shell downloads but allow API checks',
|
|||||||
assertWarmAssets([{url:'/api/version',bytes:62},{url:'/app.js?v=current',bytes:0}]);
|
assertWarmAssets([{url:'/api/version',bytes:62},{url:'/app.js?v=current',bytes:0}]);
|
||||||
assert.throws(()=>assertWarmAssets([{url:'/theme-glass.css?v=current',bytes:5000}]));
|
assert.throws(()=>assertWarmAssets([{url:'/theme-glass.css?v=current',bytes:5000}]));
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|
||||||
|
test('cold acceptance rejects a second positive app response while allowing cache revalidation',()=>{
|
||||||
|
assertColdAssets([{url:'/app.js?v=current',bytes:12000},{url:'/app.js?v=current',bytes:0}]);
|
||||||
|
assert.throws(()=>assertColdAssets([{url:'/app.js?v=current',bytes:12000},{url:'/app.js?v=current',bytes:12000}]));
|
||||||
|
assert.throws(()=>assertColdAssets([]));
|
||||||
|
});
|
||||||
|
|||||||
@@ -47,6 +47,7 @@ export function categorizeContentType(contentType, urlPath) {
|
|||||||
*/
|
*/
|
||||||
export function createThrottleProxy({ targetPort, profile = 'lte' }) {
|
export function createThrottleProxy({ targetPort, profile = 'lte' }) {
|
||||||
let activeTracking = false;
|
let activeTracking = false;
|
||||||
|
let trackingStart = 0;
|
||||||
let requestCount = 0;
|
let requestCount = 0;
|
||||||
let requests = [];
|
let requests = [];
|
||||||
let totalWireBytes = 0;
|
let totalWireBytes = 0;
|
||||||
@@ -79,6 +80,7 @@ export function createThrottleProxy({ targetPort, profile = 'lte' }) {
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const requestStart = Date.now();
|
||||||
reqIndex++;
|
reqIndex++;
|
||||||
const currentReqIndex = reqIndex;
|
const currentReqIndex = reqIndex;
|
||||||
|
|
||||||
@@ -114,7 +116,8 @@ export function createThrottleProxy({ targetPort, profile = 'lte' }) {
|
|||||||
let recorded;
|
let recorded;
|
||||||
if (activeTracking) {
|
if (activeTracking) {
|
||||||
requestCount++;
|
requestCount++;
|
||||||
recorded={url:clientReq.url,status:upRes.statusCode,bytes:0};
|
recorded={url:clientReq.url,status:upRes.statusCode,bytes:0,index:currentReqIndex,startMs:requestStart-trackingStart,headersMs:Date.now()-trackingStart};
|
||||||
|
clientRes.once('finish',()=>{recorded.endMs=Date.now()-trackingStart;});
|
||||||
requests.push(recorded);
|
requests.push(recorded);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -176,6 +179,7 @@ export function createThrottleProxy({ targetPort, profile = 'lte' }) {
|
|||||||
server.close(resolve);
|
server.close(resolve);
|
||||||
}),
|
}),
|
||||||
startTracking: () => {
|
startTracking: () => {
|
||||||
|
trackingStart = Date.now();
|
||||||
activeTracking = true;
|
activeTracking = true;
|
||||||
},
|
},
|
||||||
stopTracking: () => {
|
stopTracking: () => {
|
||||||
|
|||||||
8538
perf/results/phase6-before-phase2-lossy.json
Normal file
8538
perf/results/phase6-before-phase2-lossy.json
Normal file
File diff suppressed because it is too large
Load Diff
8538
perf/results/phase6-before-phase2.json
Normal file
8538
perf/results/phase6-before-phase2.json
Normal file
File diff suppressed because it is too large
Load Diff
6846
perf/results/phase6-before-phase3.json
Normal file
6846
perf/results/phase6-before-phase3.json
Normal file
File diff suppressed because it is too large
Load Diff
7338
perf/results/phase6-before-phase5.json
Normal file
7338
perf/results/phase6-before-phase5.json
Normal file
File diff suppressed because it is too large
Load Diff
8538
perf/results/phase6-bisect-before-loader.json
Normal file
8538
perf/results/phase6-bisect-before-loader.json
Normal file
File diff suppressed because it is too large
Load Diff
6742
perf/results/phase6-bisect-first-loader.json
Normal file
6742
perf/results/phase6-bisect-first-loader.json
Normal file
File diff suppressed because it is too large
Load Diff
17445
perf/results/phase6-chromium-cold-2026-10-08.json
Normal file
17445
perf/results/phase6-chromium-cold-2026-10-08.json
Normal file
File diff suppressed because it is too large
Load Diff
7441
perf/results/phase6-cold-smoke-2026-10-09.json
Normal file
7441
perf/results/phase6-cold-smoke-2026-10-09.json
Normal file
File diff suppressed because it is too large
Load Diff
41354
perf/results/phase6-final-2026-10-08.json
Normal file
41354
perf/results/phase6-final-2026-10-08.json
Normal file
File diff suppressed because it is too large
Load Diff
7338
perf/results/phase6-force-cache.json
Normal file
7338
perf/results/phase6-force-cache.json
Normal file
File diff suppressed because it is too large
Load Diff
3892
perf/results/phase6-handoff-mode.json
Normal file
3892
perf/results/phase6-handoff-mode.json
Normal file
File diff suppressed because it is too large
Load Diff
135
perf/results/phase6-hashing-rollback-2026-10-08.json
Normal file
135
perf/results/phase6-hashing-rollback-2026-10-08.json
Normal file
@@ -0,0 +1,135 @@
|
|||||||
|
{
|
||||||
|
"date": "2026-10-08T04:01:24.068Z",
|
||||||
|
"frontendCommit": null,
|
||||||
|
"sourceRoot": null,
|
||||||
|
"sourceCommit": "4b1f5204f8fcd207305ee50fff29985497e58e3b",
|
||||||
|
"frontendBuildTag": "bb9fea44045d",
|
||||||
|
"commit": "4b1f5204f8fcd207305ee50fff29985497e58e3b",
|
||||||
|
"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": 40,
|
||||||
|
"min": 40,
|
||||||
|
"max": 40,
|
||||||
|
"values": [
|
||||||
|
40
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"lcp": {
|
||||||
|
"median": 40,
|
||||||
|
"min": 40,
|
||||||
|
"max": 40,
|
||||||
|
"values": [
|
||||||
|
40
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"domContentLoaded": {
|
||||||
|
"median": 147,
|
||||||
|
"min": 147,
|
||||||
|
"max": 147,
|
||||||
|
"values": [
|
||||||
|
147
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"bootDone": {
|
||||||
|
"median": 152,
|
||||||
|
"min": 152,
|
||||||
|
"max": 152,
|
||||||
|
"values": [
|
||||||
|
152
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"reloadDuration": {
|
||||||
|
"median": 169,
|
||||||
|
"min": 169,
|
||||||
|
"max": 169,
|
||||||
|
"values": [
|
||||||
|
169
|
||||||
|
]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"webkit": {
|
||||||
|
"lte": {
|
||||||
|
"offline": {
|
||||||
|
"requestCount": {
|
||||||
|
"median": 0,
|
||||||
|
"min": 0,
|
||||||
|
"max": 0,
|
||||||
|
"values": [
|
||||||
|
0
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"wireBytes": {
|
||||||
|
"median": 0,
|
||||||
|
"min": 0,
|
||||||
|
"max": 0,
|
||||||
|
"values": [
|
||||||
|
0
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"fcp": {
|
||||||
|
"median": 63,
|
||||||
|
"min": 63,
|
||||||
|
"max": 63,
|
||||||
|
"values": [
|
||||||
|
63
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"lcp": {
|
||||||
|
"median": 97,
|
||||||
|
"min": 97,
|
||||||
|
"max": 97,
|
||||||
|
"values": [
|
||||||
|
97
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"domContentLoaded": {
|
||||||
|
"median": 93,
|
||||||
|
"min": 93,
|
||||||
|
"max": 93,
|
||||||
|
"values": [
|
||||||
|
93
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"bootDone": {
|
||||||
|
"median": 99,
|
||||||
|
"min": 99,
|
||||||
|
"max": 99,
|
||||||
|
"values": [
|
||||||
|
99
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"reloadDuration": {
|
||||||
|
"median": 118,
|
||||||
|
"min": 118,
|
||||||
|
"max": 118,
|
||||||
|
"values": [
|
||||||
|
118
|
||||||
|
]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
135
perf/results/phase6-hashing-rollback-2026-10-09.json
Normal file
135
perf/results/phase6-hashing-rollback-2026-10-09.json
Normal file
@@ -0,0 +1,135 @@
|
|||||||
|
{
|
||||||
|
"date": "2026-10-09T01:16:09.809Z",
|
||||||
|
"frontendCommit": null,
|
||||||
|
"sourceRoot": null,
|
||||||
|
"sourceCommit": "71608c2ce81207ca21c8adbc49edbc2893f5170e",
|
||||||
|
"frontendBuildTag": "bb9fea44045d",
|
||||||
|
"commit": "71608c2ce81207ca21c8adbc49edbc2893f5170e",
|
||||||
|
"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": 108,
|
||||||
|
"min": 108,
|
||||||
|
"max": 108,
|
||||||
|
"values": [
|
||||||
|
108
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"lcp": {
|
||||||
|
"median": 108,
|
||||||
|
"min": 108,
|
||||||
|
"max": 108,
|
||||||
|
"values": [
|
||||||
|
108
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"domContentLoaded": {
|
||||||
|
"median": 345,
|
||||||
|
"min": 345,
|
||||||
|
"max": 345,
|
||||||
|
"values": [
|
||||||
|
345
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"bootDone": {
|
||||||
|
"median": 353,
|
||||||
|
"min": 353,
|
||||||
|
"max": 353,
|
||||||
|
"values": [
|
||||||
|
353
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"reloadDuration": {
|
||||||
|
"median": 401,
|
||||||
|
"min": 401,
|
||||||
|
"max": 401,
|
||||||
|
"values": [
|
||||||
|
401
|
||||||
|
]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"webkit": {
|
||||||
|
"lte": {
|
||||||
|
"offline": {
|
||||||
|
"requestCount": {
|
||||||
|
"median": 0,
|
||||||
|
"min": 0,
|
||||||
|
"max": 0,
|
||||||
|
"values": [
|
||||||
|
0
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"wireBytes": {
|
||||||
|
"median": 0,
|
||||||
|
"min": 0,
|
||||||
|
"max": 0,
|
||||||
|
"values": [
|
||||||
|
0
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"fcp": {
|
||||||
|
"median": 127,
|
||||||
|
"min": 127,
|
||||||
|
"max": 127,
|
||||||
|
"values": [
|
||||||
|
127
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"lcp": {
|
||||||
|
"median": 184,
|
||||||
|
"min": 184,
|
||||||
|
"max": 184,
|
||||||
|
"values": [
|
||||||
|
184
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"domContentLoaded": {
|
||||||
|
"median": 180,
|
||||||
|
"min": 180,
|
||||||
|
"max": 180,
|
||||||
|
"values": [
|
||||||
|
180
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"bootDone": {
|
||||||
|
"median": 187,
|
||||||
|
"min": 187,
|
||||||
|
"max": 187,
|
||||||
|
"values": [
|
||||||
|
187
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"reloadDuration": {
|
||||||
|
"median": 213,
|
||||||
|
"min": 213,
|
||||||
|
"max": 213,
|
||||||
|
"values": [
|
||||||
|
213
|
||||||
|
]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
97
perf/results/phase6-lazy-2026-10-08.json
Normal file
97
perf/results/phase6-lazy-2026-10-08.json
Normal file
@@ -0,0 +1,97 @@
|
|||||||
|
{
|
||||||
|
"results": [
|
||||||
|
{
|
||||||
|
"browser": "chromium",
|
||||||
|
"sourceBuildTag": "31617662d2f0",
|
||||||
|
"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": "31617662d2f0",
|
||||||
|
"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:41337/api/version.",
|
||||||
|
"disconnected": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"message": "TypeError: Load failed",
|
||||||
|
"disconnected": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"message": "/127.0.0.1:41337/api/recommendations?fp=163577c02a53bc40.",
|
||||||
|
"disconnected": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"message": "TypeError: Load failed",
|
||||||
|
"disconnected": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"message": "/127.0.0.1:41337/api/version.",
|
||||||
|
"disconnected": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"message": "TypeError: Load failed",
|
||||||
|
"disconnected": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"message": "/127.0.0.1:41337/api/recommendations?fp=163577c02a53bc40.",
|
||||||
|
"disconnected": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"message": "TypeError: Load failed",
|
||||||
|
"disconnected": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"message": "/127.0.0.1:41337/api/version.",
|
||||||
|
"disconnected": true
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
97
perf/results/phase6-lazy-2026-10-09.json
Normal file
97
perf/results/phase6-lazy-2026-10-09.json
Normal file
@@ -0,0 +1,97 @@
|
|||||||
|
{
|
||||||
|
"results": [
|
||||||
|
{
|
||||||
|
"browser": "chromium",
|
||||||
|
"sourceBuildTag": "31617662d2f0",
|
||||||
|
"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": "31617662d2f0",
|
||||||
|
"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:38297/api/version.",
|
||||||
|
"disconnected": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"message": "TypeError: Load failed",
|
||||||
|
"disconnected": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"message": "/127.0.0.1:38297/api/recommendations?fp=163577c02a53bc40.",
|
||||||
|
"disconnected": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"message": "TypeError: Load failed",
|
||||||
|
"disconnected": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"message": "/127.0.0.1:38297/api/version.",
|
||||||
|
"disconnected": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"message": "TypeError: Load failed",
|
||||||
|
"disconnected": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"message": "/127.0.0.1:38297/api/recommendations?fp=163577c02a53bc40.",
|
||||||
|
"disconnected": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"message": "TypeError: Load failed",
|
||||||
|
"disconnected": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"message": "/127.0.0.1:38297/api/version.",
|
||||||
|
"disconnected": true
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
5849
perf/results/phase6-low-app-priority.json
Normal file
5849
perf/results/phase6-low-app-priority.json
Normal file
File diff suppressed because it is too large
Load Diff
1145
perf/results/phase6-matched-phase5-2026-10-08.json
Normal file
1145
perf/results/phase6-matched-phase5-2026-10-08.json
Normal file
File diff suppressed because it is too large
Load Diff
5781
perf/results/phase6-matched-phase5-cold.json
Normal file
5781
perf/results/phase6-matched-phase5-cold.json
Normal file
File diff suppressed because it is too large
Load Diff
5781
perf/results/phase6-merged-bootstrap-cold.json
Normal file
5781
perf/results/phase6-merged-bootstrap-cold.json
Normal file
File diff suppressed because it is too large
Load Diff
191
perf/results/phase6-merged-bootstrap-warm.json
Normal file
191
perf/results/phase6-merged-bootstrap-warm.json
Normal file
@@ -0,0 +1,191 @@
|
|||||||
|
{
|
||||||
|
"date": "2026-10-08T03:15:23.029Z",
|
||||||
|
"frontendCommit": null,
|
||||||
|
"sourceRoot": "/home/josh/development/personal/ytplayer-codex/wt-assets/perf/.tmp/phase6-merged-bootstrap",
|
||||||
|
"sourceCommit": "9408ee07e56db5bbb8319f8da862b549ab948bce",
|
||||||
|
"frontendBuildTag": "c4bb42682ab8",
|
||||||
|
"commit": "9408ee07e56db5bbb8319f8da862b549ab948bce",
|
||||||
|
"runsConfigured": 5,
|
||||||
|
"browsers": {
|
||||||
|
"chromium": {
|
||||||
|
"lte": {
|
||||||
|
"warm": {
|
||||||
|
"requestCount": {
|
||||||
|
"median": 4,
|
||||||
|
"min": 3,
|
||||||
|
"max": 5,
|
||||||
|
"values": [
|
||||||
|
4,
|
||||||
|
4,
|
||||||
|
5,
|
||||||
|
4,
|
||||||
|
3
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"wireBytes": {
|
||||||
|
"median": 7990,
|
||||||
|
"min": 7979,
|
||||||
|
"max": 7990,
|
||||||
|
"values": [
|
||||||
|
7990,
|
||||||
|
7990,
|
||||||
|
7990,
|
||||||
|
7990,
|
||||||
|
7979
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"fcp": {
|
||||||
|
"median": 136,
|
||||||
|
"min": 108,
|
||||||
|
"max": 144,
|
||||||
|
"values": [
|
||||||
|
140,
|
||||||
|
116,
|
||||||
|
136,
|
||||||
|
108,
|
||||||
|
144
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"lcp": {
|
||||||
|
"median": 148,
|
||||||
|
"min": 136,
|
||||||
|
"max": 168,
|
||||||
|
"values": [
|
||||||
|
168,
|
||||||
|
148,
|
||||||
|
136,
|
||||||
|
156,
|
||||||
|
144
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"domContentLoaded": {
|
||||||
|
"median": 409,
|
||||||
|
"min": 382,
|
||||||
|
"max": 418,
|
||||||
|
"values": [
|
||||||
|
409,
|
||||||
|
382,
|
||||||
|
418,
|
||||||
|
412,
|
||||||
|
402
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"bootDone": {
|
||||||
|
"median": 425,
|
||||||
|
"min": 398,
|
||||||
|
"max": 433,
|
||||||
|
"values": [
|
||||||
|
425,
|
||||||
|
398,
|
||||||
|
433,
|
||||||
|
430,
|
||||||
|
417
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"longTaskTotal": {
|
||||||
|
"median": 63,
|
||||||
|
"min": 56,
|
||||||
|
"max": 65,
|
||||||
|
"values": [
|
||||||
|
63,
|
||||||
|
65,
|
||||||
|
63,
|
||||||
|
57,
|
||||||
|
56
|
||||||
|
]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"webkit": {
|
||||||
|
"lte": {
|
||||||
|
"warm": {
|
||||||
|
"requestCount": {
|
||||||
|
"median": 0,
|
||||||
|
"min": 0,
|
||||||
|
"max": 0,
|
||||||
|
"values": [
|
||||||
|
0,
|
||||||
|
0,
|
||||||
|
0,
|
||||||
|
0,
|
||||||
|
0
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"wireBytes": {
|
||||||
|
"median": 0,
|
||||||
|
"min": 0,
|
||||||
|
"max": 0,
|
||||||
|
"values": [
|
||||||
|
0,
|
||||||
|
0,
|
||||||
|
0,
|
||||||
|
0,
|
||||||
|
0
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"fcp": {
|
||||||
|
"median": 92,
|
||||||
|
"min": 79,
|
||||||
|
"max": 94,
|
||||||
|
"values": [
|
||||||
|
92,
|
||||||
|
92,
|
||||||
|
87,
|
||||||
|
94,
|
||||||
|
79
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"lcp": {
|
||||||
|
"median": 92,
|
||||||
|
"min": 79,
|
||||||
|
"max": 94,
|
||||||
|
"values": [
|
||||||
|
92,
|
||||||
|
92,
|
||||||
|
87,
|
||||||
|
94,
|
||||||
|
79
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"domContentLoaded": {
|
||||||
|
"median": 89,
|
||||||
|
"min": 77,
|
||||||
|
"max": 92,
|
||||||
|
"values": [
|
||||||
|
89,
|
||||||
|
90,
|
||||||
|
84,
|
||||||
|
92,
|
||||||
|
77
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"bootDone": {
|
||||||
|
"median": 95,
|
||||||
|
"min": 83,
|
||||||
|
"max": 98,
|
||||||
|
"values": [
|
||||||
|
95,
|
||||||
|
96,
|
||||||
|
91,
|
||||||
|
98,
|
||||||
|
83
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"longTaskTotal": {
|
||||||
|
"median": 0,
|
||||||
|
"min": 0,
|
||||||
|
"max": 0,
|
||||||
|
"values": [
|
||||||
|
0,
|
||||||
|
0,
|
||||||
|
0,
|
||||||
|
0,
|
||||||
|
0
|
||||||
|
]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
41
perf/results/phase6-migration-lossy-2026-10-08.json
Normal file
41
perf/results/phase6-migration-lossy-2026-10-08.json
Normal file
@@ -0,0 +1,41 @@
|
|||||||
|
{
|
||||||
|
"commit": "b77938a",
|
||||||
|
"results": [
|
||||||
|
{
|
||||||
|
"browser": "chromium",
|
||||||
|
"oldTag": "1175f1a1d2c1",
|
||||||
|
"newTag": "31617662d2f0",
|
||||||
|
"banners": 1,
|
||||||
|
"updateReloads": 1,
|
||||||
|
"cachedFiles": 122,
|
||||||
|
"durationMs": 25233,
|
||||||
|
"wireBytes": 631091,
|
||||||
|
"offline": true,
|
||||||
|
"legacyURLs": 21,
|
||||||
|
"profile": "lossy",
|
||||||
|
"userInitiatedOnly": true,
|
||||||
|
"playbackGuard": true,
|
||||||
|
"evictionRepair": true,
|
||||||
|
"incrementalRefresh": true,
|
||||||
|
"offlineTransportErrors": 0
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"browser": "webkit",
|
||||||
|
"oldTag": "1175f1a1d2c1",
|
||||||
|
"newTag": "31617662d2f0",
|
||||||
|
"banners": 1,
|
||||||
|
"updateReloads": 1,
|
||||||
|
"cachedFiles": 122,
|
||||||
|
"durationMs": 22860,
|
||||||
|
"wireBytes": 741571,
|
||||||
|
"offline": true,
|
||||||
|
"legacyURLs": 21,
|
||||||
|
"profile": "lossy",
|
||||||
|
"userInitiatedOnly": true,
|
||||||
|
"playbackGuard": true,
|
||||||
|
"evictionRepair": true,
|
||||||
|
"incrementalRefresh": true,
|
||||||
|
"offlineTransportErrors": 4
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
41
perf/results/phase6-migration-lossy-2026-10-09.json
Normal file
41
perf/results/phase6-migration-lossy-2026-10-09.json
Normal file
@@ -0,0 +1,41 @@
|
|||||||
|
{
|
||||||
|
"commit": "b77938a",
|
||||||
|
"results": [
|
||||||
|
{
|
||||||
|
"browser": "chromium",
|
||||||
|
"oldTag": "1175f1a1d2c1",
|
||||||
|
"newTag": "31617662d2f0",
|
||||||
|
"banners": 1,
|
||||||
|
"updateReloads": 1,
|
||||||
|
"cachedFiles": 122,
|
||||||
|
"durationMs": 27355,
|
||||||
|
"wireBytes": 631091,
|
||||||
|
"offline": true,
|
||||||
|
"legacyURLs": 21,
|
||||||
|
"profile": "lossy",
|
||||||
|
"userInitiatedOnly": true,
|
||||||
|
"playbackGuard": true,
|
||||||
|
"evictionRepair": true,
|
||||||
|
"incrementalRefresh": true,
|
||||||
|
"offlineTransportErrors": 0
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"browser": "webkit",
|
||||||
|
"oldTag": "1175f1a1d2c1",
|
||||||
|
"newTag": "31617662d2f0",
|
||||||
|
"banners": 1,
|
||||||
|
"updateReloads": 1,
|
||||||
|
"cachedFiles": 122,
|
||||||
|
"durationMs": 24426,
|
||||||
|
"wireBytes": 730395,
|
||||||
|
"offline": true,
|
||||||
|
"legacyURLs": 21,
|
||||||
|
"profile": "lossy",
|
||||||
|
"userInitiatedOnly": true,
|
||||||
|
"playbackGuard": true,
|
||||||
|
"evictionRepair": true,
|
||||||
|
"incrementalRefresh": true,
|
||||||
|
"offlineTransportErrors": 4
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
41
perf/results/phase6-migration-unthrottled-2026-10-08.json
Normal file
41
perf/results/phase6-migration-unthrottled-2026-10-08.json
Normal file
@@ -0,0 +1,41 @@
|
|||||||
|
{
|
||||||
|
"commit": "b77938a",
|
||||||
|
"results": [
|
||||||
|
{
|
||||||
|
"browser": "chromium",
|
||||||
|
"oldTag": "1175f1a1d2c1",
|
||||||
|
"newTag": "31617662d2f0",
|
||||||
|
"banners": 1,
|
||||||
|
"updateReloads": 1,
|
||||||
|
"cachedFiles": 122,
|
||||||
|
"durationMs": 9809,
|
||||||
|
"wireBytes": 640960,
|
||||||
|
"offline": true,
|
||||||
|
"legacyURLs": 21,
|
||||||
|
"profile": "unthrottled",
|
||||||
|
"userInitiatedOnly": true,
|
||||||
|
"playbackGuard": true,
|
||||||
|
"evictionRepair": true,
|
||||||
|
"incrementalRefresh": true,
|
||||||
|
"offlineTransportErrors": 0
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"browser": "webkit",
|
||||||
|
"oldTag": "1175f1a1d2c1",
|
||||||
|
"newTag": "31617662d2f0",
|
||||||
|
"banners": 1,
|
||||||
|
"updateReloads": 1,
|
||||||
|
"cachedFiles": 122,
|
||||||
|
"durationMs": 9902,
|
||||||
|
"wireBytes": 755652,
|
||||||
|
"offline": true,
|
||||||
|
"legacyURLs": 21,
|
||||||
|
"profile": "unthrottled",
|
||||||
|
"userInitiatedOnly": true,
|
||||||
|
"playbackGuard": true,
|
||||||
|
"evictionRepair": true,
|
||||||
|
"incrementalRefresh": true,
|
||||||
|
"offlineTransportErrors": 4
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
41
perf/results/phase6-migration-unthrottled-2026-10-09.json
Normal file
41
perf/results/phase6-migration-unthrottled-2026-10-09.json
Normal file
@@ -0,0 +1,41 @@
|
|||||||
|
{
|
||||||
|
"commit": "b77938a",
|
||||||
|
"results": [
|
||||||
|
{
|
||||||
|
"browser": "chromium",
|
||||||
|
"oldTag": "1175f1a1d2c1",
|
||||||
|
"newTag": "31617662d2f0",
|
||||||
|
"banners": 1,
|
||||||
|
"updateReloads": 1,
|
||||||
|
"cachedFiles": 122,
|
||||||
|
"durationMs": 13290,
|
||||||
|
"wireBytes": 640960,
|
||||||
|
"offline": true,
|
||||||
|
"legacyURLs": 21,
|
||||||
|
"profile": "unthrottled",
|
||||||
|
"userInitiatedOnly": true,
|
||||||
|
"playbackGuard": true,
|
||||||
|
"evictionRepair": true,
|
||||||
|
"incrementalRefresh": true,
|
||||||
|
"offlineTransportErrors": 0
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"browser": "webkit",
|
||||||
|
"oldTag": "1175f1a1d2c1",
|
||||||
|
"newTag": "31617662d2f0",
|
||||||
|
"banners": 1,
|
||||||
|
"updateReloads": 1,
|
||||||
|
"cachedFiles": 122,
|
||||||
|
"durationMs": 11711,
|
||||||
|
"wireBytes": 766828,
|
||||||
|
"offline": true,
|
||||||
|
"legacyURLs": 21,
|
||||||
|
"profile": "unthrottled",
|
||||||
|
"userInitiatedOnly": true,
|
||||||
|
"playbackGuard": true,
|
||||||
|
"evictionRepair": true,
|
||||||
|
"incrementalRefresh": true,
|
||||||
|
"offlineTransportErrors": 4
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
41830
perf/results/phase6-native-auto-priority-2026-10-08.json
Normal file
41830
perf/results/phase6-native-auto-priority-2026-10-08.json
Normal file
File diff suppressed because it is too large
Load Diff
191
perf/results/phase6-native-cached-warm.json
Normal file
191
perf/results/phase6-native-cached-warm.json
Normal file
@@ -0,0 +1,191 @@
|
|||||||
|
{
|
||||||
|
"date": "2026-10-08T03:05:33.492Z",
|
||||||
|
"frontendCommit": null,
|
||||||
|
"sourceRoot": "/home/josh/development/personal/ytplayer-codex/wt-assets/perf/.tmp/phase6-native-cached",
|
||||||
|
"sourceCommit": "9408ee07e56db5bbb8319f8da862b549ab948bce",
|
||||||
|
"frontendBuildTag": "46ccefbb78f5",
|
||||||
|
"commit": "9408ee07e56db5bbb8319f8da862b549ab948bce",
|
||||||
|
"runsConfigured": 5,
|
||||||
|
"browsers": {
|
||||||
|
"chromium": {
|
||||||
|
"lte": {
|
||||||
|
"warm": {
|
||||||
|
"requestCount": {
|
||||||
|
"median": 4,
|
||||||
|
"min": 2,
|
||||||
|
"max": 4,
|
||||||
|
"values": [
|
||||||
|
4,
|
||||||
|
4,
|
||||||
|
2,
|
||||||
|
3,
|
||||||
|
4
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"wireBytes": {
|
||||||
|
"median": 7996,
|
||||||
|
"min": 86,
|
||||||
|
"max": 7996,
|
||||||
|
"values": [
|
||||||
|
7996,
|
||||||
|
7996,
|
||||||
|
86,
|
||||||
|
7985,
|
||||||
|
7996
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"fcp": {
|
||||||
|
"median": 124,
|
||||||
|
"min": 116,
|
||||||
|
"max": 132,
|
||||||
|
"values": [
|
||||||
|
128,
|
||||||
|
124,
|
||||||
|
116,
|
||||||
|
124,
|
||||||
|
132
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"lcp": {
|
||||||
|
"median": 148,
|
||||||
|
"min": 148,
|
||||||
|
"max": 156,
|
||||||
|
"values": [
|
||||||
|
156,
|
||||||
|
148,
|
||||||
|
148,
|
||||||
|
148,
|
||||||
|
156
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"domContentLoaded": {
|
||||||
|
"median": 432,
|
||||||
|
"min": 397,
|
||||||
|
"max": 464,
|
||||||
|
"values": [
|
||||||
|
464,
|
||||||
|
397,
|
||||||
|
432,
|
||||||
|
438,
|
||||||
|
413
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"bootDone": {
|
||||||
|
"median": 448,
|
||||||
|
"min": 413,
|
||||||
|
"max": 482,
|
||||||
|
"values": [
|
||||||
|
482,
|
||||||
|
413,
|
||||||
|
448,
|
||||||
|
457,
|
||||||
|
430
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"longTaskTotal": {
|
||||||
|
"median": 61,
|
||||||
|
"min": 58,
|
||||||
|
"max": 79,
|
||||||
|
"values": [
|
||||||
|
79,
|
||||||
|
58,
|
||||||
|
60,
|
||||||
|
70,
|
||||||
|
61
|
||||||
|
]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"webkit": {
|
||||||
|
"lte": {
|
||||||
|
"warm": {
|
||||||
|
"requestCount": {
|
||||||
|
"median": 0,
|
||||||
|
"min": 0,
|
||||||
|
"max": 0,
|
||||||
|
"values": [
|
||||||
|
0,
|
||||||
|
0,
|
||||||
|
0,
|
||||||
|
0,
|
||||||
|
0
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"wireBytes": {
|
||||||
|
"median": 0,
|
||||||
|
"min": 0,
|
||||||
|
"max": 0,
|
||||||
|
"values": [
|
||||||
|
0,
|
||||||
|
0,
|
||||||
|
0,
|
||||||
|
0,
|
||||||
|
0
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"fcp": {
|
||||||
|
"median": 65,
|
||||||
|
"min": 61,
|
||||||
|
"max": 66,
|
||||||
|
"values": [
|
||||||
|
66,
|
||||||
|
63,
|
||||||
|
65,
|
||||||
|
61,
|
||||||
|
65
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"lcp": {
|
||||||
|
"median": 96,
|
||||||
|
"min": 92,
|
||||||
|
"max": 97,
|
||||||
|
"values": [
|
||||||
|
97,
|
||||||
|
96,
|
||||||
|
92,
|
||||||
|
93,
|
||||||
|
96
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"domContentLoaded": {
|
||||||
|
"median": 93,
|
||||||
|
"min": 90,
|
||||||
|
"max": 94,
|
||||||
|
"values": [
|
||||||
|
94,
|
||||||
|
93,
|
||||||
|
90,
|
||||||
|
91,
|
||||||
|
93
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"bootDone": {
|
||||||
|
"median": 98,
|
||||||
|
"min": 94,
|
||||||
|
"max": 98,
|
||||||
|
"values": [
|
||||||
|
98,
|
||||||
|
98,
|
||||||
|
94,
|
||||||
|
95,
|
||||||
|
98
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"longTaskTotal": {
|
||||||
|
"median": 0,
|
||||||
|
"min": 0,
|
||||||
|
"max": 0,
|
||||||
|
"values": [
|
||||||
|
0,
|
||||||
|
0,
|
||||||
|
0,
|
||||||
|
0,
|
||||||
|
0
|
||||||
|
]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
101
perf/results/phase6-native-chromium-warm.json
Normal file
101
perf/results/phase6-native-chromium-warm.json
Normal file
@@ -0,0 +1,101 @@
|
|||||||
|
{
|
||||||
|
"date": "2026-10-08T01:29:49.903Z",
|
||||||
|
"frontendCommit": null,
|
||||||
|
"sourceRoot": null,
|
||||||
|
"sourceCommit": "c4a512bea63da4c9c875501038727768080496c1",
|
||||||
|
"frontendBuildTag": "5fe61861d759",
|
||||||
|
"commit": "c4a512bea63da4c9c875501038727768080496c1",
|
||||||
|
"runsConfigured": 5,
|
||||||
|
"browsers": {
|
||||||
|
"chromium": {
|
||||||
|
"lte": {
|
||||||
|
"warm": {
|
||||||
|
"requestCount": {
|
||||||
|
"median": 4,
|
||||||
|
"min": 3,
|
||||||
|
"max": 5,
|
||||||
|
"values": [
|
||||||
|
5,
|
||||||
|
4,
|
||||||
|
3,
|
||||||
|
4,
|
||||||
|
3
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"wireBytes": {
|
||||||
|
"median": 7869,
|
||||||
|
"min": 97,
|
||||||
|
"max": 7869,
|
||||||
|
"values": [
|
||||||
|
7869,
|
||||||
|
7869,
|
||||||
|
97,
|
||||||
|
7869,
|
||||||
|
7858
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"fcp": {
|
||||||
|
"median": 140,
|
||||||
|
"min": 120,
|
||||||
|
"max": 152,
|
||||||
|
"values": [
|
||||||
|
152,
|
||||||
|
148,
|
||||||
|
120,
|
||||||
|
120,
|
||||||
|
140
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"lcp": {
|
||||||
|
"median": 148,
|
||||||
|
"min": 140,
|
||||||
|
"max": 156,
|
||||||
|
"values": [
|
||||||
|
152,
|
||||||
|
148,
|
||||||
|
148,
|
||||||
|
156,
|
||||||
|
140
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"domContentLoaded": {
|
||||||
|
"median": 398,
|
||||||
|
"min": 391,
|
||||||
|
"max": 406,
|
||||||
|
"values": [
|
||||||
|
404,
|
||||||
|
398,
|
||||||
|
396,
|
||||||
|
406,
|
||||||
|
391
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"bootDone": {
|
||||||
|
"median": 414,
|
||||||
|
"min": 411,
|
||||||
|
"max": 420,
|
||||||
|
"values": [
|
||||||
|
418,
|
||||||
|
414,
|
||||||
|
411,
|
||||||
|
420,
|
||||||
|
412
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"longTaskTotal": {
|
||||||
|
"median": 62,
|
||||||
|
"min": 57,
|
||||||
|
"max": 65,
|
||||||
|
"values": [
|
||||||
|
65,
|
||||||
|
59,
|
||||||
|
63,
|
||||||
|
57,
|
||||||
|
62
|
||||||
|
]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
191
perf/results/phase6-native-parser-warm.json
Normal file
191
perf/results/phase6-native-parser-warm.json
Normal file
@@ -0,0 +1,191 @@
|
|||||||
|
{
|
||||||
|
"date": "2026-10-08T03:01:26.249Z",
|
||||||
|
"frontendCommit": null,
|
||||||
|
"sourceRoot": "/home/josh/development/personal/ytplayer-codex/wt-assets/perf/.tmp/phase6-native-parser",
|
||||||
|
"sourceCommit": "9408ee07e56db5bbb8319f8da862b549ab948bce",
|
||||||
|
"frontendBuildTag": "4ce3d20db0c7",
|
||||||
|
"commit": "9408ee07e56db5bbb8319f8da862b549ab948bce",
|
||||||
|
"runsConfigured": 5,
|
||||||
|
"browsers": {
|
||||||
|
"chromium": {
|
||||||
|
"lte": {
|
||||||
|
"warm": {
|
||||||
|
"requestCount": {
|
||||||
|
"median": 4,
|
||||||
|
"min": 3,
|
||||||
|
"max": 5,
|
||||||
|
"values": [
|
||||||
|
5,
|
||||||
|
4,
|
||||||
|
3,
|
||||||
|
4,
|
||||||
|
4
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"wireBytes": {
|
||||||
|
"median": 8001,
|
||||||
|
"min": 7990,
|
||||||
|
"max": 8001,
|
||||||
|
"values": [
|
||||||
|
8001,
|
||||||
|
8001,
|
||||||
|
7990,
|
||||||
|
8001,
|
||||||
|
8001
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"fcp": {
|
||||||
|
"median": 124,
|
||||||
|
"min": 120,
|
||||||
|
"max": 140,
|
||||||
|
"values": [
|
||||||
|
132,
|
||||||
|
124,
|
||||||
|
124,
|
||||||
|
140,
|
||||||
|
120
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"lcp": {
|
||||||
|
"median": 156,
|
||||||
|
"min": 140,
|
||||||
|
"max": 160,
|
||||||
|
"values": [
|
||||||
|
160,
|
||||||
|
148,
|
||||||
|
156,
|
||||||
|
140,
|
||||||
|
160
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"domContentLoaded": {
|
||||||
|
"median": 428,
|
||||||
|
"min": 405,
|
||||||
|
"max": 442,
|
||||||
|
"values": [
|
||||||
|
442,
|
||||||
|
428,
|
||||||
|
429,
|
||||||
|
405,
|
||||||
|
417
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"bootDone": {
|
||||||
|
"median": 446,
|
||||||
|
"min": 422,
|
||||||
|
"max": 464,
|
||||||
|
"values": [
|
||||||
|
464,
|
||||||
|
446,
|
||||||
|
448,
|
||||||
|
422,
|
||||||
|
432
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"longTaskTotal": {
|
||||||
|
"median": 65,
|
||||||
|
"min": 60,
|
||||||
|
"max": 79,
|
||||||
|
"values": [
|
||||||
|
79,
|
||||||
|
69,
|
||||||
|
65,
|
||||||
|
65,
|
||||||
|
60
|
||||||
|
]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"webkit": {
|
||||||
|
"lte": {
|
||||||
|
"warm": {
|
||||||
|
"requestCount": {
|
||||||
|
"median": 0,
|
||||||
|
"min": 0,
|
||||||
|
"max": 0,
|
||||||
|
"values": [
|
||||||
|
0,
|
||||||
|
0,
|
||||||
|
0,
|
||||||
|
0,
|
||||||
|
0
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"wireBytes": {
|
||||||
|
"median": 0,
|
||||||
|
"min": 0,
|
||||||
|
"max": 0,
|
||||||
|
"values": [
|
||||||
|
0,
|
||||||
|
0,
|
||||||
|
0,
|
||||||
|
0,
|
||||||
|
0
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"fcp": {
|
||||||
|
"median": 83,
|
||||||
|
"min": 81,
|
||||||
|
"max": 91,
|
||||||
|
"values": [
|
||||||
|
82,
|
||||||
|
91,
|
||||||
|
83,
|
||||||
|
88,
|
||||||
|
81
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"lcp": {
|
||||||
|
"median": 83,
|
||||||
|
"min": 81,
|
||||||
|
"max": 91,
|
||||||
|
"values": [
|
||||||
|
82,
|
||||||
|
91,
|
||||||
|
83,
|
||||||
|
88,
|
||||||
|
81
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"domContentLoaded": {
|
||||||
|
"median": 80,
|
||||||
|
"min": 79,
|
||||||
|
"max": 89,
|
||||||
|
"values": [
|
||||||
|
79,
|
||||||
|
89,
|
||||||
|
80,
|
||||||
|
86,
|
||||||
|
79
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"bootDone": {
|
||||||
|
"median": 87,
|
||||||
|
"min": 86,
|
||||||
|
"max": 96,
|
||||||
|
"values": [
|
||||||
|
86,
|
||||||
|
96,
|
||||||
|
87,
|
||||||
|
93,
|
||||||
|
86
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"longTaskTotal": {
|
||||||
|
"median": 0,
|
||||||
|
"min": 0,
|
||||||
|
"max": 0,
|
||||||
|
"values": [
|
||||||
|
0,
|
||||||
|
0,
|
||||||
|
0,
|
||||||
|
0,
|
||||||
|
0
|
||||||
|
]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
3736
perf/results/phase6-owned-trial.json
Normal file
3736
perf/results/phase6-owned-trial.json
Normal file
File diff suppressed because it is too large
Load Diff
7338
perf/results/phase6-preload-css.json
Normal file
7338
perf/results/phase6-preload-css.json
Normal file
File diff suppressed because it is too large
Load Diff
2199
perf/results/phase6-regression-2026-10-08.json
Normal file
2199
perf/results/phase6-regression-2026-10-08.json
Normal file
File diff suppressed because it is too large
Load Diff
28
perf/results/phase6-seams-2026-10-08.json
Normal file
28
perf/results/phase6-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": []
|
||||||
|
}
|
||||||
|
]
|
||||||
28
perf/results/phase6-seams-2026-10-09.json
Normal file
28
perf/results/phase6-seams-2026-10-09.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": []
|
||||||
|
}
|
||||||
|
]
|
||||||
7338
perf/results/phase6-static-css.json
Normal file
7338
perf/results/phase6-static-css.json
Normal file
File diff suppressed because it is too large
Load Diff
135
perf/results/phase6-sync-rollback-2026-10-08.json
Normal file
135
perf/results/phase6-sync-rollback-2026-10-08.json
Normal file
@@ -0,0 +1,135 @@
|
|||||||
|
{
|
||||||
|
"date": "2026-10-08T04:01:09.552Z",
|
||||||
|
"frontendCommit": null,
|
||||||
|
"sourceRoot": null,
|
||||||
|
"sourceCommit": "4b1f5204f8fcd207305ee50fff29985497e58e3b",
|
||||||
|
"frontendBuildTag": "2bf76ad9d08e",
|
||||||
|
"commit": "4b1f5204f8fcd207305ee50fff29985497e58e3b",
|
||||||
|
"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": 151,
|
||||||
|
"min": 151,
|
||||||
|
"max": 151,
|
||||||
|
"values": [
|
||||||
|
151
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"bootDone": {
|
||||||
|
"median": 157,
|
||||||
|
"min": 157,
|
||||||
|
"max": 157,
|
||||||
|
"values": [
|
||||||
|
157
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"reloadDuration": {
|
||||||
|
"median": 173,
|
||||||
|
"min": 173,
|
||||||
|
"max": 173,
|
||||||
|
"values": [
|
||||||
|
173
|
||||||
|
]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"webkit": {
|
||||||
|
"lte": {
|
||||||
|
"offline": {
|
||||||
|
"requestCount": {
|
||||||
|
"median": 0,
|
||||||
|
"min": 0,
|
||||||
|
"max": 0,
|
||||||
|
"values": [
|
||||||
|
0
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"wireBytes": {
|
||||||
|
"median": 0,
|
||||||
|
"min": 0,
|
||||||
|
"max": 0,
|
||||||
|
"values": [
|
||||||
|
0
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"fcp": {
|
||||||
|
"median": 68,
|
||||||
|
"min": 68,
|
||||||
|
"max": 68,
|
||||||
|
"values": [
|
||||||
|
68
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"lcp": {
|
||||||
|
"median": 101,
|
||||||
|
"min": 101,
|
||||||
|
"max": 101,
|
||||||
|
"values": [
|
||||||
|
101
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"domContentLoaded": {
|
||||||
|
"median": 98,
|
||||||
|
"min": 98,
|
||||||
|
"max": 98,
|
||||||
|
"values": [
|
||||||
|
98
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"bootDone": {
|
||||||
|
"median": 102,
|
||||||
|
"min": 102,
|
||||||
|
"max": 102,
|
||||||
|
"values": [
|
||||||
|
102
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"reloadDuration": {
|
||||||
|
"median": 118,
|
||||||
|
"min": 118,
|
||||||
|
"max": 118,
|
||||||
|
"values": [
|
||||||
|
118
|
||||||
|
]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
135
perf/results/phase6-sync-rollback-2026-10-09.json
Normal file
135
perf/results/phase6-sync-rollback-2026-10-09.json
Normal file
@@ -0,0 +1,135 @@
|
|||||||
|
{
|
||||||
|
"date": "2026-10-09T01:15:49.802Z",
|
||||||
|
"frontendCommit": null,
|
||||||
|
"sourceRoot": null,
|
||||||
|
"sourceCommit": "71608c2ce81207ca21c8adbc49edbc2893f5170e",
|
||||||
|
"frontendBuildTag": "2bf76ad9d08e",
|
||||||
|
"commit": "71608c2ce81207ca21c8adbc49edbc2893f5170e",
|
||||||
|
"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": 88,
|
||||||
|
"min": 88,
|
||||||
|
"max": 88,
|
||||||
|
"values": [
|
||||||
|
88
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"lcp": {
|
||||||
|
"median": 88,
|
||||||
|
"min": 88,
|
||||||
|
"max": 88,
|
||||||
|
"values": [
|
||||||
|
88
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"domContentLoaded": {
|
||||||
|
"median": 299,
|
||||||
|
"min": 299,
|
||||||
|
"max": 299,
|
||||||
|
"values": [
|
||||||
|
299
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"bootDone": {
|
||||||
|
"median": 305,
|
||||||
|
"min": 305,
|
||||||
|
"max": 305,
|
||||||
|
"values": [
|
||||||
|
305
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"reloadDuration": {
|
||||||
|
"median": 349,
|
||||||
|
"min": 349,
|
||||||
|
"max": 349,
|
||||||
|
"values": [
|
||||||
|
349
|
||||||
|
]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"webkit": {
|
||||||
|
"lte": {
|
||||||
|
"offline": {
|
||||||
|
"requestCount": {
|
||||||
|
"median": 0,
|
||||||
|
"min": 0,
|
||||||
|
"max": 0,
|
||||||
|
"values": [
|
||||||
|
0
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"wireBytes": {
|
||||||
|
"median": 0,
|
||||||
|
"min": 0,
|
||||||
|
"max": 0,
|
||||||
|
"values": [
|
||||||
|
0
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"fcp": {
|
||||||
|
"median": 164,
|
||||||
|
"min": 164,
|
||||||
|
"max": 164,
|
||||||
|
"values": [
|
||||||
|
164
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"lcp": {
|
||||||
|
"median": 253,
|
||||||
|
"min": 253,
|
||||||
|
"max": 253,
|
||||||
|
"values": [
|
||||||
|
253
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"domContentLoaded": {
|
||||||
|
"median": 241,
|
||||||
|
"min": 241,
|
||||||
|
"max": 241,
|
||||||
|
"values": [
|
||||||
|
241
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"bootDone": {
|
||||||
|
"median": 256,
|
||||||
|
"min": 256,
|
||||||
|
"max": 256,
|
||||||
|
"values": [
|
||||||
|
256
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"reloadDuration": {
|
||||||
|
"median": 299,
|
||||||
|
"min": 299,
|
||||||
|
"max": 299,
|
||||||
|
"values": [
|
||||||
|
299
|
||||||
|
]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
22215
perf/results/phase6-webkit-cold-2026-10-08.json
Normal file
22215
perf/results/phase6-webkit-cold-2026-10-08.json
Normal file
File diff suppressed because it is too large
Load Diff
137
plans/phase6-investigation.md
Normal file
137
plans/phase6-investigation.md
Normal file
@@ -0,0 +1,137 @@
|
|||||||
|
# Phase 6 investigation
|
||||||
|
|
||||||
|
All comparisons use the current instrumented harness with matching frontend and
|
||||||
|
server from detached worktrees beneath `perf/.tmp`. No production requests.
|
||||||
|
Command template (three fresh contexts):
|
||||||
|
|
||||||
|
```
|
||||||
|
node perf/baseline.mjs --runs 3 --browser webkit --profile lte --scenario cold --source-root perf/.tmp/phase6-<variant> --out perf/results/phase6-<variant>.json
|
||||||
|
```
|
||||||
|
|
||||||
|
| Source | Commit | LTE FCP median ms | Wire bytes | app.js positive responses/sample |
|
||||||
|
|---|---|---:|---:|---:|
|
||||||
|
| Phase 2 | 4950d3e | 1044 | 1191063 | 2 |
|
||||||
|
| Immediately before lazy layouts | d98d444 | 1032 | 1191686 | 2 |
|
||||||
|
| First lazy-layout cut | 5d6a154 | 1615 | 1106796 | 2 |
|
||||||
|
| Phase 3 tip | cd4a25a | 1646 | 1108191 | 2 |
|
||||||
|
| Phase 5 tip (same product as Phase 4) | 21c1273 | 1736 | 1097007 | 2 |
|
||||||
|
| Phase 5 + worker force-cache | scratch | 1745 | 1097006 | 2 |
|
||||||
|
| Phase 5 + parser-owned classic CSS | scratch | 898 | 1097051 | 2 |
|
||||||
|
| Phase 5 + classic preload | scratch | 909 | 1097032 | 2 |
|
||||||
|
|
||||||
|
The adjacent-commit measurements isolate `5d6a154`, not the Phase 4 extraction.
|
||||||
|
All three FCP samples, complete proxy waterfalls, and page Resource Timing are
|
||||||
|
stored in each result. The first sample often includes startup compression cost;
|
||||||
|
it is retained in every median, not discarded.
|
||||||
|
|
||||||
|
Phase 3 sample 2: common styles finish around 880 ms. The parser-written classic
|
||||||
|
stylesheet is discovered at 416 ms but reaches the proxy only at 1434 ms and
|
||||||
|
finishes at 1607 ms. FCP is 1646 ms. Phase 5 sample 2: classic reaches the proxy
|
||||||
|
at 1540 ms, finishes at 1711 ms, FCP is 1736 ms. Speculatively discovered body
|
||||||
|
scripts already occupy the request queue when the selected stylesheet arrives.
|
||||||
|
Phase 2 discovers its declarative styles together around 231 ms, FCP 1044 ms.
|
||||||
|
|
||||||
|
The first service-worker request is at 2458 ms in Phase 3 and 2337 ms in Phase 5,
|
||||||
|
AFTER paint. Worker install competition does not explain this FCP regression.
|
||||||
|
Font preload URLs match the font CSS; late layout CSS is the final rendering
|
||||||
|
barrier. Making classic CSS declarative again removes the delay without changing
|
||||||
|
CSS bytes or cascade order. This scratch variant unconditionally applies classic
|
||||||
|
and is diagnostic only; the shipping fix must retain selected-layout behavior.
|
||||||
|
|
||||||
|
The modern synchronizer already uses default fetch caching. Changing it to
|
||||||
|
force-cache has no effect: every WebKit sample still transfers app.js twice.
|
||||||
|
The page and worker cannot reuse these HTTP-cache responses in this harness.
|
||||||
|
The next hypothesis was a same-build page handoff reusing the page HTTP cache.
|
||||||
|
The prototype checked both the X-Asset-Hash header and decoded-body SHA-256, and
|
||||||
|
stripped compressed transport headers before constructing the cached Response.
|
||||||
|
Updates and legacy migration must continue through their existing worker path.
|
||||||
|
|
||||||
|
The shipping CSS fix is a preload, not an unconditional stylesheet. Its three-run
|
||||||
|
FCP is 909 ms; the first classic request reaches the proxy at 250 ms and completes
|
||||||
|
at 423 ms. Lazy still applies only the saved layout, in the original cascade order.
|
||||||
|
Other selected layouts may speculatively download the small classic stylesheet;
|
||||||
|
it never applies or blocks their rendering. No inline script or CSP change.
|
||||||
|
|
||||||
|
|
||||||
|
The page-to-worker prototype successfully delivered bodies, but still transferred
|
||||||
|
app.js twice. `phase6-handoff-mode.json` records the two requests: identical URL,
|
||||||
|
`Accept-Encoding: gzip, deflate`, no-cors mode and immutable response, but script
|
||||||
|
versus empty request destinations. Force-cache in the page did not reuse the
|
||||||
|
script body's cache either. This prototype was discarded, not shipped.
|
||||||
|
|
||||||
|
A fetch preload plus a later fetch also failed the one-body assertion. The
|
||||||
|
shipping fix instead starts one cache-first fetch promise in the head loader.
|
||||||
|
The final eager app-bootstrap consumes that response, verifies header AND SHA-256,
|
||||||
|
and caches it before executing unchanged app.js as a classic script. A specific
|
||||||
|
server-generated CSP hash authorizes only those exact bytes; unsafe-inline remains
|
||||||
|
absent. DOMContentLoaded boot runs exactly once; the temporary script node and
|
||||||
|
response promise are released. Native, insecure HTTP and rollback modes retain
|
||||||
|
external classic execution. Only server asset-manifest rendering changed; no
|
||||||
|
frontend imports, worker sync algorithm, update flow or playback source changed.
|
||||||
|
|
||||||
|
The initial owned-response trial passed with one app.js body, FCP 1040 ms and
|
||||||
|
981468 wire bytes (one fresh sample; not a final median). Five-run acceptance and
|
||||||
|
regression results will be recorded separately. Historical diagnostics retained
|
||||||
|
all samples. Full-harness configurations now reset to the base tree before each
|
||||||
|
browser/profile, preventing a preceding synthetic update from contaminating the
|
||||||
|
next cold build. The historical invalid media fixture remains unchanged: home
|
||||||
|
launch only, not autoplay/audio latency.
|
||||||
|
|
||||||
|
|
||||||
|
Self-review added explicit N-1 app boot after eviction: verify the retained body
|
||||||
|
and use its exact classic URL through the controlling worker. It never fills the
|
||||||
|
new key with old bytes. Stale core execution additionally requires every group
|
||||||
|
contract to match the running shell's expectations; a core or feature contract
|
||||||
|
change rejects it. Both-engine lazy smoke now changes app bytes, evicts the new
|
||||||
|
entry, boots N-1 offline, then repairs the current entry before the existing
|
||||||
|
feature pinning/contract/playback checks. No source in app.js changed.
|
||||||
|
|
||||||
|
|
||||||
|
The all-engine inline prototype's completed five-run Chromium/LTE profile had
|
||||||
|
cold boot 6748 ms, warm boot 458 ms (Phase 5: 410 ms), offline boot 204 ms, core
|
||||||
|
update 870 ms. This was a real warm regression, so the full sweep was interrupted
|
||||||
|
before accepting its results. Raw output: perf/.tmp/phase6-inline-chromium-profile.log.
|
||||||
|
The native-script controlled variant measured warm 414 ms over five samples.
|
||||||
|
Shipping capture is therefore restricted to WebKit (including iOS CriOS); other
|
||||||
|
engines preload and retain their native classic-script/code-cache path. Normal
|
||||||
|
native cache hits avoid decoding/rehashing; retained core fallback still enforces
|
||||||
|
all group contracts. The new worker marks stale responses no-store and strips
|
||||||
|
decoded transport headers, preventing browser resource-cache poisoning under a
|
||||||
|
new URL. CacheStorage's exact keys and previous entries stay unchanged.
|
||||||
|
|
||||||
|
The native-window contract lookup trial regressed Chromium warm boot (~698 ms).
|
||||||
|
It is rejected. The worker now owns the all-group stale app contract guard
|
||||||
|
(pure planner and actual worker VM tests first); native boot has no window-side
|
||||||
|
CacheStorage or hashing work. WebKit's byte-verified owned response path is
|
||||||
|
unchanged. The native fixture models the actual worker fallback decision instead
|
||||||
|
of a second page-side decision. All 221 frontend tests and 184 server tests pass;
|
||||||
|
both-engine lazy/offline/eviction/pinning/contracts/playback smoke passes.
|
||||||
|
|
||||||
|
The WebKit warm prototype also paid repeated handoff/cache/hash work (warm
|
||||||
|
~98 ms versus Phase 5 83 ms). Capture now applies only to uncontrolled WebKit
|
||||||
|
visits. Controlled warm/offline/update pages use the worker's native exact-URL
|
||||||
|
script path, including its strengthened contract guard. A failing head-loader
|
||||||
|
test precedes this change; all 222 frontend and 184 server tests and both-engine
|
||||||
|
lazy smoke pass. Cold capture and both rollback flags are unchanged.
|
||||||
|
|
||||||
|
The final native-path review caught a new Chromium paint regression: five-run
|
||||||
|
FCP 1616/2328 ms (LTE/lossy), despite faster boot. A contemporary Phase 5 LTE
|
||||||
|
control measured 1360 ms over three runs. Lowering the new app preload priority
|
||||||
|
reduced FCP to 1396 ms (three runs). Native parser execution restores app's
|
||||||
|
original DOMContentLoaded boot boundary. A rejected no-preload controlled-page
|
||||||
|
trial worsened WebKit warm boot to 98 ms; native cache preloading remains.
|
||||||
|
|
||||||
|
The standalone helper adds a CacheStorage script read to native startup. Packing
|
||||||
|
the loader into the existing final core script, section-rail.js, measured Chrome
|
||||||
|
warm 425 ms versus contemporary Phase 5 426 ms (five runs); WebKit measured
|
||||||
|
95 ms versus 88 ms, so zero warm overhead is not yet established. The merged
|
||||||
|
three-run Chrome LTE cold trial measured FCP 1384 ms and boot 6512 ms. Core's
|
||||||
|
contract becomes 2 because an old section script lacks this loader and cannot
|
||||||
|
safely substitute for it. No Phase 5 SHELL file is removed; the experimental
|
||||||
|
Phase 6 standalone helper is removed. Native-order, exact final-script packing
|
||||||
|
and contract tests precede implementation. All 223 frontend and 184 server tests
|
||||||
|
and both-engine lazy/offline/eviction/pinning/contracts/playback smoke pass.
|
||||||
|
|
||||||
|
The intermediate native-auto-priority full dataset is retained as a rejected
|
||||||
|
trial; it is not the final acceptance result. Source directories named in trial
|
||||||
|
JSONs are dirty diagnostic worktrees of 9408ee0 with exactly the changes above.
|
||||||
11
plans/phase6-plan.md
Normal file
11
plans/phase6-plan.md
Normal file
@@ -0,0 +1,11 @@
|
|||||||
|
# Phase 6 plan
|
||||||
|
|
||||||
|
Start from Phase 5 21c1273. Fix cold first-visit delivery only; preserve playback, banner, migration, exact hashes, contracts/pinning, offline and both rollback flags.
|
||||||
|
|
||||||
|
1. Extend perf/baseline.mjs/proxy.mjs with page Resource Timing and complete request waterfalls, and a source-worktree option. Create detached historical worktrees beneath perf/.tmp for Phase 2, 3 and 5 tips. Run isolated WebKit/LTE cold measurements, >=3 runs each. Bisect Phase 3 commits with the same instrumented harness. Test head-loader/CSS discovery, fonts, worker start relative to FCP and immutable HTTP-cache behavior with controlled scratch variants. Record actual evidence in plans/phase6-investigation.md.
|
||||||
|
2. Add failing tests before each root fix. Likely files: frontend/index.html, lazy.js or a tiny layout bootstrap; frontend/sw.js and asset-sync-core.js only as evidence warrants. Keep the legacy install path and update all-or-nothing behavior intact; cache completed immutable responses safely without any unverifiable body. Preserve selected layout before paint and existing CSS cascade order.
|
||||||
|
3. Commit each fix separately with frontend and isolated Bun server suites green. Revert individual fixes for rollback; ASSET_HASHING=0/ASSET_SYNC=0 stay supported. Do not deploy or contact production.
|
||||||
|
4. Run final five-run WebKit cold LTE and lossy medians: FCP <= measured Phase 2 (targets ~1,300/3,800 ms); app.js positive-body responses exactly one; cold wire clearly below baseline. Measure Chromium and warm/offline/core-JS/CSS/feature updates against Phase 5, with repeated medians where regression risk exists. Rerun migration both engines with and without stalls, hidden timers, lazy/seam offline checks, rollback paths and all tests.
|
||||||
|
5. Self-review master sections 3.2/3.3/6/6b and full diff. Commit <=350-word plans/phase6-report.md with actual before/after table and explicit unmet targets; overwrite DONE06 signal and end.
|
||||||
|
|
||||||
|
Risks: CSS cascade/no-flash preservation, duplicate requests crossing browser/worker HTTP-cache partitions, race between page handoff and install, timeout fallbacks delaying cache readiness, CPU contention invalidating timing. Historical worktrees/scratch data stay inside wt-assets; run benchmarks sequentially and preserve the invalid historical media fixture only for comparable home-launch metrics, not playback claims.
|
||||||
46
plans/phase6-report.md
Normal file
46
plans/phase6-report.md
Normal file
@@ -0,0 +1,46 @@
|
|||||||
|
# Phase 6 report
|
||||||
|
|
||||||
|
Tested `4b1f520`, from Phase 5 `21c1273`. Adjacent-commit measurements isolate
|
||||||
|
`5d6a154`: WebKit/LTE FCP 1032→1615 ms. Its selected classic CSS arrived late
|
||||||
|
(1540–1711 ms); worker installation started after paint. Fonts matched their
|
||||||
|
preloads. Default/force-cache and HTTP-cache handoff trials still transferred
|
||||||
|
app.js twice.
|
||||||
|
|
||||||
|
Shipped early classic style discovery (`e7a7cdc`), a single verified page-owned
|
||||||
|
WebKit response and specific CSP hash (`a86fa1c`, refined through `9408ee0`),
|
||||||
|
worker-owned stale-core contract protection (`97eb31e`), and native parser boot
|
||||||
|
with low-priority app preloading (`4b1f520`). The loader shares the existing final
|
||||||
|
core script; core contract 2 prevents unsafe old packing fallback. app.js,
|
||||||
|
playback and sw-update.js are unchanged.
|
||||||
|
|
||||||
|
Five-run final medians:
|
||||||
|
|
||||||
|
| Engine/profile | Phase 5→6 FCP ms | Phase 5→6 cold bytes | app bodies |
|
||||||
|
|---|---:|---:|---:|
|
||||||
|
| WebKit/LTE | 1739→905 | 1097048→983326 | 2→1 |
|
||||||
|
| WebKit/lossy | 4303→2748 | 1105402→990152 | 2→1 |
|
||||||
|
| Chromium/LTE | 1356→1388 | 651012→653267 | 1→1 |
|
||||||
|
| Chromium/lossy | 2052→1976 | 651096→653267 | 1→1 |
|
||||||
|
|
||||||
|
WebKit paint, one-body and below-baseline wire targets pass (−16.6%/−16.1%
|
||||||
|
versus original wire baseline). Phase 2 measured 1044/4045 ms over three runs.
|
||||||
|
|
||||||
|
Strict zero regression is **not achieved**: Chrome/LTE FCP +32 ms, cold bytes
|
||||||
|
+0.35%, long tasks 278→304 ms; warm boots Chrome 410/403→418/442 and WebKit
|
||||||
|
83/85→88/89 ms. Metadata adds ~0.4 KB per core update. Added loader/security
|
||||||
|
work has a cost; complete timing attribution remains unproved. Contemporary
|
||||||
|
Phase 5 controls and all offline/update deltas appear in
|
||||||
|
[phase6-verification.md](phase6-verification.md), with actual command excerpts.
|
||||||
|
Chromium cold boot improves 7043/11342→6478/10314 ms.
|
||||||
|
|
||||||
|
Migration passes both engines with/without stalls; banner once, explicit legacy
|
||||||
|
refresh, old URLs, N-1, offline and playback guards. Lazy/layout/feature seams,
|
||||||
|
hidden timers, both rollback modes, 223 frontend/184 server/3 measurement tests
|
||||||
|
pass. No fonts/icons re-downloaded. ASSET_HASHING=0 and ASSET_SYNC=0 remain.
|
||||||
|
|
||||||
|
Real iPhone install/update, quota pressure and 30-minute locked-screen audio
|
||||||
|
require owner checks. The historical fixture measures home launch, not autoplay.
|
||||||
|
|
||||||
|
Post-crash verification retained in `6f0235f`: both-engine migrations, offline
|
||||||
|
layouts/features, rollback and cold smoke pass. WebKit smoke FCP 1253/2881 ms
|
||||||
|
(LTE/lossy), one app response each. Durable five-run results remain authoritative.
|
||||||
219
plans/phase6-verification.md
Normal file
219
plans/phase6-verification.md
Normal file
@@ -0,0 +1,219 @@
|
|||||||
|
# Phase 6 verification
|
||||||
|
|
||||||
|
Final product under test: `4b1f520` (Phase 5 parent `21c1273`). No production requests.
|
||||||
|
All historical source worktrees contain matching server and frontend revisions.
|
||||||
|
The complete diagnostic waterfalls and five-run acceptance samples are committed
|
||||||
|
under `perf/results/phase6-*.json`. KB in these tables means 1,000 bytes.
|
||||||
|
|
||||||
|
## Cause and controlled trials
|
||||||
|
|
||||||
|
See [phase6-investigation.md](phase6-investigation.md) for the three-run tip
|
||||||
|
comparisons, adjacent-commit bisect, rejected cache/handoff prototypes and fixes.
|
||||||
|
The regressing commit is `5d6a154`: LTE FCP 1,032 → 1,615 ms immediately across
|
||||||
|
that cut. Phase 2/3/5 medians are 1,044/1,646/1,736 ms. Phase 4 extraction did
|
||||||
|
not introduce the paint regression.
|
||||||
|
|
||||||
|
Representative WebKit/LTE sample 2, proxy times relative to tracking start:
|
||||||
|
phase6-before-phase5.json versus phase6-webkit-cold-2026-10-08.json (9408ee0
|
||||||
|
owned-response diagnostic, before final loader packing). Final medians follow.
|
||||||
|
|
||||||
|
| Request | Phase 5 start/end ms | Phase 6 start/end ms |
|
||||||
|
|---|---:|---:|
|
||||||
|
| lazy.js | 250/417 | 250/418 |
|
||||||
|
| styles.css | 251/570 | 251/570 |
|
||||||
|
| theme-classic.css | 1540/1711 | 250/421 |
|
||||||
|
| page app.js | 1524/2273 | 1552/2297 |
|
||||||
|
| first sw.js | 2337/2533 | 2367/2564 |
|
||||||
|
| worker app.js | 2903/3651 | absent |
|
||||||
|
| FCP | 1736 | 905 |
|
||||||
|
|
||||||
|
The stylesheet becomes available before the parser applies it. The worker starts
|
||||||
|
after paint. Default fetch caching was already in the modern synchronizer;
|
||||||
|
force-cache and a page-to-worker HTTP-cache handoff both still transferred two
|
||||||
|
app bodies in WebKit. A single page-owned response, verified and cached before
|
||||||
|
boot registers the worker, removes the redundant app transfer. Controlled pages
|
||||||
|
and other engines retain native external scripts; no window cache/hash lookup
|
||||||
|
is added to those paths. Each retained stale app requires all shell contracts
|
||||||
|
to match, enforced by the worker.
|
||||||
|
|
||||||
|
## Final measurements
|
||||||
|
|
||||||
|
Exact final command (120 samples: both engines, both profiles, six scenarios,
|
||||||
|
five fresh samples each):
|
||||||
|
|
||||||
|
```sh
|
||||||
|
node perf/baseline.mjs --runs 5 --browser all --profile all --scenario all --assert-cold --out perf/results/phase6-final-2026-10-08.json
|
||||||
|
```
|
||||||
|
|
||||||
|
Final app.js positive-byte responses are exactly one in all 20 cold samples.
|
||||||
|
The earlier separate cold/regression datasets used 9408ee0 and are diagnostics,
|
||||||
|
not the final acceptance build. `phase6-native-auto-priority-2026-10-08.json`
|
||||||
|
combines that rejected intermediate sweep. Final core contract is 2: the final
|
||||||
|
section-rail script now owns the loader; a stale pre-packing section script would
|
||||||
|
not load app.js. No Phase 5 SHELL file was deleted. app.js and sw-update.js remain
|
||||||
|
byte-for-byte unchanged.
|
||||||
|
|
||||||
|
Actual final command output excerpts:
|
||||||
|
|
||||||
|
```text
|
||||||
|
Chromium/LTE: run 4/5... done (653267 bytes, 115 reqs, fcp=1396ms, boot=6478ms)
|
||||||
|
Chromium/lossy: run 2/5... done (653267 bytes, 115 reqs, fcp=1972ms, boot=10292ms)
|
||||||
|
WebKit/LTE: run 2/5... done (983326 bytes, 158 reqs, fcp=905ms, boot=2335ms)
|
||||||
|
WebKit/lossy: run 2/5... done (990152 bytes, 161 reqs, fcp=2748ms, boot=5046ms)
|
||||||
|
```
|
||||||
|
|
||||||
|
WebKit FCP targets (≤1300/3800 ms) pass. Contemporary Phase 2 three-run medians:
|
||||||
|
1044/4045 ms; its lossy samples are 4045/3218/5297, all retained. Original baseline
|
||||||
|
WebKit cold wire is 1,179,624 bytes. Final LTE/lossy wire is 983,326/990,152 bytes
|
||||||
|
(16.6%/16.1% lower). The final cold set transfers app.js once in both engines.
|
||||||
|
|
||||||
|
|
||||||
|
## Complete final comparison
|
||||||
|
|
||||||
|
Five-run medians. Each cell is Phase 5 → Phase 6; bytes are exact body wire bytes.
|
||||||
|
|
||||||
|
| Browser/profile | Cold requests | Cold bytes | FCP ms | LCP ms | Boot ms | Long tasks ms |
|
||||||
|
|---|---:|---:|---:|---:|---:|---:|
|
||||||
|
| chromium/lte | 115 → 115 | 651012 → 653267 | 1356 → 1388 | 1356 → 1388 | 7043 → 6478 | 278 → 304 |
|
||||||
|
| chromium/lossy | 115 → 115 | 651096 → 653267 | 2052 → 1976 | 2052 → 1976 | 11342 → 10314 | 295 → 297 |
|
||||||
|
| webkit/lte | 159 → 158 | 1097048 → 983326 | 1739 → 905 | 1810 → 982 | 2315 → 2335 | 0 → 0 |
|
||||||
|
| webkit/lossy | 162 → 161 | 1105402 → 990152 | 4303 → 2748 | 4371 → 2821 | 5573 → 5046 | 0 → 0 |
|
||||||
|
|
||||||
|
WebKit does not expose the Long Tasks API; its zero fields mean unmeasured.
|
||||||
|
|
||||||
|
| Browser/profile | Warm boot ms | Offline boot ms | Offline reload ms | Core update ms/bytes | CSS update ms/bytes | Feature update ms/bytes |
|
||||||
|
|---|---:|---:|---:|---:|---:|---:|
|
||||||
|
| chromium/lte | 410 → 418 | 185 → 188 | 202 → 206 | 745/125795 → 872/126186 | 1380/35363 → 1388/35760 | 1378/33408 → 1392/33762 |
|
||||||
|
| chromium/lossy | 403 → 442 | 189 → 194 | 204 → 207 | 747/125795 → 805/126186 | 1377/35363 → 1383/35760 | 1375/33408 → 1386/33762 |
|
||||||
|
| webkit/lte | 83 → 88 | 86 → 88 | 100 → 102 | 1890/147259 → 2015/147626 | 1455/45659 → 1467/46254 | 1455/43318 → 1380/41860 |
|
||||||
|
| webkit/lossy | 85 → 89 | 81 → 85 | 95 → 98 | 4192/147259 → 2934/147626 | 3647/43705 → 3687/44211 | 3644/41364 → 3693/41860 |
|
||||||
|
|
||||||
|
## Limits and exact unmet parity targets
|
||||||
|
|
||||||
|
Strict zero regression versus the recorded Phase 5 run is NOT achieved.
|
||||||
|
Chromium/LTE FCP is 1388 versus 1356 ms (+32); cold bytes 653267 versus 651012
|
||||||
|
(+2255, 0.35%); long tasks 304 versus 278 ms. The verified loader, CSP permission,
|
||||||
|
contract checks and metadata add code/bytes; parser insertion also changes task
|
||||||
|
boundaries. The whole timing delta has not been causally isolated. Three-run
|
||||||
|
contemporary Phase 5 LTE medians were FCP 1368, boot 7090 and long tasks 294 ms.
|
||||||
|
Final Chrome boot improves to 6478/10314 ms (Phase 5 7043/11342).
|
||||||
|
|
||||||
|
Warm/offline/update deltas in the complete table remain explicit. Contemporary
|
||||||
|
five-run Phase 5 warm boots were Chrome 426/446 and WebKit 88/88 ms; final values
|
||||||
|
418/442 and 88/89 ms. Contemporary Phase 5 core-update times were Chrome 869/799
|
||||||
|
and WebKit 1955/2776 ms; final 872/805 and 2015/2934 ms. Metadata adds roughly
|
||||||
|
0.4 KB to core updates. Periodic lossy stalls depend on request order; timing
|
||||||
|
comparisons cannot establish exact parity. The extra CPU/cache work is small,
|
||||||
|
but zero cost was not proved, and the positive timing deltas are not dismissed.
|
||||||
|
|
||||||
|
Every final update passed assertChangedAssets: only app.js, theme-glass.css or
|
||||||
|
presenter.js is downloaded as application code, with index/API/worker metadata;
|
||||||
|
fonts/icons are not re-downloaded. Whole CSS-update wire remains above 30 KB,
|
||||||
|
a pre-existing Phase 5 gap. Readiness and completion remain separately measured.
|
||||||
|
|
||||||
|
Real iPhone Safari/PWA code-cache behavior, OPFS quota pressure, forced cache
|
||||||
|
eviction and uninterrupted locked-screen/background audio for 30 minutes were
|
||||||
|
not verified. The historical media fixture is invalid for autoplay: these are
|
||||||
|
home-launch metrics, not media-start measurements. No playback code changed.
|
||||||
|
Other selected layouts may prefetch 4079 unused classic CSS bytes; that preload
|
||||||
|
never applies, and the selected stylesheet retains its original cascade order.
|
||||||
|
|
||||||
|
## Compatibility verification on the final product
|
||||||
|
|
||||||
|
All commands below ran on 4b1f520, build 31617662d2f0:
|
||||||
|
|
||||||
|
```sh
|
||||||
|
node perf/migration.mjs --browser all --profile unthrottled --out perf/results/phase6-migration-unthrottled-2026-10-08.json
|
||||||
|
node perf/migration.mjs --browser all --profile lossy --out perf/results/phase6-migration-lossy-2026-10-08.json
|
||||||
|
node perf/hidden-timers.mjs
|
||||||
|
node perf/lazy.mjs --browser all --out perf/results/phase6-lazy-2026-10-08.json
|
||||||
|
node perf/seams.mjs --browser all
|
||||||
|
ASSET_SYNC=0 node perf/baseline.mjs --runs 1 --browser all --profile lte --scenario offline --out perf/results/phase6-sync-rollback-2026-10-08.json
|
||||||
|
ASSET_HASHING=0 node perf/baseline.mjs --runs 1 --browser all --profile lte --scenario offline --out perf/results/phase6-hashing-rollback-2026-10-08.json
|
||||||
|
node --test frontend/*.test.js
|
||||||
|
node --test perf/measurement.test.mjs
|
||||||
|
(cd server && bun install && PATH="$PWD/../perf/.tmp/test-venv/bin:$PATH" bun run test)
|
||||||
|
```
|
||||||
|
|
||||||
|
Actual output excerpts:
|
||||||
|
|
||||||
|
```text
|
||||||
|
Migration unthrottled: chromium banners=1 updateReloads=1 cachedFiles=122 userInitiatedOnly=true offline=true playbackGuard=true evictionRepair=true
|
||||||
|
Migration unthrottled: webkit banners=1 updateReloads=1 cachedFiles=122 userInitiatedOnly=true offline=true playbackGuard=true evictionRepair=true
|
||||||
|
Migration lossy: chromium durationMs=25233 banners=1 updateReloads=1 legacyURLs=21 userInitiatedOnly=true offline=true
|
||||||
|
Migration lossy: webkit durationMs=22860 banners=1 updateReloads=1 legacyURLs=21 userInitiatedOnly=true offline=true
|
||||||
|
Hidden timers: Page errors: none; Result: ALL ASSERTIONS PASSED
|
||||||
|
Lazy: chromium PASS; webkit PASS
|
||||||
|
Seams: chromium checked=[shell,presenter,remote,party,share,stats] offlineNetworkRequests=0 errors=[]
|
||||||
|
Seams: webkit checked=[shell,presenter,remote,party,share,stats] offlineNetworkRequests=0 errors=[]
|
||||||
|
Frontend: tests 223; pass 223; fail 0; skipped 0
|
||||||
|
Measurement: tests 3; pass 3; fail 0
|
||||||
|
Server: sum of per-file output: 184 pass; 0 fail
|
||||||
|
```
|
||||||
|
|
||||||
|
Both rollback modes launched offline with zero network in Chromium and WebKit.
|
||||||
|
The archived b77938a fixture was rebuilt by the migration script each time.
|
||||||
|
Legacy reload remains user-initiated; its pre-existing Refresh UI timer exception
|
||||||
|
is unchanged. New-page playback guards, old-client readiness, one banner, old
|
||||||
|
plain/?v=oldTag/?__ytpfresh URLs, already-open old tabs and cached offline reload
|
||||||
|
remain covered. Four expected WebKit disconnected API transport errors per
|
||||||
|
migration were recorded and matched to failed API requests; no script errors.
|
||||||
|
The seam harness's historical Phase 4 artifact was restored after saving the
|
||||||
|
new output as phase6-seams-2026-10-08.json.
|
||||||
|
|
||||||
|
Self-review re-read the complete product diff and master §§3.2/3.3/6/6b.
|
||||||
|
No frontend import was added to server code; no original shell file was removed;
|
||||||
|
no auto skipWaiting, live stateful re-evaluation, playback edit, push or deploy.
|
||||||
|
Cache responses are exact-key checked; stale bodies under a new URL are no-store
|
||||||
|
and carry no decoded transport encoding. Core contract 2 blocks loader-free N-1
|
||||||
|
packing, while same-contract app fallback and executed feature pinning remain
|
||||||
|
covered by both-engine smoke. Failed quota writes never delete saved music.
|
||||||
|
|
||||||
|
## Post-crash completion — 2026-10-09
|
||||||
|
|
||||||
|
Resumed from ff3f934 on codex/phase6. Commit 71608c2 clarifies the diagnostic
|
||||||
|
waterfall provenance; product code and served build 31617662d2f0 are unchanged.
|
||||||
|
The durable five-run/120-sample final measurements above were preserved.
|
||||||
|
Reinstalled Playwright browsers passed fresh compatibility checks:
|
||||||
|
|
||||||
|
```sh
|
||||||
|
node perf/migration.mjs --browser all --profile unthrottled --out perf/results/phase6-migration-unthrottled-2026-10-09.json
|
||||||
|
node perf/migration.mjs --browser all --profile lossy --out perf/results/phase6-migration-lossy-2026-10-09.json
|
||||||
|
node perf/hidden-timers.mjs
|
||||||
|
node perf/lazy.mjs --browser all --out perf/results/phase6-lazy-2026-10-09.json
|
||||||
|
node perf/seams.mjs --browser all
|
||||||
|
ASSET_SYNC=0 node perf/baseline.mjs --runs 1 --browser all --profile lte --scenario offline --out perf/results/phase6-sync-rollback-2026-10-09.json
|
||||||
|
ASSET_HASHING=0 node perf/baseline.mjs --runs 1 --browser all --profile lte --scenario offline --out perf/results/phase6-hashing-rollback-2026-10-09.json
|
||||||
|
node --test frontend/*.test.js
|
||||||
|
node --test perf/measurement.test.mjs
|
||||||
|
(cd server && bun install && PATH="$PWD/../perf/.tmp/test-venv/bin:$PATH" bun run test)
|
||||||
|
```
|
||||||
|
|
||||||
|
Fresh output excerpts (all commands exited successfully):
|
||||||
|
|
||||||
|
```text
|
||||||
|
Migration unthrottled: chromium durationMs=13290; webkit durationMs=11711
|
||||||
|
Migration lossy: chromium durationMs=27355; webkit durationMs=24426
|
||||||
|
All four: banners=1 updateReloads=1 cachedFiles=122 legacyURLs=21
|
||||||
|
All four: userInitiatedOnly=true offline=true playbackGuard=true evictionRepair=true incrementalRefresh=true
|
||||||
|
Hidden timers: Page errors: none; Result: ALL ASSERTIONS PASSED
|
||||||
|
Lazy: chromium PASS; webkit PASS
|
||||||
|
Seams: chromium and webkit checked=[shell,presenter,remote,party,share,stats] offlineNetworkRequests=0 errors=[]
|
||||||
|
Both rollback flags, both engines: requestCount=0 wireBytes=0
|
||||||
|
Frontend: tests 223; pass 223; fail 0; skipped 0
|
||||||
|
Measurement: tests 3; pass 3; fail 0
|
||||||
|
Server: per-file totals 184 pass; 0 fail
|
||||||
|
```
|
||||||
|
|
||||||
|
The seam output is saved as phase6-seams-2026-10-09.json; the historical Phase 4
|
||||||
|
result was restored. Offline API transport errors remain distinguished from
|
||||||
|
script failures. No playback or sw-update.js changes were added. bun.lock is
|
||||||
|
excluded from commits.
|
||||||
|
|
||||||
|
Post-crash cold smoke command:
|
||||||
|
|
||||||
|
```sh
|
||||||
|
node perf/baseline.mjs --runs 1 --browser webkit --profile all --scenario cold --assert-cold --out perf/results/phase6-cold-smoke-2026-10-09.json
|
||||||
|
```
|
||||||
|
|
||||||
|
LTE/lossy FCP was 1253/2881 ms, wire 983326/990152 bytes; both samples passed exactly one positive-byte app.js response. These single-run rechecks do not replace the durable five-run medians.
|
||||||
@@ -8,12 +8,12 @@ const tagHash = bytes => createHash('sha256').update(bytes).digest('hex').slice(
|
|||||||
export const injectBuildTag = (source, tag) => source.replace(
|
export const injectBuildTag = (source, tag) => source.replace(
|
||||||
/typeof __BUILD_TAG__ !== 'undefined' \? __BUILD_TAG__ : '[^']*'/, JSON.stringify(tag));
|
/typeof __BUILD_TAG__ !== 'undefined' \? __BUILD_TAG__ : '[^']*'/, JSON.stringify(tag));
|
||||||
|
|
||||||
function embedAssets(source, files, groups, buildTag, hashing) {
|
function embedAssets(source, files, groups, buildTag, hashing, assetSync) {
|
||||||
const pageFiles = {};
|
const pageFiles = {};
|
||||||
for (const group of Object.values(groups)) for (const path of group.files) {
|
for (const group of Object.values(groups)) for (const path of group.files) {
|
||||||
if (path !== '/index.html' && path !== '/sw.js') pageFiles[path] = { ...files[path], h: hashing ? files[path].h : buildTag };
|
if (path !== '/index.html' && path !== '/sw.js') pageFiles[path] = { ...files[path], h: hashing ? files[path].h : buildTag };
|
||||||
}
|
}
|
||||||
const json = JSON.stringify({ buildTag, groups, files: pageFiles }).replace(/</g, '\\u003c');
|
const json = JSON.stringify({ buildTag, groups, files: pageFiles, appCache: assetSync }).replace(/</g, '\\u003c');
|
||||||
return source.replace(/(<script type="application\/json" id="ytp-assets">)[\s\S]*?(<\/script>)/, (_, open, close) => open + json + close);
|
return source.replace(/(<script type="application\/json" id="ytp-assets">)[\s\S]*?(<\/script>)/, (_, open, close) => open + json + close);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -70,16 +70,17 @@ export function createAssetManifest(publicDir = './public', { hashing = true, bu
|
|||||||
}
|
}
|
||||||
const files = {};
|
const files = {};
|
||||||
for (const url of [...bytes.keys()].sort()) files[url] = { h: assetHash(bytes.get(url)), s: bytes.get(url).length, g: membership.get(url) || 'core' };
|
for (const url of [...bytes.keys()].sort()) files[url] = { h: assetHash(bytes.get(url)), s: bytes.get(url).length, g: membership.get(url) || 'core' };
|
||||||
const source = bytes.get('/index.html')?.toString() ?? null;
|
const source = bytes.get('/index.html')?.toString()
|
||||||
|
.replaceAll('__APP_SCRIPT_HASH__', createHash('sha256').update(bytes.get('/app.js') || '').digest('base64')) ?? null;
|
||||||
const swSource = bytes.get('/sw.js')?.toString() ?? null;
|
const swSource = bytes.get('/sw.js')?.toString() ?? null;
|
||||||
// Derived build metadata cannot be an input to its own hash. Canonicalize
|
// Derived build metadata cannot be an input to its own hash. Canonicalize
|
||||||
// the index meta and SW injected tag, then publish hashes of the final bytes.
|
// the index meta and SW injected tag, then publish hashes of the final bytes.
|
||||||
// All original bytes (including index/SW source) remain inputs via source hashes.
|
// All original bytes (including index/SW source) remain inputs via source hashes.
|
||||||
let canonicalIndex = source === null ? null : stampIndex(embedAssets(source, files, groups, '__BUILD_TAG__', hashing), files, { hashing });
|
let canonicalIndex = source === null ? null : stampIndex(embedAssets(source, files, groups, '__BUILD_TAG__', hashing, assetSync), files, { hashing });
|
||||||
const canonical = { assetSync, files: { ...files }, groups, contracts };
|
const canonical = { assetSync, files: { ...files }, groups, contracts };
|
||||||
if (canonicalIndex !== null && hashing) canonical.files['/index.html'] = { ...files['/index.html'], h: assetHash(canonicalIndex), s: Buffer.byteLength(canonicalIndex), source: files['/index.html'].h };
|
if (canonicalIndex !== null && hashing) canonical.files['/index.html'] = { ...files['/index.html'], h: assetHash(canonicalIndex), s: Buffer.byteLength(canonicalIndex), source: files['/index.html'].h };
|
||||||
const buildTag = hashing ? tagHash(JSON.stringify(canonical)) : (override || legacy.digest('hex').slice(0, 12));
|
const buildTag = hashing ? tagHash(JSON.stringify(canonical)) : (override || legacy.digest('hex').slice(0, 12));
|
||||||
const index = source === null ? null : embedAssets(stampIndex(source, files, { hashing, buildTag }), files, groups, buildTag, hashing);
|
const index = source === null ? null : embedAssets(stampIndex(source, files, { hashing, buildTag }), files, groups, buildTag, hashing, assetSync);
|
||||||
const sw = swSource === null ? null : injectBuildTag(swSource, buildTag)
|
const sw = swSource === null ? null : injectBuildTag(swSource, buildTag)
|
||||||
.replace("typeof __ASSET_SYNC__ !== 'undefined' ? __ASSET_SYNC__ : true", JSON.stringify(assetSync))
|
.replace("typeof __ASSET_SYNC__ !== 'undefined' ? __ASSET_SYNC__ : true", JSON.stringify(assetSync))
|
||||||
.replace("importScripts('/asset-sync-core.js')", "importScripts('/asset-sync-core.js?v=" + (files['/asset-sync-core.js']?.h || '') + "')");
|
.replace("importScripts('/asset-sync-core.js')", "importScripts('/asset-sync-core.js?v=" + (files['/asset-sync-core.js']?.h || '') + "')");
|
||||||
|
|||||||
@@ -88,3 +88,15 @@ test('index embeds build-local group hashes without recursively embedding index/
|
|||||||
const legacy=JSON.parse(off.index.match(/id="ytp-assets">([^<]+)<\/script>/)[1]);
|
const legacy=JSON.parse(off.index.match(/id="ytp-assets">([^<]+)<\/script>/)[1]);
|
||||||
expect(legacy.files['/app.js'].h).toBe(off.manifest.buildTag);
|
expect(legacy.files['/app.js'].h).toBe(off.manifest.buildTag);
|
||||||
}));
|
}));
|
||||||
|
|
||||||
|
test('app bootstrap CSP hash follows exact app bytes and rollback keeps external execution',()=>fixture(dir=>{
|
||||||
|
writeFileSync(join(dir,'index.html'),'<meta http-equiv="Content-Security-Policy" content="script-src \'self\' \'sha256-__APP_SCRIPT_HASH__\'"><script type="application/json" id="ytp-assets">{}</script>');
|
||||||
|
const result=createAssetManifest(dir),digest=createHash('sha256').update('window.app=1').digest('base64');
|
||||||
|
expect(result.index).toContain("'sha256-"+digest+"'");
|
||||||
|
|
||||||
|
expect(JSON.parse(result.index.match(/id="ytp-assets">([^<]+)</)[1]).appCache).toBe(true);
|
||||||
|
expect(JSON.parse(createAssetManifest(dir,{assetSync:false}).index.match(/id="ytp-assets">([^<]+)</)[1]).appCache).toBe(false);
|
||||||
|
expect(JSON.parse(createAssetManifest(dir,{hashing:false}).index.match(/id="ytp-assets">([^<]+)</)[1]).appCache).toBe(false);
|
||||||
|
writeFileSync(join(dir,'app.js'),'window.app=2');
|
||||||
|
expect(createAssetManifest(dir).index).not.toContain(digest);
|
||||||
|
}));
|
||||||
|
|||||||
@@ -13,7 +13,7 @@ mkdirSync(publicDir);
|
|||||||
mkdirSync(dataDir);
|
mkdirSync(dataDir);
|
||||||
mkdirSync(join(publicDir, 'fonts'));
|
mkdirSync(join(publicDir, 'fonts'));
|
||||||
mkdirSync(join(publicDir, 'icons'));
|
mkdirSync(join(publicDir, 'icons'));
|
||||||
writeFileSync(join(publicDir, 'index.html'), '<!doctype html><script type="application/json" id="ytp-assets">{}</script><meta name="ytp-build" content="__BUILD_TAG__"><link rel="stylesheet" href="app.css"><script src="app.js"></script><script src="https://cdn.example/x.js"></script><link rel="stylesheet" href="/root.css">');
|
writeFileSync(join(publicDir, 'index.html'), '<!doctype html><script type="application/json" id="ytp-assets">{}</script><meta name="ytp-build" content="__BUILD_TAG__"><link rel="preload" href="app.css" as="style"><link rel="stylesheet" href="app.css"><script src="app.js"></script><script src="https://cdn.example/x.js"></script><link rel="stylesheet" href="/root.css">');
|
||||||
writeFileSync(join(publicDir, 'app.css'), 'body{}');
|
writeFileSync(join(publicDir, 'app.css'), 'body{}');
|
||||||
writeFileSync(join(publicDir, 'app.js'), 'window.app=true;');
|
writeFileSync(join(publicDir, 'app.js'), 'window.app=true;');
|
||||||
writeFileSync(join(publicDir, 'root.css'), 'body{color:red}');
|
writeFileSync(join(publicDir, 'root.css'), 'body{color:red}');
|
||||||
@@ -99,7 +99,8 @@ describe('static delivery characterization', () => {
|
|||||||
const html = await index.text();
|
const html = await index.text();
|
||||||
const { buildTag, files } = await (await fetch(`${server.base}/api/manifest`)).json();
|
const { buildTag, files } = await (await fetch(`${server.base}/api/manifest`)).json();
|
||||||
expect(html).toContain(`<meta name="ytp-build" content="${buildTag}">`);
|
expect(html).toContain(`<meta name="ytp-build" content="${buildTag}">`);
|
||||||
expect(html).toContain(`href="app.css?v=${files['/app.css'].h}"`);
|
expect(html).toContain(`<link rel="preload" href="app.css?v=${files['/app.css'].h}" as="style">`);
|
||||||
|
expect(html.match(new RegExp('app\\.css\\?v='+files['/app.css'].h,'g'))).toHaveLength(2);
|
||||||
expect(html).toContain(`src="app.js?v=${files['/app.js'].h}"`);
|
expect(html).toContain(`src="app.js?v=${files['/app.js'].h}"`);
|
||||||
expect(html).toContain('https://cdn.example/x.js');
|
expect(html).toContain('https://cdn.example/x.js');
|
||||||
expect(html).toContain(`href="/root.css?v=${files['/root.css'].h}"`);
|
expect(html).toContain(`href="/root.css?v=${files['/root.css'].h}"`);
|
||||||
|
|||||||
Reference in New Issue
Block a user