DFiP_Budget_planing/perf/e2e/scenarios/svod2-memory.spec.ts
Andrey Epifancev 9272ea2527 perf: правки финального ревью фазы 3 (I-4, I-5, M-11, M-13, M-14, T15.2, T17.1, T17.3)
Селектор строк данных свода 2 исключает скелетоны MRT, settle ждёт 0 скелетонов;
first_rows_ms через waitForFunction(raf); счётчики загрузки не пишутся в запись фильтра;
scale_compare различает view/search; api_split не считает строки для facets;
README: порядок до/после и новые метрики.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-29 00:46:05 +04:00

111 lines
5.7 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

/**
* Сценарий памяти свода 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, 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<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);
}
}
/** Ждём окончания загрузки: число строк, заглушек и скелетонов не меняются 3 опроса подряд (интервал 300 мс). */
async function settle(page: Page): Promise<number> {
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<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 {
// выбор и данные хранятся по вкладкам (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();
}
});
}