/**
* Сценарий perf — базовый замер производительности (baseline оптимизаций).
* Не гейтовский чек: числа фиксируются в коммитах до/после, ассерты только
* на «всё живо» (fps > 0, путь найден, инварианты чисты).
* Важно: launchBrowser гоняет рендер на swiftshader (программный) — fps здесь
* уровень слабого железа, а не обычного десктопа; сравнивать можно только
* числа одинаковых прогонов (до/после), абсолютные значения — не про-player.
* 1) старт: страница+ассеты до маяка (включая фиксированные 1,5 с сна моста —
* вычитать при сравнении), затем newGame() → location;
* 2) путь: время scene:route (один A*) и walkTo через луга (один scene:walk);
* 3) бой: погоня + урон + эффекты — fps/heap после боя;
* 4) fps меряется в реальном времени (rAF), не в ручных шагах моста:
* окно ~0,9 с реального времени, затем снапшот + performance.memory.
* Запуск: node tools/agent.mjs run tools/checks/perf.mjs [--pretty]
*/
import { withChecks } from '../lib.mjs';
/** Пауза в реальном времени (rAF-цикл живёт, fpsMeter копит кадры). */
const idle = (page, ms) => page.evaluate((m) => new Promise((r) => setTimeout(r, m)), ms);
export default async function ({ pretty }) {
return withChecks(
'perf',
async (t) => {
const { c } = t;
await c.run('старт: страница+ассеты, затем newGame → location', async () => {
const t0 = Date.now();
await t.open({ newGame: false }); // до маяка «ассеты загружены» + 1,5 c сна
const bootMs = Date.now() - t0;
await t.ctx.agent.waitFor('!s.transitioning', { timeoutTicks: 600 });
const t1 = Date.now();
await t.ctx.agent.newGame(); // сам ждёт s.scene === 'location'
const newGameMs = Date.now() - t1;
const s = await t.ctx.agent.snapshot();
c.expect(s.scene === 'location', 'после newGame не в локации', s.scene);
return { bootMs: bootMs - 1500, bootMsRaw: bootMs, newGameMs, fps: s.fps };
});
await c.run('путь: один A* (scene:route) и walkTo через луга', async () => {
const t0 = Date.now();
const route = await t.ctx.agent.command('scene:route', { x: 24, y: 14 });
const routeMs = Date.now() - t0;
c.expect(Array.isArray(route) && route.length > 0, 'маршрут на восток лугов не найден', route);
const t1 = Date.now();
const walked = await t.ctx.agent.walkTo(24, 14, { timeoutTicks: 3000 });
const walkMs = Date.now() - t1;
c.expect(walked, 'walkTo(24,14) не дошёл', null);
return { routeMs, routeLen: route.length, walkMs };
});
await c.run('fps/heap в покое после прогулки', async () => {
await idle(t.ctx.page, 900);
const s = await t.ctx.agent.snapshot();
const heap = await t.ctx.page.evaluate(
() => (performance.memory ? Math.round(performance.memory.usedJSHeapSize / 1048576) : null)
);
c.expect(s.fps > 0, 'fps-метр молчит (нет кадров?)', s.fps);
return { fps: s.fps, heapMB: heap };
});
await c.run('бой: погоня, урон, эффекты — fps/heap под нагрузкой', async () => {
await t.ctx.agent.command('scene:sleepAll'); // чистый старт сценария боя
await t.ctx.agent.command('scene:teleport', { x: 7, y: 9 }); // у сталкера (8,9)
await t.ctx.agent.command('scene:noise', { x: 8, y: 9, level: 1 }); // будим
const w = await t.ctx.agent.waitFor(
's.enemies.some((e) => e.state === "chase")',
{ timeoutTicks: 300 }
);
c.expect(w.ok, 'после шума никто не погнался', w.snapshot?.enemies);
// Полноценный бой: сгусток догоняет и бьёт, эффекты/частицы живут.
await t.ctx.agent.step(360);
for (let i = 0; i < 3; i++) {
await t.ctx.agent.command('scene:damageEnemy', { value: 3 });
await t.ctx.agent.step(60);
}
await idle(t.ctx.page, 900);
const s = await t.ctx.agent.snapshot();
const heap = await t.ctx.page.evaluate(
() => (performance.memory ? Math.round(performance.memory.usedJSHeapSize / 1048576) : null)
);
return { fps: s.fps, heapMB: heap, heroHp: s.hero?.hp, enemies: s.enemies?.length };
});
await c.run('инварианты чисты под нагрузкой', async () => {
await t.expectInvariantsClean('в perf-сценарии');
return null;
});
},
{ pretty }
);
}