Andrey Epifancev 29867535b0 perf: полный прогон run-all.sh и правки по ревью
- perf/run-all.sh: масштабы 1 и 2, шаги reset -> gen -> verify -> stand ->
  preflight -> scroll -> coauthor -> grow -> modals -> modals_profiling ->
  memory -> memtrend -> [control4x] -> bench -> aggregate; лог и
  run-all.status.json, возобновление (PERF_STEPS, PERF_SKIP_RESET),
  сухой режим с оценкой длительности, PERF_S2_REPEATS/PERF_S2_TARGETS,
  контроль CPU 4x (PERF_CONTROL_TARGETS).
- Основная метрика: main_thread_ms и main_thread_ms_per_step в скролле
  (CDP TaskDuration), frame_excess_ms; в summary.md первыми строками.
- SQL-статистика по шагам: PERF_SQLSTATS_TAG -> sql/<шаг>/ (sqlstats --out).
- netlog: тела /auth/ не пишутся, поля password/token/secret вырезаются.
- scroll.spec: уникальные имена целей (лист ССП с суффиксом /ssp, год в
  отчёте проекта) - иначе Playwright не запускал файл из-за дублей.
- PERF_TRACE=0 для прогонов с одним повтором.
- README: scroll-coauthor, run-all, возобновление, без CPU 4/6 в матрицах.
- design.md: уточнена формулировка R-2, решения Task 10a.

Refs #202

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-26 03:01:33 +04:00

93 lines
4.0 KiB
TypeScript

/**
* Init-script для page.addInitScript(PROBE_SCRIPT).
* window.__perf.start() / stop() -> {long_task_n, long_task_ms, frame_n, heavy_frame_n,
* frame_p50_ms, frame_p95_ms, frame_excess_ms, duration_ms}.
* Long task: > 50 мс (PerformanceObserver 'longtask'). Тяжёлый кадр: интервал между rAF > 50 мс.
* frame_excess_ms: сумма превышений интервала кадра над бюджетом 16,7 мс (60 Гц).
*
* Основная метрика (решение человека 2026-10-25, design.md): суммарное время main thread на действие.
* Её даёт Node-сторона: startProbe(page) / stop() добавляет main_thread_ms = прирост CDP Performance.TaskDuration
* (все задачи main thread: скрипт, стили, layout, paint-подготовка; задачи < 50 мс тоже) между start и концом действия
* (до 100 мс ожидания longtask в __perf.stop). perStep() даёт main_thread_ms_per_step для скролла.
*/
import type { Page } from '@playwright/test';
import { mainThreadMs } from './cdp';
export const HEAVY_FRAME_MS = 50;
export const FRAME_BUDGET_MS = 1000 / 60;
export interface ProbeMetrics {
long_task_n: number;
long_task_ms: number;
frame_n: number;
heavy_frame_n: number;
frame_p50_ms: number;
frame_p95_ms: number;
frame_excess_ms: number;
duration_ms: number;
}
export interface ProbeResult extends ProbeMetrics { main_thread_ms: number }
export const PROBE_SCRIPT = `(() => {
if (window.__perf) return;
const HEAVY = ${HEAVY_FRAME_MS}, BUDGET = ${FRAME_BUDGET_MS};
let active = false, t0 = 0, tasks = [], frames = [], last = 0, raf = 0;
try {
new PerformanceObserver((list) => {
if (!active) return;
for (const e of list.getEntries()) tasks.push(e.duration);
}).observe({ type: 'longtask', buffered: false });
} catch (e) {}
const tick = (now) => {
if (!active) return;
if (last) frames.push(now - last);
last = now;
raf = requestAnimationFrame(tick);
};
const pct = (s, p) => s.length ? s[Math.max(Math.ceil(p * s.length), 1) - 1] : 0;
window.__perf = {
start() {
tasks = []; frames = []; last = 0; t0 = performance.now(); active = true;
raf = requestAnimationFrame(tick);
},
async stop() {
// дать долгим задачам, завершившимся перед stop, доехать до observer
await new Promise((r) => setTimeout(r, 100));
active = false; cancelAnimationFrame(raf);
const s = frames.slice().sort((a, b) => a - b);
return {
long_task_n: tasks.length,
long_task_ms: tasks.reduce((a, b) => a + b, 0),
frame_n: frames.length,
heavy_frame_n: frames.filter((f) => f > HEAVY).length,
frame_p50_ms: pct(s, 0.5),
frame_p95_ms: pct(s, 0.95),
frame_excess_ms: frames.reduce((a, f) => a + Math.max(0, f - BUDGET), 0),
duration_ms: performance.now() - t0,
};
},
};
})();`;
/**
* Замер действия: const p = await startProbe(page); ...действие...; const m = await p.stop();
* Требует PROBE_SCRIPT в init-скриптах страницы. main_thread_ms снимается сразу по окончании действия.
*/
export async function startProbe(page: Page): Promise<{ stop: () => Promise<ProbeResult> }> {
const t0 = await mainThreadMs(page);
await page.evaluate(() => (window as any).__perf.start());
return {
stop: async () => {
const t1 = await mainThreadMs(page);
const m: ProbeMetrics = await page.evaluate(() => (window as any).__perf.stop());
return { ...m, main_thread_ms: t1 - t0 };
},
};
}
/** Метрики на шаг скролла; при steps = 0 (фильтр, no_overflow) - пусто. */
export function perStep(m: ProbeResult, steps: number): { main_thread_ms_per_step?: number } {
return steps > 0 ? { main_thread_ms_per_step: m.main_thread_ms / steps } : {};
}