fix: cache dashboard stats snapshots

This commit is contained in:
Kayzo 2026-04-22 09:30:19 +00:00
parent 80920ed0e8
commit 2b1ab269ca
4 changed files with 292 additions and 34 deletions

View file

@ -55,12 +55,12 @@
<div class="inline-flex rounded-lg border border-border bg-surface p-1">
<button class="px-3 py-1.5 text-sm rounded-md transition-colors"
:class="viewMode === 'session' ? 'bg-accent text-black' : 'text-gray-400 hover:text-gray-200'"
@click="viewMode = 'session'">
@click="setViewMode('session')">
Session
</button>
<button class="px-3 py-1.5 text-sm rounded-md transition-colors"
:class="viewMode === 'history' ? 'bg-accent text-black' : 'text-gray-400 hover:text-gray-200'"
@click="viewMode = 'history'">
@click="setViewMode('history')">
Historical
</button>
</div>
@ -89,7 +89,7 @@
</div>
<button x-show="log_full_messages" id="feed-toggle"
class="px-3 py-1.5 text-sm rounded-md border border-border bg-surface text-gray-300 hover:text-white transition-colors"
@click="feedOpen = !feedOpen"
@click="toggleFeed()"
:class="feedOpen ? 'bg-accent text-black' : ''">
Live Feed
</button>
@ -1256,6 +1256,11 @@
savingsHistory: [],
expandedRows: {},
pollInterval: null,
statsPollMs: 5000,
historyPollMs: 30000,
feedPollMs: 5000,
lastHistoryFetchMs: 0,
lastFeedFetchMs: 0,
feedOpen: false,
transformations: [],
feedScrolled_: false,
@ -1267,31 +1272,55 @@
async init() {
await this.fetchStats();
await this.fetchTransformations();
this.pollInterval = setInterval(() => {
this.fetchStats();
this.fetchTransformations();
}, 3000);
this.pollDashboard();
}, this.statsPollMs);
// Keyboard shortcuts
document.addEventListener('keydown', (e) => {
if (e.key === 'r' || e.key === 'R') {
this.fetchStats();
this.pollDashboard(true);
}
});
},
async pollDashboard(force = false) {
if (!force && document.hidden) return;
await this.fetchStats();
const now = Date.now();
if (this.viewMode === 'history' && (force || now - this.lastHistoryFetchMs >= this.historyPollMs)) {
await this.fetchHistoryStats();
}
if (this.feedOpen && (force || now - this.lastFeedFetchMs >= this.feedPollMs)) {
await this.fetchTransformations();
}
},
async setViewMode(mode) {
this.viewMode = mode;
if (mode === 'history') {
await this.fetchHistoryStats();
}
},
async toggleFeed() {
this.feedOpen = !this.feedOpen;
if (this.feedOpen) {
await this.fetchTransformations();
}
},
async fetchStats() {
try {
const [statsRes, historyRes, healthRes] = await Promise.all([
fetch('/stats'),
fetch('/stats-history'),
const [statsRes, healthRes] = await Promise.all([
fetch('/stats?cached=1'),
fetch('/health')
]);
this.stats = await statsRes.json();
this.historyStats = await historyRes.json();
const health = await healthRes.json();
this.healthy = health.status === 'healthy';
this.version = health.version || '0.3.0';
@ -1312,6 +1341,18 @@
}
},
async fetchHistoryStats() {
try {
const response = await fetch('/stats-history');
if (response.ok) {
this.historyStats = await response.json();
this.lastHistoryFetchMs = Date.now();
}
} catch (e) {
console.error('Failed to fetch history stats:', e);
}
},
async fetchTransformations() {
try {
const prevLen = this.transformations.length;
@ -1324,6 +1365,7 @@
}
this.transformations = data.transformations || [];
this.log_full_messages = data.log_full_messages ?? this.log_full_messages;
this.lastFeedFetchMs = Date.now();
this.renderTransformations();
}
} catch (e) {

View file

@ -11,6 +11,8 @@ from __future__ import annotations
import json
import logging
import random
import threading
import time
from pathlib import Path
from typing import TYPE_CHECKING, Any
@ -21,6 +23,14 @@ if TYPE_CHECKING:
logger = logging.getLogger("headroom.proxy")
RTK_STATS_CACHE_TTL_SECONDS = 5.0
_rtk_stats_cache_lock = threading.Lock()
_rtk_stats_cache: dict[str, Any] = {
"expires_at": 0.0,
"has_value": False,
"value": None,
}
# Maximum request body size (100MB - increased to support image-heavy requests)
MAX_REQUEST_BODY_SIZE = 100 * 1024 * 1024
@ -114,11 +124,18 @@ def _get_rtk_stats() -> dict[str, Any] | None:
"""Get rtk (Rust Token Killer) savings stats if rtk is installed.
Reads from rtk's tracking database via `rtk gain --format json`.
Returns None if rtk is not installed.
Results are memoized briefly so dashboard polling does not spawn a new
subprocess on every refresh.
"""
import shutil
import subprocess as _sp
now = time.monotonic()
with _rtk_stats_cache_lock:
if _rtk_stats_cache["has_value"] and now < float(_rtk_stats_cache["expires_at"]):
return _rtk_stats_cache["value"]
payload: dict[str, Any] | None
rtk_bin = shutil.which("rtk")
if not rtk_bin:
# Check headroom-managed install. Preserve the historical Unix-name
@ -128,7 +145,16 @@ def _get_rtk_stats() -> dict[str, Any] | None:
if rtk_managed.exists():
rtk_bin = str(rtk_managed)
else:
return None
payload = None
with _rtk_stats_cache_lock:
_rtk_stats_cache.update(
{
"expires_at": time.monotonic() + RTK_STATS_CACHE_TTL_SECONDS,
"has_value": True,
"value": payload,
}
)
return payload
try:
result = _sp.run(
@ -140,21 +166,36 @@ def _get_rtk_stats() -> dict[str, Any] | None:
if result.returncode == 0 and result.stdout.strip():
data = json.loads(result.stdout)
summary = data.get("summary", {})
return {
payload = {
"installed": True,
"total_commands": summary.get("total_commands", 0),
"tokens_saved": summary.get("total_saved", 0),
"avg_savings_pct": summary.get("avg_savings_pct", 0.0),
}
else:
payload = {
"installed": True,
"total_commands": 0,
"tokens_saved": 0,
"avg_savings_pct": 0.0,
}
except Exception:
pass
payload = {
"installed": True,
"total_commands": 0,
"tokens_saved": 0,
"avg_savings_pct": 0.0,
}
return {
"installed": True,
"total_commands": 0,
"tokens_saved": 0,
"avg_savings_pct": 0.0,
}
with _rtk_stats_cache_lock:
_rtk_stats_cache.update(
{
"expires_at": time.monotonic() + RTK_STATS_CACHE_TTL_SECONDS,
"has_value": True,
"value": payload,
}
)
return payload
def is_anthropic_auth(headers: dict[str, str]) -> bool:

View file

@ -1433,19 +1433,12 @@ def create_app(config: ProxyConfig | None = None) -> FastAPI:
"""Serve the Headroom dashboard UI."""
return get_dashboard_html()
@app.get("/stats")
async def stats():
"""Get comprehensive proxy statistics.
DASHBOARD_STATS_CACHE_TTL_SECONDS = 5.0
_stats_snapshot_lock = asyncio.Lock()
_stats_snapshot: dict[str, Any] = {"expires_at": 0.0, "value": None}
This is the main stats endpoint - it aggregates data from all subsystems:
- Request metrics (total, cached, failed, by model/provider)
- Token usage and savings
- Cost tracking
- Canonical persisted display_session metrics for downstream dashboards
- Compression (CCR) statistics
- Telemetry/TOIN (data flywheel) statistics
- Cache and rate limiter stats
"""
async def _build_stats_payload() -> dict[str, Any]:
"""Build the full `/stats` response payload."""
m = proxy.metrics
# Calculate average latency
@ -1664,6 +1657,44 @@ def create_app(config: ProxyConfig | None = None) -> FastAPI:
**get_quota_registry().get_all_stats(),
}
async def _get_cached_stats_payload() -> dict[str, Any]:
"""Return a short-TTL cached `/stats` snapshot for dashboard polling."""
now = time.monotonic()
cached_payload = _stats_snapshot.get("value")
if cached_payload is not None and now < float(_stats_snapshot["expires_at"]):
return cached_payload
async with _stats_snapshot_lock:
now = time.monotonic()
cached_payload = _stats_snapshot.get("value")
if cached_payload is not None and now < float(_stats_snapshot["expires_at"]):
return cached_payload
payload = await _build_stats_payload()
_stats_snapshot["value"] = payload
_stats_snapshot["expires_at"] = time.monotonic() + DASHBOARD_STATS_CACHE_TTL_SECONDS
return payload
@app.get("/stats")
async def stats(cached: bool = False):
"""Get comprehensive proxy statistics.
This is the main stats endpoint - it aggregates data from all subsystems:
- Request metrics (total, cached, failed, by model/provider)
- Token usage and savings
- Cost tracking
- Canonical persisted display_session metrics for downstream dashboards
- Compression (CCR) statistics
- Telemetry/TOIN (data flywheel) statistics
- Cache and rate limiter stats
Use ``?cached=1`` for the dashboard fast path. That returns a short-TTL
snapshot to avoid rebuilding the full payload on every UI poll.
"""
if cached:
return await _get_cached_stats_payload()
return await _build_stats_payload()
@app.get("/stats-history")
async def stats_history(
format: Literal["json", "csv"] = "json",

View file

@ -0,0 +1,144 @@
from __future__ import annotations
import json
import shutil
import subprocess
from types import SimpleNamespace
import pytest
from headroom.dashboard import get_dashboard_html
from headroom.proxy import helpers as proxy_helpers
class _StatsStub:
def __init__(self, calls: dict[str, int], key: str, payload: dict):
self._calls = calls
self._key = key
self._payload = payload
def get_stats(self) -> dict:
self._calls[self._key] += 1
return dict(self._payload)
class _ToinStub:
def get_stats(self) -> dict:
return {"patterns": 0}
@pytest.fixture(autouse=True)
def _reset_rtk_stats_cache() -> None:
proxy_helpers._rtk_stats_cache.update({"expires_at": 0.0, "has_value": False, "value": None})
def test_get_rtk_stats_memoizes_subprocess_calls(monkeypatch: pytest.MonkeyPatch) -> None:
now = {"value": 100.0}
calls = {"run": 0}
def _fake_run(*args, **kwargs):
calls["run"] += 1
return SimpleNamespace(
returncode=0,
stdout=json.dumps({"summary": {"total_commands": 7, "total_saved": 1234}}),
)
monkeypatch.setattr(proxy_helpers.time, "monotonic", lambda: now["value"])
monkeypatch.setattr(shutil, "which", lambda name: "/usr/bin/rtk")
monkeypatch.setattr(subprocess, "run", _fake_run)
first = proxy_helpers._get_rtk_stats()
second = proxy_helpers._get_rtk_stats()
assert first == second
assert first == {
"installed": True,
"total_commands": 7,
"tokens_saved": 1234,
"avg_savings_pct": 0.0,
}
assert calls["run"] == 1
now["value"] += proxy_helpers.RTK_STATS_CACHE_TTL_SECONDS + 0.1
third = proxy_helpers._get_rtk_stats()
assert third == first
assert calls["run"] == 2
def test_stats_cached_query_reuses_short_ttl_snapshot(monkeypatch: pytest.MonkeyPatch) -> None:
pytest.importorskip("fastapi")
from fastapi.testclient import TestClient
import headroom.proxy.server as server
from headroom.proxy.server import ProxyConfig, create_app
calls = {"store": 0, "telemetry": 0, "feedback": 0, "rtk": 0}
now = {"value": 100.0}
monkeypatch.setattr(server.time, "monotonic", lambda: now["value"])
monkeypatch.setattr(
server,
"get_compression_store",
lambda: _StatsStub(calls, "store", {"entry_count": 1, "max_entries": 100}),
)
monkeypatch.setattr(
server,
"get_telemetry_collector",
lambda: _StatsStub(calls, "telemetry", {"enabled": True}),
)
monkeypatch.setattr(
server,
"get_compression_feedback",
lambda: _StatsStub(calls, "feedback", {}),
)
def _fake_rtk_stats() -> dict[str, int | bool | float]:
calls["rtk"] += 1
return {
"installed": True,
"total_commands": 1,
"tokens_saved": 5,
"avg_savings_pct": 10.0,
}
monkeypatch.setattr(server, "_get_rtk_stats", _fake_rtk_stats)
monkeypatch.setattr(server, "get_toin", lambda: _ToinStub())
app = create_app(
ProxyConfig(
optimize=False,
cache_enabled=False,
rate_limit_enabled=False,
cost_tracking_enabled=False,
log_requests=False,
ccr_inject_tool=False,
ccr_handle_responses=False,
ccr_context_tracking=False,
)
)
with TestClient(app) as client:
first = client.get("/stats?cached=1")
second = client.get("/stats?cached=1")
now["value"] += 5.1
third = client.get("/stats?cached=1")
uncached = client.get("/stats")
assert first.status_code == 200
assert second.status_code == 200
assert third.status_code == 200
assert uncached.status_code == 200
assert calls == {"store": 3, "telemetry": 3, "feedback": 3, "rtk": 3}
assert first.json()["cli_filtering"]["tokens_saved"] == 5
def test_dashboard_uses_cached_stats_and_lazy_history_feed_polling() -> None:
html = get_dashboard_html()
assert "fetch('/stats?cached=1')" in html
assert "@click=\"setViewMode('history')\"" in html
assert '@click="toggleFeed()"' in html
assert "this.viewMode === 'history'" in html
assert "this.feedOpen" in html