"""Запись результатов прогонов. Файл: perf/results//.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") # сетевой лог (Task 9) лежит рядом, но это не записи замеров NON_RESULT_FILES = ("network.jsonl",) 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")): if f.name in NON_RESULT_FILES: continue for line in f.read_text(encoding="utf-8").splitlines(): if line.strip(): out.append(json.loads(line)) return out