diff --git a/perf/e2e/lib/svod2.ts b/perf/e2e/lib/svod2.ts index c135dc8..0707a19 100644 --- a/perf/e2e/lib/svod2.ts +++ b/perf/e2e/lib/svod2.ts @@ -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[] { + return env.PERF_SVOD2_MEM_SCROLL === '1' ? ['go', 'rf', 'rf_scrolled', 'final'] : ['go', 'rf', 'final']; +} diff --git a/perf/e2e/scenarios/svod2-memory.spec.ts b/perf/e2e/scenarios/svod2-memory.spec.ts new file mode 100644 index 0000000..f6476f0 --- /dev/null +++ b/perf/e2e/scenarios/svod2-memory.spec.ts @@ -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/". + * 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 { + 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 { + 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(); + } + }); +} diff --git a/perf/e2e/tests/svod2-metrics.test.ts b/perf/e2e/tests/svod2-metrics.test.ts index 404b2f4..55e7d82 100644 --- a/perf/e2e/tests/svod2-metrics.test.ts +++ b/perf/e2e/tests/svod2-metrics.test.ts @@ -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']); +});