feat: add selectable performance history windows
This commit is contained in:
+96
-1
@@ -82,6 +82,10 @@ CHAT_HEARTBEAT_INTERVAL = 5.0
|
||||
HARNESS_MIN_VALIDATORS = 1
|
||||
HARNESS_MAX_DRAFT_CHARS = 24_000
|
||||
HARNESS_MAX_VALIDATION_CHARS = 8_000
|
||||
PERFORMANCE_HISTORY_FILE = "performance-history.jsonl"
|
||||
PERFORMANCE_HISTORY_BUCKET_SECONDS = 60
|
||||
PERFORMANCE_HISTORY_MAX_SAMPLES = 24 * 60
|
||||
PERFORMANCE_HISTORY_WINDOWS = {1, 6, 9, 12, 24}
|
||||
|
||||
_jobs: dict[str, dict[str, Any]] = {}
|
||||
_jobs_lock = threading.Lock()
|
||||
@@ -1003,6 +1007,83 @@ def _runtime_snapshot() -> dict[str, Any]:
|
||||
}
|
||||
|
||||
|
||||
def _performance_sample(runtime: dict[str, Any]) -> dict[str, Any]:
|
||||
total = int(runtime.get("memory_total_bytes") or 0)
|
||||
used = int(runtime.get("memory_used_bytes") or 0)
|
||||
gpu = runtime.get("gpu") or {}
|
||||
gpus = gpu.get("gpus") or []
|
||||
gpu_total = sum(int(item.get("total_bytes") or 0) for item in gpus)
|
||||
gpu_used = sum(int(item.get("used_bytes") or 0) for item in gpus)
|
||||
swap_total = int(runtime.get("swap_total_bytes") or 0)
|
||||
swap_used = int(runtime.get("swap_used_bytes") or 0)
|
||||
cpu = runtime.get("cpu") or {}
|
||||
disk = runtime.get("disk") or {}
|
||||
load_average = cpu.get("load_average") or []
|
||||
captured_at = float(runtime.get("captured_at") or time.time())
|
||||
bucket = int(captured_at // PERFORMANCE_HISTORY_BUCKET_SECONDS) * PERFORMANCE_HISTORY_BUCKET_SECONDS
|
||||
return {
|
||||
"captured_at": bucket,
|
||||
"cpu_usage_percent": cpu.get("usage_percent"),
|
||||
"cpu_load_1m": load_average[0] if load_average else None,
|
||||
"memory_used_percent": used * 100 / total if total else None,
|
||||
"gpu_usage_percent": gpu.get("utilization_percent"),
|
||||
"gpu_vram_used_percent": gpu_used * 100 / gpu_total if gpu_total else None,
|
||||
"disk_used_percent": disk.get("used_percent"),
|
||||
"swap_used_percent": swap_used * 100 / swap_total if swap_total else None,
|
||||
"ollama_model_gib": int(runtime.get("ollama_model_bytes") or 0) / (1024 * 1024 * 1024),
|
||||
"resident_model_count": len(runtime.get("model_memory") or []),
|
||||
}
|
||||
|
||||
|
||||
def _read_performance_history() -> list[dict[str, Any]]:
|
||||
path = _home() / PERFORMANCE_HISTORY_FILE
|
||||
rows: dict[int, dict[str, Any]] = {}
|
||||
try:
|
||||
for line in path.read_text(encoding="utf-8").splitlines():
|
||||
try:
|
||||
sample = json.loads(line)
|
||||
bucket = int(sample.get("captured_at"))
|
||||
if bucket > 0:
|
||||
rows[bucket] = sample
|
||||
except (TypeError, ValueError, json.JSONDecodeError):
|
||||
continue
|
||||
except OSError:
|
||||
return []
|
||||
return [rows[key] for key in sorted(rows)][-PERFORMANCE_HISTORY_MAX_SAMPLES:]
|
||||
|
||||
|
||||
_performance_history_lock = threading.Lock()
|
||||
|
||||
|
||||
def _record_performance_sample(runtime: dict[str, Any]) -> list[dict[str, Any]]:
|
||||
sample = _performance_sample(runtime)
|
||||
path = _home() / PERFORMANCE_HISTORY_FILE
|
||||
with _performance_history_lock:
|
||||
rows = {int(item["captured_at"]): item for item in _read_performance_history() if item.get("captured_at")}
|
||||
rows[int(sample["captured_at"])] = sample
|
||||
cutoff = int(time.time()) - 24 * 60 * 60
|
||||
history = [rows[key] for key in sorted(rows) if key >= cutoff][-PERFORMANCE_HISTORY_MAX_SAMPLES:]
|
||||
temporary = path.with_name(f".{path.name}.{os.getpid()}.tmp")
|
||||
temporary.write_text("".join(json.dumps(item, separators=(",", ":")) + "\n" for item in history), encoding="utf-8")
|
||||
temporary.replace(path)
|
||||
return history
|
||||
|
||||
|
||||
def _performance_history(hours: int) -> list[dict[str, Any]]:
|
||||
cutoff = time.time() - hours * 60 * 60
|
||||
return [sample for sample in _read_performance_history() if float(sample.get("captured_at") or 0) >= cutoff]
|
||||
|
||||
|
||||
def _validate_performance_hours(hours: int) -> int:
|
||||
try:
|
||||
value = int(hours)
|
||||
except (TypeError, ValueError) as exc:
|
||||
raise HTTPException(400, "History window must be 1, 6, 9, 12, or 24 hours") from exc
|
||||
if value not in PERFORMANCE_HISTORY_WINDOWS:
|
||||
raise HTTPException(400, "History window must be 1, 6, 9, 12, or 24 hours")
|
||||
return value
|
||||
|
||||
|
||||
def _validate_public_url(value: str) -> str:
|
||||
parsed = urlparse(value.strip())
|
||||
if parsed.scheme not in {"http", "https"} or not parsed.hostname:
|
||||
@@ -2300,7 +2381,21 @@ def metrics(limit: int = 100) -> dict[str, Any]:
|
||||
|
||||
@router.get("/runtime")
|
||||
def runtime() -> dict[str, Any]:
|
||||
return _runtime_snapshot()
|
||||
snapshot = _runtime_snapshot()
|
||||
_record_performance_sample(snapshot)
|
||||
return snapshot
|
||||
|
||||
|
||||
@router.get("/runtime/history")
|
||||
def runtime_history(hours: int = 24) -> dict[str, Any]:
|
||||
selected_hours = _validate_performance_hours(hours)
|
||||
snapshot = _runtime_snapshot()
|
||||
_record_performance_sample(snapshot)
|
||||
return {
|
||||
"hours": selected_hours,
|
||||
"bucket_seconds": PERFORMANCE_HISTORY_BUCKET_SECONDS,
|
||||
"samples": _performance_history(selected_hours),
|
||||
}
|
||||
|
||||
|
||||
@router.post("/chat/load")
|
||||
|
||||
Reference in New Issue
Block a user