/** * Сценарий памяти свода 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, SVOD2_PLACEHOLDER_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); } } /** Ждём окончания загрузки: число строк, заглушек и скелетонов не меняются 3 опроса подряд (интервал 300 мс). */ async function settle(page: Page): Promise { let prev = ''; let same = 0; const t0 = Date.now(); for (;;) { const cur = await page.evaluate( ([sel, rs, ph]) => `${document.querySelectorAll(`${sel} ${rs}`).length}/${document.querySelectorAll(`${sel} ${ph}`).length}/${document.querySelectorAll(`${sel} .MuiSkeleton-root`).length}`, [CONTAINER, SVOD2_DATA_ROW_SELECTOR, SVOD2_PLACEHOLDER_SELECTOR], ); same = cur === prev ? same + 1 : 0; prev = cur; if (same >= 3 && cur.endsWith('/0')) return Number(cur.split('/')[0]); // готово: строки стабильны и в контейнере 0 скелетонов if (Date.now() - t0 > LOAD_TIMEOUT) throw new Error(`settle: строки не стабилизировались за ${LOAD_TIMEOUT} мс (${cur})`); await page.waitForTimeout(300); } } /** «Все ССП/РФ» + «Загрузить» (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 { // выбор и данные хранятся по вкладкам (SvodContext, useSvod2Pages): повторный loadAll снял бы «Все ССП/РФ» await page.getByRole('tab').nth(0).click(); rows = await waitRows(page, point); } rows = await settle(page); await sample(point, rows); } } finally { await ctx.close(); } }); }