perf: каркас замеров фронта
Refs #202 Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
parent
fb80d8c636
commit
efa537e008
2
.gitignore
vendored
2
.gitignore
vendored
@ -341,3 +341,5 @@ perf/.venv/
|
||||
perf/e2e/node_modules/
|
||||
perf/.run/
|
||||
!perf/docker-compose.yml
|
||||
perf/e2e/test-results/
|
||||
!perf/e2e/lib/
|
||||
|
||||
@ -121,3 +121,8 @@
|
||||
| 2026-10-25 | В `Sheet` манифеста добавлено поле `owner` ("rf"/"ssp", по умолчанию "rf"); в манифест попадают листы только ССП-01 и РФ-01 | Манифест без признака владельца не позволяет отличить листы, недоступные `rf`; полный список форм раздул бы сценарии | Сценарии T5 должны фильтровать по owner |
|
||||
| 2026-10-25 | Лист свода кодируется в `Page.url` после `#` (`/svod#FORM_1`, `/svod/2#RF`) | Вкладка свода — состояние страницы, не маршрут; хеш не влияет на роутинг, имена `svod1_*`, `svod2_*` уникальны | Сценарий обязан выбирать вкладку по хешу/имени |
|
||||
| 2026-10-25 | В `perf/stand.sh` добавлена `reset`; `start_bg` отвязывает подоболочку от stdout | Требование задачи; без отвязки `stand.sh api \| tail` висел | Правка поведения стенда Task 1 |
|
||||
| 2026-10-25 | Версии `@playwright/test`, `@types/node`, `typescript` в `perf/e2e/package.json` зафиксированы точно (без `^`) | Воспроизводимость замеров между прогонами «до/после» | Обновление версий вручную |
|
||||
| 2026-10-25 | Тест `results format` пишет в каталог через `PERF_RESULTS_DIR` и после проверки удаляет `perf/report-data/<имя>` | aggregate пишет в `perf/report-data/<run_id>`; не должно оставаться мусора | Мусор в report-data при падении теста |
|
||||
| 2026-10-25 | `probe.stop()` ждёт 100 мс перед сбором (PerformanceObserver доставляет longtask асинхронно) | Иначе последняя длинная задача перед `stop` теряется | +100 мс к duration_ms |
|
||||
| 2026-10-25 | `heapUsed` делает GC перед `Runtime.getHeapUsage`; `startTrace/stopTrace` через `browser.startTracing` Playwright | Стабильные значения кучи; штатный API трассы | Трасса содержит события всего браузера |
|
||||
| 2026-10-25 | В `.gitignore` добавлены `!perf/e2e/lib/` и `perf/e2e/test-results/` | Корневой `lib/` игнорирует каталог библиотек каркаса; test-results — артефакты Playwright | Библиотеки не попадут в коммит / мусор в коммите |
|
||||
|
||||
@ -34,3 +34,18 @@ cd perf && .venv/bin/python -m perfkit.gen.verify --scale 1 # должен н
|
||||
у них нет HTTP-ручки). Пользователи: `perf_rf`, `perf_rf_coauthor` (одна РФ, роль 3) и `perf_dfip`
|
||||
(роль 2, все оргединицы), пароль `perf`; в манифесте они под ключами `rf`, `rf_coauthor`, `dfip`.
|
||||
Числа плана и их обоснование — в `perf/perfkit/gen/plan.py`.
|
||||
|
||||
## Фронт: замеры
|
||||
|
||||
Каркас: `perf/e2e` (Playwright + CDP). Версии: `@playwright/test` 1.64.1, Chromium 156.0.8078.4 (headless shell, revision 1248).
|
||||
|
||||
```
|
||||
cd perf/e2e && npm install && npx playwright install chromium # npm_config_registry=https://registry.npmjs.org
|
||||
npx playwright test tests/harness.spec.ts # самопроверка каркаса
|
||||
```
|
||||
|
||||
- `PERF_RUN_ID` — имя каталога прогона `perf/results/<run_id>/` (по умолчанию `YYYYMMDD-HHMM`).
|
||||
- `PERF_SCALE` — масштаб манифеста `perf/results/manifest-s<scale>.json` (по умолчанию 1).
|
||||
- `PERF_API`, `PERF_WEB` — адреса стенда (`http://localhost:8000`, `http://localhost:8080`).
|
||||
- Агрегация: `cd perf && .venv/bin/python -m perfkit.aggregate results/<run_id>`.
|
||||
- Библиотеки `perf/e2e/lib/`: `auth.loginAs`, `cdp.*` (throttle, trace, gc, heap), `probe` (long tasks > 50 мс, кадры rAF; тяжёлый кадр — интервал > 50 мс), `results.append`, `manifest.loadManifest`.
|
||||
|
||||
8
perf/e2e/fixtures/busy.html
Normal file
8
perf/e2e/fixtures/busy.html
Normal file
@ -0,0 +1,8 @@
|
||||
<!doctype html>
|
||||
<meta charset="utf-8">
|
||||
<title>busy</title>
|
||||
<button id="busy">busy</button>
|
||||
<script>
|
||||
window.busyLoop = (ms) => { const t = performance.now(); while (performance.now() - t < ms) {} return performance.now() - t; };
|
||||
document.getElementById('busy').addEventListener('click', () => window.busyLoop(120));
|
||||
</script>
|
||||
25
perf/e2e/lib/auth.ts
Normal file
25
perf/e2e/lib/auth.ts
Normal file
@ -0,0 +1,25 @@
|
||||
import type { BrowserContext } from '@playwright/test';
|
||||
import type { User } from './manifest';
|
||||
|
||||
export const API = process.env.PERF_API ?? 'http://localhost:8000';
|
||||
export const WEB = process.env.PERF_WEB ?? 'http://localhost:8080';
|
||||
|
||||
// Ключи localStorage: web/src/api/utils/token.js (saveAuthTokens), их читает AuthProvider.jsx.
|
||||
export const ACCESS_KEY = 'access_token';
|
||||
export const REFRESH_KEY = 'refresh_token';
|
||||
|
||||
/** Логин через API и запись токенов в localStorage origin'а фронта (init-script, до загрузки страницы). */
|
||||
export async function loginAs(context: BrowserContext, user: User): Promise<void> {
|
||||
const r = await context.request.post(`${API}/api/v1/auth/login`, {
|
||||
data: { username: user.username, password: user.password },
|
||||
});
|
||||
if (!r.ok()) throw new Error(`login ${user.username}: ${r.status()} ${await r.text()}`);
|
||||
const d = await r.json();
|
||||
await context.addInitScript(
|
||||
([a, rk, at, rt]) => {
|
||||
localStorage.setItem(a, at);
|
||||
if (rt) localStorage.setItem(rk, rt);
|
||||
},
|
||||
[ACCESS_KEY, REFRESH_KEY, d.access_token, d.refresh_token ?? ''] as const,
|
||||
);
|
||||
}
|
||||
63
perf/e2e/lib/cdp.ts
Normal file
63
perf/e2e/lib/cdp.ts
Normal file
@ -0,0 +1,63 @@
|
||||
import type { CDPSession, Page } from '@playwright/test';
|
||||
import { createWriteStream, mkdirSync, writeFileSync } from 'node:fs';
|
||||
import path from 'node:path';
|
||||
|
||||
const sessions = new WeakMap<Page, CDPSession>();
|
||||
|
||||
async function cdp(page: Page): Promise<CDPSession> {
|
||||
let s = sessions.get(page);
|
||||
if (!s) {
|
||||
s = await page.context().newCDPSession(page);
|
||||
sessions.set(page, s);
|
||||
}
|
||||
return s;
|
||||
}
|
||||
|
||||
/** CPU throttling: rate=1 — без замедления, 4 — в 4 раза медленнее. */
|
||||
export async function throttle(page: Page, rate: number): Promise<void> {
|
||||
await (await cdp(page)).send('Emulation.setCPUThrottlingRate', { rate });
|
||||
}
|
||||
|
||||
export async function startTrace(page: Page): Promise<void> {
|
||||
await page.context().browser()!.startTracing(page, {
|
||||
categories: ['devtools.timeline', 'disabled-by-default-devtools.timeline', 'v8.execute', 'blink.user_timing'],
|
||||
screenshots: false,
|
||||
});
|
||||
}
|
||||
|
||||
export async function stopTrace(page: Page, file: string): Promise<void> {
|
||||
mkdirSync(path.dirname(file), { recursive: true });
|
||||
const buf = await page.context().browser()!.stopTracing();
|
||||
writeFileSync(file, buf);
|
||||
}
|
||||
|
||||
export async function gc(page: Page): Promise<void> {
|
||||
const s = await cdp(page);
|
||||
await s.send('HeapProfiler.enable');
|
||||
await s.send('HeapProfiler.collectGarbage');
|
||||
}
|
||||
|
||||
export async function domCounters(page: Page): Promise<{ documents: number; nodes: number; jsEventListeners: number }> {
|
||||
return (await (await cdp(page)).send('Memory.getDOMCounters')) as any;
|
||||
}
|
||||
|
||||
/** Размер JS-кучи после GC, байты. */
|
||||
export async function heapUsed(page: Page): Promise<number> {
|
||||
await gc(page);
|
||||
const s = await cdp(page);
|
||||
await s.send('Runtime.enable');
|
||||
const r = (await s.send('Runtime.getHeapUsage')) as { usedSize: number };
|
||||
return r.usedSize;
|
||||
}
|
||||
|
||||
export async function heapSnapshot(page: Page, file: string): Promise<void> {
|
||||
mkdirSync(path.dirname(file), { recursive: true });
|
||||
const s = await cdp(page);
|
||||
const out = createWriteStream(file);
|
||||
const onChunk = (e: { chunk: string }) => { out.write(e.chunk); };
|
||||
s.on('HeapProfiler.addHeapSnapshotChunk', onChunk);
|
||||
await s.send('HeapProfiler.enable');
|
||||
await s.send('HeapProfiler.takeHeapSnapshot', { reportProgress: false });
|
||||
s.off('HeapProfiler.addHeapSnapshotChunk', onChunk);
|
||||
await new Promise<void>((res) => out.end(res));
|
||||
}
|
||||
37
perf/e2e/lib/manifest.ts
Normal file
37
perf/e2e/lib/manifest.ts
Normal file
@ -0,0 +1,37 @@
|
||||
import { readFileSync } from 'node:fs';
|
||||
import path from 'node:path';
|
||||
|
||||
export interface User { username: string; password: string; role_id: number }
|
||||
export interface Sheet {
|
||||
form_id: number; form_type: string; sheet: string; direction: string | null; year: number;
|
||||
url: string; rows: number; owner: 'rf' | 'ssp';
|
||||
}
|
||||
export interface ProjectReport { project_id: number; year: number; report_type: string; url: string; rows: number }
|
||||
export interface Page { name: string; url: string }
|
||||
export interface Manifest {
|
||||
scale: number;
|
||||
users: Record<string, User>;
|
||||
sheets: Sheet[];
|
||||
project_reports: ProjectReport[];
|
||||
svods: Page[];
|
||||
modal_pages: Page[];
|
||||
}
|
||||
|
||||
export const PERF_DIR = path.resolve(__dirname, '../..');
|
||||
|
||||
export function scale(): number {
|
||||
return Number(process.env.PERF_SCALE ?? 1);
|
||||
}
|
||||
|
||||
export function loadManifest(sc: number = scale()): Manifest {
|
||||
const p = path.join(PERF_DIR, 'results', `manifest-s${sc}.json`);
|
||||
const d = JSON.parse(readFileSync(p, 'utf-8'));
|
||||
return {
|
||||
scale: d.scale,
|
||||
users: d.users ?? {},
|
||||
sheets: (d.sheets ?? []).map((s: Sheet) => ({ owner: 'rf', ...s })),
|
||||
project_reports: d.project_reports ?? [],
|
||||
svods: d.svods ?? [],
|
||||
modal_pages: d.modal_pages ?? [],
|
||||
};
|
||||
}
|
||||
57
perf/e2e/lib/probe.ts
Normal file
57
perf/e2e/lib/probe.ts
Normal file
@ -0,0 +1,57 @@
|
||||
/**
|
||||
* 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, duration_ms}.
|
||||
* Long task: > 50 мс (PerformanceObserver 'longtask'). Тяжёлый кадр: интервал между rAF > 50 мс.
|
||||
*/
|
||||
export const HEAVY_FRAME_MS = 50;
|
||||
|
||||
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;
|
||||
duration_ms: number;
|
||||
}
|
||||
|
||||
export const PROBE_SCRIPT = `(() => {
|
||||
if (window.__perf) return;
|
||||
const HEAVY = ${HEAVY_FRAME_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),
|
||||
duration_ms: performance.now() - t0,
|
||||
};
|
||||
},
|
||||
};
|
||||
})();`;
|
||||
45
perf/e2e/lib/results.ts
Normal file
45
perf/e2e/lib/results.ts
Normal file
@ -0,0 +1,45 @@
|
||||
import { appendFileSync, mkdirSync } from 'node:fs';
|
||||
import path from 'node:path';
|
||||
import { PERF_DIR } from './manifest';
|
||||
|
||||
function pad(n: number) { return String(n).padStart(2, '0'); }
|
||||
|
||||
export function runId(): string {
|
||||
if (process.env.PERF_RUN_ID) return process.env.PERF_RUN_ID;
|
||||
const d = new Date();
|
||||
return `${d.getFullYear()}${pad(d.getMonth() + 1)}${pad(d.getDate())}-${pad(d.getHours())}${pad(d.getMinutes())}`;
|
||||
}
|
||||
|
||||
/** Каталог прогона: PERF_RESULTS_DIR (тесты) или perf/results/<run_id>. */
|
||||
export function runDir(): string {
|
||||
return process.env.PERF_RESULTS_DIR ?? path.join(PERF_DIR, 'results', runId());
|
||||
}
|
||||
|
||||
export interface ResultRecord {
|
||||
run_id?: string;
|
||||
scenario: string;
|
||||
target: string;
|
||||
role: string;
|
||||
scale: number;
|
||||
cpu_rate: number;
|
||||
repeat: number;
|
||||
metrics: Record<string, number>;
|
||||
artifacts: string[];
|
||||
incomplete?: boolean;
|
||||
ts?: string;
|
||||
}
|
||||
|
||||
/** Формат — docstring perf/perfkit/results.py: perf/results/<run_id>/<scenario>.jsonl. */
|
||||
export function append(rec: ResultRecord): string {
|
||||
const dir = runDir();
|
||||
mkdirSync(dir, { recursive: true });
|
||||
const full = {
|
||||
run_id: runId(),
|
||||
incomplete: false,
|
||||
ts: new Date().toISOString().replace(/\.\d+Z$/, 'Z'),
|
||||
...rec,
|
||||
};
|
||||
const file = path.join(dir, `${rec.scenario}.jsonl`);
|
||||
appendFileSync(file, JSON.stringify(full) + '\n', 'utf-8');
|
||||
return file;
|
||||
}
|
||||
454
perf/e2e/package-lock.json
generated
Normal file
454
perf/e2e/package-lock.json
generated
Normal file
@ -0,0 +1,454 @@
|
||||
{
|
||||
"name": "perf-e2e",
|
||||
"version": "0.0.0",
|
||||
"lockfileVersion": 3,
|
||||
"requires": true,
|
||||
"packages": {
|
||||
"": {
|
||||
"name": "perf-e2e",
|
||||
"version": "0.0.0",
|
||||
"devDependencies": {
|
||||
"@playwright/test": "1.64.1",
|
||||
"@types/node": "26.6.5",
|
||||
"typescript": "7.0.2"
|
||||
}
|
||||
},
|
||||
"node_modules/@playwright/test": {
|
||||
"version": "1.64.1",
|
||||
"resolved": "https://registry.npmjs.org/@playwright/test/-/test-1.64.1.tgz",
|
||||
"integrity": "sha512-IaYGml/nSOiZWROfPtuIL7neuu9qDOZ3TNvNljUIrhDMxiGTgPjzj/g9p9RxpS2tSepPDRY+UwAaEeiKtUvIAg==",
|
||||
"dev": true,
|
||||
"license": "Apache-2.0",
|
||||
"dependencies": {
|
||||
"playwright": "1.64.1"
|
||||
},
|
||||
"bin": {
|
||||
"playwright": "cli.js"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=20"
|
||||
}
|
||||
},
|
||||
"node_modules/@types/node": {
|
||||
"version": "26.6.5",
|
||||
"resolved": "https://registry.npmjs.org/@types/node/-/node-26.6.5.tgz",
|
||||
"integrity": "sha512-uMzsv9stDdofIZBx4UGh4Z9ZgXnGC9seRDscilBcTHAeOFk67MZg/k9bWVfO+bk4qZmG+l52hRnLwUiH2LnJTg==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"undici-types": "~8.9.0"
|
||||
}
|
||||
},
|
||||
"node_modules/@typescript/typescript-aix-ppc64": {
|
||||
"version": "7.0.2",
|
||||
"resolved": "https://registry.npmjs.org/@typescript/typescript-aix-ppc64/-/typescript-aix-ppc64-7.0.2.tgz",
|
||||
"integrity": "sha512-MTKKkWB7p/0E9xi1d1tHtZ5PiLkGEMIq88pK2CubZjOsLtYTLqhgIgi6zepFa+9GHZ6h05NMCkQxGKiPXMxXtQ==",
|
||||
"cpu": [
|
||||
"ppc64"
|
||||
],
|
||||
"dev": true,
|
||||
"license": "Apache-2.0",
|
||||
"optional": true,
|
||||
"os": [
|
||||
"aix"
|
||||
],
|
||||
"engines": {
|
||||
"node": ">=16.20.0"
|
||||
}
|
||||
},
|
||||
"node_modules/@typescript/typescript-darwin-arm64": {
|
||||
"version": "7.0.2",
|
||||
"resolved": "https://registry.npmjs.org/@typescript/typescript-darwin-arm64/-/typescript-darwin-arm64-7.0.2.tgz",
|
||||
"integrity": "sha512-gowzar9MwS/aRWp6f3a4KUqzRjAZjOsmGNCM6LcTgXum+dBfgsBVMN+AgvOCCbguXyick6LJhpBszxMebJ8syA==",
|
||||
"cpu": [
|
||||
"arm64"
|
||||
],
|
||||
"dev": true,
|
||||
"license": "Apache-2.0",
|
||||
"optional": true,
|
||||
"os": [
|
||||
"darwin"
|
||||
],
|
||||
"engines": {
|
||||
"node": ">=16.20.0"
|
||||
}
|
||||
},
|
||||
"node_modules/@typescript/typescript-darwin-x64": {
|
||||
"version": "7.0.2",
|
||||
"resolved": "https://registry.npmjs.org/@typescript/typescript-darwin-x64/-/typescript-darwin-x64-7.0.2.tgz",
|
||||
"integrity": "sha512-SZ9xZInqApNlNGc9s0W1VSsktYSOe9cFqNOIqmN1Gs8SmkjKZYFt017G4VwPxASInODuAdbTW7sXiFUf893RgA==",
|
||||
"cpu": [
|
||||
"x64"
|
||||
],
|
||||
"dev": true,
|
||||
"license": "Apache-2.0",
|
||||
"optional": true,
|
||||
"os": [
|
||||
"darwin"
|
||||
],
|
||||
"engines": {
|
||||
"node": ">=16.20.0"
|
||||
}
|
||||
},
|
||||
"node_modules/@typescript/typescript-freebsd-arm64": {
|
||||
"version": "7.0.2",
|
||||
"resolved": "https://registry.npmjs.org/@typescript/typescript-freebsd-arm64/-/typescript-freebsd-arm64-7.0.2.tgz",
|
||||
"integrity": "sha512-W5NH4y/J0plIIS5b2xvTEkU7JFxyqdMAOgf+Ilhl0vHQXKO5dZoxd+C/jEtq56c4F3wk71RB4BMRQ2XdI+bwYQ==",
|
||||
"cpu": [
|
||||
"arm64"
|
||||
],
|
||||
"dev": true,
|
||||
"license": "Apache-2.0",
|
||||
"optional": true,
|
||||
"os": [
|
||||
"freebsd"
|
||||
],
|
||||
"engines": {
|
||||
"node": ">=16.20.0"
|
||||
}
|
||||
},
|
||||
"node_modules/@typescript/typescript-freebsd-x64": {
|
||||
"version": "7.0.2",
|
||||
"resolved": "https://registry.npmjs.org/@typescript/typescript-freebsd-x64/-/typescript-freebsd-x64-7.0.2.tgz",
|
||||
"integrity": "sha512-UMGDx5sTpzNw3WiPebH7l90IWfJggEd+egHt/q6p7/Cm3zqoV7VxkGXt+3DxPIw8CcmvAB0j3sVVfbhX+M4Tpw==",
|
||||
"cpu": [
|
||||
"x64"
|
||||
],
|
||||
"dev": true,
|
||||
"license": "Apache-2.0",
|
||||
"optional": true,
|
||||
"os": [
|
||||
"freebsd"
|
||||
],
|
||||
"engines": {
|
||||
"node": ">=16.20.0"
|
||||
}
|
||||
},
|
||||
"node_modules/@typescript/typescript-linux-arm": {
|
||||
"version": "7.0.2",
|
||||
"resolved": "https://registry.npmjs.org/@typescript/typescript-linux-arm/-/typescript-linux-arm-7.0.2.tgz",
|
||||
"integrity": "sha512-gffT3xPz9sR7j/YJExkyPntrI0P2EP9XbOyWzth2/Gs0RstK+90RBcO0ncXoXy/beYll1SXw846Nf2zdnEz0QQ==",
|
||||
"cpu": [
|
||||
"arm"
|
||||
],
|
||||
"dev": true,
|
||||
"license": "Apache-2.0",
|
||||
"optional": true,
|
||||
"os": [
|
||||
"linux"
|
||||
],
|
||||
"engines": {
|
||||
"node": ">=16.20.0"
|
||||
}
|
||||
},
|
||||
"node_modules/@typescript/typescript-linux-arm64": {
|
||||
"version": "7.0.2",
|
||||
"resolved": "https://registry.npmjs.org/@typescript/typescript-linux-arm64/-/typescript-linux-arm64-7.0.2.tgz",
|
||||
"integrity": "sha512-Qh4eU4/y3yDjnfjjyPYihMj5/ODIlmt+Bzu17OI+fiSRDW57QmU5SiN63exPRNJPKUzcc1INa1NXdrJ+MqHjUQ==",
|
||||
"cpu": [
|
||||
"arm64"
|
||||
],
|
||||
"dev": true,
|
||||
"license": "Apache-2.0",
|
||||
"optional": true,
|
||||
"os": [
|
||||
"linux"
|
||||
],
|
||||
"engines": {
|
||||
"node": ">=16.20.0"
|
||||
}
|
||||
},
|
||||
"node_modules/@typescript/typescript-linux-loong64": {
|
||||
"version": "7.0.2",
|
||||
"resolved": "https://registry.npmjs.org/@typescript/typescript-linux-loong64/-/typescript-linux-loong64-7.0.2.tgz",
|
||||
"integrity": "sha512-uEHck9i8hoAzXPiYRib1O7miOnz23SxIeVl6F4LXox+qov1K35jHcEW6VHKvZI+pyvl7fZEP4MCU5LYvIq1GuQ==",
|
||||
"cpu": [
|
||||
"loong64"
|
||||
],
|
||||
"dev": true,
|
||||
"license": "Apache-2.0",
|
||||
"optional": true,
|
||||
"os": [
|
||||
"linux"
|
||||
],
|
||||
"engines": {
|
||||
"node": ">=16.20.0"
|
||||
}
|
||||
},
|
||||
"node_modules/@typescript/typescript-linux-mips64el": {
|
||||
"version": "7.0.2",
|
||||
"resolved": "https://registry.npmjs.org/@typescript/typescript-linux-mips64el/-/typescript-linux-mips64el-7.0.2.tgz",
|
||||
"integrity": "sha512-R4KvAMnE43W5Qeqb0Ly56O3mWMWIAgsMyz36DCaycd5nbg/9kzm0liw3JocfRqyJY0KPmzFjbswozXyW0DnIYA==",
|
||||
"cpu": [
|
||||
"mips64el"
|
||||
],
|
||||
"dev": true,
|
||||
"license": "Apache-2.0",
|
||||
"optional": true,
|
||||
"os": [
|
||||
"linux"
|
||||
],
|
||||
"engines": {
|
||||
"node": ">=16.20.0"
|
||||
}
|
||||
},
|
||||
"node_modules/@typescript/typescript-linux-ppc64": {
|
||||
"version": "7.0.2",
|
||||
"resolved": "https://registry.npmjs.org/@typescript/typescript-linux-ppc64/-/typescript-linux-ppc64-7.0.2.tgz",
|
||||
"integrity": "sha512-DORx5b3sd/4S7eayxm4FQv+A7CrkUIGRaHiwI8oiHTAI1fAPWhF4J0vAlkC8biAlHSVVwxMQ3tjZ2/DVbnQiiA==",
|
||||
"cpu": [
|
||||
"ppc64"
|
||||
],
|
||||
"dev": true,
|
||||
"license": "Apache-2.0",
|
||||
"optional": true,
|
||||
"os": [
|
||||
"linux"
|
||||
],
|
||||
"engines": {
|
||||
"node": ">=16.20.0"
|
||||
}
|
||||
},
|
||||
"node_modules/@typescript/typescript-linux-riscv64": {
|
||||
"version": "7.0.2",
|
||||
"resolved": "https://registry.npmjs.org/@typescript/typescript-linux-riscv64/-/typescript-linux-riscv64-7.0.2.tgz",
|
||||
"integrity": "sha512-wf0jqEDOjrPRnKwYRyyJDRo11KMbvMFrU+q4zqKyChODBzvlkbhNQfKvLxQCcwTpdDaXSHZTVuh0JoCrKCUMHQ==",
|
||||
"cpu": [
|
||||
"riscv64"
|
||||
],
|
||||
"dev": true,
|
||||
"license": "Apache-2.0",
|
||||
"optional": true,
|
||||
"os": [
|
||||
"linux"
|
||||
],
|
||||
"engines": {
|
||||
"node": ">=16.20.0"
|
||||
}
|
||||
},
|
||||
"node_modules/@typescript/typescript-linux-s390x": {
|
||||
"version": "7.0.2",
|
||||
"resolved": "https://registry.npmjs.org/@typescript/typescript-linux-s390x/-/typescript-linux-s390x-7.0.2.tgz",
|
||||
"integrity": "sha512-IkwJc3L7yhytWd/ewjyxNDfOmswCm9GWMJT/ue/dU4aZNbwZeYAetq42VyLmsmSjvoX7z74X6ZaYCtzAr0EuGw==",
|
||||
"cpu": [
|
||||
"s390x"
|
||||
],
|
||||
"dev": true,
|
||||
"license": "Apache-2.0",
|
||||
"optional": true,
|
||||
"os": [
|
||||
"linux"
|
||||
],
|
||||
"engines": {
|
||||
"node": ">=16.20.0"
|
||||
}
|
||||
},
|
||||
"node_modules/@typescript/typescript-linux-x64": {
|
||||
"version": "7.0.2",
|
||||
"resolved": "https://registry.npmjs.org/@typescript/typescript-linux-x64/-/typescript-linux-x64-7.0.2.tgz",
|
||||
"integrity": "sha512-EYdf2cNg7rgCWJnxCdJ+F3V39O8ihb37eHAu1LK8oAFizgTQbPOK7zHHXbPt8rX24COqODXeI3sIf0fCXG7H/A==",
|
||||
"cpu": [
|
||||
"x64"
|
||||
],
|
||||
"dev": true,
|
||||
"license": "Apache-2.0",
|
||||
"optional": true,
|
||||
"os": [
|
||||
"linux"
|
||||
],
|
||||
"engines": {
|
||||
"node": ">=16.20.0"
|
||||
}
|
||||
},
|
||||
"node_modules/@typescript/typescript-netbsd-arm64": {
|
||||
"version": "7.0.2",
|
||||
"resolved": "https://registry.npmjs.org/@typescript/typescript-netbsd-arm64/-/typescript-netbsd-arm64-7.0.2.tgz",
|
||||
"integrity": "sha512-+polYF4MF04aPpO5FTkHran9yUQDSXqy5GiSDKpsll5jy3l3+g9QLhpf39T+ePtefhXLOGrLl0QIjkQP6VnelA==",
|
||||
"cpu": [
|
||||
"arm64"
|
||||
],
|
||||
"dev": true,
|
||||
"license": "Apache-2.0",
|
||||
"optional": true,
|
||||
"os": [
|
||||
"netbsd"
|
||||
],
|
||||
"engines": {
|
||||
"node": ">=16.20.0"
|
||||
}
|
||||
},
|
||||
"node_modules/@typescript/typescript-netbsd-x64": {
|
||||
"version": "7.0.2",
|
||||
"resolved": "https://registry.npmjs.org/@typescript/typescript-netbsd-x64/-/typescript-netbsd-x64-7.0.2.tgz",
|
||||
"integrity": "sha512-8YIT0EHM/3dq10ZOVF/A7pc/YSMtbcecct4rWtexrnSCHOPcpC2KTLXfTCR6vDpnSiY12heNb1GiN/wu+T/FyA==",
|
||||
"cpu": [
|
||||
"x64"
|
||||
],
|
||||
"dev": true,
|
||||
"license": "Apache-2.0",
|
||||
"optional": true,
|
||||
"os": [
|
||||
"netbsd"
|
||||
],
|
||||
"engines": {
|
||||
"node": ">=16.20.0"
|
||||
}
|
||||
},
|
||||
"node_modules/@typescript/typescript-openbsd-arm64": {
|
||||
"version": "7.0.2",
|
||||
"resolved": "https://registry.npmjs.org/@typescript/typescript-openbsd-arm64/-/typescript-openbsd-arm64-7.0.2.tgz",
|
||||
"integrity": "sha512-APT8+ClYnuYm1u9+kgGXoMj2VzWzcymwh2gNSQVySHfkRDGOTVkoWLjCmOQSaO+PoqQ57B0flRp9SA+7GnnkzQ==",
|
||||
"cpu": [
|
||||
"arm64"
|
||||
],
|
||||
"dev": true,
|
||||
"license": "Apache-2.0",
|
||||
"optional": true,
|
||||
"os": [
|
||||
"openbsd"
|
||||
],
|
||||
"engines": {
|
||||
"node": ">=16.20.0"
|
||||
}
|
||||
},
|
||||
"node_modules/@typescript/typescript-openbsd-x64": {
|
||||
"version": "7.0.2",
|
||||
"resolved": "https://registry.npmjs.org/@typescript/typescript-openbsd-x64/-/typescript-openbsd-x64-7.0.2.tgz",
|
||||
"integrity": "sha512-yX7s+Q0Dln0Dt9tEzZsAjXXR/+ytBM7AlglaqyeMPxQszJ1JhlJdZ6jLA+IzldHtflX81em7lDao1xXu+aRRkg==",
|
||||
"cpu": [
|
||||
"x64"
|
||||
],
|
||||
"dev": true,
|
||||
"license": "Apache-2.0",
|
||||
"optional": true,
|
||||
"os": [
|
||||
"openbsd"
|
||||
],
|
||||
"engines": {
|
||||
"node": ">=16.20.0"
|
||||
}
|
||||
},
|
||||
"node_modules/@typescript/typescript-sunos-x64": {
|
||||
"version": "7.0.2",
|
||||
"resolved": "https://registry.npmjs.org/@typescript/typescript-sunos-x64/-/typescript-sunos-x64-7.0.2.tgz",
|
||||
"integrity": "sha512-dLJDGaLZ1D4HPQn62u1n8mBDkJREwMsAkCdkwd4Ieqw+x3TUyTsqY0YiBCtE6H6OzzgGk3iuZ3vFWRS+E8/d1g==",
|
||||
"cpu": [
|
||||
"x64"
|
||||
],
|
||||
"dev": true,
|
||||
"license": "Apache-2.0",
|
||||
"optional": true,
|
||||
"os": [
|
||||
"sunos"
|
||||
],
|
||||
"engines": {
|
||||
"node": ">=16.20.0"
|
||||
}
|
||||
},
|
||||
"node_modules/@typescript/typescript-win32-arm64": {
|
||||
"version": "7.0.2",
|
||||
"resolved": "https://registry.npmjs.org/@typescript/typescript-win32-arm64/-/typescript-win32-arm64-7.0.2.tgz",
|
||||
"integrity": "sha512-Gyl1Vy6OsWesLzmq+EP0Fb7b4Nid5232AvcA2SFcdYreldpNtYFFofPjnt62y9hQy7VTaZp65ICJjuAQRaVcIQ==",
|
||||
"cpu": [
|
||||
"arm64"
|
||||
],
|
||||
"dev": true,
|
||||
"license": "Apache-2.0",
|
||||
"optional": true,
|
||||
"os": [
|
||||
"win32"
|
||||
],
|
||||
"engines": {
|
||||
"node": ">=16.20.0"
|
||||
}
|
||||
},
|
||||
"node_modules/@typescript/typescript-win32-x64": {
|
||||
"version": "7.0.2",
|
||||
"resolved": "https://registry.npmjs.org/@typescript/typescript-win32-x64/-/typescript-win32-x64-7.0.2.tgz",
|
||||
"integrity": "sha512-0BQ3HkAHHlKLSp1qRvf3SUhGpGsDuhB/jgFw75guyqbxJqEaS0Cw/VFO8i2nHglJUzQCRtMMR/IBAKE3ETMC4g==",
|
||||
"cpu": [
|
||||
"x64"
|
||||
],
|
||||
"dev": true,
|
||||
"license": "Apache-2.0",
|
||||
"optional": true,
|
||||
"os": [
|
||||
"win32"
|
||||
],
|
||||
"engines": {
|
||||
"node": ">=16.20.0"
|
||||
}
|
||||
},
|
||||
"node_modules/playwright": {
|
||||
"version": "1.64.1",
|
||||
"resolved": "https://registry.npmjs.org/playwright/-/playwright-1.64.1.tgz",
|
||||
"integrity": "sha512-HCrVhHr5QQuwYoswEUGTb7gGp522yflbv/ST8nF4sFWGuLjQCt17Q/EubpGGSNKYn1tsTCPZxqHhHCfVURu7UA==",
|
||||
"dev": true,
|
||||
"license": "Apache-2.0",
|
||||
"dependencies": {
|
||||
"playwright-core": "1.64.1"
|
||||
},
|
||||
"bin": {
|
||||
"playwright": "cli.js"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=20"
|
||||
}
|
||||
},
|
||||
"node_modules/playwright-core": {
|
||||
"version": "1.64.1",
|
||||
"resolved": "https://registry.npmjs.org/playwright-core/-/playwright-core-1.64.1.tgz",
|
||||
"integrity": "sha512-BC4s8y6SEVA6Q1rSxZTto8hI1KUI3xXlHGZgvGqP4IqPdI/a6FX9H29IDNz7vKo9l21b6LfYosScPL6M3xxyDQ==",
|
||||
"dev": true,
|
||||
"license": "Apache-2.0",
|
||||
"bin": {
|
||||
"playwright-core": "cli.js"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=20"
|
||||
}
|
||||
},
|
||||
"node_modules/typescript": {
|
||||
"version": "7.0.2",
|
||||
"resolved": "https://registry.npmjs.org/typescript/-/typescript-7.0.2.tgz",
|
||||
"integrity": "sha512-8FYau96o3NKOhbjKi/qNvG/W5jhzxkbdm5sj9AbZ/5T5sWqn3hJgLfGx27sRKZWTvyzCP8dLRBTf5tBTSRVUNA==",
|
||||
"dev": true,
|
||||
"license": "Apache-2.0",
|
||||
"bin": {
|
||||
"tsc": "bin/tsc"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=16.20.0"
|
||||
},
|
||||
"optionalDependencies": {
|
||||
"@typescript/typescript-aix-ppc64": "7.0.2",
|
||||
"@typescript/typescript-darwin-arm64": "7.0.2",
|
||||
"@typescript/typescript-darwin-x64": "7.0.2",
|
||||
"@typescript/typescript-freebsd-arm64": "7.0.2",
|
||||
"@typescript/typescript-freebsd-x64": "7.0.2",
|
||||
"@typescript/typescript-linux-arm": "7.0.2",
|
||||
"@typescript/typescript-linux-arm64": "7.0.2",
|
||||
"@typescript/typescript-linux-loong64": "7.0.2",
|
||||
"@typescript/typescript-linux-mips64el": "7.0.2",
|
||||
"@typescript/typescript-linux-ppc64": "7.0.2",
|
||||
"@typescript/typescript-linux-riscv64": "7.0.2",
|
||||
"@typescript/typescript-linux-s390x": "7.0.2",
|
||||
"@typescript/typescript-linux-x64": "7.0.2",
|
||||
"@typescript/typescript-netbsd-arm64": "7.0.2",
|
||||
"@typescript/typescript-netbsd-x64": "7.0.2",
|
||||
"@typescript/typescript-openbsd-arm64": "7.0.2",
|
||||
"@typescript/typescript-openbsd-x64": "7.0.2",
|
||||
"@typescript/typescript-sunos-x64": "7.0.2",
|
||||
"@typescript/typescript-win32-arm64": "7.0.2",
|
||||
"@typescript/typescript-win32-x64": "7.0.2"
|
||||
}
|
||||
},
|
||||
"node_modules/undici-types": {
|
||||
"version": "8.9.0",
|
||||
"resolved": "https://registry.npmjs.org/undici-types/-/undici-types-8.9.0.tgz",
|
||||
"integrity": "sha512-KTDyRTYX8sWmKXAikPHHSyc63CRPETMctyjKFupcC6OBLXT3xsN0e9aF7m+mIXutFWpUXuedtowG7iLOzp0kQg==",
|
||||
"dev": true,
|
||||
"license": "MIT"
|
||||
}
|
||||
}
|
||||
}
|
||||
13
perf/e2e/package.json
Normal file
13
perf/e2e/package.json
Normal file
@ -0,0 +1,13 @@
|
||||
{
|
||||
"name": "perf-e2e",
|
||||
"private": true,
|
||||
"version": "0.0.0",
|
||||
"scripts": {
|
||||
"test": "playwright test"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@playwright/test": "1.64.1",
|
||||
"@types/node": "26.6.5",
|
||||
"typescript": "7.0.2"
|
||||
}
|
||||
}
|
||||
14
perf/e2e/playwright.config.ts
Normal file
14
perf/e2e/playwright.config.ts
Normal file
@ -0,0 +1,14 @@
|
||||
import { defineConfig } from '@playwright/test';
|
||||
|
||||
export default defineConfig({
|
||||
testDir: './tests',
|
||||
workers: 1,
|
||||
fullyParallel: false,
|
||||
retries: 0,
|
||||
timeout: 10 * 60 * 1000,
|
||||
reporter: 'list',
|
||||
use: {
|
||||
browserName: 'chromium',
|
||||
baseURL: process.env.PERF_WEB ?? 'http://localhost:8080',
|
||||
},
|
||||
});
|
||||
68
perf/e2e/tests/harness.spec.ts
Normal file
68
perf/e2e/tests/harness.spec.ts
Normal file
@ -0,0 +1,68 @@
|
||||
import { test, expect, chromium } from '@playwright/test';
|
||||
import { execFileSync } from 'node:child_process';
|
||||
import { mkdtempSync, rmSync } from 'node:fs';
|
||||
import { tmpdir } from 'node:os';
|
||||
import path from 'node:path';
|
||||
import { PROBE_SCRIPT } from '../lib/probe';
|
||||
import { throttle } from '../lib/cdp';
|
||||
import { append } from '../lib/results';
|
||||
|
||||
const BUSY = 'file://' + path.resolve(__dirname, '../fixtures/busy.html');
|
||||
const PERF_DIR = path.resolve(__dirname, '../..');
|
||||
|
||||
test('probe detects long task', async ({ page }) => {
|
||||
await page.addInitScript(PROBE_SCRIPT);
|
||||
await page.goto(BUSY);
|
||||
await page.evaluate(() => (window as any).__perf.start());
|
||||
await page.click('#busy');
|
||||
const m = await page.evaluate(() => (window as any).__perf.stop());
|
||||
expect(m.long_task_n).toBeGreaterThanOrEqual(1);
|
||||
expect(m.long_task_ms).toBeGreaterThanOrEqual(100);
|
||||
expect(m.frame_n).toBeGreaterThan(0);
|
||||
expect(m.duration_ms).toBeGreaterThan(0);
|
||||
});
|
||||
|
||||
test('throttle slows script', async ({ page }) => {
|
||||
await page.goto(BUSY);
|
||||
const loop = () => page.evaluate(() => {
|
||||
const t = performance.now();
|
||||
let x = 0;
|
||||
for (let i = 0; i < 3e7; i++) x += i % 7;
|
||||
return performance.now() - t + x * 0;
|
||||
});
|
||||
await throttle(page, 1);
|
||||
const base = await loop();
|
||||
await throttle(page, 4);
|
||||
const slow = await loop();
|
||||
expect(slow).toBeGreaterThanOrEqual(2 * base);
|
||||
});
|
||||
|
||||
test('fresh context isolation', async ({ browser }) => {
|
||||
const a = await browser.newContext();
|
||||
const pa = await a.newPage();
|
||||
await pa.goto(BUSY);
|
||||
await pa.evaluate(() => localStorage.setItem('k', 'v'));
|
||||
const b = await browser.newContext();
|
||||
const pb = await b.newPage();
|
||||
await pb.goto(BUSY);
|
||||
expect(await pb.evaluate(() => localStorage.getItem('k'))).toBeNull();
|
||||
await a.close();
|
||||
await b.close();
|
||||
});
|
||||
|
||||
test('results format', async () => {
|
||||
const tmp = mkdtempSync(path.join(tmpdir(), 'perf-'));
|
||||
const runDir = path.join(tmp, `harness-${Date.now()}`);
|
||||
process.env.PERF_RESULTS_DIR = runDir;
|
||||
append({
|
||||
scenario: 'harness', target: 'prod', role: 'admin', scale: 1, cpu_rate: 1, repeat: 0,
|
||||
metrics: { long_task_ms: 123.4 }, artifacts: [],
|
||||
});
|
||||
const py = path.join(PERF_DIR, '.venv/bin/python');
|
||||
const out = execFileSync(py, ['-m', 'perfkit.aggregate', runDir], { cwd: PERF_DIR }).toString().trim();
|
||||
const summary = JSON.parse(execFileSync('cat', [path.join(out, 'summary.json')]).toString());
|
||||
expect(summary.groups).toHaveLength(1);
|
||||
expect(summary.groups[0].n).toBe(1);
|
||||
rmSync(tmp, { recursive: true, force: true });
|
||||
rmSync(out, { recursive: true, force: true });
|
||||
});
|
||||
Loading…
x
Reference in New Issue
Block a user