174 lines
8.7 KiB
JavaScript
174 lines
8.7 KiB
JavaScript
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; });
|