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

80 lines
4.3 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

/**
* Сетевой лог сценариев (Task 9): все запросы браузера к API -> perf/results/<run_id>/network.jsonl.
* Формат строки: perfkit/bench.py (docstring). Подключается во всех сценариях одной строкой после newContext:
* const nl = attachNetlog(ctx, { scenario, target, role }); ... finally { await nl.flush(); await ctx.close(); }
* Размер тела - request.sizes().responseBodySize (по CDP, работает и для ответов 30-47 МБ, где response.body() нет).
* Тело запроса пишется для не-GET (для повтора в bench); ограничено 64 КБ. Секреты не пишутся (sanitizePost):
* для путей с /auth/ тело = null (логин, refresh), из остальных тел вырезаются поля password/token/secret.
*/
import type { BrowserContext, Request } from '@playwright/test';
import { appendFileSync, mkdirSync } from 'node:fs';
import path from 'node:path';
import { API, WEB } from './auth';
import { runDir, runId } from './results';
export interface NetMeta { scenario: string; target: string; role: string }
const MAX_POST = 64 * 1024;
const SECRET_KEY = /password|passwd|^pass$|token|secret/i;
function stripSecrets(v: unknown): unknown {
if (Array.isArray(v)) return v.map(stripSecrets);
if (v && typeof v === 'object') {
return Object.fromEntries(Object.entries(v as Record<string, unknown>).filter(([k]) => !SECRET_KEY.test(k)).map(([k, x]) => [k, stripSecrets(x)]));
}
return v;
}
/**
* Тело не-GET запроса для лога: null для /auth/ и пустых тел; JSON и form-urlencoded - без полей password/token/secret;
* неразбираемое тело, в котором встречается такое слово, - null (лучше потерять тело, чем записать пароль).
*/
export function sanitizePost(pathname: string, post: string | null): string | null {
if (!post || pathname.includes('/auth/')) return null;
try {
const parsed = JSON.parse(post);
const clean = stripSecrets(parsed);
return JSON.stringify(clean) === JSON.stringify(parsed) ? post : JSON.stringify(clean);
} catch { /* не JSON */ }
if (/^[^\s=&]+=[^&]*(&[^\s=&]+=[^&]*)*$/.test(post)) {
const q = new URLSearchParams(post);
let changed = false;
for (const k of [...q.keys()]) if (SECRET_KEY.test(k)) { q.delete(k); changed = true; }
return changed ? q.toString() : post;
}
return SECRET_KEY.test(post) ? null : post;
}
export function netlogFile(): string { return path.join(runDir(), 'network.jsonl'); }
export function attachNetlog(ctx: BrowserContext, meta: NetMeta): { flush: () => Promise<void> } {
const pending = new Set<Promise<void>>();
const handle = async (req: Request) => {
const url = req.url();
// фронт ходит в API через proxy своего origin (:8080/api), прямые вызовы - на PERF_API
if (!(url.startsWith(WEB + '/api/') || url.startsWith(API + '/api/'))) return;
let status = 0, body_size = -1;
try { status = (await req.response())?.status() ?? 0; } catch { /* контекст закрыт */ }
try { body_size = (await req.sizes()).responseBodySize; } catch { /* контекст закрыт */ }
const t = req.timing();
const u = new URL(url);
const post = req.method() === 'GET' ? null : sanitizePost(u.pathname, req.postData());
const rec = {
run_id: runId(), ts: new Date().toISOString(), ...meta,
method: req.method(), url, path: u.pathname, query: u.search.replace(/^\?/, ''), status,
resource_type: req.resourceType(),
post_data: post && post.length <= MAX_POST ? post : null,
duration_ms: t.responseEnd >= 0 ? Math.round(t.responseEnd * 10) / 10 : null,
ttfb_ms: t.responseStart >= 0 ? Math.round(t.responseStart * 10) / 10 : null,
body_size,
};
mkdirSync(runDir(), { recursive: true });
appendFileSync(netlogFile(), JSON.stringify(rec) + '\n', 'utf-8');
};
ctx.on('requestfinished', (req) => {
const p = handle(req).catch((e) => { if (process.env.PERF_NETLOG_DEBUG) console.log("netlog err", e); }).finally(() => pending.delete(p));
pending.add(p);
});
return { flush: async () => { await Promise.all([...pending]); } };
}