Andrey Epifancev 0b389ee7e4 perf: формат манифеста и результатов, агрегатор
Refs #202

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-25 21:08:45 +04:00

50 lines
2.2 KiB
Python

"""Запись результатов прогонов.
Файл: perf/results/<run_id>/<scenario>.jsonl, одна JSON-строка на один замер (repeat).
Формат строки (Node-сторона, Task 4, пишет тот же формат):
{
"run_id": "2026-10-25T12-00-00", # str, имя каталога прогона
"scenario": "open-sheet", # str
"target": "dev" | "prod" | ..., # str, какая сборка фронта
"role": "admin", # str, ключ из manifest.users
"scale": 1, # int
"cpu_rate": 1, # int/float, CPU throttling rate
"repeat": 0, # int, номер повтора
"metrics": {"long_task_ms": 123.4}, # {str: number}; нечисловые значения агрегатор игнорирует
"artifacts": ["trace.json"], # list[str], пути относительно каталога прогона
"incomplete": false, # bool; true = замер не завершён, в статистику не входит
"ts": "2026-10-25T12:00:01Z" # str, ISO-8601 UTC
}
Кодировка UTF-8, без переносов строк внутри записи.
"""
from __future__ import annotations
import json
from pathlib import Path
REQUIRED = ("run_id", "scenario", "target", "role", "scale", "cpu_rate", "repeat",
"metrics", "artifacts", "incomplete", "ts")
def append(run_dir, record: dict) -> Path:
missing = [k for k in REQUIRED if k not in record]
if missing:
raise ValueError(f"record missing keys: {missing}")
run_dir = Path(run_dir)
run_dir.mkdir(parents=True, exist_ok=True)
f = run_dir / f"{record['scenario']}.jsonl"
with f.open("a", encoding="utf-8") as fh:
fh.write(json.dumps(record, ensure_ascii=False) + "\n")
return f
def read_all(run_dir) -> list[dict]:
out = []
for f in sorted(Path(run_dir).glob("*.jsonl")):
for line in f.read_text(encoding="utf-8").splitlines():
if line.strip():
out.append(json.loads(line))
return out