трейс и первая оптимизация

This commit is contained in:
gorbenko.pavel
2026-08-20 16:40:05 +05:00
parent e90a8ea3cc
commit c6c549eae9
9 changed files with 690 additions and 155 deletions
@@ -0,0 +1,196 @@
/**
* Замер времени ЭТАПОВ загрузки — «на что именно ушло ожидание», а не просто
* «страница долго грузится».
*
* Зачем отдельно от вкладки Network: браузер показывает запросы, но не
* логические этапы. Из списка URL не видно, что десяток одинаковых
* `.../rootfolder` — это резолв адресов списков согласования по партициям, а
* два одинаковых Search-запроса подряд — это один и тот же индекс, собранный
* дважды (счётчиками меню и самой таблицей). Метки дают этот срез.
*
* Включение/выключение БЕЗ пересборки — из консоли браузера:
* window.rpPerf = true // включить
* window.rpPerf = false // выключить
*
* Флаг ЗАПОМИНАЕТСЯ в localStorage: выставил один раз — работает и после
* перезагрузки страницы, и завтра. Разбор производительности обычно идёт
* несколькими заходами с F5 между ними, и переписывать флаг в консоль каждый
* раз — ровно та мелочь, из-за которой замером перестают пользоваться.
*
* Сделано через свойство-аксессор на window, а не через отдельную функцию
* setPerf(): в консоли естественно написать `window.rpPerf = true`, и это
* должно работать. Сеттер кладёт значение в localStorage, геттер отдаёт
* текущее — снаружи флаг выглядит обычным полем.
*
* По умолчанию выключено: предыдущая версия этого модуля (коммит 7bd540e)
* писала в консоль всегда, без флага, и её пришлось снимать целиком вместе со
* всеми обёртками. С флагом обёртки могут жить в коде постоянно — выключенный
* traced() не делает ничего, кроме вызова переданной функции.
*/
const DEFAULT_PERF: boolean = false;
const PREFIX = '[Перф]';
// Ключ в localStorage совпадает с именем флага — чтобы в DevTools → Application
// его можно было найти и снять, не зная про этот модуль.
const STORAGE_KEY = 'rpPerf';
/** Накопленное по одной метке за текущую верхнеуровневую операцию. */
interface IStat {
calls: number;
totalMs: number;
maxMs: number;
}
const stats = new Map<string, IStat>();
// Глубина открытых perfScope. Сводка печатается, когда закрылась ПОСЛЕДНЯЯ
// внешняя операция: этапы почти всегда идут параллельно (Promise.all), и
// печатать таблицу на каждом вложенном завершении значило бы печатать её
// десятками кусков. Если две верхнеуровневые операции наложились по времени
// (типичный случай: счётчики меню и загрузка таблицы стартуют вместе), их
// метки попадут в одну таблицу — это и нужно, там как раз видны дубли.
let openScopes = 0;
let scopeStart = 0;
let scopeLabels: string[] = [];
// localStorage может быть недоступен (приватный режим, политика браузера,
// iframe с ограничениями) — отладочный флаг не повод падать, поэтому обе
// операции обёрнуты и молча откатываются к значению в памяти.
function readStoredFlag(): boolean | undefined {
try {
const raw = window.localStorage.getItem(STORAGE_KEY);
if (raw === null) return undefined;
return raw === 'true';
} catch {
return undefined;
}
}
function writeStoredFlag(value: boolean): void {
try {
window.localStorage.setItem(STORAGE_KEY, value ? 'true' : 'false');
} catch {
// Значение всё равно применится — просто не переживёт перезагрузку.
}
}
let enabled: boolean = readStoredFlag() ?? DEFAULT_PERF;
function perfEnabled(): boolean {
return enabled;
}
/**
* Вешает window.rpPerf как свойство с геттером и сеттером.
*
* Вызывается один раз при загрузке модуля. Значение, которое кто-то успел
* присвоить ДО этого (например, в снипете, выполненном раньше), не теряем —
* подхватываем и запоминаем.
*/
function installFlag(): void {
const w = window as unknown as { rpPerf?: boolean };
const existing = Object.getOwnPropertyDescriptor(w, 'rpPerf');
if (existing && existing.get) return;
const preset = typeof w.rpPerf === 'boolean' ? w.rpPerf : undefined;
Object.defineProperty(w, 'rpPerf', {
configurable: true,
get: (): boolean => enabled,
set: (value: unknown): void => {
enabled = !!value;
writeStoredFlag(enabled);
console.log(`${PREFIX} замер ${enabled ? 'включён' : 'выключен'} (запомнено до отмены)`);
}
});
if (preset !== undefined) {
enabled = preset;
writeStoredFlag(preset);
} else if (enabled) {
// Иначе легко забыть, что флаг остался включённым с прошлого раза, и
// потом гадать, откуда в консоли таблицы.
console.log(`${PREFIX} замер включён (запомнен ранее). Выключить: window.rpPerf = false`);
}
}
installFlag();
function record(label: string, ms: number): void {
const stat = stats.get(label) ?? { calls: 0, totalMs: 0, maxMs: 0 };
stat.calls += 1;
stat.totalMs += ms;
stat.maxMs = Math.max(stat.maxMs, ms);
stats.set(label, stat);
}
/**
* Обернуть один этап замером. Метку делай осмысленной и повторяемой: одинаковые
* этапы разных партиций должны попадать в ОДНУ строку сводки (число вызовов
* важнее, чем какая именно партиция), поэтому в метку не надо подставлять
* listId — только имя этапа и, при необходимости, тип заявки.
*/
export async function traced<T>(label: string, fn: () => Promise<T>): Promise<T> {
if (!perfEnabled()) return fn();
const start = performance.now();
try {
return await fn();
} finally {
record(label, performance.now() - start);
}
}
/**
* Верхнеуровневая операция (открытие вкладки, счётчики меню, открытие заявки).
* Когда она завершается, печатается сводная таблица по всем меткам внутри.
*
* Смотреть надо на ДВА числа сразу: «сумма» показывает, куда ушла работа, а
* «стена» — сколько на самом деле ждал пользователь. Этапы идут параллельно,
* поэтому сумма бывает в разы больше стены, и оптимизировать по одной только
* сумме — значит чинить не то.
*/
export async function perfScope<T>(label: string, fn: () => Promise<T>): Promise<T> {
if (!perfEnabled()) return fn();
if (openScopes === 0) {
stats.clear();
scopeStart = performance.now();
scopeLabels = [];
}
openScopes += 1;
scopeLabels.push(label);
try {
return await fn();
} finally {
openScopes -= 1;
if (openScopes === 0) print(performance.now() - scopeStart);
}
}
function print(wallMs: number): void {
if (stats.size === 0) return;
const rows = Array.from(stats.entries())
.map(([label, s]) => ({
'этап': label,
'вызовов': s.calls,
'сумма, мс': Math.round(s.totalMs),
'макс, мс': Math.round(s.maxMs)
}))
.sort((a, b) => b['сумма, мс'] - a['сумма, мс']);
const total = rows.reduce((acc, r) => acc + r['сумма, мс'], 0);
console.log(
`${PREFIX} ${scopeLabels.join(' + ')}: стена ${Math.round(wallMs)} мс, сумма по этапам ${total} мс`
);
console.table(rows);
stats.clear();
scopeLabels = [];
}