rpki/scripts/soak/summarize_feature149_cohort.py

170 lines
7.1 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.

#!/usr/bin/env python3
"""Validate and summarize one Feature #149 six-run cohort."""
from __future__ import annotations
import argparse
import json
import statistics
from datetime import datetime, timezone
from pathlib import Path
from typing import Any
REQUIRED_ARTIFACTS = {
"run-meta.json",
"run-summary.json",
"input.cir",
"result.ccr",
"report.json",
"vrps.csv",
"vaps.csv",
"stage-timing.json",
"process-time.txt",
}
def load_jsonl(path: Path) -> list[dict[str, Any]]:
records: list[dict[str, Any]] = []
for line_number, line in enumerate(path.read_text(encoding="utf-8").splitlines(), 1):
if line.strip():
value = json.loads(line)
if not isinstance(value, dict):
raise ValueError(f"{path}:{line_number} is not an object")
records.append(value)
return records
def require_success(records: list[dict[str, Any]], package_root: Path) -> list[dict[str, Any]]:
if len(records) != 6:
raise ValueError(f"expected 6 records (snapshot + 5 delta), found {len(records)}")
for index, record in enumerate(records):
expected_mode = "snapshot" if index == 0 else "delta"
if record.get("status") != "success":
raise ValueError(f"run {index + 1} status is {record.get('status')!r}")
if record.get("syncMode") != expected_mode:
raise ValueError(
f"run {index + 1} syncMode is {record.get('syncMode')!r}, expected {expected_mode!r}"
)
run_dir = package_root / "runs" / f"run_{int(record['maxRunIndexAfter']):04d}"
missing = sorted(name for name in REQUIRED_ARTIFACTS if not (run_dir / name).is_file())
record["runDir"] = str(run_dir)
record["artifactsComplete"] = not missing
record["missingArtifacts"] = missing
if missing:
raise ValueError(f"run {index + 1} missing required artifacts: {', '.join(missing)}")
return records
def percentile(values: list[float], fraction: float) -> float | None:
if not values:
return None
ordered = sorted(values)
position = (len(ordered) - 1) * fraction
lower = int(position)
upper = min(lower + 1, len(ordered) - 1)
return ordered[lower] + (ordered[upper] - ordered[lower]) * (position - lower)
def stats(values: list[int | float | None]) -> dict[str, float | None]:
nums = [float(value) for value in values if isinstance(value, (int, float))]
if not nums:
return {"count": 0, "min": None, "median": None, "p90": None, "max": None, "mean": None}
return {
"count": len(nums),
"min": min(nums),
"median": statistics.median(nums),
"p90": percentile(nums, 0.9),
"max": max(nums),
"mean": statistics.fmean(nums),
}
def iso(epoch: Any) -> str | None:
if not isinstance(epoch, (int, float)):
return None
return datetime.fromtimestamp(epoch, timezone.utc).isoformat().replace("+00:00", "Z")
def main() -> None:
parser = argparse.ArgumentParser()
parser.add_argument("--cohort-id", required=True)
parser.add_argument("--cohort-disk", required=True, choices=("system", "data"))
parser.add_argument("--cohort-profile", required=True, choices=("prefetch-pp-object", "pp-object"))
parser.add_argument("--package-root", required=True, type=Path)
parser.add_argument("--summary-jsonl", required=True, type=Path)
parser.add_argument("--out-dir", required=True, type=Path)
args = parser.parse_args()
records = require_success(load_jsonl(args.summary_jsonl), args.package_root)
delta = records[1:]
elapsed_seconds = records[-1]["completedEpoch"] - records[0]["actualStartEpoch"]
report = {
"feature": "149",
"createdAtUtc": datetime.now(timezone.utc).isoformat().replace("+00:00", "Z"),
"cohortId": args.cohort_id,
"disk": args.cohort_disk,
"profile": args.cohort_profile,
"packageRoot": str(args.package_root),
"runCount": len(records),
"successCount": len(records),
"artifactStatus": "complete",
"elapsedSeconds": elapsed_seconds,
"actualStartedAtUtc": iso(records[0].get("actualStartEpoch")),
"completedAtUtc": iso(records[-1].get("completedEpoch")),
"maxRssKb": max(int(record["maxRssKb"]) for record in records if isinstance(record.get("maxRssKb"), (int, float))),
"snapshot": {"wallMs": records[0].get("wallMs"), "maxRssKb": records[0].get("maxRssKb")},
"deltaWallMs": stats([record.get("wallMs") for record in delta]),
"stableDeltaWallMs": stats([record.get("wallMs") for record in delta[1:]]),
"latestCounts": {
key: records[-1].get(key)
for key in ("vrps", "vaps", "publicationPoints", "warnings")
},
"runs": records,
}
args.out_dir.mkdir(parents=True, exist_ok=True)
json_path = args.out_dir / "cohort-summary.json"
markdown_path = args.out_dir / "cohort-summary.md"
json_path.write_text(json.dumps(report, ensure_ascii=False, indent=2, sort_keys=True) + "\n", encoding="utf-8")
latest = report["latestCounts"]
markdown_path.write_text(
"\n".join(
[
f"# Feature #149 cohort: {args.cohort_id}",
"",
f"- 磁盘:`{args.cohort_disk}`",
f"- profile`{args.cohort_profile}`",
f"- 状态:`success`6/6 run 成功,必要产物完整",
f"- 总执行耗时:`{elapsed_seconds}s`",
f"- 最高 RSS`{report['maxRssKb']} KiB`",
f"- snapshot wall`{records[0].get('wallMs')} ms`",
f"- delta median / p90`{report['deltaWallMs']['median']} ms` / `{report['deltaWallMs']['p90']} ms`",
f"- 最新产物VRP `{latest['vrps']}`、VAP `{latest['vaps']}`、PP `{latest['publicationPoints']}`、warnings `{latest['warnings']}`",
"",
"| run | mode | wall ms | max RSS KiB | VRP | VAP | PP | warnings | artifacts |",
"| --- | --- | ---: | ---: | ---: | ---: | ---: | ---: | --- |",
*[
"| {run} | {mode} | {wall} | {rss} | {vrps} | {vaps} | {pps} | {warnings} | {artifacts} |".format(
run=index + 1,
mode=record.get("syncMode"),
wall=record.get("wallMs"),
rss=record.get("maxRssKb"),
vrps=record.get("vrps"),
vaps=record.get("vaps"),
pps=record.get("publicationPoints"),
warnings=record.get("warnings"),
artifacts="complete" if record.get("artifactsComplete") else "missing",
)
for index, record in enumerate(records)
],
"",
]
),
encoding="utf-8",
)
print(json.dumps({"cohortSummaryJson": str(json_path), "cohortSummaryMarkdown": str(markdown_path), "elapsedSeconds": elapsed_seconds, "maxRssKb": report["maxRssKb"]}, ensure_ascii=False))
if __name__ == "__main__":
main()