Newer
Older
rpg / apps / game / tools / checks / perf.mjs
/**
 * Сценарий perf — базовый замер производительности (baseline оптимизаций).
 * Не гейтовский чек: числа фиксируются в коммитах до/после, ассерты только
 * на «всё живо» (fps > 0, путь найден, инварианты чисты).
 * Важно: launchBrowser гоняет рендер на swiftshader (программный) — fps здесь
 * уровень слабого железа, а не обычного десктопа; сравнивать можно только
 * числа одинаковых прогонов (до/после), абсолютные значения — не про-player.
 * 1) старт: страница+ассеты до маяка (включая фиксированные 1,5 с сна моста —
 *    вычитать при сравнении), затем newGame() → location;
 * 2) путь: время scene:route (один A*) и walkTo через луга (клики по узлам);
 * 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 }
    );
}