Measure complete CSS updates and fully cached warm reloads

This commit is contained in:
Jonathan Sykes
2026-10-08 05:12:32 +08:00
parent ce5a159d4a
commit a08d363f2d
5 changed files with 88 additions and 28 deletions

View File

@@ -26,6 +26,7 @@
*/
import assert from 'node:assert/strict';
import { 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';
@@ -461,6 +462,8 @@ async function runWarmScenario({ browserType, server, proxy, profile }) {
});
});
await waitForIdleAssets(page);
// Now measure warm re-navigation
proxy.resetStats();
proxy.startTracking();
@@ -470,6 +473,7 @@ async function runWarmScenario({ browserType, server, proxy, profile }) {
proxy.stopTracking();
const stats = proxy.getStats();
assertWarmAssets(stats.requests);
const pageMetrics = await page.evaluate(() => {
const nav = performance.getEntriesByType('navigation')[0];
@@ -622,24 +626,7 @@ async function runUpdateScenario({ browserType, server, proxy, profile, updateKi
// Phase 3 activation precedes idle feature caching. Measure updates from a
// genuinely warm build, rather than counting unfinished first-install bytes.
if (await page.evaluate(()=>!!window.Lazy && !!window.Lazy.warm)) {
await page.evaluate(()=>Lazy.warm());
const end=Date.now()+90000;
while (true) {
const ready=await page.evaluate(async()=>{
const c=await caches.open('ytplayer-assets'),r=await c.match('/__ytp_asset_state');
if(!r)return false;
const m=(await r.json()).current;
return (await Promise.all(Object.values(m.groups).flatMap(g=>g.files).map(async p=>{
const response=await c.match(p+'?v='+m.files[p].h);
return response?.headers.get('X-Asset-Hash')===m.files[p].h;
}))).every(Boolean);
});
if(ready)break;
if(Date.now()>end)throw Error('Idle warm did not complete before update benchmark');
await sleep(100);
}
}
await waitForIdleAssets(page);
// Switch server to Tree N+1 (app.js or theme-glass.css modification)
const targetTree = updateKind === 'feature' ? trees.treeUpdateFeature : updateKind === 'js' ? trees.treeUpdateJs : trees.treeUpdateCss;
@@ -662,20 +649,23 @@ async function runUpdateScenario({ browserType, server, proxy, profile, updateKi
await sleep(50);
}
if (updateKind === 'feature') {
const readyDuration = Date.now() - t0;
const readyWireBytes = proxy.getStats().totalWireBytes;
if (updateKind === 'feature' || updateKind === 'css') {
const changedPath=updateKind==='feature'?'/presenter.js':'/theme-glass.css';
// Readiness excludes background chunks. Include activation and the changed
// feature's actual idle transfer rather than reporting readiness as its cost.
const expected = createHash('sha256').update(readFileSync(path.join(targetTree,'presenter.js'))).digest('hex').slice(0,10);
const expected = createHash('sha256').update(readFileSync(path.join(targetTree,changedPath.slice(1)))).digest('hex').slice(0,10);
await page.evaluate(async()=>{const r=await navigator.serviceWorker.getRegistration();r.waiting.postMessage({type:'SKIP_WAITING'});});
const end=Date.now()+90000;
while(true) {
const complete=await page.evaluate(async h=>{
const complete=await page.evaluate(async ({h,p})=>{
const r=await navigator.serviceWorker.getRegistration();
if(r.waiting || r.installing)return false;
r.active?.postMessage({type:'WARM_ASSETS',saveData:false});
const c=await caches.open('ytplayer-assets'),entry=await c.match('/presenter.js?v='+h);
const c=await caches.open('ytplayer-assets'),entry=await c.match(p+'?v='+h);
return entry?.headers.get('X-Asset-Hash')===h;
},expected);
},{h:expected,p:changedPath});
if(complete)break;
if(Date.now()>end)throw Error('Changed feature was not cached after activation');
await sleep(100);
@@ -684,14 +674,13 @@ async function runUpdateScenario({ browserType, server, proxy, profile, updateKi
const duration = Date.now() - t0;
proxy.stopTracking();
const stats = proxy.getStats();
if(updateKind==='feature') {
const assetDownloads=stats.requests.filter(r=>r.bytes>0).map(r=>new URL(r.url,navUrl).pathname).filter(p=>/\.(?:js|css|woff2?|png|svg)$/.test(p)&&!['/sw.js','/asset-sync-core.js'].includes(p));
assert.deepEqual([...new Set(assetDownloads)],['/presenter.js'],'feature edit transfers only its chunk; no app/core/font/icon payload');
}
assertChangedAssets(stats.requests, updateKind==='feature'?'/presenter.js':updateKind==='css'?'/theme-glass.css':'/app.js');
await browser.close();
return {
updateKind,
readyDuration,
readyWireBytes,
requests: stats.requests,
duration,
requestCount: stats.requestCount,
@@ -895,6 +884,8 @@ async function main() {
}
results.browsers[bName][prof].updateCss = {
readyDuration: summarizeList(updCssRuns.map(r=>r.readyDuration)),
readyWireBytes: summarizeList(updCssRuns.map(r=>r.readyWireBytes)),
requestCount: summarizeList(updCssRuns.map((r) => r.requestCount)),
wireBytes: summarizeList(updCssRuns.map((r) => r.wireBytes)),
duration: summarizeList(updCssRuns.map((r) => r.duration)),