DFiP_Budget_planing/perf/analysis/hotspots_summary.py
Andrey Epifancev 9923a7b88d perf: контроль CPU 4x, API против X-DB-Time-Ms, рендеры модалок
- report-data/20261026-control-s1: CPU 4x, 1 повтор (FORM_2/AHR, свод 1 MAIN,
  свод 2 GO, модалка выбора ВСП на большом листе), отдельный run_id
- analysis/api-split: полное время API против заголовка X-DB-Time-Ms
  (вместо py-spy, которому на macOS нужен root)
- analysis/modal-renders, hotspots: рендеры модалок прогона s1, циклы CSS и
  matchMedia со вторым листом

Refs #202

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-26 13:08:12 +04:00

110 lines
9.0 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
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)", "",
"| 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)", "",
"| 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()