DFiP_Budget_planing/perf/analysis/hotspots_summary.py
Andrey Epifancev 8e9f53a478 perf: скрипты разбора и сводные данные прогонов 20261026
- perf/analysis: разбор trace (timeline_breakdown), CPU-профилей с source maps
  (cpuprofile_top), heap snapshots (heapdiff), циклов памяти, сравнения масштабов,
  ручной EXPLAIN вложенных запросов (explain_manual), API против X-DB-Time-Ms
  (api_split), сводка горячих точек; тесты ядра в perf/tests/test_analysis.py
- perf/e2e/scenarios/hotspots.spec.ts: CPU-профиль, рендеры React, мутации DOM
  после фильтра, рост CSS-правил по циклам (вне run-all)
- prod-сборка с source maps: PERF_SOURCEMAP=1, perf/stand.sh web-sourcemap (:8082)
- perf/report-data/20261026-s1, 20261026-s2 (summary, sql, analysis, explain-manual),
  t5-types (иллюстрация CPU 6x)
- design.md: решения T10b без обсуждения

Refs #202

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

98 lines
7.8 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)
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 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 / "hotspots.json").write_text(json.dumps(res, ensure_ascii=False, indent=1))
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"]:
md += ["", f"## Рост CSS-правил и matchMedia по циклам: {g['target']}", "",
"| цикл | 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 / "hotspots.md").write_text("\n".join(md) + "\n")
print("\n".join(md))
if __name__ == "__main__":
main()