- C1: trace только на повторе 0 при числе повторов > 1 (lib/trace.ts shouldTrace) в scroll и scroll-coauthor: admin-контроль, control4x и PERF_S2_REPEATS=1 больше не теряют единственную запись (traced=1). - I1: масштаб 2 - PERF_LOAD_TIMEOUT_MS=40 мин и PERF_TEST_TIMEOUT_MS=90 мин, если не заданы. - I2: шаг aggregate падает при сбое perfkit.aggregate/копирования и если summary.json не создан; тесты run-all (perf/tests/test_run_all.py). - M2: после modals_profiling останавливается preview :8081. - README, design.md: решения раунда. Refs #202 Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
115 lines
4.8 KiB
TypeScript
115 lines
4.8 KiB
TypeScript
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, startProbe } from '../lib/probe';
|
|
import { throttle } from '../lib/cdp';
|
|
import { append } from '../lib/results';
|
|
import { shouldTrace } from '../lib/trace';
|
|
|
|
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('probe: main_thread_ms (CDP TaskDuration) и frame_excess_ms учитывают задачу 120 мс', async ({ page }) => {
|
|
await page.addInitScript(PROBE_SCRIPT);
|
|
await page.goto(BUSY);
|
|
const p = await startProbe(page);
|
|
await page.click('#busy');
|
|
const m = await p.stop();
|
|
expect(m.main_thread_ms).toBeGreaterThanOrEqual(100);
|
|
expect(m.main_thread_ms).toBeLessThan(m.duration_ms);
|
|
expect(m.frame_excess_ms).toBeGreaterThanOrEqual(80); // кадр ~120 мс сверх бюджета 16,7 мс
|
|
expect(m.long_task_ms).toBeGreaterThanOrEqual(100);
|
|
});
|
|
|
|
test('probe: main_thread_ms на простое - малый', async ({ page }) => {
|
|
await page.addInitScript(PROBE_SCRIPT);
|
|
await page.goto(BUSY);
|
|
const p = await startProbe(page);
|
|
await page.waitForTimeout(300);
|
|
const m = await p.stop();
|
|
expect(m.main_thread_ms).toBeGreaterThanOrEqual(0);
|
|
expect(m.main_thread_ms).toBeLessThan(50);
|
|
});
|
|
|
|
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 });
|
|
});
|
|
|
|
test('globalTeardown: PERF_SQLSTATS_TAG -> dump --out <run_dir>/sql/<tag>', async () => {
|
|
const { dumpArgs } = await import('../lib/global-teardown');
|
|
process.env.PERF_RESULTS_DIR = '/tmp/perf-rd';
|
|
try {
|
|
delete process.env.PERF_SQLSTATS_TAG;
|
|
expect(dumpArgs()).toEqual(['dump', '/tmp/perf-rd']);
|
|
process.env.PERF_SQLSTATS_TAG = 'modals profiling';
|
|
expect(dumpArgs()).toEqual(['dump', '/tmp/perf-rd', '--out', '/tmp/perf-rd/sql/modals_profiling']);
|
|
} finally {
|
|
delete process.env.PERF_SQLSTATS_TAG;
|
|
delete process.env.PERF_RESULTS_DIR;
|
|
}
|
|
});
|
|
|
|
test('shouldTrace: trace только на повторе 0 и только если повторов > 1 (иначе aggregate исключил бы единственную запись)', () => {
|
|
expect(shouldTrace({ repeat: 0, repeats: 3 }, {})).toBe(true);
|
|
expect(shouldTrace({ repeat: 1, repeats: 3 }, {})).toBe(false);
|
|
expect(shouldTrace({ repeat: 0, repeats: 1 }, {})).toBe(false); // admin-контроль, control4x, PERF_S2_REPEATS=1
|
|
expect(shouldTrace({ repeat: 0, repeats: 2 }, {})).toBe(true);
|
|
expect(shouldTrace({ repeat: 0, repeats: 3, filter: true }, {})).toBe(false); // смена фильтра не трассируется
|
|
expect(shouldTrace({ repeat: 0, repeats: 3 }, { PERF_TRACE: '0' })).toBe(false);
|
|
});
|