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,
}
}));
}