use super::*; use iced::widget::column; impl App { pub(super) fn stats_dashboard(&self) -> Element<'_, Message> { let stats = &self.metrics_snapshot; let context_fraction = if stats.context_limit == 0 { 0.0 } else { stats.context_used.min(stats.context_limit) as f32 / stats.context_limit as f32 }; let cache_fraction = if stats.last_prompt_tokens == 0 { 0.0 } else { stats.last_cached_tokens as f32 / stats.last_prompt_tokens as f32 }; let cache_hit_fraction = if stats.kv_lookups == 0 { 0.0 } else { stats.kv_hits as f32 / stats.kv_lookups as f32 }; let endpoint = if stats.server_listening { format!("Listening · 127.0.0.1:{}", stats.server_port) } else { "Stopped".to_owned() }; let phase_color = match stats.phase { crate::metrics::RuntimePhase::Generating => Color::from_rgb8(84, 170, 255), crate::metrics::RuntimePhase::Prefilling | crate::metrics::RuntimePhase::Loading => { Color::from_rgb8(240, 180, 70) } crate::metrics::RuntimePhase::Ready => Color::from_rgb8(72, 176, 112), crate::metrics::RuntimePhase::Failed => Color::from_rgb8(220, 80, 86), crate::metrics::RuntimePhase::Unloaded => muted_text(), }; let heading = container( row![ column![ text("Runtime observability").size(24), text(format!( "{} · {} · uptime {}", stats.model, stats.source.label(), format_duration(stats.uptime_seconds as f64) )) .size(12) .color(muted_text()), ] .spacing(5), Space::with_width(Length::Fill), container(text(stats.phase.label()).size(12).color(phase_color)) .padding([7, 11]) .style(move |_| status_badge_style(phase_color)), ] .align_y(Alignment::Center), ) .padding(16) .style(overview_style); let headline = column![ row![ metric_card( "DECODE", format!("{:.1} tok/s", stats.decode_tokens_per_second), format!( "{} completion tokens total", format_count(stats.completion_tokens) ), ), metric_card( "PREFILL", format!("{:.1} tok/s", stats.prefill_tokens_per_second), format!("{} prompt tokens total", format_count(stats.prompt_tokens)), ), ] .spacing(10), row![ metric_card( "CONTEXT", format!( "{} / {}", format_count(u64::from(stats.context_used)), format_count(u64::from(stats.context_limit)) ), format!("{:.0}% occupied", context_fraction * 100.0), ), metric_card( "WORK", format!( "{} active · {} queued", stats.http_active, stats.queue_depth ), format!("{} runtime requests", format_count(stats.runtime_requests)), ), ] .spacing(10), ] .spacing(10); let throughput = stats_panel( "MODEL ACTIVITY · LAST 24 SECONDS", column![ mini_chart( &self.metrics_history, |point| point.decode_tokens_per_second, Color::from_rgb8(84, 170, 255), ), row![ text("Decode") .size(12) .color(Color::from_rgb8(84, 170, 255)), Space::with_width(Length::Fill), text(format!("{:.1} tok/s", stats.decode_tokens_per_second)) .size(12) .color(muted_text()), ], mini_chart( &self.metrics_history, |point| point.prefill_tokens_per_second, Color::from_rgb8(157, 119, 255), ), row![ text("Prefill") .size(12) .color(Color::from_rgb8(157, 119, 255)), Space::with_width(Length::Fill), text(format!("{:.1} tok/s", stats.prefill_tokens_per_second)) .size(12) .color(muted_text()), ], ] .spacing(7) .into(), ); let requests = stats_panel( "SERVER REQUEST RATE · LAST 24 SECONDS", column![ mini_chart( &self.metrics_history, |point| point.http_requests_per_second, Color::from_rgb8(72, 176, 112), ), row![ text(format!("{} requests", format_count(stats.http_requests))).size(12), Space::with_width(Length::Fill), text(format!( "{} errors · {} streaming", stats.http_errors, stats.http_streaming_requests )) .size(12) .color(muted_text()), ], ] .spacing(7) .into(), ); let latest = self.metrics_history.back().copied().unwrap_or_default(); let kv_io = stats_panel( "KV CHECKPOINT I/O · LAST 24 SECONDS", column![ mini_chart( &self.metrics_history, |point| point.kv_read_bytes_per_second, Color::from_rgb8(67, 194, 203), ), row![ text(if stats.kv_read_active { "Disk read · active" } else { "Disk read" }) .size(12) .color(Color::from_rgb8(67, 194, 203)), Space::with_width(Length::Fill), text(format_rate(latest.kv_read_bytes_per_second)) .size(12) .color(muted_text()), ], mini_chart( &self.metrics_history, |point| point.kv_write_bytes_per_second, Color::from_rgb8(240, 180, 70), ), row![ text(if stats.kv_write_active { "Disk write · active" } else { "Disk write" }) .size(12) .color(Color::from_rgb8(240, 180, 70)), Space::with_width(Length::Fill), text(format_rate(latest.kv_write_bytes_per_second)) .size(12) .color(muted_text()), ], ] .spacing(7) .into(), ); let ssd_activity = stats_panel( "SSD STREAMING ACTIVITY · LAST 24 SECONDS", column![ mini_chart( &self.metrics_history, |point| point.ssd_requests_per_second, Color::from_rgb8(240, 180, 70), ), row![ text("Selected loads") .size(12) .color(Color::from_rgb8(240, 180, 70)), Space::with_width(Length::Fill), text(format!("{:.1}/s", latest.ssd_requests_per_second)) .size(12) .color(muted_text()), ], mini_chart( &self.metrics_history, |point| point.ssd_bytes_per_second, Color::from_rgb8(67, 194, 203), ), row![ text("Requested expert data") .size(12) .color(Color::from_rgb8(67, 194, 203)), Space::with_width(Length::Fill), text(format_rate(latest.ssd_bytes_per_second)) .size(12) .color(muted_text()), ], mini_chart( &self.metrics_history, |point| point.ssd_wait_ms_per_second, Color::from_rgb8(220, 80, 86), ), row![ text("Inference wait") .size(12) .color(Color::from_rgb8(220, 80, 86)), Space::with_width(Length::Fill), text(format!("{:.0} ms/s", latest.ssd_wait_ms_per_second)) .size(12) .color(muted_text()), ], ] .spacing(7) .into(), ); let model = stats_panel( "MODEL CORE", column![ metric_row("State", stats.phase.label()), metric_row("Loaded model", stats.model), metric_row("Mapped weights", format_bytes(stats.model_bytes)), metric_row("Tensors", format_count(stats.tensor_count)), metric_row("Vocabulary", format_count(stats.vocabulary_size)), metric_row("Last load", format_milliseconds(stats.model_load_ms)), metric_row( "Lifecycle", format!( "{} loads · {} unloads", stats.model_loads, stats.model_unloads ), ), ] .spacing(9) .into(), ); let runtime = stats_panel( "GENERATION", column![ metric_row("Last runtime", format_milliseconds(stats.last_runtime_ms)), metric_row( "Average runtime", format_milliseconds(stats.average_runtime_ms) ), metric_row("Last prompt", format_count(stats.last_prompt_tokens)), metric_row("Last reused", format_count(stats.last_cached_tokens)), metric_row( "Last completion", format_count(stats.last_completion_tokens) ), metric_row("Cached tokens total", format_count(stats.cached_tokens)), metric_row( "Cache reuse", format!("{:.0}% of last prompt", cache_fraction * 100.0), ), metric_row( "Results", format!( "{} completed · {} failed", stats.completed_requests, stats.failed_requests ), ), ] .spacing(9) .into(), ); let acceptance = if stats.drafted_tokens == 0 { 0.0 } else { stats.accepted_draft_tokens as f64 / stats.drafted_tokens as f64 }; let target_passes = stats .speculative_cycles .saturating_add(stats.verifier_passes); let effective_speedup = if target_passes == 0 { 1.0 } else { stats .speculative_cycles .saturating_add(stats.accepted_draft_tokens) as f64 / target_passes as f64 }; let speculative = stats_panel( "SPECULATIVE DECODING", column![ metric_row( "Mode", match stats.speculative_mode { 1 => "Legacy MTP", 2 => "DSpark", _ => "Off", }, ), metric_row("Cycles", format_count(stats.speculative_cycles)), metric_row("Drafted", format_count(stats.drafted_tokens)), metric_row("Accepted", format_count(stats.accepted_draft_tokens)), metric_row("Acceptance", format!("{:.1}%", acceptance * 100.0)), metric_row( "Target verifier passes", format_count(stats.verifier_passes) ), metric_row("Verifier wall time", format_milliseconds(stats.verifier_ms)), metric_row( "Effective target-pass speedup", format!("{effective_speedup:.2}×") ), ] .spacing(9) .into(), ); let ssd = stats_panel( "SSD EXPERT STREAMING", column![ metric_row("State", if stats.ssd_enabled { "Enabled" } else { "Off" }), metric_row("Resident weights", format_bytes(stats.ssd_resident_bytes)), metric_row("Expert cache", format_bytes(stats.ssd_cache_bytes)), metric_row( "Cache capacity", format!("{} experts", stats.ssd_cache_experts) ), metric_row( "Preloaded", format!("{} experts", stats.ssd_preloaded_experts) ), metric_row( "Selected-load requests", format_count(stats.ssd_selected_requests) ), metric_row( "Requested expert bytes", format_bytes(stats.ssd_requested_bytes) ), metric_row("Selected-load wait", format_milliseconds(stats.ssd_wait_ms)), metric_row( "Average load wait", format_milliseconds( stats .ssd_wait_ms .checked_div(stats.ssd_selected_requests) .unwrap_or(0) ) ), ] .spacing(9) .into(), ); let cache = stats_panel( "KV CACHE", column![ metric_row( "Total", format!( "{} · {} files", format_bytes(stats.kv_bytes), stats.kv_files ) ), metric_row( "Local sessions", format!( "{} · {} files", format_bytes(stats.local_kv_bytes), stats.local_kv_files ), ), metric_row( "HTTP transient", format!( "{} · {} files", format_bytes(stats.http_kv_bytes), stats.http_kv_files ), ), metric_row("Checkpoint writes", format_count(stats.checkpoint_writes)), metric_row( "Exact hits", format!( "{} · {} memory / {} disk", stats.kv_hits, stats.kv_memory_hits, stats.kv_disk_hits ), ), metric_row( "Misses", format!("{} · {} invalid", stats.kv_misses, stats.kv_invalid), ), metric_row("Lookups", format_count(stats.kv_lookups)), metric_row( "Exact hit rate", format!("{:.1}%", cache_hit_fraction * 100.0) ), metric_row("Prefix hits", format_count(stats.kv_prefix_hits)), metric_row( "Reads", format!( "{} · {} · {} errors · last {}", stats.kv_read_operations, format_bytes(stats.kv_read_bytes), stats.kv_read_errors, format_milliseconds(stats.last_kv_read_ms), ), ), metric_row( "Writes", format!( "{} · {} · {} errors · last {}", stats.kv_write_operations, format_bytes(stats.kv_write_bytes), stats.kv_write_errors, format_milliseconds(stats.last_kv_write_ms), ), ), progress_bar(0.0..=1.0, cache_fraction.min(1.0)).height(4), ] .spacing(9) .into(), ); let disc = stats_panel("KV CACHE DISC USAGE", self.cache_explorer()); let server = stats_panel( "LOCAL SERVER", column![ metric_row("Endpoint", endpoint), metric_row("Active", format_count(u64::from(stats.http_active))), metric_row("Completed", format_count(stats.http_completed)), metric_row("Chat completions", format_count(stats.http_chat_requests)), metric_row("Model queries", format_count(stats.http_model_requests)), metric_row( "Runtime sources", format!( "{} local · {} HTTP", stats.local_requests, stats.endpoint_generations ), ), metric_row("Received", format_bytes(stats.http_bytes_received)), metric_row("Last latency", format_milliseconds(stats.last_http_ms)), metric_row( "Average latency", format_milliseconds(stats.average_http_ms) ), ] .spacing(9) .into(), ); scrollable( container( column![ heading, headline, throughput, ssd_activity, kv_io, requests, disc, row![model, runtime].spacing(10), row![speculative, ssd].spacing(10), row![cache, server].spacing(10), text("Counters are published by the runtime with relaxed atomics and sampled by the UI every 200 ms.") .size(11) .color(muted_text()), ] .spacing(12), ) .padding(24) .max_width(960) .center_x(Length::Fill), ) .height(Length::Fill) .into() } /// Disc usage of both cache buckets. The bar splits the transient store by /// age — the order the budget evicts in — and carries the session /// checkpoints as one further segment, since those are only freed with /// their session and so compete for disc without ever ageing out. fn cache_explorer(&self) -> Element<'_, Message> { let usage = &self.kv_cache_report; let total = usage.total_bytes(); let capacity = total.max(1); let segments = crate::metrics::CACHE_AGE_BUCKETS .iter() .enumerate() .map(|(index, (label, _))| (*label, usage.age_bytes[index], AGE_COLORS[index])) .chain([("Sessions", usage.session_bytes, SESSION_COLOR)]); let mut bar = row![].height(14).spacing(2); let mut legend = column![].spacing(6); for (label, bytes, color) in segments { if bytes == 0 { continue; } bar = bar.push( container(Space::new(Length::Fill, Length::Fill)) .width(Length::FillPortion(portion(bytes, capacity))) .style(move |_| chart_bar_style(color)), ); legend = legend.push( row![ container(Space::new(9, 9)).style(move |_| chart_bar_style(color)), text(label).size(12).color(muted_text()), Space::with_width(Length::Fill), text(format!( "{} · {:.0}%", format_bytes(bytes), bytes as f32 / total.max(1) as f32 * 100.0 )) .size(12), ] .spacing(8) .align_y(Alignment::Center), ); } if total == 0 { bar = bar.push( container(Space::new(Length::Fill, Length::Fill)) .style(|_| chart_bar_style(muted_text().scale_alpha(0.25))), ); } let mut rows = column![].spacing(7); for entry in &usage.entries { let path = entry.path.clone(); let label = entry .session .and_then(|id| self.session_title(id)) .map_or_else( || entry.name.clone(), |title| format!("{}: {}", entry.name, shorten(&title)), ); rows = rows.push( row![ text(label).size(12), Space::with_width(Length::Fill), text(format!( "{} · {} old", format_bytes(entry.bytes), format_duration(entry.age_seconds as f64) )) .size(12) .color(muted_text()), action_button(text("Discard").size(12)) .on_press(Message::DiscardCacheEntry(path)), ] .spacing(10) .align_y(Alignment::Center), ); } column![ metric_row( "Stored", format!( "{} · {} files", format_bytes(total), usage.session_files + usage.transient_files ), ), container(bar).height(14).width(Length::Fill), legend, metric_row( "Sessions", format!( "{} · {} files · not evicted", format_bytes(usage.session_bytes), usage.session_files ), ), metric_row( "Transient", format!( "{} of {} budget · {} files · {:.0}% full", format_bytes(usage.transient_bytes), format_bytes(usage.budget_bytes), usage.transient_files, usage.transient_bytes as f32 / usage.budget_bytes.max(1) as f32 * 100.0, ), ), Space::with_height(2), text(if usage.entries.is_empty() { "No checkpoints stored yet." } else { "OLDEST CHECKPOINTS" }) .size(10) .color(muted_text()), rows, row![ text("Discarding a checkpoint costs one prefill to rebuild it.") .size(11) .color(muted_text()), Space::with_width(Length::Fill), action_button(text("Clear transient cache").size(12)) .on_press(Message::ClearTransientCache), ] .spacing(10) .align_y(Alignment::Center), ] .spacing(9) .into() } } /// Sessions sit outside the age scale: they are not evicted, only deleted. const SESSION_COLOR: Color = Color::from_rgb(0.85, 0.44, 0.56); /// Age buckets from green (fresh) to grey (past every hit half-life). const AGE_COLORS: [Color; 5] = [ Color::from_rgb(0.28, 0.69, 0.44), Color::from_rgb(0.33, 0.67, 1.0), Color::from_rgb(0.62, 0.47, 1.0), Color::from_rgb(0.94, 0.71, 0.27), Color::from_rgb(0.45, 0.45, 0.48), ]; /// Keeps a long session title from crowding out the size and the action. fn shorten(title: &str) -> String { const LIMIT: usize = 44; if title.chars().count() <= LIMIT { return title.to_owned(); } title.chars().take(LIMIT - 1).chain(['…']).collect() } fn portion(bytes: u64, capacity: u64) -> u16 { ((bytes.saturating_mul(1000) / capacity.max(1)) as u16).max(1) }