- run-all.sh: копия run-all.status.json в report-data с относительными путями; пример control4x с отдельным тегом и предупреждением (I-3, M-8); очищены три закоммиченные копии статуса - lib/modal.ts: long tasks по пересечению окна (tasksInWindow), тест (M-7) - scale_compare.py: строки листов на момент скролла (до grow), пояснения (M-6) - hotspots_summary.py: оговорка о разбавлении «на шаг» (M-5) - netlog_sizes.py + report-data/20261026-s1/analysis/netlog-sizes.md (M-3) Refs #202 Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
116 lines
9.7 KiB
Python
116 lines
9.7 KiB
Python
"""Сводка прогона hotspots.spec.ts (+ модалки справочника ВСП) в небольшой файл для report-data.
|
||
|
||
cd perf && .venv/bin/python analysis/hotspots_summary.py results/20261026-hot --out report-data/20261026-s1/analysis
|
||
|
||
Читает <run_dir>/hotspot_cpu.jsonl, hotspot_react.jsonl, modal.jsonl, modal_renders.jsonl (сырые данные в git не
|
||
попадают) и пишет <out>/hotspots.json и hotspots.md:
|
||
- react: на шаг колеса - коммиты, рендеры компонентов, рендеры ForwardRef(Styled(td)); топ-10 компонентов по self_ms;
|
||
- cpu: main thread на шаг (профайлер включён - абсолютные значения выше, чем в scroll.spec);
|
||
- filter: хронология мутаций DOM контейнера после ввода фильтра (ответ на «filter did not stabilize»);
|
||
- cssgrowth: CSS-правила документа, @media, вызовы window.matchMedia и main thread на шаг по циклам;
|
||
- модалки (если есть): клик -> кадр и рендеры.
|
||
"""
|
||
from __future__ import annotations
|
||
|
||
import argparse
|
||
import json
|
||
from pathlib import Path
|
||
|
||
|
||
STEP_NOTE = ("Шаги: hotspots.spec.ts всегда делает 40 шагов вперёд и 40 назад без проверки конца прокрутки. На коротких "
|
||
"страницах (свод 1 MAIN — 17 шагов до конца, AHR_RENT — 31 строка) часть шагов ничего не прокручивает, и "
|
||
"«на шаг» разбавлено: с main_thread_ms_per_step основного прогона (scroll.spec) не сравнивать; использовать доли, "
|
||
"рендеры на коммит и состав топа.")
|
||
|
||
|
||
def load(p: Path) -> list[dict]:
|
||
return [json.loads(x) for x in p.read_text().splitlines()] if p.exists() else []
|
||
|
||
|
||
def main(argv=None):
|
||
ap = argparse.ArgumentParser()
|
||
ap.add_argument("run_dir")
|
||
ap.add_argument("--out", required=True)
|
||
ap.add_argument("--name", default="hotspots", help="базовое имя выходных файлов")
|
||
ap.add_argument("--renders-only", action="store_true", help="только modal_renders (например для основного прогона)")
|
||
a = ap.parse_args(argv)
|
||
rd, out = Path(a.run_dir), Path(a.out)
|
||
out.mkdir(parents=True, exist_ok=True)
|
||
res = {"react": [], "cpu": [], "filter": [], "cssgrowth": [], "modal": [], "modal_renders": []}
|
||
md = [f"# Горячие точки: сводка {rd.name} (hotspots.spec.ts, hotspots_summary.py)", ""]
|
||
for r in load(rd / "hotspot_react.jsonl"):
|
||
m = r["metrics"]
|
||
if not m.get("steps"):
|
||
if m.get("mutations"):
|
||
res["filter"].append({"mode": "react", "target": r["target"], "role": r["role"], "main_thread_ms": round(m["main_thread_ms"]),
|
||
"commits": m.get("commits"), "renders": m.get("total_renders"), "mutations": m["mutations"]})
|
||
continue
|
||
td = next((x["renders"] for x in m["renders_top"] if x["component"] == "ForwardRef(Styled(td))"), 0)
|
||
res["react"].append({"target": r["target"], "role": r["role"], "steps": m["steps"], "commits_per_step": round(m["commits_per_step"], 2),
|
||
"renders_per_step": round(m["renders_per_step"]), "td_renders_per_step": round(td / m["steps"]),
|
||
"react_self_ms": round(m["total_self_ms"]), "main_thread_ms_per_step": round(m["main_thread_ms_per_step"], 1),
|
||
"top": [[x["component"], x["renders"], round(x["self_ms"], 1)] for x in m["renders_top"][:10]]})
|
||
for r in load(rd / "hotspot_cpu.jsonl"):
|
||
m = r["metrics"]
|
||
if m.get("css_cycles"):
|
||
res["cssgrowth"].append({"target": r["target"], "cycles": m["css_cycles"]})
|
||
elif m.get("mutations"):
|
||
res["filter"].append({"mode": "cpu", "target": r["target"], "role": r["role"], "main_thread_ms": round(m["main_thread_ms"]), "mutations": m["mutations"]})
|
||
elif m.get("steps"):
|
||
res["cpu"].append({"target": r["target"], "role": r["role"], "steps": m["steps"], "main_thread_ms_per_step": round(m["main_thread_ms_per_step"], 1),
|
||
"artifacts": r["artifacts"]})
|
||
for r in ([] if a.renders_only else load(rd / "modal.jsonl")):
|
||
res["modal"].append({"target": r["target"], "role": r["role"], "click_to_visible_ms": round(r["metrics"]["click_to_visible_ms"], 1),
|
||
"main_thread_ms": round(r["metrics"]["main_thread_ms"], 1)})
|
||
for r in load(rd / "modal_renders.jsonl"):
|
||
m = r["metrics"]
|
||
res["modal_renders"].append({"target": r["target"], "commits": m["commits"], "renders": m["total_renders"],
|
||
"self_ms": round(m["total_self_ms"], 1), "top": [[x["component"], x["renders"], round(x["self_ms"], 1)] for x in m["renders_top"][:6]]})
|
||
(out / f"{a.name}.json").write_text(json.dumps(res, ensure_ascii=False, indent=1))
|
||
|
||
if a.renders_only:
|
||
md = [f"# Рендеры React при открытии модалок: {rd.name} (modal_renders.jsonl, hotspots_summary.py --renders-only)", "",
|
||
"| модалка | коммитов | рендеров | self, мс | топ компонентов (рендеры/self мс) |", "|---|---|---|---|---|"]
|
||
md += [f"| {r['target']} | {r['commits']} | {r['renders']} | {r['self_ms']} | " + "; ".join(f"{c} {n}/{s_}" for c, n, s_ in r["top"][:6]) + " |"
|
||
for r in res["modal_renders"]]
|
||
(out / f"{a.name}.md").write_text("\n".join(md) + "\n")
|
||
print("\n".join(md))
|
||
return
|
||
md += ["## Рендеры React на шаг колеса 200 px (профилирующая сборка :8081)", "", STEP_NOTE, "",
|
||
"| target | роль | шагов | коммитов/шаг | рендеров компонентов/шаг | из них Styled(td)/шаг | self React, мс | main thread/шаг, мс |", "|---|---|---|---|---|---|---|---|"]
|
||
md += [f"| {r['target']} | {r['role']} | {r['steps']} | {r['commits_per_step']} | {r['renders_per_step']} | {r['td_renders_per_step']} | {r['react_self_ms']} | {r['main_thread_ms_per_step']} |" for r in res["react"]]
|
||
for r in res["react"]:
|
||
md.append(f"- {r['target']}: " + "; ".join(f"{c} {n}/{s} мс" for c, n, s in r["top"][:6]))
|
||
md += ["", "## CPU-профили (prod-сборка с source maps :8082; разбор - cpuprofile-top.md)", "", STEP_NOTE, "",
|
||
"| target | роль | шагов | main thread/шаг под профайлером, мс | профиль |", "|---|---|---|---|---|"]
|
||
md += [f"| {r['target']} | {r['role']} | {r['steps']} | {r['main_thread_ms_per_step']} | `{', '.join(r['artifacts'])}` |" for r in res["cpu"]]
|
||
md += ["", "## Фильтр свода: мутации DOM контейнера `.MuiTableContainer-root` за окно наблюдения после ввода «1»", "",
|
||
"| режим | target | main thread за окно, мс | кадров | кадров с мутацией | первая мутация (кадр) | long tasks | виды мутаций | рендеры React |", "|---|---|---|---|---|---|---|---|---|"]
|
||
for r in res["filter"]:
|
||
mu = r["mutations"]
|
||
kinds = "; ".join(f"{k} ×{n}" for k, n in mu["mutation_kinds_top"][:3]) or "нет"
|
||
md.append(f"| {r['mode']} | {r['target']} | {r['main_thread_ms']} | {mu['frames']} | {mu['frames_with_mutation']} | {mu['first_mutation_frame']} | "
|
||
f"{mu['long_task_n']} ({', '.join(f'{d} мс' for _, d in mu['long_tasks'][:3])}) | {kinds} | {r.get('commits', '')} коммитов / {r.get('renders', '')} |")
|
||
for g in res["cssgrowth"]:
|
||
variant = ("цикл AHR -> скролл -> /tasks -> второй лист -> скролл -> /tasks, со счётчиком matchMedia"
|
||
if any("matchmedia_calls" in c for c in g["cycles"]) else "цикл AHR -> скролл 40 шагов -> /tasks")
|
||
md += ["", f"## Рост CSS-правил и matchMedia по циклам: {g['target']} ({variant})", "",
|
||
"| цикл | main thread/шаг, мс | CSS-правил | @media | style | вызовов matchMedia | нетто listeners matchMedia |", "|---|---|---|---|---|---|---|"]
|
||
for c in g["cycles"]:
|
||
md.append(f"| {c['cycle']} | {c.get('scroll_ms_per_step', '')} | {c['css_rules']} | {c['css_media_rules']} | {c['style_elements']} | {c.get('matchmedia_calls', '')} | {c.get('matchmedia_listeners_net', '')} |")
|
||
last = g["cycles"][-1]
|
||
if last.get("matchmedia_top"):
|
||
md += ["", "Топ мест вызова matchMedia (последний цикл, нарастающим итогом):", ""] + [f"- {n} × `{k[:240]}`" for k, n in last["matchmedia_top"]]
|
||
if res["modal"]:
|
||
md += ["", "## Модалки (повтор на восстановленных данных)", "", "| модалка | роль | клик → кадр, мс | main thread, мс |", "|---|---|---|---|"]
|
||
md += [f"| {r['target']} | {r['role']} | {r['click_to_visible_ms']} | {r['main_thread_ms']} |" for r in res["modal"]]
|
||
if res["modal_renders"]:
|
||
md += ["", "| модалка | коммитов | рендеров | self, мс | топ |", "|---|---|---|---|---|"]
|
||
md += [f"| {r['target']} | {r['commits']} | {r['renders']} | {r['self_ms']} | " + "; ".join(f"{c} {n}/{s}" for c, n, s in r["top"][:4]) + " |" for r in res["modal_renders"]]
|
||
(out / f"{a.name}.md").write_text("\n".join(md) + "\n")
|
||
print("\n".join(md))
|
||
|
||
|
||
if __name__ == "__main__":
|
||
main()
|