dashboard #38

Merged
zacheryasc merged 10 commits from dashboard into master 2026-02-13 14:42:11 +00:00
8 changed files with 141 additions and 7 deletions
Showing only changes of commit 982d82776d - Show all commits

View file

@ -62,6 +62,18 @@ pub const ACTOR_DETAIL_HTML: &str = r##"<!DOCTYPE html>
.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##"<!DOCTYPE html>
<svg id="mboxSpark" viewBox="0 0 400 50" preserveAspectRatio="none"></svg>
</div>
<div class="type-breakdown" id="typeBreakdown" style="display:none;">
<h3>Message Types</h3>
<div id="typeRows"></div>
</div>
<div class="logs-panel">
<h3>
<span>Logs</span>
@ -257,6 +274,38 @@ pub const ACTOR_DETAIL_HTML: &str = r##"<!DOCTYPE html>
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 += '<div class="type-row">' +
'<span class="type-name" title="' + escapeHtml(name) + '">' + escapeHtml(shortName) + '</span>' +
'<div class="type-bar-bg"><div class="type-bar-fill" style="width:' + barPct + '%;background:' + color + ';"></div></div>' +
'<span class="type-count">' + count.toLocaleString() + '</span>' +
'<span class="type-pct">' + pct + '%</span>' +
'</div>';
}
container.innerHTML = html;
}
// ─── Logging ─────────────────────────────────────────────────

View file

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

View file

@ -258,6 +258,7 @@ mod tests {
last_msg_type: None,
messages_processed: msgs,
poisoned: false,
message_type_counts: Vec::new(),
}
}

View file

@ -38,6 +38,8 @@ pub struct ActorRow {
pub sparkline_mailbox: Vec<u64>,
/// Per-actor message rate sparkline.
pub sparkline_rates: Vec<u64>,
/// 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();

View file

@ -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<Line> = 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) {

View file

@ -232,6 +232,7 @@ mod tests {
last_msg_type: None,
messages_processed: msgs,
poisoned,
message_type_counts: Vec::new(),
}
}

View file

@ -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.

View file

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