DFiP_Budget_planing/perf/analysis/hotspots_summary.py
Andrey Epifancev 5f4afc0926 perf: правки по финальному ревью — скрипты, данные, фильтр long tasks
- 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>
2026-10-26 13:34:58 +04:00

116 lines
9.7 KiB
Python
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

"""Сводка прогона 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()