/** Чистые помощники замеров свода 2 (без импортов, стираемый TS: проверяются `node --test`). */ /** Строка данных: не заглушка окна и без скелетонов MRT (isLoading рисует .MuiSkeleton-root в ячейках даже при непустых данных). */ export const SVOD2_DATA_ROW_SELECTOR = 'tbody tr:not([data-svod2-placeholder]):not(:has(.MuiSkeleton-root))'; export const SVOD2_PLACEHOLDER_SELECTOR = 'tbody tr[data-svod2-placeholder]'; /** Ответы свода 2: страница `/svod/2//...` и `/svod/2///facets`. */ export function svod2RespRe(sheet: string): RegExp { return new RegExp(`/api/v1/svod/2/${sheet}/`); } /** Запрос свода 2 с параметром search= (серверный поиск). */ export function isSvod2SearchUrl(url: string): boolean { const q = url.indexOf('?'); if (q < 0 || !/\/api\/v1\/svod\/2\//.test(url.slice(0, q))) return false; return url .slice(q + 1) .split('#')[0] .split('&') .some((p) => p === 'search' || p.startsWith('search=')); } /** Число запросов страниц (offset= в query). */ export function countPagesRequested(urls: string[]): number { let n = 0; for (const u of urls) { const q = u.indexOf('?'); if (q < 0) continue; if (u.slice(q + 1).split('#')[0].split('&').some((p) => p === 'offset' || p.startsWith('offset='))) n++; } return n; } /** Доля кадров, где видна хотя бы одна заглушка. */ export function placeholderShare(samples: boolean[]): number { if (samples.length === 0) return 0; let k = 0; for (const s of samples) if (s) k++; return k / samples.length; } /** Байты ответов по requestId: sheet - максимум одного ответа, total - сумма, first - сумма до markFirstRows. */ export class ByteCounter { perRequest: Map = new Map(); totalBytes = 0; firstBytes = 0; marked = false; add(requestId: string, bytes: number): void { this.perRequest.set(requestId, (this.perRequest.get(requestId) ?? 0) + bytes); this.totalBytes += bytes; if (!this.marked) this.firstBytes += bytes; } markFirstRows(): void { this.marked = true; } get sheet(): number { let m = 0; for (const v of this.perRequest.values()) if (v > m) m = v; return m; } get total(): number { return this.totalBytes; } get first(): number { return this.firstBytes; } } /** filter_result_ms: от fill до стабилизации; ответ поиска (если был) не должен быть позже конца; таймаут ожидания (respAt=null) не учитывается. */ 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']; }