dashboard #38
8 changed files with 141 additions and 7 deletions
|
|
@ -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 ─────────────────────────────────────────────────
|
||||
|
|
|
|||
|
|
@ -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(),
|
||||
}));
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -258,6 +258,7 @@ mod tests {
|
|||
last_msg_type: None,
|
||||
messages_processed: msgs,
|
||||
poisoned: false,
|
||||
message_type_counts: Vec::new(),
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -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();
|
||||
|
|
|
|||
|
|
@ -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) {
|
||||
|
|
|
|||
|
|
@ -232,6 +232,7 @@ mod tests {
|
|||
last_msg_type: None,
|
||||
messages_processed: msgs,
|
||||
poisoned,
|
||||
message_type_counts: Vec::new(),
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -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.
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
}
|
||||
}));
|
||||
}
|
||||
|
|
|
|||
Loading…
Reference in a new issue