mirror of
https://github.com/retroSoC/retroSoC.git
synced 2026-11-04 22:32:36 +00:00
169 lines
6.9 KiB
Python
169 lines
6.9 KiB
Python
#!/usr/bin/env python3
|
|
|
|
from __future__ import annotations
|
|
|
|
import argparse
|
|
import json
|
|
import re
|
|
import statistics
|
|
import sys
|
|
from pathlib import Path
|
|
from typing import Any
|
|
|
|
|
|
ROOT = Path(__file__).resolve().parents[1]
|
|
sys.path.insert(0, str(ROOT))
|
|
from scripts.setup_helpers import atomic_write # noqa: E402
|
|
|
|
|
|
AREA_RE = re.compile(r"Chip area for top module[^:]*:\s*([0-9.eE+-]+)")
|
|
HIERARCHY_RE = re.compile(r"^\s*(\d+)\s+[0-9.eE+-]+\s+retrosoc_asic\s*$", re.MULTILINE)
|
|
|
|
|
|
def collect(args: argparse.Namespace) -> int:
|
|
root = args.variant_root.resolve()
|
|
synth_root = (args.synth_root or root / "syn/yosys").resolve()
|
|
sta_root = (args.sta_root or root / "sta/opensta").resolve()
|
|
metrics: dict[str, Any] = {
|
|
"schema_version": 2,
|
|
"policy": "observe",
|
|
"firmware": {},
|
|
"synthesis": {"recipe": args.recipe},
|
|
"timing": {},
|
|
"flows": {},
|
|
}
|
|
for binary in sorted((root / "sw").glob("*.bin")):
|
|
metrics["firmware"][binary.name] = {"bytes": binary.stat().st_size}
|
|
|
|
area_json = synth_root / "rpt/retrosoc_asic_area.json"
|
|
area_report = synth_root / "rpt/retrosoc_asic_area.rpt"
|
|
if area_json.is_file():
|
|
design = json.loads(area_json.read_text(encoding="utf-8"))["design"]
|
|
metrics["synthesis"]["top_area"] = float(design["area"])
|
|
metrics["synthesis"]["top_cells"] = int(design["num_cells"])
|
|
elif area_report.is_file():
|
|
text = area_report.read_text(encoding="utf-8", errors="replace")
|
|
areas = AREA_RE.findall(text)
|
|
hierarchy = HIERARCHY_RE.findall(text)
|
|
if areas:
|
|
metrics["synthesis"]["top_area"] = float(areas[-1])
|
|
if hierarchy:
|
|
metrics["synthesis"]["top_cells"] = int(hierarchy[-1])
|
|
|
|
timing_report = sta_root / "timing_metrics.rpt"
|
|
if timing_report.is_file():
|
|
labels = ("wns_min", "wns_max", "tns_min", "tns_max")
|
|
timing_text = timing_report.read_text(encoding="utf-8", errors="replace")
|
|
for line in timing_text.splitlines():
|
|
label, separator, value = line.partition("=")
|
|
numbers = re.findall(r"[-+]?(?:\d+\.\d+|\d+)", value)
|
|
if separator and label in labels and numbers:
|
|
metrics["timing"][label] = float(numbers[-1])
|
|
if not metrics["timing"]:
|
|
values = [
|
|
float(value)
|
|
for value in re.findall(r"[-+]?(?:\d+\.\d+|\d+)", timing_text)
|
|
]
|
|
metrics["timing"].update(dict(zip(labels, values[-4:])))
|
|
|
|
for result in sorted(root.glob("**/result*.json")):
|
|
try:
|
|
data = json.loads(result.read_text(encoding="utf-8"))
|
|
except (OSError, json.JSONDecodeError):
|
|
continue
|
|
metrics["flows"][str(result.relative_to(root))] = {
|
|
"status": data.get("status"),
|
|
"duration_seconds": data.get("duration_seconds"),
|
|
}
|
|
for label, result in (
|
|
("synthesis", synth_root / "result-synth.json"),
|
|
("sta", sta_root / "result-sta.json"),
|
|
):
|
|
if result.is_file():
|
|
try:
|
|
data = json.loads(result.read_text(encoding="utf-8"))
|
|
except (OSError, json.JSONDecodeError):
|
|
continue
|
|
metrics["flows"][label] = {
|
|
"status": data.get("status"),
|
|
"duration_seconds": data.get("duration_seconds"),
|
|
}
|
|
atomic_write(args.output, json.dumps(metrics, indent=2, sort_keys=True) + "\n")
|
|
print(f"metrics: {args.output.resolve()}")
|
|
return 0
|
|
|
|
|
|
def numeric_values(value: Any, prefix: str = "") -> dict[str, float]:
|
|
output: dict[str, float] = {}
|
|
if isinstance(value, dict):
|
|
for key, child in value.items():
|
|
path = f"{prefix}.{key}" if prefix else key
|
|
output.update(numeric_values(child, path))
|
|
elif isinstance(value, (int, float)) and not isinstance(value, bool):
|
|
output[prefix] = float(value)
|
|
return output
|
|
|
|
|
|
def baseline(args: argparse.Namespace) -> int:
|
|
if len(args.input) != 10:
|
|
raise SystemExit("exactly 10 --input metrics files are required")
|
|
samples = [numeric_values(json.loads(path.read_text(encoding="utf-8"))) for path in args.input]
|
|
common = set.intersection(*(set(sample) for sample in samples))
|
|
medians = {key: statistics.median(sample[key] for sample in samples) for key in sorted(common)}
|
|
data = {"schema_version": 1, "sample_count": 10, "values": medians}
|
|
atomic_write(args.output, json.dumps(data, indent=2, sort_keys=True) + "\n")
|
|
print(f"metrics baseline: {args.output.resolve()}")
|
|
return 0
|
|
|
|
|
|
def check(args: argparse.Namespace) -> int:
|
|
policy = json.loads(args.policy.read_text(encoding="utf-8"))
|
|
current = numeric_values(json.loads(args.metrics.read_text(encoding="utf-8")))
|
|
mode = policy.get("mode")
|
|
if mode == "observe":
|
|
print("metrics policy: observe")
|
|
return 0
|
|
if mode != "gate":
|
|
raise SystemExit(f"unsupported metrics policy mode: {mode}")
|
|
if args.baseline is None or not args.baseline.is_file():
|
|
raise SystemExit("a metrics baseline is required when policy mode is gate")
|
|
baseline_data = json.loads(args.baseline.read_text(encoding="utf-8"))["values"]
|
|
failures: list[str] = []
|
|
for key, maximum_growth in policy["maximum_growth"].items():
|
|
if key in current and key in baseline_data and current[key] > baseline_data[key] * (1 + maximum_growth):
|
|
failures.append(key)
|
|
wns_key = "timing.wns_max"
|
|
if wns_key in current and wns_key in baseline_data:
|
|
if current[wns_key] < baseline_data[wns_key] - policy["maximum_wns_regression"]:
|
|
failures.append(wns_key)
|
|
print("metrics check: " + ("failed: " + ", ".join(failures) if failures else "passed"))
|
|
return int(bool(failures))
|
|
|
|
|
|
def parse_args() -> argparse.Namespace:
|
|
parser = argparse.ArgumentParser(description="Collect and compare retroSoC metrics")
|
|
subparsers = parser.add_subparsers(dest="command", required=True)
|
|
collect_parser = subparsers.add_parser("collect")
|
|
collect_parser.add_argument("--variant-root", type=Path, required=True)
|
|
collect_parser.add_argument("--synth-root", type=Path)
|
|
collect_parser.add_argument("--sta-root", type=Path)
|
|
collect_parser.add_argument("--recipe", choices=("balanced", "area", "speed"), default="balanced")
|
|
collect_parser.add_argument("--output", type=Path, required=True)
|
|
baseline_parser = subparsers.add_parser("baseline")
|
|
baseline_parser.add_argument("--input", type=Path, action="append", required=True)
|
|
baseline_parser.add_argument("--output", type=Path, required=True)
|
|
check_parser = subparsers.add_parser("check")
|
|
check_parser.add_argument("--metrics", type=Path, required=True)
|
|
check_parser.add_argument("--baseline", type=Path)
|
|
check_parser.add_argument("--policy", type=Path, required=True)
|
|
return parser.parse_args()
|
|
|
|
|
|
def main() -> int:
|
|
args = parse_args()
|
|
return {"collect": collect, "baseline": baseline, "check": check}[args.command](args)
|
|
|
|
|
|
if __name__ == "__main__":
|
|
raise SystemExit(main())
|