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

83 lines
3.2 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 (без импортов, стираемый 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/<sheet>/...` и `/svod/2/<sheet>/<year>/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<string, number> = 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, string | undefined>): string[] {
return env.PERF_SVOD2_MEM_SCROLL === '1' ? ['go', 'rf', 'rf_scrolled', 'final'] : ['go', 'rf', 'final'];
}