2026-02-11 15:23:26 +00:00
|
|
|
pub mod collector;
|
2026-02-13 17:34:01 +00:00
|
|
|
pub mod command;
|
2026-02-13 14:42:10 +00:00
|
|
|
pub mod history;
|
2026-06-23 15:42:28 +00:00
|
|
|
mod html;
|
2026-02-12 09:13:50 +00:00
|
|
|
pub mod investigate;
|
2026-02-09 09:04:57 +00:00
|
|
|
pub mod layer;
|
2026-02-24 09:12:28 +00:00
|
|
|
pub mod plugin;
|
2026-02-09 09:04:57 +00:00
|
|
|
mod server;
|
2026-06-23 15:42:28 +00:00
|
|
|
pub mod telemetry;
|
2026-02-13 14:42:10 +00:00
|
|
|
pub mod topology;
|
2026-06-23 15:42:28 +00:00
|
|
|
pub mod warnings;
|
2026-02-09 09:04:57 +00:00
|
|
|
|
2026-02-11 15:23:26 +00:00
|
|
|
#[cfg(feature = "tui")]
|
|
|
|
|
pub mod tui;
|
|
|
|
|
|
2026-06-05 07:25:43 +00:00
|
|
|
pub mod datastream_source;
|
|
|
|
|
|
|
|
|
|
/// The canonical Distribution page (the SWIM connection-graph view). Owned by
|
|
|
|
|
/// the dashboard crate so every front-end that serves it — a live node's
|
|
|
|
|
/// `DistributionPlugin` and the datastream dashboard — renders the exact same
|
2026-06-09 09:29:07 +00:00
|
|
|
/// page and chrome, fed by the distribution-page JSON the datastream consumer
|
|
|
|
|
/// reconstructs (see [`crate::datastream_source`]).
|
2026-06-05 07:25:43 +00:00
|
|
|
pub const DISTRIBUTION_PAGE_HTML: &str = include_str!("distribution_page.html");
|
|
|
|
|
|
2026-02-09 09:04:57 +00:00
|
|
|
use std::io;
|
|
|
|
|
use std::sync::atomic::{AtomicBool, Ordering};
|
|
|
|
|
use std::sync::{Arc, Mutex};
|
|
|
|
|
use std::thread;
|
|
|
|
|
use std::time::Duration;
|
|
|
|
|
|
2026-02-11 15:23:26 +00:00
|
|
|
use crossbeam_queue::ArrayQueue;
|
2026-02-09 09:04:57 +00:00
|
|
|
use swactor::config::RuntimeConfig;
|
|
|
|
|
use swactor::runtime::{Runtime, RuntimeHandle};
|
|
|
|
|
use tracing_subscriber::layer::SubscriberExt;
|
|
|
|
|
use tracing_subscriber::util::SubscriberInitExt;
|
|
|
|
|
|
refactor: remove dead code, consolidate files (#52)
Remove the unused TCP transport and dead CLI/simulation scaffolding and collapse scattered single-purpose modules into consolidated files across the dashboard, process, and simulation crates.
- crates/transport: drop the TCP transport (src/tcp.rs and its tcp feature), leaving only ed25519 identity and encoding utilities behind the iroh transport
- crates/dashboard: collapse actor_detail_html/actors_html/dashboard_html/topology_html into a single html.rs, drop command/parse.rs, and inline the trace types into lib.rs
- crates/process: fold driver, pipeline_types, queue, subscriber, waker, and local pipes/signal/wait into local/mod.rs, pipeline.rs, and a unified types.rs, consolidating the public re-exports
- crates/simulation: remove the ci subdirectory (local_sim, sim) and dead node/config/trace modules, and flatten the distribution subdirectory into top-level files
- crates/datastore: remove the unused store_cli, cli, and in-memory storage, and deduplicate crypto.rs across datastore and distribution (about 190 lines of shared code removed)
- crates/distribution: drop dead codec code and trim the messages module
Signed-off-by: Zachery Aaron Shores-Chmielewski <zacheryasc@gmail.com>
2026-02-25 14:33:04 +00:00
|
|
|
use serde::{Deserialize, Serialize};
|
|
|
|
|
use swactor::stats::RuntimeStats;
|
|
|
|
|
|
2026-02-11 15:23:26 +00:00
|
|
|
use crate::collector::StatsCollector;
|
2026-02-13 14:42:10 +00:00
|
|
|
use crate::history::{DashboardHistory, HistoryConfig};
|
2026-06-23 15:42:28 +00:00
|
|
|
use crate::layer::{DashboardEvent, DashboardLayer, EventStore, now_ms};
|
2026-02-24 09:12:28 +00:00
|
|
|
use crate::plugin::PluginRegistry;
|
refactor: remove dead code, consolidate files (#52)
Remove the unused TCP transport and dead CLI/simulation scaffolding and collapse scattered single-purpose modules into consolidated files across the dashboard, process, and simulation crates.
- crates/transport: drop the TCP transport (src/tcp.rs and its tcp feature), leaving only ed25519 identity and encoding utilities behind the iroh transport
- crates/dashboard: collapse actor_detail_html/actors_html/dashboard_html/topology_html into a single html.rs, drop command/parse.rs, and inline the trace types into lib.rs
- crates/process: fold driver, pipeline_types, queue, subscriber, waker, and local pipes/signal/wait into local/mod.rs, pipeline.rs, and a unified types.rs, consolidating the public re-exports
- crates/simulation: remove the ci subdirectory (local_sim, sim) and dead node/config/trace modules, and flatten the distribution subdirectory into top-level files
- crates/datastore: remove the unused store_cli, cli, and in-memory storage, and deduplicate crypto.rs across datastore and distribution (about 190 lines of shared code removed)
- crates/distribution: drop dead codec code and trim the messages module
Signed-off-by: Zachery Aaron Shores-Chmielewski <zacheryasc@gmail.com>
2026-02-25 14:33:04 +00:00
|
|
|
|
|
|
|
|
// ─── Trace Types ────────────────────────────────────────────────────────────
|
|
|
|
|
|
|
|
|
|
/// Complete trace of a runtime execution, suitable for saving/loading.
|
|
|
|
|
#[derive(Debug, Serialize, Deserialize)]
|
|
|
|
|
pub struct RuntimeTrace {
|
|
|
|
|
pub events: Vec<DashboardEvent>,
|
|
|
|
|
pub stats_timeline: Vec<TimestampedStats>,
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/// A stats snapshot with a wall-clock timestamp.
|
|
|
|
|
#[derive(Debug, Clone, Serialize, Deserialize)]
|
|
|
|
|
pub struct TimestampedStats {
|
|
|
|
|
pub timestamp_ms: u64,
|
|
|
|
|
pub stats: RuntimeStats,
|
|
|
|
|
}
|
2026-02-09 09:04:57 +00:00
|
|
|
|
2026-02-25 11:11:03 +00:00
|
|
|
/// Peer info sent through the join channel.
|
|
|
|
|
pub struct JoinPeerInfo {
|
|
|
|
|
pub node_id: [u8; 32],
|
|
|
|
|
pub relay_url: Option<String>,
|
|
|
|
|
pub direct_addrs: Vec<std::net::SocketAddr>,
|
|
|
|
|
}
|
2026-02-19 14:39:33 +00:00
|
|
|
|
2026-02-09 09:04:57 +00:00
|
|
|
/// Configuration for the runtime dashboard.
|
|
|
|
|
#[derive(Debug, Clone)]
|
|
|
|
|
pub struct DashboardConfig {
|
|
|
|
|
pub port: u16,
|
|
|
|
|
pub event_capacity: usize,
|
2026-02-11 15:23:26 +00:00
|
|
|
/// Enable trace recording for `save_trace()`. When true, events and stats
|
|
|
|
|
/// are kept in lock-free bounded ring buffers and stats are periodically sampled.
|
2026-02-09 09:04:57 +00:00
|
|
|
pub record: bool,
|
2026-02-11 15:23:26 +00:00
|
|
|
/// Maximum events retained in the recording log. Only used when `record = true`.
|
|
|
|
|
pub record_event_capacity: usize,
|
|
|
|
|
/// Maximum stats snapshots retained in the timeline. Only used when `record = true`.
|
|
|
|
|
pub record_stats_capacity: usize,
|
2026-02-09 09:04:57 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
impl Default for DashboardConfig {
|
|
|
|
|
fn default() -> Self {
|
|
|
|
|
Self {
|
|
|
|
|
port: 9090,
|
|
|
|
|
event_capacity: 10_000,
|
|
|
|
|
record: false,
|
2026-02-11 15:23:26 +00:00
|
|
|
record_event_capacity: 100_000,
|
|
|
|
|
record_stats_capacity: 18_000,
|
2026-02-09 09:04:57 +00:00
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/// Configuration for replaying a recorded trace.
|
|
|
|
|
#[derive(Debug, Clone)]
|
|
|
|
|
pub struct ReplayConfig {
|
|
|
|
|
pub port: u16,
|
|
|
|
|
/// Playback speed multiplier (1.0 = real-time, 2.0 = double speed).
|
|
|
|
|
pub speed: f64,
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
impl Default for ReplayConfig {
|
|
|
|
|
fn default() -> Self {
|
|
|
|
|
Self {
|
|
|
|
|
port: 9090,
|
|
|
|
|
speed: 1.0,
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/// Handle to a running dashboard. Allows attaching a runtime after creation.
|
|
|
|
|
pub struct DashboardHandle {
|
|
|
|
|
store: Arc<EventStore>,
|
|
|
|
|
runtime: Arc<Mutex<Option<Arc<Runtime>>>>,
|
2026-02-11 15:23:26 +00:00
|
|
|
collector: Arc<Mutex<Option<Arc<StatsCollector>>>>,
|
2026-06-05 07:25:43 +00:00
|
|
|
/// Externally pushed stats, used when no live `Runtime` is attached (e.g. a
|
|
|
|
|
/// datastream-backed source synthesizes `RuntimeStats` and pushes them here).
|
|
|
|
|
pushed_stats: Arc<Mutex<Option<RuntimeStats>>>,
|
2026-02-09 09:04:57 +00:00
|
|
|
shutdown: Arc<AtomicBool>,
|
2026-02-19 14:39:33 +00:00
|
|
|
shutdown_notify: Arc<tokio::sync::Notify>,
|
2026-02-11 15:23:26 +00:00
|
|
|
stats_timeline: Arc<ArrayQueue<TimestampedStats>>,
|
2026-02-13 14:42:10 +00:00
|
|
|
history: Arc<DashboardHistory>,
|
2026-02-09 09:04:57 +00:00
|
|
|
recording: bool,
|
2026-02-19 14:39:33 +00:00
|
|
|
port: u16,
|
2026-02-24 09:12:28 +00:00
|
|
|
plugin_registry: Arc<PluginRegistry>,
|
2026-02-19 14:39:33 +00:00
|
|
|
standalone_rt: Mutex<Option<tokio::runtime::Runtime>>,
|
2026-06-06 17:53:25 +00:00
|
|
|
/// Optional override for the `/` landing page (e.g. a host serving a fleet
|
|
|
|
|
/// board instead of the runtime-less actor dashboard).
|
2026-05-29 08:55:10 +00:00
|
|
|
landing_html: Mutex<Option<Arc<str>>>,
|
2026-06-06 17:53:25 +00:00
|
|
|
/// Optional extra axum router merged into the live server, for hosts that
|
|
|
|
|
/// add disjoint routes of their own.
|
2026-05-29 08:55:10 +00:00
|
|
|
extra_router: Mutex<Option<axum::Router>>,
|
2026-02-09 09:04:57 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
impl DashboardHandle {
|
|
|
|
|
/// Install a global tracing subscriber with the dashboard layer.
|
|
|
|
|
pub fn install_tracing(&self) {
|
|
|
|
|
tracing_subscriber::registry()
|
|
|
|
|
.with(DashboardLayer::new(Arc::clone(&self.store)))
|
|
|
|
|
.init();
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/// Return the raw `DashboardLayer` for users who want to compose their own subscriber.
|
|
|
|
|
pub fn layer(&self) -> DashboardLayer {
|
|
|
|
|
DashboardLayer::new(Arc::clone(&self.store))
|
|
|
|
|
}
|
|
|
|
|
|
2026-02-11 15:23:26 +00:00
|
|
|
/// Attach a runtime and its stats collector, enabling stats polling.
|
|
|
|
|
pub fn set_runtime(&self, runtime: Arc<Runtime>, collector: Arc<StatsCollector>) {
|
2026-02-09 09:04:57 +00:00
|
|
|
*self.runtime.lock().unwrap() = Some(runtime);
|
2026-02-11 15:23:26 +00:00
|
|
|
*self.collector.lock().unwrap() = Some(collector);
|
2026-02-09 09:04:57 +00:00
|
|
|
}
|
|
|
|
|
|
2026-06-05 07:25:43 +00:00
|
|
|
/// Push a stats snapshot from an external source (latest wins). When no live
|
|
|
|
|
/// `Runtime` is attached, the SSE loop serves these to the dashboard exactly
|
|
|
|
|
/// as if they came from a runtime. Used by the datastream source.
|
|
|
|
|
pub fn set_stats(&self, stats: RuntimeStats) {
|
|
|
|
|
*self.pushed_stats.lock().unwrap() = Some(stats);
|
|
|
|
|
}
|
|
|
|
|
|
2026-02-09 09:04:57 +00:00
|
|
|
/// Whether trace recording is enabled.
|
|
|
|
|
pub fn is_recording(&self) -> bool {
|
|
|
|
|
self.recording
|
|
|
|
|
}
|
|
|
|
|
|
2026-02-24 09:12:28 +00:00
|
|
|
/// Register a plugin with the dashboard.
|
|
|
|
|
pub fn register_plugin(&self, plugin: Arc<dyn plugin::DashboardPlugin>) {
|
|
|
|
|
self.plugin_registry.register(plugin);
|
2026-02-19 14:39:33 +00:00
|
|
|
}
|
|
|
|
|
|
2026-05-29 08:55:10 +00:00
|
|
|
/// Override the `/` landing page with custom HTML. Used when the dashboard
|
|
|
|
|
/// runs without a swactor runtime (e.g. the live collector) so `/` shows the
|
|
|
|
|
/// unified fleet board rather than the empty actor dashboard.
|
|
|
|
|
pub fn set_landing_html(&self, html: impl Into<Arc<str>>) {
|
|
|
|
|
*self.landing_html.lock().unwrap() = Some(html.into());
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/// Merge an extra axum router into the live HTTP server. The routes must be
|
|
|
|
|
/// disjoint from the dashboard's own (the collector's `/diag/*` are). Must be
|
|
|
|
|
/// called before `start_http`/`start_http_standalone`.
|
|
|
|
|
pub fn set_extra_router(&self, router: axum::Router) {
|
|
|
|
|
*self.extra_router.lock().unwrap() = Some(router);
|
|
|
|
|
}
|
|
|
|
|
|
2026-02-13 14:42:10 +00:00
|
|
|
/// Access the time-series history store (for TUI sparklines, etc.).
|
|
|
|
|
pub fn history(&self) -> &Arc<DashboardHistory> {
|
|
|
|
|
&self.history
|
|
|
|
|
}
|
|
|
|
|
|
2026-02-09 09:04:57 +00:00
|
|
|
/// Signal the dashboard to shut down (SSE clients receive "done").
|
|
|
|
|
pub fn shutdown(&self) {
|
|
|
|
|
self.shutdown.store(true, Ordering::Release);
|
2026-02-19 14:39:33 +00:00
|
|
|
self.shutdown_notify.notify_waiters();
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/// Start the HTTP server on the provided tokio handle.
|
|
|
|
|
/// Use this when a tokio runtime already exists (e.g. IrohDriver's runtime).
|
|
|
|
|
pub fn start_http(&self, handle: tokio::runtime::Handle) {
|
|
|
|
|
let state = self.build_app_state();
|
2026-05-29 08:55:10 +00:00
|
|
|
let extra = self.extra_router.lock().unwrap().take();
|
2026-02-19 14:39:33 +00:00
|
|
|
let port = self.port;
|
|
|
|
|
handle.spawn(async move {
|
2026-05-29 08:55:10 +00:00
|
|
|
server::run_server(state, port, extra).await;
|
2026-02-19 14:39:33 +00:00
|
|
|
});
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/// Start the HTTP server on a standalone tokio runtime (1 worker thread).
|
refactor: remove dead code, consolidate files (#52)
Remove the unused TCP transport and dead CLI/simulation scaffolding and collapse scattered single-purpose modules into consolidated files across the dashboard, process, and simulation crates.
- crates/transport: drop the TCP transport (src/tcp.rs and its tcp feature), leaving only ed25519 identity and encoding utilities behind the iroh transport
- crates/dashboard: collapse actor_detail_html/actors_html/dashboard_html/topology_html into a single html.rs, drop command/parse.rs, and inline the trace types into lib.rs
- crates/process: fold driver, pipeline_types, queue, subscriber, waker, and local pipes/signal/wait into local/mod.rs, pipeline.rs, and a unified types.rs, consolidating the public re-exports
- crates/simulation: remove the ci subdirectory (local_sim, sim) and dead node/config/trace modules, and flatten the distribution subdirectory into top-level files
- crates/datastore: remove the unused store_cli, cli, and in-memory storage, and deduplicate crypto.rs across datastore and distribution (about 190 lines of shared code removed)
- crates/distribution: drop dead codec code and trim the messages module
Signed-off-by: Zachery Aaron Shores-Chmielewski <zacheryasc@gmail.com>
2026-02-25 14:33:04 +00:00
|
|
|
/// Use this when no external tokio runtime is available (e.g. non-async transport).
|
2026-02-19 14:39:33 +00:00
|
|
|
pub fn start_http_standalone(&self) {
|
|
|
|
|
let rt = tokio::runtime::Builder::new_multi_thread()
|
|
|
|
|
.worker_threads(1)
|
|
|
|
|
.enable_all()
|
|
|
|
|
.build()
|
|
|
|
|
.expect("failed to create tokio runtime for dashboard HTTP");
|
|
|
|
|
let handle = rt.handle().clone();
|
|
|
|
|
*self.standalone_rt.lock().unwrap() = Some(rt);
|
|
|
|
|
self.start_http(handle);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
fn build_app_state(&self) -> server::AppState {
|
|
|
|
|
server::AppState {
|
|
|
|
|
store: Arc::clone(&self.store),
|
|
|
|
|
runtime: Arc::clone(&self.runtime),
|
|
|
|
|
collector: Arc::clone(&self.collector),
|
2026-06-05 07:25:43 +00:00
|
|
|
pushed_stats: Arc::clone(&self.pushed_stats),
|
2026-02-19 14:39:33 +00:00
|
|
|
shutdown: Arc::clone(&self.shutdown),
|
|
|
|
|
shutdown_notify: Arc::clone(&self.shutdown_notify),
|
|
|
|
|
history: Arc::clone(&self.history),
|
|
|
|
|
cmd_router: Arc::new(crate::command::CommandRouter::with_builtins()),
|
2026-02-24 09:12:28 +00:00
|
|
|
plugins: Arc::clone(&self.plugin_registry),
|
2026-05-29 08:55:10 +00:00
|
|
|
landing: self.landing_html.lock().unwrap().clone(),
|
2026-02-19 14:39:33 +00:00
|
|
|
}
|
2026-02-09 09:04:57 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/// Save the recorded trace to a JSON file.
|
|
|
|
|
///
|
|
|
|
|
/// Only works when `DashboardConfig::record` was set to `true`.
|
2026-02-11 15:23:26 +00:00
|
|
|
/// This drains the recording buffers — each call consumes the buffered data.
|
2026-02-09 09:04:57 +00:00
|
|
|
pub fn save_trace(&self, path: &str) -> io::Result<()> {
|
|
|
|
|
let events = self.store.all_events().ok_or_else(|| {
|
2026-06-23 15:42:28 +00:00
|
|
|
io::Error::other("recording not enabled (set DashboardConfig::record = true)")
|
2026-02-09 09:04:57 +00:00
|
|
|
})?;
|
2026-02-11 15:23:26 +00:00
|
|
|
let mut stats_timeline = Vec::new();
|
|
|
|
|
while let Some(ts) = self.stats_timeline.pop() {
|
|
|
|
|
stats_timeline.push(ts);
|
|
|
|
|
}
|
2026-02-09 09:04:57 +00:00
|
|
|
let trace = RuntimeTrace {
|
|
|
|
|
events,
|
|
|
|
|
stats_timeline,
|
|
|
|
|
};
|
2026-06-23 15:42:28 +00:00
|
|
|
let json = serde_json::to_string(&trace).map_err(|e| io::Error::other(e))?;
|
2026-02-09 09:04:57 +00:00
|
|
|
std::fs::write(path, json)
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
2026-02-19 14:39:33 +00:00
|
|
|
/// Start a dashboard and return a handle.
|
2026-02-09 09:04:57 +00:00
|
|
|
///
|
2026-02-19 14:39:33 +00:00
|
|
|
/// The dashboard state is created immediately but the HTTP server is NOT started.
|
|
|
|
|
/// Call `start_http()` or `start_http_standalone()` to begin serving.
|
|
|
|
|
/// Call `install_tracing()` to set up the global subscriber, and `set_runtime()`
|
|
|
|
|
/// to enable stats polling.
|
2026-02-09 09:04:57 +00:00
|
|
|
pub fn start_dashboard(config: DashboardConfig) -> DashboardHandle {
|
2026-02-11 15:23:26 +00:00
|
|
|
let store = Arc::new(EventStore::new(
|
|
|
|
|
config.event_capacity,
|
|
|
|
|
config.record,
|
|
|
|
|
config.record_event_capacity,
|
|
|
|
|
));
|
2026-02-09 09:04:57 +00:00
|
|
|
let runtime: Arc<Mutex<Option<Arc<Runtime>>>> = Arc::new(Mutex::new(None));
|
2026-02-11 15:23:26 +00:00
|
|
|
let collector: Arc<Mutex<Option<Arc<StatsCollector>>>> = Arc::new(Mutex::new(None));
|
2026-06-05 07:25:43 +00:00
|
|
|
let pushed_stats: Arc<Mutex<Option<RuntimeStats>>> = Arc::new(Mutex::new(None));
|
2026-02-09 09:04:57 +00:00
|
|
|
let shutdown = Arc::new(AtomicBool::new(false));
|
2026-02-19 14:39:33 +00:00
|
|
|
let shutdown_notify = Arc::new(tokio::sync::Notify::new());
|
2026-02-11 15:23:26 +00:00
|
|
|
let stats_timeline = Arc::new(ArrayQueue::new(config.record_stats_capacity.max(1)));
|
2026-02-13 14:42:10 +00:00
|
|
|
let history = Arc::new(DashboardHistory::new(HistoryConfig::default()));
|
2026-02-09 09:04:57 +00:00
|
|
|
|
|
|
|
|
// Start stats recorder thread when recording is enabled
|
|
|
|
|
if config.record {
|
|
|
|
|
let rt_ref = Arc::clone(&runtime);
|
2026-02-11 15:23:26 +00:00
|
|
|
let col_ref = Arc::clone(&collector);
|
2026-02-09 09:04:57 +00:00
|
|
|
let timeline = Arc::clone(&stats_timeline);
|
|
|
|
|
let stop = Arc::clone(&shutdown);
|
|
|
|
|
thread::spawn(move || {
|
|
|
|
|
loop {
|
|
|
|
|
if stop.load(Ordering::Relaxed) {
|
|
|
|
|
return;
|
|
|
|
|
}
|
|
|
|
|
let maybe_rt = rt_ref.lock().unwrap().clone();
|
|
|
|
|
if let Some(rt) = maybe_rt {
|
2026-02-11 15:23:26 +00:00
|
|
|
let mut stats = rt.stats();
|
|
|
|
|
if let Some(col) = col_ref.lock().unwrap().as_ref() {
|
|
|
|
|
col.enrich(&mut stats);
|
|
|
|
|
}
|
2026-02-09 09:04:57 +00:00
|
|
|
let ts = TimestampedStats {
|
|
|
|
|
timestamp_ms: now_ms(),
|
|
|
|
|
stats,
|
|
|
|
|
};
|
2026-02-11 15:23:26 +00:00
|
|
|
let _ = timeline.force_push(ts);
|
2026-02-09 09:04:57 +00:00
|
|
|
}
|
|
|
|
|
thread::sleep(Duration::from_millis(200));
|
|
|
|
|
}
|
|
|
|
|
});
|
|
|
|
|
}
|
|
|
|
|
|
2026-02-19 14:39:33 +00:00
|
|
|
let port = config.port;
|
2026-02-24 09:12:28 +00:00
|
|
|
let plugin_registry = Arc::new(PluginRegistry::new());
|
2026-02-09 09:04:57 +00:00
|
|
|
|
|
|
|
|
DashboardHandle {
|
|
|
|
|
store,
|
|
|
|
|
runtime,
|
2026-02-11 15:23:26 +00:00
|
|
|
collector,
|
2026-06-05 07:25:43 +00:00
|
|
|
pushed_stats,
|
2026-02-09 09:04:57 +00:00
|
|
|
shutdown,
|
2026-02-19 14:39:33 +00:00
|
|
|
shutdown_notify,
|
2026-02-09 09:04:57 +00:00
|
|
|
stats_timeline,
|
2026-02-13 14:42:10 +00:00
|
|
|
history,
|
2026-02-09 09:04:57 +00:00
|
|
|
recording: config.record,
|
2026-02-19 14:39:33 +00:00
|
|
|
port,
|
2026-02-24 09:12:28 +00:00
|
|
|
plugin_registry,
|
2026-02-19 14:39:33 +00:00
|
|
|
standalone_rt: Mutex::new(None),
|
2026-05-29 08:55:10 +00:00
|
|
|
landing_html: Mutex::new(None),
|
|
|
|
|
extra_router: Mutex::new(None),
|
2026-02-09 09:04:57 +00:00
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/// Convenience: create a runtime, start a dashboard, install tracing, and run.
|
|
|
|
|
///
|
|
|
|
|
/// Returns the runtime handle and dashboard handle.
|
|
|
|
|
pub fn run_with_dashboard(
|
|
|
|
|
rt_config: RuntimeConfig,
|
|
|
|
|
dash_config: DashboardConfig,
|
|
|
|
|
) -> (RuntimeHandle, DashboardHandle) {
|
|
|
|
|
let dash = start_dashboard(dash_config);
|
|
|
|
|
dash.install_tracing();
|
2026-02-19 14:39:33 +00:00
|
|
|
dash.start_http_standalone();
|
2026-02-09 09:04:57 +00:00
|
|
|
|
2026-06-23 15:42:28 +00:00
|
|
|
let num_workers = if rt_config.num_threads < 2 {
|
|
|
|
|
1
|
|
|
|
|
} else {
|
|
|
|
|
rt_config.num_threads
|
|
|
|
|
};
|
2026-02-11 15:23:26 +00:00
|
|
|
let collector = StatsCollector::new(num_workers);
|
|
|
|
|
|
|
|
|
|
let mut rt = Runtime::new(rt_config);
|
|
|
|
|
rt.set_stats_hook(collector.clone());
|
2026-02-09 09:04:57 +00:00
|
|
|
let handle = rt.run().expect("failed to start runtime");
|
2026-02-11 15:23:26 +00:00
|
|
|
dash.set_runtime(Arc::clone(&handle.runtime), collector);
|
2026-02-09 09:04:57 +00:00
|
|
|
|
|
|
|
|
(handle, dash)
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/// Load a trace file and serve a replay dashboard. Blocks indefinitely.
|
|
|
|
|
pub fn serve_replay(path: &str, config: ReplayConfig) -> io::Result<()> {
|
|
|
|
|
let data = std::fs::read_to_string(path)?;
|
2026-06-23 15:42:28 +00:00
|
|
|
let trace: RuntimeTrace =
|
|
|
|
|
serde_json::from_str(&data).map_err(|e| io::Error::new(io::ErrorKind::InvalidData, e))?;
|
2026-02-09 09:04:57 +00:00
|
|
|
|
2026-02-19 14:39:33 +00:00
|
|
|
let rt = tokio::runtime::Builder::new_multi_thread()
|
|
|
|
|
.worker_threads(1)
|
|
|
|
|
.enable_all()
|
|
|
|
|
.build()
|
2026-02-24 09:12:28 +00:00
|
|
|
.map_err(|e| io::Error::other(e))?;
|
2026-02-19 14:39:33 +00:00
|
|
|
|
|
|
|
|
let state = server::ReplayState {
|
|
|
|
|
trace: Arc::new(trace),
|
|
|
|
|
speed: config.speed,
|
|
|
|
|
};
|
|
|
|
|
let port = config.port;
|
|
|
|
|
|
|
|
|
|
rt.spawn(async move {
|
|
|
|
|
server::run_replay_server(state, port).await;
|
|
|
|
|
});
|
2026-02-09 09:04:57 +00:00
|
|
|
|
|
|
|
|
eprintln!("Replay dashboard at http://localhost:{}", config.port);
|
|
|
|
|
eprintln!("Press Ctrl+C to stop");
|
|
|
|
|
|
|
|
|
|
loop {
|
|
|
|
|
thread::sleep(Duration::from_secs(3600));
|
|
|
|
|
}
|
|
|
|
|
}
|