трейс и первая оптимизация
This commit is contained in:
@@ -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 = [];
|
||||
}
|
||||
Reference in New Issue
Block a user