perf: compile scene shaders off-thread before the first frame
I warm the world's programs with renderer.compileAsync() at the end of the async build, with the composer's scene target bound so tone mapping and output colour space key the same programs the composer draws with, and with hidden subtrees revealed so the light count matches the frame-0 warm render. The wait is capped at 2.5 s; the warm render stays as the safety net for the post passes, texture uploads and anything a slow or extension-less driver left. Test builds report renderer.info.programs, and a browser spec pins zero program growth after frame 0. On the Intel ARL laptop the frame-0 task drops from ~1.74 s to ~0.34 s with a pixel-identical first frame; measurements in docs/overnight/compile-async-results.md.
This commit is contained in:
106
docs/overnight/compile-async-measure.mjs
Normal file
106
docs/overnight/compile-async-measure.mjs
Normal file
@@ -0,0 +1,106 @@
|
||||
// usage: node measure.mjs <label> <runs> [--shot] [--nokhr] [--noscroll]
|
||||
import { chromium } from 'playwright';
|
||||
import { writeFileSync, mkdirSync, appendFileSync } from 'node:fs';
|
||||
|
||||
const [label, runsArg] = process.argv.slice(2);
|
||||
const SHOT = process.argv.includes('--shot');
|
||||
const NOKHR = process.argv.includes('--nokhr');
|
||||
const NOSCROLL = process.argv.includes('--noscroll');
|
||||
const runs = Number(runsArg ?? 5);
|
||||
const out = process.env.OUT ?? 'compile-async-out'; // runs from the repo root; serve dist on 127.0.0.1:5181 first
|
||||
mkdirSync(out, { recursive: true });
|
||||
const sleep = (ms) => new Promise((r) => setTimeout(r, ms));
|
||||
|
||||
const init = `(() => {
|
||||
const T = { links: [], lit: null, linksAtLit: null, longtasks: [], frames: [], shot: null };
|
||||
window.__m = T;
|
||||
for (const C of [WebGL2RenderingContext, WebGLRenderingContext]) {
|
||||
const link = C.prototype.linkProgram;
|
||||
C.prototype.linkProgram = function (p) { T.links.push(performance.now()); return link.call(this, p); };
|
||||
if (${NOKHR}) {
|
||||
const ge = C.prototype.getExtension;
|
||||
C.prototype.getExtension = function (n) { return n === 'KHR_parallel_shader_compile' ? null : ge.call(this, n); };
|
||||
const gs = C.prototype.getSupportedExtensions;
|
||||
C.prototype.getSupportedExtensions = function () { return gs.call(this).filter((e) => e !== 'KHR_parallel_shader_compile'); };
|
||||
}
|
||||
}
|
||||
new PerformanceObserver((l) => { for (const e of l.getEntries()) T.longtasks.push([e.startTime, e.duration]); })
|
||||
.observe({ type: 'longtask', buffered: true });
|
||||
const mo = new MutationObserver(() => {
|
||||
if (T.lit !== null) return;
|
||||
const c = document.querySelector('canvas.journey-canvas');
|
||||
if (!c || c.dataset.lit === undefined) return;
|
||||
T.lit = performance.now();
|
||||
T.linksAtLit = T.links.length;
|
||||
if (${SHOT}) T.shot = c.toDataURL('image/png');
|
||||
});
|
||||
mo.observe(document, { subtree: true, attributes: true, attributeFilter: ['data-lit'] });
|
||||
let last = null;
|
||||
const loop = (t) => { if (last !== null) T.frames.push([t, t - last]); last = t; requestAnimationFrame(loop); };
|
||||
requestAnimationFrame(loop);
|
||||
})();`;
|
||||
|
||||
const results = [];
|
||||
for (let i = 0; i < runs; i += 1) {
|
||||
const browser = await chromium.launch({
|
||||
channel: 'chromium',
|
||||
headless: process.env.HEADED !== '1',
|
||||
args: ['--use-gl=angle', '--use-angle=gl', '--ignore-gpu-blocklist', '--enable-gpu-rasterization',
|
||||
'--enable-webgl', '--disable-gpu-sandbox', '--disable-gpu-shader-disk-cache'],
|
||||
});
|
||||
const context = await browser.newContext({ viewport: { width: 1440, height: 900 }, deviceScaleFactor: 1 });
|
||||
const page = await context.newPage();
|
||||
const errors = [];
|
||||
page.on('console', (m) => { if (m.type() === 'error') errors.push(m.text()); });
|
||||
await page.addInitScript(init);
|
||||
await page.goto('http://127.0.0.1:5181/', { waitUntil: 'load' });
|
||||
await page.waitForFunction(() => window.__m.lit !== null, null, { timeout: 20000 });
|
||||
const env = await page.evaluate(() => {
|
||||
const gl = document.createElement('canvas').getContext('webgl2');
|
||||
const ext = gl.getExtension('WEBGL_debug_renderer_info');
|
||||
return {
|
||||
renderer: ext ? gl.getParameter(ext.UNMASKED_RENDERER_WEBGL) : gl.getParameter(gl.RENDERER),
|
||||
khr: gl.getExtension('KHR_parallel_shader_compile') !== null,
|
||||
};
|
||||
});
|
||||
await sleep(2500);
|
||||
const scrollStart = await page.evaluate(() => performance.now());
|
||||
if (!NOSCROLL) {
|
||||
await page.mouse.move(720, 450);
|
||||
const t0 = Date.now();
|
||||
while (Date.now() - t0 < 10000) {
|
||||
await page.mouse.wheel(0, 200);
|
||||
await sleep(80);
|
||||
}
|
||||
} else {
|
||||
await sleep(10000);
|
||||
}
|
||||
const scrollEnd = await page.evaluate(() => performance.now());
|
||||
const m = await page.evaluate(() => window.__m);
|
||||
const nav = await page.evaluate(() => performance.getEntriesByType('navigation')[0].toJSON());
|
||||
const r = {
|
||||
run: i,
|
||||
renderer: env.renderer,
|
||||
khr: env.khr,
|
||||
litMs: Math.round(m.lit),
|
||||
linksBeforeLit: m.linksAtLit,
|
||||
linksAfterLit: m.links.length - m.linksAtLit,
|
||||
linksAfterLitTimes: m.links.slice(m.linksAtLit).map((t) => Math.round(t - m.lit)),
|
||||
frame0Task: (() => { const t = m.longtasks.find(([st, d]) => st <= m.lit && m.lit <= st + d + 5); return t ? Math.round(t[1]) : null; })(),
|
||||
linksInFrame0Task: (() => { const t = m.longtasks.find(([st, d]) => st <= m.lit && m.lit <= st + d + 5); return t ? m.links.filter((x) => x >= t[0] && x <= m.lit).length : null; })(),
|
||||
timeline: (() => { const t = m.longtasks.find(([st, d]) => st <= m.lit && m.lit <= st + d + 5); const f0 = t ? t[0] : m.lit; const pre = m.links.filter((x) => x < f0); return { firstLink: Math.round(m.links[0]), lastBuildLink: pre.length ? Math.round(pre[pre.length - 1]) : null, buildLinks: pre.length, frame0Start: Math.round(f0), lit: Math.round(m.lit) }; })(),
|
||||
longestTask: Math.round(Math.max(0, ...m.longtasks.map((x) => x[1]))),
|
||||
longTasks: m.longtasks.filter((x) => x[1] > 100).map((x) => [Math.round(x[0]), Math.round(x[1])]),
|
||||
framesOver50Idle: m.frames.filter(([t, d]) => t > m.lit + 50 && t < scrollStart && d > 50).map(([t, d]) => [Math.round(t - m.lit), Math.round(d)]),
|
||||
framesOver50Scroll: m.frames.filter(([t, d]) => t >= scrollStart && t <= scrollEnd && d > 50).map(([t, d]) => [Math.round(t - scrollStart), Math.round(d)]),
|
||||
worstScrollFrame: Math.round(Math.max(0, ...m.frames.filter(([t]) => t >= scrollStart && t <= scrollEnd).map((x) => x[1]))),
|
||||
scrollY: await page.evaluate(() => scrollY),
|
||||
domContentLoaded: Math.round(nav.domContentLoadedEventEnd),
|
||||
errors,
|
||||
};
|
||||
if (SHOT && m.shot) writeFileSync(`${out}/${label}-frame0-${Date.now()}.png`, Buffer.from(m.shot.split(',')[1], 'base64'));
|
||||
console.log(JSON.stringify(r));
|
||||
results.push(r);
|
||||
await browser.close();
|
||||
}
|
||||
appendFileSync(`${out}/${label}.jsonl`, results.map((r) => JSON.stringify(r)).join('\n') + '\n');
|
||||
Reference in New Issue
Block a user