166 lines
5.8 KiB
Python
166 lines
5.8 KiB
Python
# SPDX-License-Identifier: Apache-2.0
|
|
"""Pure data transforms for the local performance dashboard.
|
|
|
|
The functions in this module operate on normalized records from
|
|
``fastvideo/tests/performance/compare_baseline.py``. They intentionally avoid
|
|
network and FastAPI concerns so they can be tested with in-memory fixtures.
|
|
"""
|
|
|
|
from __future__ import annotations
|
|
|
|
import statistics
|
|
from collections import defaultdict
|
|
from datetime import datetime, timezone
|
|
from typing import Any
|
|
|
|
from fastvideo.tests.performance.hf_store import safe_float
|
|
|
|
from .metrics import METRICS
|
|
|
|
Record = dict[str, Any]
|
|
|
|
|
|
def parse_timestamp(value: Any) -> datetime | None:
|
|
if not value:
|
|
return None
|
|
if isinstance(value, datetime):
|
|
ts = value
|
|
else:
|
|
try:
|
|
ts = datetime.fromisoformat(str(value))
|
|
except ValueError:
|
|
return None
|
|
if ts.tzinfo is None:
|
|
return ts.replace(tzinfo=timezone.utc)
|
|
return ts.astimezone(timezone.utc)
|
|
|
|
|
|
def record_sort_key(record: Record) -> tuple[datetime, str]:
|
|
ts = parse_timestamp(record.get("timestamp"))
|
|
return (ts or datetime.min.replace(tzinfo=timezone.utc), str(record.get("commit_sha") or ""))
|
|
|
|
|
|
def filter_records(
|
|
records: list[Record],
|
|
*,
|
|
model_id: str | None = None,
|
|
gpu_type: str | None = None,
|
|
success: bool | None = None,
|
|
) -> list[Record]:
|
|
filtered = records
|
|
if model_id:
|
|
filtered = [record for record in filtered if record.get("model_id") == model_id]
|
|
if gpu_type:
|
|
filtered = [record for record in filtered if record.get("gpu_type") == gpu_type]
|
|
if success is not None:
|
|
filtered = [record for record in filtered if bool(record.get("success", True)) == success]
|
|
return sorted(filtered, key=record_sort_key)
|
|
|
|
|
|
def group_by_model_gpu(records: list[Record]) -> dict[tuple[str, str], list[Record]]:
|
|
groups: dict[tuple[str, str], list[Record]] = defaultdict(list)
|
|
for record in records:
|
|
model_id = str(record.get("model_id") or "unknown")
|
|
gpu_type = str(record.get("gpu_type") or "unknown")
|
|
groups[(model_id, gpu_type)].append(record)
|
|
return {key: sorted(value, key=record_sort_key) for key, value in groups.items()}
|
|
|
|
|
|
def baseline_value(records: list[Record], metric_key: str) -> float | None:
|
|
values = [safe_float(record.get(metric_key)) for record in records]
|
|
values = [value for value in values if value is not None]
|
|
if not values:
|
|
return None
|
|
return float(statistics.median(values))
|
|
|
|
|
|
def regression_percent(metric_key: str, current: float | None, baseline: float | None) -> float | None:
|
|
if current is None or baseline is None or baseline <= 0:
|
|
return None
|
|
metric = next(metric for metric in METRICS if metric.key == metric_key)
|
|
if metric.lower_is_better:
|
|
return (current - baseline) / baseline * 100.0
|
|
return (baseline - current) / baseline * 100.0
|
|
|
|
|
|
def build_latest_summary(records: list[Record],
|
|
*,
|
|
baseline_window: int = 5,
|
|
max_regression: float = 0.05) -> list[Record]:
|
|
rows: list[Record] = []
|
|
for (model_id, gpu_type), group in group_by_model_gpu(records).items():
|
|
latest = group[-1]
|
|
earlier_successes = [record for record in group[:-1] if record.get("success", True)]
|
|
baseline_records = earlier_successes[-baseline_window:]
|
|
|
|
metrics: dict[str, Record] = {}
|
|
regressions: list[float] = []
|
|
for metric in METRICS:
|
|
current = safe_float(latest.get(metric.key))
|
|
baseline = baseline_value(baseline_records, metric.key)
|
|
regression = regression_percent(metric.key, current, baseline)
|
|
metrics[metric.key] = {
|
|
"current": current,
|
|
"baseline": baseline,
|
|
"regression_pct": regression,
|
|
"label": metric.label,
|
|
"lower_is_better": metric.lower_is_better,
|
|
"precision": metric.precision,
|
|
}
|
|
if regression is not None:
|
|
regressions.append(regression)
|
|
|
|
worst_regression = max(regressions) if regressions else None
|
|
success = bool(latest.get("success", True))
|
|
status = "pass" if success else "fail"
|
|
|
|
rows.append({
|
|
"model_id":
|
|
model_id,
|
|
"gpu_type":
|
|
gpu_type,
|
|
"timestamp":
|
|
latest.get("timestamp"),
|
|
"commit_sha":
|
|
latest.get("commit_sha"),
|
|
"success":
|
|
success,
|
|
"baseline_n":
|
|
len(baseline_records),
|
|
"worst_regression_pct":
|
|
worst_regression,
|
|
"regression_threshold_pct":
|
|
max_regression * 100.0,
|
|
"computed_regression_status":
|
|
"fail" if worst_regression is not None and worst_regression > max_regression * 100.0 else "pass",
|
|
"status":
|
|
status,
|
|
"metrics":
|
|
metrics,
|
|
})
|
|
|
|
return sorted(rows, key=lambda row: (row["status"] != "fail", row["model_id"], row["gpu_type"]))
|
|
|
|
|
|
def build_trends(records: list[Record]) -> list[Record]:
|
|
trends: list[Record] = []
|
|
for (model_id, gpu_type), group in group_by_model_gpu(records).items():
|
|
points = []
|
|
for record in group:
|
|
point = {
|
|
"timestamp": record.get("timestamp"),
|
|
"commit_sha": record.get("commit_sha"),
|
|
"success": bool(record.get("success", True)),
|
|
"metrics": {
|
|
metric.key: safe_float(record.get(metric.key))
|
|
for metric in METRICS
|
|
},
|
|
}
|
|
points.append(point)
|
|
trends.append({
|
|
"model_id": model_id,
|
|
"gpu_type": gpu_type,
|
|
"points": points,
|
|
})
|
|
return sorted(trends, key=lambda trend: (trend["model_id"], trend["gpu_type"]))
|