swactor/crates/runtime-dashboard/src/collector.rs
Developer bf79d63be4 feat: command interface — frontend-agnostic dispatch crate
Add swactor-command crate that decouples command dispatch from the
dashboard, enabling the same commands from REPL, REST, TUI, or any
future frontend.

- CommandRouter with dispatch(), CommandHandler trait, CommandMeta
- CommandRequest/CommandResponse (serde-serializable JSON protocol)
- CommandContext with Arc<Runtime> + optional StatsEnricher trait
- 9 built-in commands extracted from investigate.rs:
  help, overview, workers, worker, actors, actor, hot, phases, diff,
  shutdown
- REPL line parser (parse_line) with positional arg mapping
- REST adapter (from_query_params) for HTTP query parameters
- Dashboard integration: StatsEnricher impl for StatsCollector,
  investigate.rs refactored to thin CommandRouter wrapper,
  server.rs updated to share CommandRouter across handler threads
- 18 behavioral tests in crates/command/tests/command_api.rs

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-02-13 07:25:50 +00:00

66 lines
2.2 KiB
Rust

//! Lock-free-ish stats collector that implements [`StatsHook`].
//!
//! Workers push per-actor snapshots here; dashboard code reads them back
//! via [`actor_details()`](StatsCollector::actor_details) or
//! [`enrich()`](StatsCollector::enrich).
use std::sync::{Arc, RwLock};
use swactor::stats::{ActorInfo, ActorSnapshot, RuntimeStats, StatsHook};
/// Collects per-actor snapshots pushed by worker threads.
///
/// Create with [`new()`](Self::new), pass to both
/// [`Runtime::set_stats_hook()`](swactor::runtime::Runtime::set_stats_hook)
/// and the dashboard.
pub struct StatsCollector {
/// One slot per worker — writers never contend with each other.
slots: Vec<RwLock<Vec<ActorInfo>>>,
}
impl StatsCollector {
pub fn new(num_workers: usize) -> Arc<Self> {
let slots = (0..num_workers)
.map(|_| RwLock::new(Vec::new()))
.collect();
Arc::new(Self { slots })
}
/// Collect all per-actor details across workers.
pub fn actor_details(&self) -> Vec<ActorInfo> {
let mut out = Vec::new();
for slot in &self.slots {
let guard = slot.read().unwrap();
out.extend(guard.iter().cloned());
}
out
}
/// Patch `actor_details` into an existing [`RuntimeStats`].
pub fn enrich(&self, stats: &mut RuntimeStats) {
stats.actor_details = self.actor_details();
}
}
impl swactor_command::StatsEnricher for StatsCollector {
fn enrich(&self, stats: &mut swactor::stats::RuntimeStats) {
stats.actor_details = self.actor_details();
}
}
impl StatsHook for StatsCollector {
fn on_tick(&self, worker_id: usize, snapshots: &[ActorSnapshot]) {
if let Some(slot) = self.slots.get(worker_id) {
let mut guard = slot.write().unwrap();
guard.clear();
guard.extend(snapshots.iter().map(|s| ActorInfo {
address: s.address,
worker_id,
mailbox_depth: s.mailbox_depth,
last_msg_type: s.last_msg_type.map(|t| t.to_string()),
messages_processed: s.messages_processed,
poisoned: s.poisoned,
}));
}
}
}