# SPDX-License-Identifier: Apache-2.0 """FastAPI app for the local performance dashboard.""" from __future__ import annotations import os import threading from datetime import datetime, timezone from typing import Any from fastapi import FastAPI, Query from fastapi.middleware.cors import CORSMiddleware from fastapi.responses import FileResponse from fastapi.staticfiles import StaticFiles from fastvideo.performance import hf_store from .service import build_latest_summary, build_trends, filter_records DEFAULT_TRACKING_ROOT = "/tmp/fastvideo-perf-dashboard" DEFAULT_DAYS = 90 FRONTEND_DIST = os.path.abspath( os.path.join(os.path.dirname(__file__), "..", "..", "performance_dashboard", "frontend", "dist")) def _matches_display_filters(record: dict[str, Any], model_id: str | None, gpu_type: str | None) -> bool: return (not model_id or record.get("model_id") == model_id) and (not gpu_type or record.get("gpu_type") == gpu_type) class PerformanceDataStore: def __init__(self, tracking_root: str | None = None) -> None: self.tracking_root = tracking_root or os.environ.get("PERFORMANCE_TRACKING_ROOT", DEFAULT_TRACKING_ROOT) self._lock = threading.RLock() self.last_sync_at: str | None = None self.last_sync_error: str | None = None @property def repo_id(self) -> str: return hf_store.HF_REPO_ID def sync(self) -> dict[str, Any]: with self._lock: try: local_dir = hf_store.sync_from_hf(self.tracking_root, reuse_existing=False) self.last_sync_at = datetime.now(timezone.utc).isoformat() self.last_sync_error = None return { "ok": True, "repo_id": self.repo_id, "tracking_root": local_dir, "last_sync_at": self.last_sync_at, "last_sync_error": None, } except Exception as exc: self.last_sync_error = str(exc) return { "ok": False, "repo_id": self.repo_id, "tracking_root": self.tracking_root, "last_sync_at": self.last_sync_at, "last_sync_error": self.last_sync_error, } def ensure_synced(self) -> None: if self.last_sync_at is not None: return with self._lock: if self.last_sync_at is None: hf_store.sync_from_hf(self.tracking_root, reuse_existing=True) self.last_sync_at = datetime.now(timezone.utc).isoformat() def load_records(self, *, days: int | None = None, successful_only: bool = False) -> list[dict[str, Any]]: self.ensure_synced() return hf_store.load_records(self.tracking_root, days=days, successful_only=successful_only) def health(self) -> dict[str, Any]: return { "ok": self.last_sync_error is None, "repo_id": self.repo_id, "tracking_root": self.tracking_root, "last_sync_at": self.last_sync_at, "last_sync_error": self.last_sync_error, } def create_app(store: PerformanceDataStore | None = None) -> FastAPI: data_store = store or PerformanceDataStore() app = FastAPI(title="FastVideo Performance Dashboard") app.state.performance_store = data_store app.add_middleware( CORSMiddleware, allow_origins=[ "http://localhost:5173", "http://127.0.0.1:5173", "http://localhost:3000", "http://127.0.0.1:3000", ], allow_credentials=True, allow_methods=["*"], allow_headers=["*"], ) @app.get("/api/performance/health") def health() -> dict[str, Any]: return data_store.health() @app.post("/api/performance/refresh") def refresh() -> dict[str, Any]: return data_store.sync() @app.get("/api/performance/records") def records( days: int = Query(DEFAULT_DAYS, ge=1, le=3650), model_id: str | None = None, gpu_type: str | None = None, run_source: str | None = None, success: bool | None = None, ) -> dict[str, Any]: loaded = data_store.load_records(days=days) filtered = filter_records( loaded, model_id=model_id, gpu_type=gpu_type, run_source=run_source, success=success, ) return { "records": filtered, "count": len(filtered), "filters": { "days": days, "model_id": model_id, "gpu_type": gpu_type, "run_source": run_source, "success": success, }, "sync": data_store.health(), } @app.get("/api/performance/summary") def summary( days: int = Query(DEFAULT_DAYS, ge=1, le=3650), model_id: str | None = None, gpu_type: str | None = None, run_source: str | None = None, ) -> dict[str, Any]: # Latest status should be stable when users change the trend window. # Use all cached records for latest/baseline computation; the ``days`` # query is kept only so the frontend can share filter state across # endpoints without affecting the summary semantics. loaded = data_store.load_records(days=None) rows = [ row for row in build_latest_summary(loaded, run_source=run_source) if _matches_display_filters(row, model_id, gpu_type) ] return { "rows": rows, "count": len(rows), "status_counts": { "pass": sum(1 for row in rows if row["status"] == "pass"), "fail": sum(1 for row in rows if row["status"] == "fail"), }, "filters": { "days": None, "trend_window_days": days, "model_id": model_id, "gpu_type": gpu_type, "run_source": run_source, }, "sync": data_store.health(), } @app.get("/api/performance/trends") def trends( days: int = Query(DEFAULT_DAYS, ge=1, le=3650), model_id: str | None = None, gpu_type: str | None = None, run_source: str | None = None, ) -> dict[str, Any]: loaded = data_store.load_records(days=days) source_filtered = filter_records(loaded, run_source=run_source) groups = [ group for group in build_trends(source_filtered) if _matches_display_filters(group, model_id, gpu_type) ] return { "groups": groups, "count": len(groups), "filters": { "days": days, "model_id": model_id, "gpu_type": gpu_type, "run_source": run_source, }, "sync": data_store.health(), } assets_dir = os.path.join(FRONTEND_DIST, "assets") index_file = os.path.join(FRONTEND_DIST, "index.html") if os.path.isdir(assets_dir) and os.path.isfile(index_file): app.mount("/assets", StaticFiles(directory=assets_dir), name="performance-dashboard-assets") @app.get("/{full_path:path}", include_in_schema=False) def frontend(full_path: str) -> FileResponse: return FileResponse(index_file) return app app = create_app()