отказ от лишнего реста
This commit is contained in:
@@ -67,12 +67,44 @@ interface IStat {
|
||||
maxMs: number;
|
||||
}
|
||||
|
||||
/** Накопленное по одной операции за ВСЕ её прогоны — база для среднего. */
|
||||
/**
|
||||
* Сэмплы одной метки по всем прогонам — база для МЕДИАНЫ, а не среднего.
|
||||
*
|
||||
* Среднее одним выбросом (случайный 429 с ретраем, холодный кеш на первом
|
||||
* заходе) утаскивает за собой всю картину надолго — а именно выбросы здесь
|
||||
* основной случай, не редкость. Медиана его игнорирует: важнее «как обычно»,
|
||||
* а не «как в сумме с той единственной аномалией».
|
||||
*/
|
||||
interface ILabelSamples {
|
||||
/** totalMs метки за каждый прогон, где она встретилась (метка не обязана быть в каждом). */
|
||||
totalMs: number[];
|
||||
calls: number[];
|
||||
/** Худший случай за всё время — в отличие от totalMs, это не медиана, а running max. */
|
||||
maxMs: number;
|
||||
}
|
||||
|
||||
// Сколько последних прогонов метки хранить ради медианы. Не безлимитный рост:
|
||||
// в долгой сессии старые сэмплы устаревают быстрее, чем на них смотрят, а лимит
|
||||
// защищает от утечки памяти на вкладке, оставленной открытой на весь день.
|
||||
const MAX_SAMPLES = 50;
|
||||
|
||||
function pushSample(list: number[], value: number): void {
|
||||
list.push(value);
|
||||
if (list.length > MAX_SAMPLES) list.shift();
|
||||
}
|
||||
|
||||
function median(values: number[]): number {
|
||||
if (values.length === 0) return 0;
|
||||
const sorted = [...values].sort((a, b) => a - b);
|
||||
const mid = Math.floor(sorted.length / 2);
|
||||
return sorted.length % 2 === 1 ? sorted[mid] : (sorted[mid - 1] + sorted[mid]) / 2;
|
||||
}
|
||||
|
||||
/** Накопленное по одной операции за ВСЕ её прогоны — база для медианы. */
|
||||
interface IRunHistory {
|
||||
runs: number;
|
||||
wallTotalMs: number;
|
||||
stages: Map<string, IStat>;
|
||||
requests: Map<string, IStat>;
|
||||
wallSamples: number[];
|
||||
stages: Map<string, ILabelSamples>;
|
||||
requests: Map<string, ILabelSamples>;
|
||||
}
|
||||
|
||||
// Текущий (ещё не напечатанный) прогон.
|
||||
@@ -142,6 +174,18 @@ function installFlag(): void {
|
||||
set: (value: unknown): void => {
|
||||
enabled = !!value;
|
||||
writeStoredFlag(enabled);
|
||||
if (!enabled) {
|
||||
// Тишина СРАЗУ, а не после того, что уже накопилось. Без этого operация,
|
||||
// стартовавшая ДО выключения (perfScope с сетевым шагом внутри) или
|
||||
// ждущая debounce-таймера (см. scheduleIdleFlush), всё равно печатала
|
||||
// итог ПОСЛЕ выключения — выглядело так, будто window.rpPerf = false
|
||||
// не действует.
|
||||
if (idleTimer !== undefined) {
|
||||
window.clearTimeout(idleTimer);
|
||||
idleTimer = undefined;
|
||||
}
|
||||
reset();
|
||||
}
|
||||
console.log(`${PREFIX} замер ${enabled ? 'включён' : 'выключен'} (запомнено до отмены)`);
|
||||
}
|
||||
});
|
||||
@@ -181,7 +225,7 @@ function scheduleIdleFlush(): void {
|
||||
if (idleTimer !== undefined) window.clearTimeout(idleTimer);
|
||||
idleTimer = window.setTimeout(() => {
|
||||
idleTimer = undefined;
|
||||
if (openScopes === 0) print();
|
||||
if (openScopes === 0 && perfEnabled()) print();
|
||||
}, IDLE_FLUSH_MS);
|
||||
}
|
||||
|
||||
@@ -223,7 +267,11 @@ export async function perfScope<T>(label: string, fn: () => Promise<T>): Promise
|
||||
return await fn();
|
||||
} finally {
|
||||
openScopes -= 1;
|
||||
if (openScopes === 0) print();
|
||||
// perfEnabled() — на выходе, а не только на входе: операцию могли выключить,
|
||||
// пока она ещё выполнялась (см. setter window.rpPerf выше), и тогда печатать
|
||||
// её итог уже не нужно — accumulated-данные к этому моменту сам setter уже
|
||||
// сбросил.
|
||||
if (openScopes === 0 && perfEnabled()) print();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -307,19 +355,19 @@ function reset(): void {
|
||||
* Ключ, под которым прогоны копятся в историю.
|
||||
*
|
||||
* Номер страницы из метки убираем: пролистать вкладку по страницам — самый
|
||||
* удобный способ набрать несколько замеров одной и той же операции, и усреднять
|
||||
* их надо ВМЕСТЕ. Иначе каждая страница завела бы свою историю из одного
|
||||
* прогона, и среднее никогда бы не набралось.
|
||||
* удобный способ набрать несколько замеров одной и той же операции, и считать
|
||||
* медиану надо по ним ВМЕСТЕ. Иначе каждая страница завела бы свою историю из
|
||||
* одного прогона, и медиане не по чему было бы считаться.
|
||||
*/
|
||||
function historyKey(title: string): string {
|
||||
return title.replace(/\s*стр\.\d+/g, '').trim();
|
||||
}
|
||||
|
||||
function mergeInto(target: Map<string, IStat>, source: Map<string, IStat>): void {
|
||||
function recordSamples(target: Map<string, ILabelSamples>, source: Map<string, IStat>): void {
|
||||
source.forEach((s, label) => {
|
||||
const t = target.get(label) ?? { calls: 0, totalMs: 0, maxMs: 0 };
|
||||
t.calls += s.calls;
|
||||
t.totalMs += s.totalMs;
|
||||
const t = target.get(label) ?? { totalMs: [], calls: [], maxMs: 0 };
|
||||
pushSample(t.totalMs, s.totalMs);
|
||||
pushSample(t.calls, s.calls);
|
||||
t.maxMs = Math.max(t.maxMs, s.maxMs);
|
||||
target.set(label, t);
|
||||
});
|
||||
@@ -346,8 +394,10 @@ function totalCalls(source: Map<string, IStat>): number {
|
||||
* window.rpPerfDetail(), и её зовут тогда, когда строка показала, что смотреть
|
||||
* есть на что.
|
||||
*
|
||||
* Среднее в строке важнее текущего значения: одиночный сетевой замер шумит
|
||||
* настолько, что по нему нельзя сказать, стало лучше или просто повезло.
|
||||
* Медиана в строке важнее текущего значения: одиночный сетевой замер шумит
|
||||
* настолько, что по нему нельзя сказать, стало лучше или просто повезло, а
|
||||
* среднее в этом же шуме утаскивает один выброс (холодный кеш, ретрай на 429)
|
||||
* далеко за пределы того, что происходит обычно.
|
||||
*/
|
||||
function print(): void {
|
||||
if (idleTimer !== undefined) {
|
||||
@@ -361,15 +411,13 @@ function print(): void {
|
||||
const key = historyKey(title);
|
||||
|
||||
const hist: IRunHistory = history.get(key) ?? {
|
||||
runs: 0,
|
||||
wallTotalMs: 0,
|
||||
stages: new Map<string, IStat>(),
|
||||
requests: new Map<string, IStat>()
|
||||
wallSamples: [],
|
||||
stages: new Map<string, ILabelSamples>(),
|
||||
requests: new Map<string, ILabelSamples>()
|
||||
};
|
||||
hist.runs += 1;
|
||||
hist.wallTotalMs += wallMs;
|
||||
mergeInto(hist.stages, stages);
|
||||
mergeInto(hist.requests, requests);
|
||||
pushSample(hist.wallSamples, wallMs);
|
||||
recordSamples(hist.stages, stages);
|
||||
recordSamples(hist.requests, requests);
|
||||
history.set(key, hist);
|
||||
|
||||
// Дольше всего — по этапам, если разметка есть; иначе по видам запросов
|
||||
@@ -378,7 +426,7 @@ function print(): void {
|
||||
|
||||
console.log(
|
||||
`${PREFIX} ${key} | ожидание ${Math.round(wallMs)} мс` +
|
||||
` (сред. ${Math.round(hist.wallTotalMs / hist.runs)} за ${hist.runs})` +
|
||||
` (мед. ${Math.round(median(hist.wallSamples))} за ${hist.wallSamples.length})` +
|
||||
` | запросов ${totalCalls(requests)}` +
|
||||
(top ? ` | дольше всего: ${top}` : '')
|
||||
);
|
||||
@@ -386,18 +434,20 @@ function print(): void {
|
||||
reset();
|
||||
}
|
||||
|
||||
/** Строки полной раскладки по одной операции — средние за все её прогоны. */
|
||||
/** Строки полной раскладки по одной операции — медианы за все её прогоны. */
|
||||
function detailLines(key: string, hist: IRunHistory): string[] {
|
||||
const lines = [`${PREFIX} ${key} - прогонов ${hist.runs}, ожидание в среднем ${Math.round(hist.wallTotalMs / hist.runs)} мс`];
|
||||
const lines = [
|
||||
`${PREFIX} ${key} - прогонов ${hist.wallSamples.length}, ожидание медиана ${Math.round(median(hist.wallSamples))} мс`
|
||||
];
|
||||
|
||||
const dump = (name: string, source: Map<string, IStat>): void => {
|
||||
const dump = (name: string, source: Map<string, ILabelSamples>): void => {
|
||||
if (source.size === 0) return;
|
||||
lines.push(` ${name} (в среднем за прогон)`);
|
||||
lines.push(` ${name} (медиана за прогон)`);
|
||||
Array.from(source.entries())
|
||||
.sort((a, b) => b[1].totalMs - a[1].totalMs)
|
||||
.sort((a, b) => median(b[1].totalMs) - median(a[1].totalMs))
|
||||
.forEach(([label, s]) => {
|
||||
lines.push(
|
||||
` ${Math.round(s.totalMs / hist.runs)} мс, вызовов ${(s.calls / hist.runs).toFixed(1)}, макс ${Math.round(s.maxMs)} - ${label}`
|
||||
` ${Math.round(median(s.totalMs))} мс, вызовов ${Math.round(median(s.calls))}, макс ${Math.round(s.maxMs)} - ${label}`
|
||||
);
|
||||
});
|
||||
};
|
||||
@@ -425,9 +475,9 @@ function installDetailApi(): void {
|
||||
return text;
|
||||
};
|
||||
|
||||
// Старые прогоны тянут среднее назад, и «стало быстрее» проявится в нём
|
||||
// только через несколько заходов — перед сравнением «до/после» серию надо
|
||||
// обнулять.
|
||||
// Старые прогоны остаются в окне (см. MAX_SAMPLES) и тянут медиану назад —
|
||||
// «стало быстрее» проявится в ней только после того, как их наберётся
|
||||
// больше половины окна. Перед сравнением «до/после» серию надо обнулять.
|
||||
w.rpPerfReset = (): void => {
|
||||
history.clear();
|
||||
reset();
|
||||
|
||||
Reference in New Issue
Block a user