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>
This commit is contained in:
parent
558ad815e0
commit
9272ea2527
@ -277,9 +277,12 @@ cd ..
|
||||
|
||||
1. `perf/stand.sh reset`, `perfkit.gen.run --scale 1`, `perfkit.gen.verify --scale 1`. Своды мерить до `grow`.
|
||||
2. Скопировать `results/manifest-s1.json` в оба worktree («до» из AURORA-1842, «после» из ветки PR 3).
|
||||
3. «До»: поднять `api`, `web-build`, `web` в worktree «до», снять замеры (команды ниже).
|
||||
Код сценариев один (из ветки PR 3): в временном worktree «до» (вершина 1842) выполнить
|
||||
`git checkout feat/236-aurora-1843-svod2-server-paging-perf -- perf/` без коммита; иначе «до» снимется старым `scroll.spec.ts` без новых метрик.
|
||||
3. «До»: `stand.sh reset/gen/verify/api/web-build/web/down` для «до» запускать из worktree «до»: БД остаётся на старой миграции
|
||||
(иначе 0053 уронит 6-аргументные `v_svod2_go/rf`, и API «до» пойдёт в новые функции). Поднять `api`, `web-build`, `web`, снять замеры (команды ниже).
|
||||
4. Остановить сервисы.
|
||||
5. «После»: `alembic upgrade head`, затем `api`, `web-build`, `web`; перед замером выполнить сверку (ниже); снять замеры.
|
||||
5. «После»: `stand.sh migrate` и все сервисы «после» запускать из worktree «после»: `alembic upgrade head`, затем `api`, `web-build`, `web`; перед замером выполнить сверку (ниже); снять замеры.
|
||||
6. То же на масштабе 2 (`PERF_S2_REPEATS=1`, см. «Полный прогон»).
|
||||
|
||||
Между «до» и «после» не вливать AURORA-1917 и AURORA-1836: на стенде не должно быть других изменений кода.
|
||||
@ -295,6 +298,17 @@ node analysis/svod2_search_oracle.mjs --check --api http://localhost:8000 \
|
||||
|
||||
Код выхода 1 или `CHECK_FAILED` означает расхождение с экраном «до»: замер «после» не снимать, сначала чинить код.
|
||||
|
||||
**Новые метрики** (в записях `scroll`, цели `svod/svod2_*`; на сборке «до» часть недоступна):
|
||||
|
||||
- `first_rows_ms` - от «Загрузить» до первых 20 строк с данными (без заглушек и скелетонов), опрос каждый кадр;
|
||||
- `json_bytes_sheet` - прежний смысл: максимум одного ответа; `json_bytes_total` - сумма всех ответов; `json_bytes_first` - сумма до первых строк;
|
||||
- `pages_requested` - число запросов страниц (`offset=`); на «до» всегда 0 (экран грузит всё одним запросом) - как число страниц с «после» не сравнивать;
|
||||
- `placeholder_frame_share` - доля кадров прокрутки, где видна строка-заглушка;
|
||||
- `filter_result_ms` - от ввода в поиск до стабилизации (и ответа сервера, если он был); пишется только в записи фильтра, там же нет `json_bytes_total/first` и `pages_requested` (они включили бы исходную загрузку).
|
||||
|
||||
Сценарий `svod2-memory`: записи `scenario=svod2-memory`, `target=svod2/<point>` (`go`, `rf`, `rf_scrolled` при `PERF_SVOD2_MEM_SCROLL=1`, `final`),
|
||||
метрики `heap_used_mb`, `nodes`, `documents`, `js_event_listeners`, `rows_visible`.
|
||||
|
||||
**Команды замера** (из `perf/e2e`, затем `perfkit.aggregate`; `<before|after>` и `<N>` подставить по этапу и масштабу):
|
||||
|
||||
| что | команда | время на сторону (оценка) |
|
||||
@ -313,6 +327,6 @@ node analysis/svod2_search_oracle.mjs --check --api http://localhost:8000 \
|
||||
- `--only REGEX` — регулярное выражение по строке `METHOD url` (например `'GET /api/v1/svod/2/'`, или `'svod/2/RF.*facets'` для вариантов фильтров);
|
||||
- `--repeats N` — число повторов после прогрева (по умолчанию 20; для быстрой проверки достаточно 3–5, для замера цифр в PR — 10–20).
|
||||
|
||||
**Сравнение масштабов и разбор.** `scale_compare.py` выводит строки свода 2 тремя ключами: `GET свод 2 <лист>` (полный ответ), `GET свод 2 <лист> страница` (`limit=` в query), `GET свод 2 <лист> facets` (варианты фильтров).
|
||||
**Сравнение масштабов и разбор.** `scale_compare.py` выводит строки свода 2 ключами: `GET свод 2 <лист>` (полный ответ), `... страница` (`limit=` в query), `... facets` (варианты фильтров); с `view=` добавляется суффикс `compact`/`full`, с `search=` - `поиск`.
|
||||
|
||||
**Обновить цифры в PR** (`feat/236-aurora-1843-svod2-server-paging-perf`): PR открываются без цифр; цифры дописываются по готовности из `report-data/<run_id>/analysis/`.
|
||||
|
||||
@ -76,12 +76,13 @@ def main(argv=None):
|
||||
dt = (time.perf_counter() - t0) * 1000
|
||||
r.raise_for_status()
|
||||
if i == 0:
|
||||
rows = len(r.json().get("result") or []) # фактическое число строк ответа (манифест - до grow)
|
||||
res_j = r.json().get("result")
|
||||
rows = len(res_j) if isinstance(res_j, list) else None # строк ответа: только для списка (facets - dict)
|
||||
continue # прогрев
|
||||
tot.append(dt)
|
||||
db.append(float(r.headers.get("x-db-time-ms", "nan")))
|
||||
size = len(body)
|
||||
row = {"role": role, "request": f"{label} ({rows} строк ответа)", "rows": rows, "path": path, "total_ms": round(statistics.median(tot), 1),
|
||||
row = {"role": role, "request": (f"{label} ({rows} строк ответа)" if rows is not None else f"{label} (—)"), "rows": rows, "path": path, "total_ms": round(statistics.median(tot), 1),
|
||||
"db_ms": round(statistics.median(db), 1), "bytes": size, "content_encoding": r.headers.get("content-encoding")}
|
||||
row["python_ms"] = round(row["total_ms"] - row["db_ms"], 1)
|
||||
row["python_share"] = round(row["python_ms"] / row["total_ms"], 2)
|
||||
|
||||
@ -16,6 +16,7 @@ import re
|
||||
import statistics
|
||||
from collections import defaultdict
|
||||
from pathlib import Path
|
||||
from urllib.parse import parse_qs
|
||||
|
||||
PERF = Path(__file__).resolve().parents[1]
|
||||
|
||||
@ -31,13 +32,21 @@ def med(g, k):
|
||||
|
||||
|
||||
def svod2_key(path: str) -> str:
|
||||
"""Ключ свода 2 в таблице API: полный ответ, страница (limit в query) или варианты фильтров (/facets)."""
|
||||
"""Ключ свода 2 в таблице API: полный ответ, страница (limit в query; compact/full по view=), поиск (search=) или /facets."""
|
||||
m = re.match(r"/api/v1/svod/2/(\w+)/", path)
|
||||
base = "GET свод 2 " + m.group(1)
|
||||
if "/facets" in path:
|
||||
route, _, query = path.partition("?")
|
||||
q = parse_qs(query, keep_blank_values=True)
|
||||
if route.endswith("/facets"):
|
||||
return base + " facets"
|
||||
if "limit=" in path:
|
||||
return base + " страница"
|
||||
if "limit" in q:
|
||||
base += " страница"
|
||||
if "view" in q:
|
||||
base += " " + q["view"][0]
|
||||
elif "view" in q:
|
||||
base += " " + q["view"][0]
|
||||
if "search" in q:
|
||||
base += " поиск"
|
||||
return base
|
||||
|
||||
|
||||
|
||||
@ -1,6 +1,7 @@
|
||||
/** Чистые помощники замеров свода 2 (без импортов, стираемый TS: проверяются `node --test`). */
|
||||
|
||||
export const SVOD2_DATA_ROW_SELECTOR = 'tbody tr:not([data-svod2-placeholder])';
|
||||
/** Строка данных: не заглушка окна и без скелетонов 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`. */
|
||||
|
||||
@ -204,8 +204,19 @@ async function measure(browser: import('@playwright/test').Browser, o: RunOpts)
|
||||
let rows: number;
|
||||
let firstRowsMs = 0;
|
||||
try {
|
||||
rows = await waitRows(page, rowSel);
|
||||
if (t.kind === 'svod2') { firstRowsMs = Date.now() - tLoad; bytes.markFirstRows(); }
|
||||
if (t.kind === 'svod2') {
|
||||
// опрос каждый кадр (raf), а не раз в 500 мс: иначе first_rows_ms квантуется шагом опроса
|
||||
await page.waitForFunction(
|
||||
([sel, rs, min]) => document.querySelectorAll(`${sel} ${rs}`).length >= (min as number),
|
||||
[CONTAINER, rowSel, MIN_ROWS] as const,
|
||||
{ polling: 'raf', timeout: LOAD_TIMEOUT },
|
||||
);
|
||||
firstRowsMs = Date.now() - tLoad;
|
||||
bytes.markFirstRows();
|
||||
rows = await waitRows(page, rowSel);
|
||||
} else {
|
||||
rows = await waitRows(page, rowSel);
|
||||
}
|
||||
} catch (e) {
|
||||
if (!(e instanceof RowsError)) throw e;
|
||||
const have = await page.evaluate(([sel, rs]) => document.querySelectorAll(`${sel} ${rs}`).length, [CONTAINER, rowSel]).catch(() => -1);
|
||||
@ -275,10 +286,15 @@ async function measure(browser: import('@playwright/test').Browser, o: RunOpts)
|
||||
const svod2Metrics = t.kind === 'svod2'
|
||||
? {
|
||||
first_rows_ms: firstRowsMs,
|
||||
json_bytes_total: bytes.total,
|
||||
json_bytes_first: bytes.first,
|
||||
pages_requested: countPagesRequested(pageUrls),
|
||||
...(filter ? { filter_result_ms: filterResultMs ?? 0 } : { placeholder_frame_share: placeholderShare(placeholderSamples) }),
|
||||
// в записи фильтра счётчики включали бы исходную загрузку (до fill) - пишем их только в записи скролла
|
||||
...(filter
|
||||
? { filter_result_ms: filterResultMs ?? 0 }
|
||||
: {
|
||||
json_bytes_total: bytes.total,
|
||||
json_bytes_first: bytes.first,
|
||||
pages_requested: countPagesRequested(pageUrls),
|
||||
placeholder_frame_share: placeholderShare(placeholderSamples),
|
||||
}),
|
||||
}
|
||||
: {};
|
||||
append({
|
||||
|
||||
@ -1,7 +1,7 @@
|
||||
/**
|
||||
* Сценарий памяти свода 2 (Task 16): роль dfip, свежий контекст на каждый повтор, одна SPA-сессия внутри повтора.
|
||||
* Точки (lib/svod2.ts memoryPoints): go (вкладка GO, «Все ССП/РФ», «Загрузить») -> rf (вкладка RF, то же)
|
||||
* [-> rf_scrolled при PERF_SVOD2_MEM_SCROLL=1] -> final (возврат на GO, «Загрузить»).
|
||||
* [-> rf_scrolled при PERF_SVOD2_MEM_SCROLL=1] -> final (возврат на вкладку GO; «Загрузить» не нажимается: выбор и данные хранятся по вкладкам).
|
||||
* В каждой точке gc -> heapUsed, domCounters. Запись: scenario "svod2-memory", target "svod2/<point>".
|
||||
* Env: PERF_SCALE, PERF_RUN_ID, PERF_REPEATS (3), PERF_SVOD2_MEM_SCROLL, PERF_LOAD_TIMEOUT_MS, PERF_MAX_STEPS,
|
||||
* PERF_TEST_TIMEOUT_MS.
|
||||
@ -30,19 +30,19 @@ async function waitRows(page: Page, what: string): Promise<number> {
|
||||
}
|
||||
}
|
||||
|
||||
/** Ждём окончания загрузки: число строк и число заглушек не меняются 3 опроса подряд (интервал 300 мс). */
|
||||
/** Ждём окончания загрузки: число строк, заглушек и скелетонов не меняются 3 опроса подряд (интервал 300 мс). */
|
||||
async function settle(page: Page): Promise<number> {
|
||||
let prev = '';
|
||||
let same = 0;
|
||||
const t0 = Date.now();
|
||||
for (;;) {
|
||||
const cur = await page.evaluate(
|
||||
([sel, rs, ph]) => `${document.querySelectorAll(`${sel} ${rs}`).length}/${document.querySelectorAll(`${sel} ${ph}`).length}`,
|
||||
([sel, rs, ph]) => `${document.querySelectorAll(`${sel} ${rs}`).length}/${document.querySelectorAll(`${sel} ${ph}`).length}/${document.querySelectorAll(`${sel} .MuiSkeleton-root`).length}`,
|
||||
[CONTAINER, SVOD2_DATA_ROW_SELECTOR, SVOD2_PLACEHOLDER_SELECTOR],
|
||||
);
|
||||
same = cur === prev ? same + 1 : 0;
|
||||
prev = cur;
|
||||
if (same >= 3) return Number(cur.split('/')[0]);
|
||||
if (same >= 3 && cur.endsWith('/0')) return Number(cur.split('/')[0]); // готово: строки стабильны и в контейнере 0 скелетонов
|
||||
if (Date.now() - t0 > LOAD_TIMEOUT) throw new Error(`settle: строки не стабилизировались за ${LOAD_TIMEOUT} мс (${cur})`);
|
||||
await page.waitForTimeout(300);
|
||||
}
|
||||
|
||||
@ -61,7 +61,7 @@ test('countPagesRequested', () => {
|
||||
});
|
||||
|
||||
test('селекторы', () => {
|
||||
assert.equal(SVOD2_DATA_ROW_SELECTOR, 'tbody tr:not([data-svod2-placeholder])');
|
||||
assert.equal(SVOD2_DATA_ROW_SELECTOR, 'tbody tr:not([data-svod2-placeholder]):not(:has(.MuiSkeleton-root))');
|
||||
assert.equal(SVOD2_PLACEHOLDER_SELECTOR, 'tbody tr[data-svod2-placeholder]');
|
||||
});
|
||||
|
||||
|
||||
@ -10,7 +10,12 @@ import scale_compare # noqa: E402
|
||||
|
||||
def test_svod2_key():
|
||||
assert scale_compare.svod2_key("/api/v1/svod/2/GO/2026") == "GET свод 2 GO"
|
||||
assert scale_compare.svod2_key("/api/v1/svod/2/RF/2026?view=compact&offset=0&limit=300") == "GET свод 2 RF страница"
|
||||
assert scale_compare.svod2_key("/api/v1/svod/2/RF/2026?view=compact&offset=0&limit=300") == "GET свод 2 RF страница compact"
|
||||
assert scale_compare.svod2_key("/api/v1/svod/2/RF/2026?view=full&offset=0&limit=300") == "GET свод 2 RF страница full"
|
||||
assert scale_compare.svod2_key("/api/v1/svod/2/RF/2026?offset=0&limit=300") == "GET свод 2 RF страница"
|
||||
assert scale_compare.svod2_key("/api/v1/svod/2/RF/2026?view=compact&limit=300&search=1") == "GET свод 2 RF страница compact поиск"
|
||||
assert scale_compare.svod2_key("/api/v1/svod/2/GO/2026?view=full") == "GET свод 2 GO full"
|
||||
assert scale_compare.svod2_key("/api/v1/svod/2/GO/2026?search=1") == "GET свод 2 GO поиск"
|
||||
assert scale_compare.svod2_key("/api/v1/svod/2/GO/2026/facets?search=1") == "GET свод 2 GO facets"
|
||||
|
||||
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user