Verify six app seams and record Phase 4 performance measurements

This commit is contained in:
Jonathan Sykes
2026-10-08 05:03:55 +08:00
parent 2c5003575c
commit a7473cbca2
15 changed files with 4263 additions and 12 deletions

View File

@@ -25,6 +25,8 @@
* --compare <json-path> Compare results against a previous baseline JSON
*/
import assert from 'node:assert/strict';
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';
import path from 'node:path';
@@ -244,6 +246,7 @@ async function setupPageMetrics(page) {
window.__fcp = null;
window.__lcp = null;
window.__longTasksTotal = 0;
window.__longTasks = [];
// Observe boot completion landmark (#cards view set by renderList())
const markBoot = () => {
@@ -263,6 +266,7 @@ async function setupPageMetrics(page) {
new PerformanceObserver((list) => {
for (const entry of list.getEntries()) {
window.__longTasksTotal += entry.duration;
window.__longTasks.push({start:Math.round(entry.startTime),duration:Math.round(entry.duration)});
}
}).observe({ type: 'longtask', buffered: true });
} catch {}
@@ -374,6 +378,7 @@ async function runColdScenario({ browserType, server, proxy, profile }) {
domContentLoaded: nav ? Math.round(nav.domContentLoadedEventEnd - nav.startTime) : null,
bootDone: window.__bootDone ? Math.round(window.__bootDone) : null,
longTaskTotal: Math.round(window.__longTasksTotal || 0),
longTasks: window.__longTasks || [],
};
});
@@ -396,6 +401,7 @@ async function runColdScenario({ browserType, server, proxy, profile }) {
domContentLoaded: pageMetrics.domContentLoaded,
bootDone: pageMetrics.bootDone,
longTaskTotal: pageMetrics.longTaskTotal,
longTasks: pageMetrics.longTasks,
requests: stats.requests,
initialJsCss: pageMetrics.initialJsCss,
initialAssetRequests: pageMetrics.initialAssetRequests,
@@ -478,6 +484,7 @@ async function runWarmScenario({ browserType, server, proxy, profile }) {
domContentLoaded: nav ? Math.round(nav.domContentLoadedEventEnd - nav.startTime) : null,
bootDone: window.__bootDone ? Math.round(window.__bootDone) : null,
longTaskTotal: Math.round(window.__longTasksTotal || 0),
longTasks: window.__longTasks || [],
};
});
@@ -492,6 +499,7 @@ async function runWarmScenario({ browserType, server, proxy, profile }) {
domContentLoaded: pageMetrics.domContentLoaded,
bootDone: pageMetrics.bootDone,
longTaskTotal: pageMetrics.longTaskTotal,
longTasks: pageMetrics.longTasks,
};
}
@@ -634,7 +642,7 @@ async function runUpdateScenario({ browserType, server, proxy, profile, updateKi
}
// Switch server to Tree N+1 (app.js or theme-glass.css modification)
const targetTree = updateKind === 'js' ? trees.treeUpdateJs : trees.treeUpdateCss;
const targetTree = updateKind === 'feature' ? trees.treeUpdateFeature : updateKind === 'js' ? trees.treeUpdateJs : trees.treeUpdateCss;
await server.restartWithPublic(targetTree);
// Measure wire bytes and duration from reg.update() until new SW is waiting/installed
@@ -654,10 +662,32 @@ async function runUpdateScenario({ browserType, server, proxy, profile, updateKi
await sleep(50);
}
if (updateKind === 'feature') {
// 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);
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 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);
return entry?.headers.get('X-Asset-Hash')===h;
},expected);
if(complete)break;
if(Date.now()>end)throw Error('Changed feature was not cached after activation');
await sleep(100);
}
}
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');
}
await browser.close();
return {
@@ -696,6 +726,7 @@ async function main() {
const treeBase = path.join(tmpRoot, 'tree-base');
const treeUpdateJs = path.join(tmpRoot, 'tree-update-js');
const treeUpdateCss = path.join(tmpRoot, 'tree-update-css');
const treeUpdateFeature = path.join(tmpRoot, 'tree-update-feature');
let frontendSource=path.join(REPO_ROOT,'frontend');
if(options.frontendCommit) {
@@ -704,14 +735,15 @@ async function main() {
execFileSync('tar',['-xf',archive,'-C',tmpRoot]);
frontendSource=path.join(tmpRoot,'frontend');
}
for(const tree of [treeBase,treeUpdateJs,treeUpdateCss])cpSync(frontendSource,tree,{recursive:true});
for(const tree of [treeBase,treeUpdateJs,treeUpdateCss,treeUpdateFeature])cpSync(frontendSource,tree,{recursive:true});
// Append 1 comment line to app.js in treeUpdateJs
appendFileSync(path.join(treeUpdateJs, 'app.js'), '\n// baseline bench: 1-line app.js comment\n');
// Append 1 rule to theme-glass.css in treeUpdateCss
appendFileSync(path.join(treeUpdateCss, 'theme-glass.css'), '\n/* baseline bench: 1-line css rule */\n.baseline-marker { opacity: 0.99; }\n');
const trees = { treeBase, treeUpdateJs, treeUpdateCss };
if(existsSync(path.join(treeUpdateFeature,'presenter.js')))appendFileSync(path.join(treeUpdateFeature,'presenter.js'),'\n// baseline bench: 1-line presenter chunk comment\n');
const trees = { treeBase, treeUpdateJs, treeUpdateCss, treeUpdateFeature };
const serverScratch = path.join(tmpRoot, 'server-run');
const server = new ServerManager(serverScratch, treeBase);
@@ -721,6 +753,7 @@ async function main() {
const results = {
date: new Date().toISOString(),
frontendCommit: options.frontendCommit,
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,
browsers: {},
@@ -759,6 +792,7 @@ async function main() {
domContentLoaded: summarizeList(coldRuns.map((r) => r.domContentLoaded)),
bootDone: summarizeList(coldRuns.map((r) => r.bootDone)),
longTaskTotal: summarizeList(coldRuns.map((r) => r.longTaskTotal)),
longTasks: coldRuns.map(r=>r.longTasks),
swInstallDuration: summarizeList(coldRuns.map((r) => r.swInstallDuration)),
swInstallBytes: summarizeList(coldRuns.map((r) => r.swInstallBytes)),
storageEstimate: coldRuns[0]?.storageEstimate || null,
@@ -869,6 +903,16 @@ async function main() {
};
}
if ((options.scenario === 'all' || options.scenario === '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++) {
const res=await runUpdateScenario({browserType:bType,server,proxy,profile:prof,updateKind:'feature',trees});runs.push(res);
console.log(` run ${r+1}/${options.runs}: ${res.wireBytes} bytes, ${res.requestCount} reqs, dur=${res.duration}ms`);
}
results.browsers[bName][prof].updateFeature={requestCount:summarizeList(runs.map(r=>r.requestCount)),wireBytes:summarizeList(runs.map(r=>r.wireBytes)),duration:summarizeList(runs.map(r=>r.duration)),wireBytesByType:runs[0]?.wireBytesByType||{},requests:runs[0]?.requests||[]};
}
await proxy.close();
}
}