const { selectDefaultTestDungeon, openSelectedDungeonRoles } = require('./playtest-defaults.cjs'); const { chromium } = require('C:/Users/phenom/.cache/codex-runtimes/codex-primary-runtime/dependencies/node/node_modules/playwright'); const fs = require('node:fs'); const path = require('node:path'); const os = require('node:os'); const label = process.env.PERFORMANCE_LABEL || 'current'; const output = path.resolve('artifacts/playtest/performance', label); fs.mkdirSync(output, { recursive: true }); async function enterDungeon(page) { await page.getByRole('button', { name: /^Dungeons/ }).click(); await selectDefaultTestDungeon(page); await openSelectedDungeonRoles(page); await page.locator('.dungeon-role-option--healer').click(); await page.locator('.scene-shell').waitFor(); await page.locator('.loading-screen').waitFor({ state: 'hidden', timeout: 180_000 }); await page.locator('.dungeon-boss-intro').waitFor({ state: 'hidden', timeout: 180_000 }); await page.waitForTimeout(2000); } async function measure(page, cdp, label, seconds = 10) { const read = async () => { const { metrics } = await cdp.send('Performance.getMetrics'); return Object.fromEntries(metrics.map(({ name, value }) => [name, value])); }; const before = await read(); const drawsBefore = await page.evaluate(() => window.__performanceDraws); await page.waitForTimeout(seconds * 1000); const after = await read(); const drawsAfter = await page.evaluate(() => window.__performanceDraws); await cdp.send('HeapProfiler.collectGarbage'); const heap = await cdp.send('Runtime.getHeapUsage'); const elapsed = after.Timestamp - before.Timestamp; const sample = { label, seconds: elapsed, mainThreadBusyPercent: (after.TaskDuration - before.TaskDuration) / elapsed * 100, scriptPercent: (after.ScriptDuration - before.ScriptDuration) / elapsed * 100, layoutCount: after.LayoutCount - before.LayoutCount, drawCallsPerSecond: (drawsAfter - drawsBefore) / elapsed, heapMiB: heap.usedSize / 1048576, backingStorageMiB: (heap.backingStorageSize || 0) / 1048576, nodes: after.Nodes, documents: after.Documents, scene: await page.locator('.scene-shell').evaluateAll((elements) => elements.map((element) => ({ frameLoop: element.dataset.frameLoop, performance: document.documentElement.dataset.gamePerformance || null, }))), }; console.log(JSON.stringify(sample)); return sample; } (async () => { const { createGameServer } = await import('../server/server.mjs'); const dataDir = fs.mkdtempSync(path.join(os.tmpdir(), 'healer-performance-')); const runtime = createGameServer({ dataDir, enableShelvedModes: false }); await new Promise((resolve) => runtime.server.listen(4191, '127.0.0.1', resolve)); const browser = await chromium.launch({ channel: 'msedge', headless: true }); const page = await browser.newPage({ viewport: { width: 1280, height: 800 } }); page.setDefaultTimeout(60_000); const errors = []; const samples = []; page.on('pageerror', (error) => errors.push(error.message)); const cdp = await page.context().newCDPSession(page); await cdp.send('Performance.enable'); await cdp.send('HeapProfiler.enable'); await page.addInitScript(() => { window.__performanceDraws = 0; for (const api of [WebGLRenderingContext, WebGL2RenderingContext]) { for (const name of ['drawArrays', 'drawElements', 'drawArraysInstanced', 'drawElementsInstanced']) { const original = api.prototype[name]; if (!original) continue; api.prototype[name] = function (...args) { window.__performanceDraws += 1; return original.apply(this, args); }; } } }); try { await page.goto(process.env.PERFORMANCE_URL || 'http://127.0.0.1:4191', { waitUntil: 'domcontentloaded' }); await page.getByRole('button', { name: /Play offline/ }).click(); await page.getByRole('button', { name: /Create character/ }).click(); await page.getByRole('button', { name: /Customize & name/ }).click(); await page.getByLabel('Character name').fill('PerformanceQA'); await page.getByRole('button', { name: /Create character/ }).click(); await page.getByRole('button', { name: /Continue|Enter world/ }).first().evaluate((button) => button.click()); await page.locator('.main-menu-screen').waitFor(); samples.push(await measure(page, cdp, 'menu-cold', 5)); for (let cycle = 1; cycle <= 3; cycle += 1) { await enterDungeon(page); if (cycle === 1) { const renderer = await page.locator('.scene-shell canvas').first().evaluate((canvas) => { const gl = canvas.getContext('webgl2') || canvas.getContext('webgl'); const extension = gl?.getExtension('WEBGL_debug_renderer_info'); return { renderer: extension ? gl.getParameter(extension.UNMASKED_RENDERER_WEBGL) : null, width: canvas.width, height: canvas.height }; }); console.log(JSON.stringify(renderer)); fs.writeFileSync(path.join(output, 'renderer.json'), `${JSON.stringify(renderer, null, 2)}\n`); samples.push(await measure(page, cdp, 'active-idle')); await page.screenshot({ path: path.join(output, 'active.png') }); await cdp.send('Profiler.enable'); await cdp.send('Profiler.start'); await page.keyboard.down('w'); await page.waitForTimeout(3000); await page.keyboard.up('w'); await page.waitForTimeout(5000); const { profile } = await cdp.send('Profiler.stop'); fs.writeFileSync(path.join(output, 'active.cpuprofile'), JSON.stringify(profile)); samples.push(await measure(page, cdp, 'after-movement', 5)); } await page.keyboard.press('Escape'); await page.getByRole('dialog', { name: 'Paused' }).waitFor(); await page.waitForTimeout(1500); if (cycle === 3) { await cdp.send('Profiler.enable'); await cdp.send('Profiler.start'); } samples.push(await measure(page, cdp, `paused-cycle-${cycle}`)); if (cycle === 3) { const { profile } = await cdp.send('Profiler.stop'); fs.writeFileSync(path.join(output, 'paused.cpuprofile'), JSON.stringify(profile)); await page.screenshot({ path: path.join(output, 'paused-cycle-3.png') }); samples.push(await measure(page, cdp, 'paused-cycle-3-settled', 20)); } if (cycle === 1) { await page.screenshot({ path: path.join(output, 'paused.png') }); samples.push(await measure(page, cdp, 'paused-retention', 20)); await page.getByRole('button', { name: 'Resume game' }).click(); await page.waitForTimeout(1500); samples.push(await measure(page, cdp, 'resumed', 5)); await page.screenshot({ path: path.join(output, 'resumed.png') }); // Exercise the app's visibility handlers deterministically. This is a // synthetic lifecycle check, not a measurement of OS tab throttling. await page.evaluate(() => { window.__profileHidden = true; Object.defineProperty(document, 'visibilityState', { configurable: true, get: () => window.__profileHidden ? 'hidden' : 'visible' }); Object.defineProperty(document, 'hidden', { configurable: true, get: () => window.__profileHidden }); document.dispatchEvent(new Event('visibilitychange')); }); await page.waitForTimeout(1000); samples.push(await measure(page, cdp, 'synthetic-hidden', 5)); await page.evaluate(() => { window.__profileHidden = false; document.dispatchEvent(new Event('visibilitychange')); }); await page.waitForTimeout(1000); await page.keyboard.press('Escape'); await page.getByRole('dialog', { name: 'Paused' }).waitFor(); } await page.getByRole('button', { name: 'Main menu', exact: true }).click(); await page.locator('.main-menu-screen').waitFor(); await page.waitForTimeout(2000); samples.push(await measure(page, cdp, `menu-cycle-${cycle}`, 5)); } fs.writeFileSync(path.join(output, 'results.json'), `${JSON.stringify({ samples, errors }, null, 2)}\n`); if (errors.length) throw new Error(errors.join('\n')); } catch (error) { await page.screenshot({ path: path.join(output, 'failure.png') }); fs.writeFileSync(path.join(output, 'results.json'), `${JSON.stringify({ samples, errors }, null, 2)}\n`); console.error((await page.locator('body').innerText()).slice(-2000)); throw error; } finally { await browser.close(); await new Promise((resolve) => runtime.server.close(resolve)); const resolved = path.resolve(dataDir); if (!resolved.startsWith(path.resolve(os.tmpdir()) + path.sep)) throw new Error('Unexpected test directory'); fs.rmSync(resolved, { recursive: true, force: true }); } })().catch((error) => { console.error(error); process.exitCode = 1; });