Keep native Chromium loading and prevent stale browser cache entries
This commit is contained in:
@@ -27,11 +27,12 @@
|
|||||||
return {bytes,digest};
|
return {bytes,digest};
|
||||||
}
|
}
|
||||||
async function start() {
|
async function start() {
|
||||||
if(!manifest.appCache || !root.crypto?.subtle)return external(key);
|
if(!manifest.appCache || !root.crypto?.subtle || (!root.Lazy.captureApp&&!root.navigator?.serviceWorker?.controller))return external(key);
|
||||||
const expected=manifest.files['/app.js'].h;
|
const expected=manifest.files['/app.js'].h;
|
||||||
let cache;
|
let cache;
|
||||||
try { cache=await root.caches?.open('ytplayer-assets'); } catch {}
|
try { cache=await root.caches?.open('ytplayer-assets'); } catch {}
|
||||||
let response=await cache?.match(key);
|
let response=await cache?.match(key);
|
||||||
|
if(!root.Lazy.captureApp && response?.ok && response.headers.get('X-Asset-Hash')===expected)return external(key);
|
||||||
if(!response || response.headers.get('X-Asset-Hash')!==expected) {
|
if(!response || response.headers.get('X-Asset-Hash')!==expected) {
|
||||||
try { response=await (root.Lazy.appResponse || root.fetch(key,{credentials:'same-origin'})); } catch {}
|
try { response=await (root.Lazy.appResponse || root.fetch(key,{credentials:'same-origin'})); } catch {}
|
||||||
}
|
}
|
||||||
@@ -56,6 +57,7 @@
|
|||||||
// CacheStorage may share a full quota with saved music. Never remove data
|
// 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.
|
// 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); }
|
try { await cache?.put(key,response); } catch(error) { root.console.warn('[app-cache]',error.message); }
|
||||||
|
if(!root.Lazy.captureApp)return external(key);
|
||||||
const node=doc.createElement('script');node.textContent=new root.TextDecoder().decode(bytes);
|
const node=doc.createElement('script');node.textContent=new root.TextDecoder().decode(bytes);
|
||||||
execute(node);
|
execute(node);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -3,14 +3,14 @@ const assert=require('node:assert/strict');
|
|||||||
const {webcrypto,createHash}=require('node:crypto');
|
const {webcrypto,createHash}=require('node:crypto');
|
||||||
const vm=require('node:vm');
|
const vm=require('node:vm');
|
||||||
const {readFileSync}=require('node:fs');
|
const {readFileSync}=require('node:fs');
|
||||||
function fixture({cached=false,tampered=false,enabled=true,loading=false,quota=false,insecure=false,previous=false,contract=1}={}){
|
function fixture({cached=false,tampered=false,enabled=true,loading=false,quota=false,insecure=false,previous=false,contract=1,capture=true,controller=true}={}){
|
||||||
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 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 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=[],listeners={};let calls=0;
|
const held=new Map(), appended=[],listeners={};let calls=0;
|
||||||
const response=()=>new Response(tampered?'bad':source,{headers:{'X-Asset-Hash':hash.slice(0,10)}});
|
const response=()=>new Response(tampered?'bad':source,{headers:{'X-Asset-Hash':hash.slice(0,10)}});
|
||||||
if(cached)held.set(key,response());
|
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']}}}}));}
|
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:{}}},Lazy:{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 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',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 {vm.runInContext(node.src===oldKey?oldSource:source,context);queueMicrotask(()=>node.onload());}}}};
|
const doc=root.document={readyState:loading?'loading':'complete',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 {vm.runInContext(node.src===oldKey?oldSource:source,context);queueMicrotask(()=>node.onload());}}}};
|
||||||
root.window=root;const context=vm.createContext(root);
|
root.window=root;const context=vm.createContext(root);
|
||||||
vm.runInContext(readFileSync(require.resolve('./app-bootstrap.js'),'utf8'),context);
|
vm.runInContext(readFileSync(require.resolve('./app-bootstrap.js'),'utf8'),context);
|
||||||
@@ -61,3 +61,16 @@ test('a feature contract change also prevents stale core execution',async()=>{
|
|||||||
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));
|
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);
|
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,/hash/);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);
|
||||||
|
});
|
||||||
|
|||||||
@@ -15,10 +15,10 @@ test('staged same-contract responses use verified N-1 without poisoning the new
|
|||||||
const e=environment(); const cache=await e.storage.open(core.CACHE);
|
const e=environment(); const cache=await e.storage.open(core.CACHE);
|
||||||
const old={buildTag:'old',files:{'/extra.js':{h:'old'}},groups:{core:{files:[],contract:1},'feature:extra':{files:['/extra.js'],contract:1,background:true}}};
|
const old={buildTag:'old',files:{'/extra.js':{h:'old'}},groups:{core:{files:[],contract:1},'feature:extra':{files:['/extra.js'],contract:1,background:true}}};
|
||||||
e.m.groups.core.contract=1; e.m.groups['feature:extra']={files:['/extra.js'],contract:1,background:true}; e.m.files['/extra.js']={h:'new'};
|
e.m.groups.core.contract=1; e.m.groups['feature:extra']={files:['/extra.js'],contract:1,background:true}; e.m.files['/extra.js']={h:'new'};
|
||||||
await cache.put(core.STATE,Response.json({current:old})); await cache.put('/extra.js?v=old',new Response('old body',{headers:{'X-Asset-Hash':'old'}}));
|
await cache.put(core.STATE,Response.json({current:old})); await cache.put('/extra.js?v=old',new Response('old body',{headers:{'X-Asset-Hash':'old','Cache-Control':'public, max-age=31536000, immutable','Content-Encoding':'gzip','Content-Length':'9'}}));
|
||||||
await e.dispatch('install'); await e.dispatch('activate');
|
await e.dispatch('install'); await e.dispatch('activate');
|
||||||
assert.equal(await cache.match('/extra.js?v=new'),undefined);
|
assert.equal(await cache.match('/extra.js?v=new'),undefined);
|
||||||
const stale=await e.request('/extra.js?v=new'); assert.equal(await stale.text(),'old body');assert.equal(stale.headers.get('X-Asset-Hash'),'old');assert.equal(await cache.match('/extra.js?v=new'),undefined);
|
const stale=await e.request('/extra.js?v=new'); assert.equal(await stale.text(),'old body');assert.equal(stale.headers.get('X-Asset-Hash'),'old');assert.equal(stale.headers.get('Cache-Control'),'no-store');assert.equal(stale.headers.get('Content-Encoding'),null);assert.equal(stale.headers.get('Content-Length'),null);assert.equal(await cache.match('/extra.js?v=new'),undefined);
|
||||||
await e.dispatch('message',{data:{type:'WARM_ASSETS',saveData:true}}); assert.equal(await cache.match('/extra.js?v=new'),undefined);
|
await e.dispatch('message',{data:{type:'WARM_ASSETS',saveData:true}}); assert.equal(await cache.match('/extra.js?v=new'),undefined);
|
||||||
await e.dispatch('message',{data:{type:'WARM_ASSETS',saveData:false}}); assert.equal((await cache.match('/extra.js?v=new')).headers.get('X-Asset-Hash'),'new');
|
await e.dispatch('message',{data:{type:'WARM_ASSETS',saveData:false}}); assert.equal((await cache.match('/extra.js?v=new')).headers.get('X-Asset-Hash'),'new');
|
||||||
assert.equal((await e.request('/extra.js?v=new')).headers.get('X-Asset-Hash'),'new');
|
assert.equal((await e.request('/extra.js?v=new')).headers.get('X-Asset-Hash'),'new');
|
||||||
|
|||||||
@@ -124,7 +124,9 @@
|
|||||||
};
|
};
|
||||||
// Own this response once. Classic-script and fetch preloads do not share
|
// Own this response once. Classic-script and fetch preloads do not share
|
||||||
// their cached bodies in WebKit; the later bootstrap consumes this promise.
|
// their cached bodies in WebKit; the later bootstrap consumes this promise.
|
||||||
if(manifest.appCache && root.crypto?.subtle) {
|
const agent=root.navigator.userAgent || '';
|
||||||
|
api.captureApp=!!(manifest.appCache && root.crypto?.subtle && /AppleWebKit\//.test(agent) && !/(?:Chrome|Chromium|Edg|OPR)\//.test(agent));
|
||||||
|
if(api.captureApp) {
|
||||||
api.appResponse=(async()=>{
|
api.appResponse=(async()=>{
|
||||||
const key=url('/app.js');
|
const key=url('/app.js');
|
||||||
try {
|
try {
|
||||||
@@ -134,6 +136,9 @@
|
|||||||
return root.fetch(key,{credentials:'same-origin'});
|
return root.fetch(key,{credentials:'same-origin'});
|
||||||
})();
|
})();
|
||||||
api.appResponse.catch(()=>{});
|
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.href=url('/app.js');doc.head.append(preload);
|
||||||
}
|
}
|
||||||
root.Lazy = api;
|
root.Lazy = api;
|
||||||
root.navigator.serviceWorker?.addEventListener('message',event=>{
|
root.navigator.serviceWorker?.addEventListener('message',event=>{
|
||||||
|
|||||||
@@ -2,12 +2,12 @@ const { test } = require('node:test');
|
|||||||
const assert = require('node:assert/strict');
|
const assert = require('node:assert/strict');
|
||||||
const { readFileSync } = require('node:fs');
|
const { readFileSync } = require('node:fs');
|
||||||
const vm = require('node:vm');
|
const vm = require('node:vm');
|
||||||
function fixture(current, held = [], appCache = false) {
|
function fixture(current, held = [], appCache = false, userAgent = 'AppleWebKit/605.1 Safari/605.1') {
|
||||||
const inserted = [], listeners = {}, writes = [], fetched = [];
|
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 manifest = { buildTag: 'own', appCache, groups: { core: { files: [] }, 'layout:classic': { files: ['/classic.css'] }, 'layout:glass-stage': { files: ['/glass.css', '/one.js', '/two.js'] }, 'feature:test': { contract:1, files: ['/one.js', '/two.js'] } }, files: { '/app.js':{h:'a'}, '/classic.css': {h:'c'}, '/glass.css':{h:'g'}, '/one.js':{h:'1'}, '/two.js':{h:'2'} } };
|
||||||
const doc = { readyState:'loading', documentElement: { dataset:{} }, getElementById: () => ({ textContent:JSON.stringify(manifest) }), querySelectorAll: () => [], createElement: tag => ({ tagName:tag.toUpperCase() }), write: value => writes.push(value), addEventListener: (t,f) => listeners[t]=f };
|
const doc = { readyState:'loading', documentElement: { dataset:{} }, getElementById: () => ({ textContent:JSON.stringify(manifest) }), querySelectorAll: () => [], createElement: tag => ({ tagName:tag.toUpperCase() }), write: value => writes.push(value), addEventListener: (t,f) => listeners[t]=f };
|
||||||
doc.head = { append: node => { inserted.push(node); queueMicrotask(() => node.onload?.()); } };
|
doc.head = { append: node => { inserted.push(node); queueMicrotask(() => node.onload?.()); } };
|
||||||
const root = { document:doc, localStorage:{ getItem:() => '{"settings":{"layout":"glass-stage"}}' }, console, Promise, URL, crypto:{subtle:{}}, fetch:async key=>{fetched.push(key);return new Response('app');}, 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}, addEventListener(){} };
|
||||||
if (current) root.caches = { open: async () => ({ match: async key => key === '/__ytp_asset_state' ? new Response(JSON.stringify({current})) : held.includes(key) ? new Response('', {headers:{'X-Asset-Hash':key.split('=')[1]}}) : undefined }) };
|
if (current) root.caches = { open: async () => ({ match: async key => key === '/__ytp_asset_state' ? new Response(JSON.stringify({current})) : held.includes(key) ? new Response('', {headers:{'X-Asset-Hash':key.split('=')[1]}}) : undefined }) };
|
||||||
root.window=root; root.globalThis=root;
|
root.window=root; root.globalThis=root;
|
||||||
vm.runInNewContext(readFileSync(require.resolve('./lazy.js'),'utf8'), root);
|
vm.runInNewContext(readFileSync(require.resolve('./lazy.js'),'utf8'), root);
|
||||||
@@ -97,3 +97,14 @@ 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;
|
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,[]);
|
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');
|
||||||
|
});
|
||||||
|
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']);
|
||||||
|
});
|
||||||
|
|||||||
@@ -576,7 +576,13 @@ async function assetFetch(request, clientId) {
|
|||||||
if(state.current.files[path]?.h === h) {
|
if(state.current.files[path]?.h === h) {
|
||||||
const previousKey=AssetSyncCore.fallback(state.current,state.previous,path);
|
const previousKey=AssetSyncCore.fallback(state.current,state.previous,path);
|
||||||
const previousResponse=previousKey && await cache.match(previousKey);
|
const previousResponse=previousKey && await cache.match(previousKey);
|
||||||
if(previousResponse && previousResponse.headers.get('X-Asset-Hash')===state.previous.files[path].h) return previousResponse;
|
if(previousResponse && previousResponse.headers.get('X-Asset-Hash')===state.previous.files[path].h) {
|
||||||
|
// A stale body returned under a new URL must not poison the browser's
|
||||||
|
// resource cache either. Its decoded stream has no transport encoding.
|
||||||
|
const headers=new Headers(previousResponse.headers);headers.set('Cache-Control','no-store');
|
||||||
|
for(const name of ['Content-Encoding','Content-Length','Transfer-Encoding'])headers.delete(name);
|
||||||
|
return new Response(previousResponse.body,{status:previousResponse.status,headers});
|
||||||
|
}
|
||||||
}
|
}
|
||||||
try { const r = await fetch(key); if(r.ok && r.headers.get('X-Asset-Hash') !== h) return new Response('Asset version unavailable',{status:409}); if(r.ok && request.method !== 'HEAD') await cache.put(key,r.clone()); return r; } catch { return new Response('Offline',{status:503}); }
|
try { const r = await fetch(key); if(r.ok && r.headers.get('X-Asset-Hash') !== h) return new Response('Asset version unavailable',{status:409}); if(r.ok && request.method !== 'HEAD') await cache.put(key,r.clone()); return r; } catch { return new Response('Offline',{status:503}); }
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -354,3 +354,6 @@ when diagnosing historical regressions.
|
|||||||
Phase 6 counts its fetch-owned app.js response in initial JS/CSS bytes. Since the
|
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
|
bootstrap consumes that response asynchronously, DOMContentLoaded no longer means
|
||||||
app execution is complete; compare the existing actual boot-done landmark.
|
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.
|
||||||
|
|||||||
@@ -709,6 +709,8 @@ async function main() {
|
|||||||
if (options.browser === 'all' || options.browser === 'chromium') browsers.push(pw.chromium);
|
if (options.browser === 'all' || options.browser === 'chromium') browsers.push(pw.chromium);
|
||||||
if (options.browser === 'all' || options.browser === 'webkit') browsers.push(pw.webkit);
|
if (options.browser === 'all' || options.browser === 'webkit') browsers.push(pw.webkit);
|
||||||
|
|
||||||
|
const requestedScenarios=new Set(options.scenario.split(','));
|
||||||
|
const wants=name=>requestedScenarios.has('all')||requestedScenarios.has(name);
|
||||||
const profiles = [];
|
const profiles = [];
|
||||||
if (options.profile === 'all' || options.profile === 'lte') profiles.push('lte');
|
if (options.profile === 'all' || options.profile === 'lte') profiles.push('lte');
|
||||||
if (options.profile === 'all' || options.profile === 'lossy') profiles.push('lossy');
|
if (options.profile === 'all' || options.profile === 'lossy') profiles.push('lossy');
|
||||||
@@ -773,7 +775,7 @@ async function main() {
|
|||||||
console.log(`Proxy listening on port ${proxy.port} (mode: ${proxyProfile})`);
|
console.log(`Proxy listening on port ${proxy.port} (mode: ${proxyProfile})`);
|
||||||
|
|
||||||
// 1. Cold Scenario
|
// 1. Cold Scenario
|
||||||
if (options.scenario === 'all' || options.scenario === 'cold') {
|
if (wants('cold')) {
|
||||||
console.log(` [Scenario: Cold Load] running ${options.runs} iterations...`);
|
console.log(` [Scenario: Cold Load] running ${options.runs} iterations...`);
|
||||||
const coldRuns = [];
|
const coldRuns = [];
|
||||||
for (let r = 0; r < options.runs; r++) {
|
for (let r = 0; r < options.runs; r++) {
|
||||||
@@ -781,7 +783,7 @@ async function main() {
|
|||||||
const res = await runColdScenario({ browserType: bType, server, proxy, profile: prof });
|
const res = await runColdScenario({ browserType: bType, server, proxy, profile: prof });
|
||||||
if(options.assertCold)assertColdAssets(res.requests);
|
if(options.assertCold)assertColdAssets(res.requests);
|
||||||
coldRuns.push(res);
|
coldRuns.push(res);
|
||||||
console.log(`done (${res.wireBytes} bytes, ${res.requestCount} reqs, boot=${res.bootDone}ms)`);
|
console.log(`done (${res.wireBytes} bytes, ${res.requestCount} reqs, fcp=${res.fcp}ms, boot=${res.bootDone}ms)`);
|
||||||
}
|
}
|
||||||
|
|
||||||
results.browsers[bName][prof].cold = {
|
results.browsers[bName][prof].cold = {
|
||||||
@@ -807,7 +809,7 @@ async function main() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// 2. Warm Scenario
|
// 2. Warm Scenario
|
||||||
if (options.scenario === 'all' || options.scenario === 'warm') {
|
if (wants('warm')) {
|
||||||
console.log(` [Scenario: Warm Load] running ${options.runs} iterations...`);
|
console.log(` [Scenario: Warm Load] running ${options.runs} iterations...`);
|
||||||
const warmRuns = [];
|
const warmRuns = [];
|
||||||
for (let r = 0; r < options.runs; r++) {
|
for (let r = 0; r < options.runs; r++) {
|
||||||
@@ -829,7 +831,7 @@ async function main() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// 3. Offline Scenario
|
// 3. Offline Scenario
|
||||||
if (options.scenario === 'all' || options.scenario === 'offline') {
|
if (wants('offline')) {
|
||||||
console.log(` [Scenario: Offline Reload] running ${options.runs} iterations...`);
|
console.log(` [Scenario: Offline Reload] running ${options.runs} iterations...`);
|
||||||
const offRuns = [];
|
const offRuns = [];
|
||||||
for (let r = 0; r < options.runs; r++) {
|
for (let r = 0; r < options.runs; r++) {
|
||||||
@@ -851,7 +853,7 @@ async function main() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// 4. Update N -> N+1 (JS)
|
// 4. Update N -> N+1 (JS)
|
||||||
if (options.scenario === 'all' || options.scenario === 'update-js') {
|
if (wants('update-js')) {
|
||||||
console.log(` [Scenario: Update N->N+1 (1-line app.js)] running ${options.runs} iterations...`);
|
console.log(` [Scenario: Update N->N+1 (1-line app.js)] running ${options.runs} iterations...`);
|
||||||
const updJsRuns = [];
|
const updJsRuns = [];
|
||||||
for (let r = 0; r < options.runs; r++) {
|
for (let r = 0; r < options.runs; r++) {
|
||||||
@@ -878,7 +880,7 @@ async function main() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// 5. Update N -> N+1 (CSS)
|
// 5. Update N -> N+1 (CSS)
|
||||||
if (options.scenario === 'all' || options.scenario === 'update-css') {
|
if (wants('update-css')) {
|
||||||
console.log(` [Scenario: Update N->N+1 (1-line theme-glass.css)] running ${options.runs} iterations...`);
|
console.log(` [Scenario: Update N->N+1 (1-line theme-glass.css)] running ${options.runs} iterations...`);
|
||||||
const updCssRuns = [];
|
const updCssRuns = [];
|
||||||
for (let r = 0; r < options.runs; r++) {
|
for (let r = 0; r < options.runs; r++) {
|
||||||
@@ -906,7 +908,7 @@ async function main() {
|
|||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
if ((options.scenario === 'all' || options.scenario === 'update-feature') && existsSync(path.join(treeBase,'presenter.js'))) {
|
if ((wants('update-feature')) && existsSync(path.join(treeBase,'presenter.js'))) {
|
||||||
console.log(` [Scenario: Update split-out presenter, including idle cache fill] running ${options.runs} iterations...`);
|
console.log(` [Scenario: Update split-out presenter, including idle cache fill] running ${options.runs} iterations...`);
|
||||||
const runs=[];
|
const runs=[];
|
||||||
for(let r=0;r<options.runs;r++) {
|
for(let r=0;r<options.runs;r++) {
|
||||||
|
|||||||
@@ -2,7 +2,7 @@
|
|||||||
"results": [
|
"results": [
|
||||||
{
|
{
|
||||||
"browser": "chromium",
|
"browser": "chromium",
|
||||||
"sourceBuildTag": "41a7ba2c29a3",
|
"sourceBuildTag": "562f4031bcb7",
|
||||||
"classicSharedDefaults": true,
|
"classicSharedDefaults": true,
|
||||||
"cssOrderPreserved": true,
|
"cssOrderPreserved": true,
|
||||||
"settingsSearch": true,
|
"settingsSearch": true,
|
||||||
@@ -28,7 +28,7 @@
|
|||||||
},
|
},
|
||||||
{
|
{
|
||||||
"browser": "webkit",
|
"browser": "webkit",
|
||||||
"sourceBuildTag": "41a7ba2c29a3",
|
"sourceBuildTag": "562f4031bcb7",
|
||||||
"classicSharedDefaults": true,
|
"classicSharedDefaults": true,
|
||||||
"cssOrderPreserved": true,
|
"cssOrderPreserved": true,
|
||||||
"settingsSearch": true,
|
"settingsSearch": true,
|
||||||
@@ -56,7 +56,7 @@
|
|||||||
"disconnected": true
|
"disconnected": true
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"message": "/127.0.0.1:36117/api/version.",
|
"message": "/127.0.0.1:33967/api/version.",
|
||||||
"disconnected": true
|
"disconnected": true
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
@@ -64,7 +64,7 @@
|
|||||||
"disconnected": true
|
"disconnected": true
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"message": "/127.0.0.1:36117/api/recommendations?fp=163577c02a53bc40.",
|
"message": "/127.0.0.1:33967/api/recommendations?fp=163577c02a53bc40.",
|
||||||
"disconnected": true
|
"disconnected": true
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
@@ -72,7 +72,7 @@
|
|||||||
"disconnected": true
|
"disconnected": true
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"message": "/127.0.0.1:36117/api/version.",
|
"message": "/127.0.0.1:33967/api/version.",
|
||||||
"disconnected": true
|
"disconnected": true
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
@@ -80,7 +80,7 @@
|
|||||||
"disconnected": true
|
"disconnected": true
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"message": "/127.0.0.1:36117/api/recommendations?fp=163577c02a53bc40.",
|
"message": "/127.0.0.1:33967/api/recommendations?fp=163577c02a53bc40.",
|
||||||
"disconnected": true
|
"disconnected": true
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
@@ -88,7 +88,7 @@
|
|||||||
"disconnected": true
|
"disconnected": true
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"message": "/127.0.0.1:36117/api/version.",
|
"message": "/127.0.0.1:33967/api/version.",
|
||||||
"disconnected": true
|
"disconnected": true
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
|
|||||||
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
|
||||||
|
]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -85,3 +85,16 @@ 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
|
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
|
entry, boots N-1 offline, then repairs the current entry before the existing
|
||||||
feature pinning/contract/playback checks. No source in app.js changed.
|
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.
|
||||||
|
|||||||
Reference in New Issue
Block a user