diff --git a/dashboard/dashboard.py b/dashboard/dashboard.py index ecda580..5e9fdfa 100644 --- a/dashboard/dashboard.py +++ b/dashboard/dashboard.py @@ -101,7 +101,19 @@ body { background: #0b0f17; color: #bcc3cd; font-family: -apple-system, BlinkMac
Model Distribution
Agent Activity
- + +
📊 Performance Analytics +
+ + +
+
+
Latency — P50 / P95 / P99 (ms)
+
Throughput — Tokens / sec
+
Routing Effectiveness — by Reason
+
Agent Performance
+ +
Live Stream
@@ -188,8 +200,47 @@ var grid='';for(var g=0;g<=4;g++){var y=(g/4)*H;grid+=''+grid+paths+''; lg.innerHTML=mn.map(function(m){return''+(ML[m]||m)+'';}).join(''); } +var perfWindow='1'; +function switchPerfWindow(w){perfWindow=w;document.querySelectorAll('.btn-sm-period').forEach(function(b,i){if(i>=4)b.classList.toggle('active',b.textContent.trim().replace('h','')===w)});loadPerf();} +function loadPerf(){fetch('/api/performance?window='+perfWindow).then(function(r){return r.json()}).then(renderPerf).catch(function(){})} +function renderPerf(d){ +var models=d.models||[],reasons=d.reasons||[],agents=d.agents||[],sum=d.summary||{}; +// Latency bars: p50/p95/p99 per model +var mlab={'qwen3.6-35B-A3B':'35B MoE','qwen3.6-27B-code':'27B Dense','qwen3.5-9b-vlm':'9B VLM'}; +var mcol={'qwen3.6-35B-A3B':'#a78bfa','qwen3.6-27B-code':'#f59e0b','qwen3.5-9b-vlm':'#22c55e'}; +if(!models.length){$('perf-latency').innerHTML='
Accumulating data...
';return;} +var maxLat=Math.max(...models.map(function(m){return m.latency.p99||0}),1); +var latHTML=models.map(function(m){ +var l=m.latency||{},p50=l.p50||0,p95=l.p95||0,p99=l.p99||0,c=mcol[m.model]||'#38bdf8'; +return'
'+mlab[m.model]+''+m.count+' reqs
'+ +'
p50
'+p99+'ms
'+ +'
p50: '+p50+'msp95: '+p95+'msp99: '+p99+'ms
'; +}).join(''); +$('perf-latency').innerHTML=latHTML; +// Throughput comparison +var maxTps=Math.max(...models.map(function(m){return m.throughput.avg_tokens_per_sec||0}),1); +var tpsHTML=models.map(function(m){ +var t=m.throughput||{},avg=t.avg_tokens_per_sec||0,p50=t.p50||0,c=mcol[m.model]||'#38bdf8'; +return'
'+mlab[m.model]+''+avg+' tok/s
'+ +'
avg
'+ +'
p50
'+p50+' tok/s
'; +}).join(''); +$('perf-throughput').innerHTML=tpsHTML; +// Routing reasons table +if(reasons.length){ +var rHTML='
TimeAgentModelReasonTier
'; +reasons.forEach(function(r){rHTML+='';}); +rHTML+='
ReasonCountAvg LatP95 Lat
'+r.reason+''+r.count+''+r.avg_total_ms+'ms'+r.p95_total_ms+'ms
';$('perf-reasons').innerHTML=rHTML; +}else{$('perf-reasons').innerHTML='
-
';} +// Agent performance +if(agents.length){ +var maxAc=Math.max(...agents.map(function(a){return a.count||0}),1); +var aHTML=agents.map(function(a){return'
'+a.agent+''+a.count+' reqs
'+a.avg_total_ms+'ms avg
';}).join(''); +$('perf-agents').innerHTML=aHTML; +}else{$('perf-agents').innerHTML='
-
';} +} function poll(){fetch('/api/state').then(function(r){return r.json()}).then(function(data){render(data);$('connection-status').textContent='live';}).catch(function(){$('connection-status').textContent='reconnecting';});} -poll();setInterval(poll,3000);loadTS(); +poll();setInterval(poll,3000);loadTS();loadPerf();setInterval(loadPerf,15000); """ @@ -200,6 +251,16 @@ def dashboard(): return render_template_string(DASHBOARD_HTML) @app.route("/api/state") def api_state(): return fetch_state() +@app.route("/api/performance") +def api_performance(): + window = request.args.get("window", "24") + model = request.args.get("model", "all") + try: + r = requests.get(f"http://router:9000/metrics/performance?window={window}&model={model}", timeout=10) + if r.status_code == 200: return r.json() + except Exception: pass + return {"models": [], "reasons": [], "agents": [], "summary": {"total_requests": 0}} + @app.route("/api/timeseries") def api_timeseries(): period = request.args.get("period", "day")