Andrey Epifancev fdc8ecb2a3 perf: замеры бэка
Сетевой лог сценариев, bench запросов, pg_stat_statements и EXPLAIN,
aggregate исключает трассировочные повторы (O-9).

Refs #202

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-26 02:36:46 +04:00

49 lines
2.7 KiB
TypeScript

/**
* Сетевой лог сценариев (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 КБ.
*/
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;
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 post = req.method() === 'GET' ? null : req.postData();
const u = new URL(url);
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]); } };
}