perf: сценарий памяти свода 2 (GO «все» → RF «все» → GO)

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
Andrey Epifancev 2026-10-29 00:29:50 +04:00
parent 5dc6662767
commit 3363291801
3 changed files with 107 additions and 0 deletions

View File

@ -74,3 +74,8 @@ export class ByteCounter {
export function filterResultMs(startAt: number, stableAt: number, respAt: number | null): number {
return Math.max(stableAt, respAt ?? 0) - startAt;
}
/** Точки замера памяти свода 2: GO «все» -> RF «все» -> (опц. прокрутка RF) -> снова GO. */
export function memoryPoints(env: Record<string, string | undefined>): string[] {
return env.PERF_SVOD2_MEM_SCROLL === '1' ? ['go', 'rf', 'rf_scrolled', 'final'] : ['go', 'rf', 'final'];
}

View File

@ -0,0 +1,92 @@
/**
* Сценарий памяти свода 2 (Task 16): роль dfip, свежий контекст на каждый повтор, одна SPA-сессия внутри повтора.
* Точки (lib/svod2.ts memoryPoints): go (вкладка GO, «Все ССП/РФ», «Загрузить») -> rf (вкладка RF, то же)
* [-> rf_scrolled при PERF_SVOD2_MEM_SCROLL=1] -> final (возврат на GO, «Загрузить»).
* В каждой точке gc -> heapUsed, domCounters. Запись: scenario "svod2-memory", target "svod2/<point>".
* Env: PERF_SCALE, PERF_RUN_ID, PERF_REPEATS (3), PERF_SVOD2_MEM_SCROLL, PERF_LOAD_TIMEOUT_MS, PERF_MAX_STEPS,
* PERF_TEST_TIMEOUT_MS.
*/
import { test, type Page } from '@playwright/test';
import { loadManifest, scale } from '../lib/manifest';
import { loginAs, WEB } from '../lib/auth';
import { throttle, heapUsed, domCounters } from '../lib/cdp';
import { append } from '../lib/results';
import { scrollThrough } from '../lib/scroll';
import { SVOD2_DATA_ROW_SELECTOR, memoryPoints } from '../lib/svod2';
const CONTAINER = '.MuiTableContainer-root';
const MIN_ROWS = 20;
const REPEATS = Number(process.env.PERF_REPEATS ?? 3);
const LOAD_TIMEOUT = Number(process.env.PERF_LOAD_TIMEOUT_MS ?? 900_000);
const MAX_STEPS = Number(process.env.PERF_MAX_STEPS ?? 5000);
async function waitRows(page: Page, what: string): Promise<number> {
const t0 = Date.now();
for (;;) {
const n = await page.evaluate(([sel, rs]) => document.querySelectorAll(`${sel} ${rs}`).length, [CONTAINER, SVOD2_DATA_ROW_SELECTOR]);
if (n >= MIN_ROWS) return n;
if (Date.now() - t0 > LOAD_TIMEOUT) throw new Error(`${what}: rows<${MIN_ROWS} за ${LOAD_TIMEOUT} мс (rows=${n})`);
await page.waitForTimeout(500);
}
}
/** «Все ССП/РФ» + «Загрузить» (Svod2Page.jsx), как в scroll.spec.ts. */
async function loadAll(page: Page): Promise<void> {
await page.getByLabel('ССП/РФ').click();
await page.getByRole('option', { name: 'Все ССП/РФ' }).click();
await page.keyboard.press('Escape');
await page.getByRole('button', { name: 'Загрузить' }).click();
}
for (let repeat = 0; repeat < REPEATS; repeat++) {
test(`svod2-memory: dfip #${repeat}`, async ({ browser }) => {
test.setTimeout(Number(process.env.PERF_TEST_TIMEOUT_MS ?? 3600_000));
const user = loadManifest().users['dfip'];
if (!user) throw new Error('в манифесте нет пользователя dfip');
const points = memoryPoints(process.env);
const ctx = await browser.newContext({ viewport: { width: 1600, height: 900 } });
try {
await loginAs(ctx, user);
const page = await ctx.newPage();
page.on('pageerror', (e) => console.log('pageerror:', e.message));
await page.goto(WEB + '/svod/2');
await throttle(page, 1);
const sample = async (point: string, rows: number) => {
const heap = await heapUsed(page); // gc внутри
const dom = await domCounters(page);
append({
scenario: 'svod2-memory', target: `svod2/${point}`, role: 'dfip', scale: scale(), cpu_rate: 1, repeat,
metrics: { heap_used_mb: heap / 1048576, nodes: dom.nodes, documents: dom.documents, js_event_listeners: dom.jsEventListeners, rows_visible: rows },
artifacts: [],
});
console.log(`${point} #${repeat}: heap ${(heap / 1048576).toFixed(1)} MB, nodes ${dom.nodes}, listeners ${dom.jsEventListeners}`);
};
for (const point of points) {
let rows: number;
if (point === 'go') {
await page.getByRole('tab').nth(0).click();
await loadAll(page);
rows = await waitRows(page, point);
} else if (point === 'rf') {
await page.getByRole('tab').nth(1).click();
await loadAll(page);
rows = await waitRows(page, point);
} else if (point === 'rf_scrolled') {
const r = await scrollThrough(page, CONTAINER, 'y', 200, MAX_STEPS);
if (!r.complete) throw new Error(`rf_scrolled: скролл не дошёл до конца (${r.reason ?? 'incomplete'})`);
rows = await waitRows(page, point);
} else {
await page.getByRole('tab').nth(0).click();
await loadAll(page);
rows = await waitRows(page, point);
}
await page.waitForTimeout(500);
await sample(point, rows);
}
} finally {
await ctx.close();
}
});
}

View File

@ -7,6 +7,7 @@ import {
svod2RespRe,
countPagesRequested,
filterResultMs,
memoryPoints,
SVOD2_DATA_ROW_SELECTOR,
SVOD2_PLACEHOLDER_SELECTOR,
} from '../lib/svod2.ts';
@ -69,3 +70,12 @@ test('filterResultMs: нет запроса поиска - только стаб
assert.equal(filterResultMs(1000, 1700, 1200), 700);
assert.equal(filterResultMs(1000, 1700, 1900), 900);
});
test('memoryPoints: по умолчанию go, rf, final', () => {
assert.deepEqual(memoryPoints({}), ['go', 'rf', 'final']);
assert.deepEqual(memoryPoints({ PERF_SVOD2_MEM_SCROLL: '0' }), ['go', 'rf', 'final']);
});
test('memoryPoints: PERF_SVOD2_MEM_SCROLL=1 добавляет rf_scrolled', () => {
assert.deepEqual(memoryPoints({ PERF_SVOD2_MEM_SCROLL: '1' }), ['go', 'rf', 'rf_scrolled', 'final']);
});