diff --git a/crates/runtime-dashboard/src/actor_detail_html.rs b/crates/runtime-dashboard/src/actor_detail_html.rs index 9f6fdb5..788a54e 100644 --- a/crates/runtime-dashboard/src/actor_detail_html.rs +++ b/crates/runtime-dashboard/src/actor_detail_html.rs @@ -62,6 +62,18 @@ pub const ACTOR_DETAIL_HTML: &str = r##" .sparkline-panel h3 { font-size: 11px; color: #888; text-transform: uppercase; letter-spacing: 1px; margin-bottom: 8px; } .sparkline-panel svg { width: 100%; height: 50px; } + .type-breakdown { + background: #161822; border: 1px solid #2a2d3e; border-radius: 6px; + padding: 14px; margin-bottom: 12px; + } + .type-breakdown h3 { font-size: 11px; color: #888; text-transform: uppercase; letter-spacing: 1px; margin-bottom: 8px; } + .type-row { display: flex; align-items: center; gap: 8px; margin-bottom: 4px; font-size: 11px; } + .type-name { color: #e0e0e0; min-width: 180px; text-overflow: ellipsis; overflow: hidden; white-space: nowrap; } + .type-bar-bg { flex: 1; background: #1e2030; height: 14px; border-radius: 2px; overflow: hidden; } + .type-bar-fill { height: 100%; border-radius: 2px; } + .type-count { color: #888; min-width: 60px; text-align: right; } + .type-pct { color: #555; min-width: 40px; text-align: right; } + .logs-panel { background: #161822; border: 1px solid #2a2d3e; border-radius: 6px; padding: 14px; margin-bottom: 12px; @@ -148,6 +160,11 @@ pub const ACTOR_DETAIL_HTML: &str = r##" + +

Logs @@ -257,6 +274,38 @@ pub const ACTOR_DETAIL_HTML: &str = r##" updateSparklineSvg(document.getElementById('rateSpark'), history.rates, '#4caf50'); updateSparklineSvg(document.getElementById('mboxSpark'), history.mailbox, '#2196f3'); + + // Update message type breakdown + updateTypeBreakdown(actor.message_type_counts); + } + + var typeColors = ['#4caf50','#2196f3','#ff9800','#9c27b0','#00bcd4','#f44336','#ffeb3b','#e91e63']; + + function updateTypeBreakdown(types) { + var panel = document.getElementById('typeBreakdown'); + var container = document.getElementById('typeRows'); + if (!types || types.length === 0) { panel.style.display = 'none'; return; } + panel.style.display = ''; + var total = 0; + for (var i = 0; i < types.length; i++) total += types[i][1]; + if (total === 0) { panel.style.display = 'none'; return; } + + var html = ''; + for (var i = 0; i < types.length; i++) { + var name = types[i][0]; + var count = types[i][1]; + var pct = (count / total * 100).toFixed(1); + var barPct = (count / types[0][1] * 100).toFixed(1); + var color = typeColors[i % typeColors.length]; + var shortName = name.split('::').pop(); + html += '
' + + '' + escapeHtml(shortName) + '' + + '
' + + '' + count.toLocaleString() + '' + + '' + pct + '%' + + '
'; + } + container.innerHTML = html; } // ─── Logging ───────────────────────────────────────────────── diff --git a/crates/runtime-dashboard/src/collector.rs b/crates/runtime-dashboard/src/collector.rs index ab1dcd7..145032b 100644 --- a/crates/runtime-dashboard/src/collector.rs +++ b/crates/runtime-dashboard/src/collector.rs @@ -60,6 +60,7 @@ impl StatsHook for StatsCollector { last_msg_type: s.last_msg_type.map(|t| t.to_string()), messages_processed: s.messages_processed, poisoned: s.poisoned, + message_type_counts: s.message_type_counts.iter().map(|(k, v)| (k.to_string(), *v)).collect(), })); } } diff --git a/crates/runtime-dashboard/src/history.rs b/crates/runtime-dashboard/src/history.rs index dd5727d..51be11d 100644 --- a/crates/runtime-dashboard/src/history.rs +++ b/crates/runtime-dashboard/src/history.rs @@ -258,6 +258,7 @@ mod tests { last_msg_type: None, messages_processed: msgs, poisoned: false, + message_type_counts: Vec::new(), } } diff --git a/crates/runtime-dashboard/src/tui/app.rs b/crates/runtime-dashboard/src/tui/app.rs index 31ee12e..7406d89 100644 --- a/crates/runtime-dashboard/src/tui/app.rs +++ b/crates/runtime-dashboard/src/tui/app.rs @@ -38,6 +38,8 @@ pub struct ActorRow { pub sparkline_mailbox: Vec, /// Per-actor message rate sparkline. pub sparkline_rates: Vec, + /// Per-message-type counts, sorted descending. + pub message_type_counts: Vec<(String, u64)>, } #[derive(Clone, Copy, PartialEq, Eq)] @@ -335,6 +337,7 @@ impl App { poisoned: a.poisoned, sparkline_rates: rates_buf.iter().copied().collect(), sparkline_mailbox: mbox_buf.iter().copied().collect(), + message_type_counts: a.message_type_counts.clone(), }); } self.sort_actors(); diff --git a/crates/runtime-dashboard/src/tui/ui.rs b/crates/runtime-dashboard/src/tui/ui.rs index c97d037..db26d13 100644 --- a/crates/runtime-dashboard/src/tui/ui.rs +++ b/crates/runtime-dashboard/src/tui/ui.rs @@ -538,12 +538,20 @@ fn draw_actor_detail(f: &mut Frame, app: &App) { } }; + let has_types = !actor.message_type_counts.is_empty(); + let type_height = if has_types { + (actor.message_type_counts.len() as u16 + 2).min(10) + } else { + 0 + }; + let chunks = Layout::vertical([ - Constraint::Length(5), // Info card - Constraint::Length(5), // Rate sparkline - Constraint::Length(5), // Mailbox sparkline - Constraint::Length(1), // Help bar - Constraint::Fill(1), // Logs panel + Constraint::Length(5), // Info card + Constraint::Length(5), // Rate sparkline + Constraint::Length(5), // Mailbox sparkline + Constraint::Length(type_height), // Type breakdown + Constraint::Length(1), // Help bar + Constraint::Fill(1), // Logs panel ]) .split(f.area()); @@ -608,6 +616,11 @@ fn draw_actor_detail(f: &mut Frame, app: &App) { .style(Style::default().fg(Color::Blue)); f.render_widget(mbox_sparkline, chunks[2]); + // Message type breakdown + if has_types { + draw_type_breakdown(f, &actor.message_type_counts, chunks[3]); + } + // Help bar let level_names = ["ERR", "WARN", "INFO", "DBG", "TRC"]; let level_colors = [Color::Red, Color::Yellow, Color::Blue, Color::DarkGray, Color::DarkGray]; @@ -627,10 +640,60 @@ fn draw_actor_detail(f: &mut Frame, app: &App) { }; help_spans.push(Span::styled(format!("{}:{} ", i + 1, name), style)); } - f.render_widget(Paragraph::new(Line::from(help_spans)), chunks[3]); + f.render_widget(Paragraph::new(Line::from(help_spans)), chunks[4]); // Logs panel - draw_actor_logs(f, app, chunks[4]); + draw_actor_logs(f, app, chunks[5]); +} + +fn draw_type_breakdown(f: &mut Frame, types: &[(String, u64)], area: Rect) { + let total: u64 = types.iter().map(|(_, c)| *c).sum(); + let max_count = types.first().map(|(_, c)| *c).unwrap_or(1).max(1); + let inner_height = area.height.saturating_sub(2) as usize; + + let bar_colors = [Color::Green, Color::Blue, Color::Yellow, Color::Magenta, Color::Cyan, Color::Red]; + + let lines: Vec = types + .iter() + .take(inner_height) + .enumerate() + .map(|(i, (name, count))| { + let short = name.rsplit("::").next().unwrap_or(name); + let pct = if total > 0 { *count as f64 / total as f64 * 100.0 } else { 0.0 }; + let bar_width = 20usize; + let filled = ((*count as f64 / max_count as f64) * bar_width as f64).round() as usize; + let color = bar_colors[i % bar_colors.len()]; + + Line::from(vec![ + Span::styled( + format!(" {:>16} ", short), + Style::default().fg(Color::White), + ), + Span::styled( + "\u{2588}".repeat(filled), + Style::default().fg(color), + ), + Span::styled( + " ".repeat(bar_width.saturating_sub(filled)), + Style::default(), + ), + Span::styled( + format!(" {:>8} ", count), + Style::default().fg(Color::Cyan).add_modifier(Modifier::BOLD), + ), + Span::styled( + format!("{:>5.1}%", pct), + Style::default().fg(Color::DarkGray), + ), + ]) + }) + .collect(); + + let block = Block::default() + .borders(Borders::ALL) + .title(format!(" Message Types ({}) ", types.len())); + let paragraph = Paragraph::new(lines).block(block); + f.render_widget(paragraph, area); } fn draw_actor_logs(f: &mut Frame, app: &App, area: Rect) { diff --git a/crates/runtime-dashboard/src/warnings.rs b/crates/runtime-dashboard/src/warnings.rs index 35400ee..d1756d1 100644 --- a/crates/runtime-dashboard/src/warnings.rs +++ b/crates/runtime-dashboard/src/warnings.rs @@ -232,6 +232,7 @@ mod tests { last_msg_type: None, messages_processed: msgs, poisoned, + message_type_counts: Vec::new(), } } diff --git a/src/stats.rs b/src/stats.rs index 5cabf5b..d899b06 100644 --- a/src/stats.rs +++ b/src/stats.rs @@ -120,6 +120,8 @@ pub struct ActorSnapshot { pub last_msg_type: Option<&'static str>, pub messages_processed: u64, pub poisoned: bool, + /// Per-message-type counts, sorted descending by count. + pub message_type_counts: Vec<(&'static str, u64)>, } /// Observer hook called by workers after productive ticks. @@ -150,6 +152,9 @@ pub struct ActorInfo { /// Whether the actor has panicked and is no longer processing messages. #[cfg_attr(feature = "serde", serde(default))] pub poisoned: bool, + /// Per-message-type counts, sorted descending by count. Top 32 types. + #[cfg_attr(feature = "serde", serde(default))] + pub message_type_counts: Vec<(String, u64)>, } /// Snapshot of overall runtime state. diff --git a/src/worker.rs b/src/worker.rs index 1cd7fef..acaa33b 100644 --- a/src/worker.rs +++ b/src/worker.rs @@ -610,6 +610,8 @@ struct ActorSlot { started: bool, last_msg_type: Option<&'static str>, messages_processed: u64, + /// Per-message-type counters (bounded to 32 entries). + msg_type_counts: HashMap<&'static str, u64>, /// Per-actor mailbox capacity. 0 = unbounded. mailbox_capacity: usize, overflow_policy: MailboxOverflow, @@ -645,6 +647,7 @@ impl ActorPool { started: false, last_msg_type: None, messages_processed: 0, + msg_type_counts: HashMap::new(), mailbox_capacity: self.default_mailbox_capacity, overflow_policy: self.default_overflow_policy, }); @@ -765,6 +768,10 @@ impl ActorPool { Ok(Some(type_name)) => { slot.last_msg_type = Some(type_name); slot.messages_processed += 1; + // Track per-type counts (bounded to 32 distinct types) + if slot.msg_type_counts.len() < 32 || slot.msg_type_counts.contains_key(type_name) { + *slot.msg_type_counts.entry(type_name).or_insert(0) += 1; + } } } count += 1; @@ -837,12 +844,16 @@ impl ActorPool { pub fn mailbox_depths_into(&self, out: &mut Vec) { out.clear(); out.extend(self.actors.iter().map(|(&addr, slot)| { + let mut type_counts: Vec<(&'static str, u64)> = + slot.msg_type_counts.iter().map(|(&k, &v)| (k, v)).collect(); + type_counts.sort_by(|a, b| b.1.cmp(&a.1)); ActorSnapshot { address: addr, mailbox_depth: slot.mailbox.len(), last_msg_type: slot.last_msg_type, messages_processed: slot.messages_processed, poisoned: slot.poisoned, + message_type_counts: type_counts, } })); }