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',()=>{
|
||||
const html=readFileSync(join(__dirname,'index.html'),'utf8');
|
||||
for(const file of ['shell-core.js','views-core.js','section-rail.js']) {
|
||||
assert.ok(html.indexOf('src="'+file+'"')<html.indexOf('src="app.js"'));
|
||||
const vm=require('node:vm');const context=vm.createContext({window:{},document:{}});
|
||||
assert.ok(html.indexOf('src="'+file+'"')<=html.indexOf('src="section-rail.js"'));
|
||||
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.
|
||||
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));
|
||||
if (!group || !previous.groups[group]?.files.includes(path) ||
|
||||
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]);
|
||||
}
|
||||
|
||||
|
||||
@@ -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(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 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'};
|
||||
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();
|
||||
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);
|
||||
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: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');
|
||||
@@ -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;
|
||||
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": {
|
||||
"core": {
|
||||
"contract": 1,
|
||||
"contract": 2,
|
||||
"eager": true,
|
||||
"files": [
|
||||
"/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 });
|
||||
},
|
||||
};
|
||||
// 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.navigator.serviceWorker?.addEventListener('message',event=>{
|
||||
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 { readFileSync } = require('node:fs');
|
||||
const vm = require('node:vm');
|
||||
function fixture(current, held = []) {
|
||||
const inserted = [], listeners = {}, writes = [];
|
||||
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'} } };
|
||||
function fixture(current, held = [], appCache = false, userAgent = 'AppleWebKit/605.1 Safari/605.1', controlled = false) {
|
||||
const inserted = [], listeners = {}, writes = [], fetched = [];
|
||||
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 };
|
||||
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 }) };
|
||||
root.window=root; root.globalThis=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', () => {
|
||||
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']);
|
||||
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 };
|
||||
})();
|
||||
|
||||
/* 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 described = new Set();
|
||||
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');
|
||||
for (const asset of group.files) {
|
||||
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 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)) {
|
||||
// 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];
|
||||
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));
|
||||
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) {
|
||||
const previousKey=AssetSyncCore.fallback(state.current,state.previous,path);
|
||||
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}); }
|
||||
}
|
||||
|
||||
@@ -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.
|
||||
|
||||
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 { assertChangedAssets, assertWarmAssets, waitForIdleAssets } from './measurement.mjs';
|
||||
import { assertColdAssets, assertChangedAssets, assertWarmAssets, waitForIdleAssets } from './measurement.mjs';
|
||||
import { createHash } from 'node:crypto';
|
||||
import { spawn, spawnSync, execFileSync } from 'node:child_process';
|
||||
import { existsSync, mkdirSync, rmSync, cpSync, appendFileSync, statSync, writeFileSync, readFileSync, symlinkSync, unlinkSync } from 'node:fs';
|
||||
@@ -62,6 +62,8 @@ function parseArgs() {
|
||||
out: null,
|
||||
compare: null,
|
||||
frontendCommit: null,
|
||||
sourceRoot: null,
|
||||
assertCold: false,
|
||||
};
|
||||
|
||||
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 === '--scenario' && i + 1 < args.length) options.scenario = 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 === '--compare' && i + 1 < args.length) options.compare = args[++i];
|
||||
}
|
||||
@@ -151,7 +155,8 @@ async function seedDatabase(dbPath, mediaFiles) {
|
||||
* Bun Server Manager
|
||||
*/
|
||||
class ServerManager {
|
||||
constructor(scratchDir, publicDir) {
|
||||
constructor(scratchDir, publicDir, sourceRoot = REPO_ROOT) {
|
||||
this.sourceRoot = sourceRoot;
|
||||
this.scratchDir = scratchDir;
|
||||
this.publicDir = publicDir;
|
||||
this.dataDir = path.join(scratchDir, 'data');
|
||||
@@ -184,7 +189,7 @@ class ServerManager {
|
||||
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], {
|
||||
cwd: srvCwd,
|
||||
env,
|
||||
@@ -372,8 +377,8 @@ async function runColdScenario({ browserType, server, proxy, profile }) {
|
||||
const fcp = paint.find((p) => p.name === 'first-contentful-paint');
|
||||
|
||||
return {
|
||||
initialJsCss: performance.getEntriesByType('resource').filter(entry=>/\.(?:js|css)(?:[?#]|$)/.test(entry.name)&&entry.startTime <= nav.domContentLoadedEventEnd&&['script','link','css'].includes(entry.initiatorType)).reduce((sum,entry)=>sum+entry.encodedBodySize,0),
|
||||
initialAssetRequests: performance.getEntriesByType('resource').filter(entry=>/\.(?:js|css)(?:[?#]|$)/.test(entry.name)&&entry.startTime <= nav.domContentLoadedEventEnd&&['script','link','css'].includes(entry.initiatorType)).map(entry=>({url:entry.name,bytes:entry.encodedBodySize})),
|
||||
initialJsCss: performance.getEntriesByType('resource').filter(entry=>/\.(?:js|css)(?:[?#]|$)/.test(entry.name)&&entry.startTime <= nav.domContentLoadedEventEnd&&(['script','link','css'].includes(entry.initiatorType)||(entry.initiatorType==='fetch'&&new URL(entry.name).pathname==='/app.js'))).reduce((sum,entry)=>sum+entry.encodedBodySize,0),
|
||||
initialAssetRequests: performance.getEntriesByType('resource').filter(entry=>/\.(?:js|css)(?:[?#]|$)/.test(entry.name)&&entry.startTime <= nav.domContentLoadedEventEnd&&(['script','link','css'].includes(entry.initiatorType)||(entry.initiatorType==='fetch'&&new URL(entry.name).pathname==='/app.js'))).map(entry=>({url:entry.name,bytes:entry.encodedBodySize,start:entry.startTime,responseStart:entry.responseStart,end:entry.responseEnd,initiator:entry.initiatorType})),
|
||||
fcp: fcp ? Math.round(fcp.startTime) : null,
|
||||
lcp: window.__lcp ? Math.round(window.__lcp) : null,
|
||||
domContentLoaded: nav ? Math.round(nav.domContentLoadedEventEnd - nav.startTime) : null,
|
||||
@@ -481,8 +486,8 @@ async function runWarmScenario({ browserType, server, proxy, profile }) {
|
||||
const fcp = paint.find((p) => p.name === 'first-contentful-paint');
|
||||
|
||||
return {
|
||||
initialJsCss: performance.getEntriesByType('resource').filter(entry=>/\.(?:js|css)(?:[?#]|$)/.test(entry.name)&&entry.startTime <= nav.domContentLoadedEventEnd&&['script','link','css'].includes(entry.initiatorType)).reduce((sum,entry)=>sum+entry.encodedBodySize,0),
|
||||
initialAssetRequests: performance.getEntriesByType('resource').filter(entry=>/\.(?:js|css)(?:[?#]|$)/.test(entry.name)&&entry.startTime <= nav.domContentLoadedEventEnd&&['script','link','css'].includes(entry.initiatorType)).map(entry=>({url:entry.name,bytes:entry.encodedBodySize})),
|
||||
initialJsCss: performance.getEntriesByType('resource').filter(entry=>/\.(?:js|css)(?:[?#]|$)/.test(entry.name)&&entry.startTime <= nav.domContentLoadedEventEnd&&(['script','link','css'].includes(entry.initiatorType)||(entry.initiatorType==='fetch'&&new URL(entry.name).pathname==='/app.js'))).reduce((sum,entry)=>sum+entry.encodedBodySize,0),
|
||||
initialAssetRequests: performance.getEntriesByType('resource').filter(entry=>/\.(?:js|css)(?:[?#]|$)/.test(entry.name)&&entry.startTime <= nav.domContentLoadedEventEnd&&(['script','link','css'].includes(entry.initiatorType)||(entry.initiatorType==='fetch'&&new URL(entry.name).pathname==='/app.js'))).map(entry=>({url:entry.name,bytes:entry.encodedBodySize,start:entry.startTime,responseStart:entry.responseStart,end:entry.responseEnd,initiator:entry.initiatorType})),
|
||||
fcp: fcp ? Math.round(fcp.startTime) : null,
|
||||
lcp: window.__lcp ? Math.round(window.__lcp) : null,
|
||||
domContentLoaded: nav ? Math.round(nav.domContentLoadedEventEnd - nav.startTime) : null,
|
||||
@@ -562,8 +567,8 @@ async function runOfflineScenario({ browserType, server, proxy, profile }) {
|
||||
const fcp = paint.find((p) => p.name === 'first-contentful-paint');
|
||||
|
||||
return {
|
||||
initialJsCss: performance.getEntriesByType('resource').filter(entry=>/\.(?:js|css)(?:[?#]|$)/.test(entry.name)&&entry.startTime <= nav.domContentLoadedEventEnd&&['script','link','css'].includes(entry.initiatorType)).reduce((sum,entry)=>sum+entry.encodedBodySize,0),
|
||||
initialAssetRequests: performance.getEntriesByType('resource').filter(entry=>/\.(?:js|css)(?:[?#]|$)/.test(entry.name)&&entry.startTime <= nav.domContentLoadedEventEnd&&['script','link','css'].includes(entry.initiatorType)).map(entry=>({url:entry.name,bytes:entry.encodedBodySize})),
|
||||
initialJsCss: performance.getEntriesByType('resource').filter(entry=>/\.(?:js|css)(?:[?#]|$)/.test(entry.name)&&entry.startTime <= nav.domContentLoadedEventEnd&&(['script','link','css'].includes(entry.initiatorType)||(entry.initiatorType==='fetch'&&new URL(entry.name).pathname==='/app.js'))).reduce((sum,entry)=>sum+entry.encodedBodySize,0),
|
||||
initialAssetRequests: performance.getEntriesByType('resource').filter(entry=>/\.(?:js|css)(?:[?#]|$)/.test(entry.name)&&entry.startTime <= nav.domContentLoadedEventEnd&&(['script','link','css'].includes(entry.initiatorType)||(entry.initiatorType==='fetch'&&new URL(entry.name).pathname==='/app.js'))).map(entry=>({url:entry.name,bytes:entry.encodedBodySize,start:entry.startTime,responseStart:entry.responseStart,end:entry.responseEnd,initiator:entry.initiatorType})),
|
||||
fcp: fcp ? Math.round(fcp.startTime) : null,
|
||||
lcp: window.__lcp ? Math.round(window.__lcp) : null,
|
||||
domContentLoaded: nav ? Math.round(nav.domContentLoadedEventEnd - nav.startTime) : null,
|
||||
@@ -704,6 +709,8 @@ async function main() {
|
||||
if (options.browser === 'all' || options.browser === 'chromium') browsers.push(pw.chromium);
|
||||
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 = [];
|
||||
if (options.profile === 'all' || options.profile === 'lte') profiles.push('lte');
|
||||
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 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) {
|
||||
const archive=path.join(tmpRoot,'frontend.tar');
|
||||
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 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();
|
||||
console.log(`Bun server running on ephemeral port ${server.port}`);
|
||||
|
||||
const results = {
|
||||
date: new Date().toISOString(),
|
||||
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,
|
||||
commit: spawnSync('git', ['rev-parse', 'HEAD'], { cwd: REPO_ROOT }).stdout.toString().trim(),
|
||||
runsConfigured: options.runs,
|
||||
@@ -754,6 +763,9 @@ async function main() {
|
||||
results.browsers[bName] = {};
|
||||
|
||||
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}]`);
|
||||
results.browsers[bName][prof] = {};
|
||||
|
||||
@@ -763,14 +775,15 @@ async function main() {
|
||||
console.log(`Proxy listening on port ${proxy.port} (mode: ${proxyProfile})`);
|
||||
|
||||
// 1. Cold Scenario
|
||||
if (options.scenario === 'all' || options.scenario === 'cold') {
|
||||
if (wants('cold')) {
|
||||
console.log(` [Scenario: Cold Load] running ${options.runs} iterations...`);
|
||||
const coldRuns = [];
|
||||
for (let r = 0; r < options.runs; r++) {
|
||||
process.stdout.write(` run ${r + 1}/${options.runs}... `);
|
||||
const res = await runColdScenario({ browserType: bType, server, proxy, profile: prof });
|
||||
if(options.assertCold)assertColdAssets(res.requests);
|
||||
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 = {
|
||||
@@ -791,11 +804,12 @@ async function main() {
|
||||
requests: coldRuns[0]?.requests || [],
|
||||
initialJsCss: summarizeList(coldRuns.map(r=>r.initialJsCss)),
|
||||
initialAssetRequests: coldRuns[0]?.initialAssetRequests || [],
|
||||
waterfalls: coldRuns.map(r=>({fcp:r.fcp,requests:r.requests,page:r.initialAssetRequests})),
|
||||
};
|
||||
}
|
||||
|
||||
// 2. Warm Scenario
|
||||
if (options.scenario === 'all' || options.scenario === 'warm') {
|
||||
if (wants('warm')) {
|
||||
console.log(` [Scenario: Warm Load] running ${options.runs} iterations...`);
|
||||
const warmRuns = [];
|
||||
for (let r = 0; r < options.runs; r++) {
|
||||
@@ -817,7 +831,7 @@ async function main() {
|
||||
}
|
||||
|
||||
// 3. Offline Scenario
|
||||
if (options.scenario === 'all' || options.scenario === 'offline') {
|
||||
if (wants('offline')) {
|
||||
console.log(` [Scenario: Offline Reload] running ${options.runs} iterations...`);
|
||||
const offRuns = [];
|
||||
for (let r = 0; r < options.runs; r++) {
|
||||
@@ -839,7 +853,7 @@ async function main() {
|
||||
}
|
||||
|
||||
// 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...`);
|
||||
const updJsRuns = [];
|
||||
for (let r = 0; r < options.runs; r++) {
|
||||
@@ -866,7 +880,7 @@ async function main() {
|
||||
}
|
||||
|
||||
// 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...`);
|
||||
const updCssRuns = [];
|
||||
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...`);
|
||||
const runs=[];
|
||||
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');});
|
||||
assert.equal(proxy.getStats().requestCount,before,'all warmed extension assets stay offline');
|
||||
await start();disconnected=false;await proxy.setOffline(false);await page.evaluate(()=>localStorage.removeItem('perf-offline'));
|
||||
// Change only app bytes, then prove loss of its current cache entry can boot
|
||||
// the compatible N-1 core offline without weakening the inline CSP policy.
|
||||
await stop();writeFileSync(join(cwd,'public/app.js'),readFileSync(join(cwd,'public/app.js'),'utf8')+'\n// phase6 app eviction probe\n');await start();
|
||||
const appTarget=await(await fetch(origin+'/api/manifest')).json();
|
||||
await page.evaluate(async()=>{const r=await navigator.serviceWorker.getRegistration();await r.update();});
|
||||
await wait(page,async tag=>{const r=await navigator.serviceWorker.getRegistration();return r?.waiting&&(await SwUpdate.askCacheStatus(r.waiting))?.version===tag;},appTarget.buildTag);
|
||||
await page.evaluate(async()=>{const r=await navigator.serviceWorker.getRegistration();r.waiting.postMessage({type:'SKIP_WAITING'});});
|
||||
await wait(page,async tag=>{const c=await caches.open('ytplayer-assets');return(await(await c.match('/__ytp_asset_state')).json()).current.buildTag===tag;},appTarget.buildTag);
|
||||
const appKey='/app.js?v='+appTarget.files['/app.js'].h;
|
||||
await page.evaluate(async key=>{await(await caches.open('ytplayer-assets')).delete(key);},appKey);
|
||||
await stop();disconnected=true;await proxy.setOffline(true);
|
||||
const probe=await context.newPage();probe.on('pageerror',e=>errors.push({message:e.message,disconnected}));
|
||||
const offlineBefore=proxy.getStats().requestCount;await probe.goto(origin);await probe.waitForFunction(()=>typeof data!=='undefined'&&typeof Player!=='undefined'&&document.querySelector('#carModeBtn'));
|
||||
assert.equal(await probe.evaluate(async key=>!!await(await caches.open('ytplayer-assets')).match(key),appKey),false,'N-1 never poisons the current app key');
|
||||
assert.equal(proxy.getStats().requestCount,offlineBefore,'N-1 app boot is entirely offline');
|
||||
await probe.close();await start();disconnected=false;await proxy.setOffline(false);
|
||||
await page.evaluate(async key=>{const r=await fetch(key+'&__ytpfresh=1');if(r.headers.get('X-Asset-Hash')!==key.split('v=')[1])throw Error('Wrong repair body');await(await caches.open('ytplayer-assets')).put(key,r);},appKey);
|
||||
// Keep an old tab whose core and group contracts remain those of build N.
|
||||
const old=await context.newPage();old.on('pageerror',e=>errors.push({message:e.message,disconnected}));await old.goto(origin);await old.waitForSelector('#carModeBtn');
|
||||
await old.evaluate(async()=>{window.__saveData=true;await Lazy.load('feature:setlist-import');window.__pinned=window.SetlistImport;});
|
||||
@@ -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)));
|
||||
assert.deepEqual(errors.filter(error=>!offlineTransportErrors.includes(error)),[]);
|
||||
if(offlineTransportErrors.length)assert.ok(failedRequests.some(url=>/\/api\/(?:recommendations|version)/.test(url)));
|
||||
results.push({browser:name,sourceBuildTag:previous.buildTag,classicSharedDefaults:true,cssOrderPreserved:true,settingsSearch:true,carControls:4,offlineFeature:true,firstUseNetwork:0,layoutsOffline:['glass-stage','bento-hub','f7-swipe','classic'],pinned:true,compatibleUnexecutedNewURL:true,incompatibleN1ReloadRequired:true,stagedN1:true,playbackGuard:true,prePaintLayout:true,offlineSettingsPanels:true,scriptErrors:[],offlineTransportErrors});
|
||||
results.push({browser:name,sourceBuildTag:previous.buildTag,classicSharedDefaults:true,cssOrderPreserved:true,settingsSearch:true,carControls:4,offlineFeature:true,firstUseNetwork:0,layoutsOffline:['glass-stage','bento-hub','f7-swipe','classic'],pinned:true,compatibleUnexecutedNewURL:true,incompatibleN1ReloadRequired:true,stagedN1:true,appEvictionN1Offline:true,playbackGuard:true,prePaintLayout:true,offlineSettingsPanels:true,scriptErrors:[],offlineTransportErrors});
|
||||
console.log(name,'PASS');
|
||||
} 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'));
|
||||
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 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',()=>{
|
||||
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');
|
||||
@@ -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}]);
|
||||
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' }) {
|
||||
let activeTracking = false;
|
||||
let trackingStart = 0;
|
||||
let requestCount = 0;
|
||||
let requests = [];
|
||||
let totalWireBytes = 0;
|
||||
@@ -79,6 +80,7 @@ export function createThrottleProxy({ targetPort, profile = 'lte' }) {
|
||||
return;
|
||||
}
|
||||
|
||||
const requestStart = Date.now();
|
||||
reqIndex++;
|
||||
const currentReqIndex = reqIndex;
|
||||
|
||||
@@ -114,7 +116,8 @@ export function createThrottleProxy({ targetPort, profile = 'lte' }) {
|
||||
let recorded;
|
||||
if (activeTracking) {
|
||||
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);
|
||||
}
|
||||
|
||||
@@ -176,6 +179,7 @@ export function createThrottleProxy({ targetPort, profile = 'lte' }) {
|
||||
server.close(resolve);
|
||||
}),
|
||||
startTracking: () => {
|
||||
trackingStart = Date.now();
|
||||
activeTracking = true;
|
||||
},
|
||||
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(
|
||||
/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 = {};
|
||||
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 };
|
||||
}
|
||||
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);
|
||||
}
|
||||
|
||||
@@ -70,16 +70,17 @@ export function createAssetManifest(publicDir = './public', { hashing = true, bu
|
||||
}
|
||||
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' };
|
||||
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;
|
||||
// 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.
|
||||
// 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 };
|
||||
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 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)
|
||||
.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 || '') + "')");
|
||||
|
||||
@@ -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]);
|
||||
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(join(publicDir, 'fonts'));
|
||||
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.js'), 'window.app=true;');
|
||||
writeFileSync(join(publicDir, 'root.css'), 'body{color:red}');
|
||||
@@ -99,7 +99,8 @@ describe('static delivery characterization', () => {
|
||||
const html = await index.text();
|
||||
const { buildTag, files } = await (await fetch(`${server.base}/api/manifest`)).json();
|
||||
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('https://cdn.example/x.js');
|
||||
expect(html).toContain(`href="/root.css?v=${files['/root.css'].h}"`);
|
||||
|
||||
Reference in New Issue
Block a user