swactor/examples/pipeline-parallel-inference/src/netmap_plugin.rs
Zachery Aaron Shores-Chmielewski 89e9b5b587 feat(distribution): add vastai telemetry layer and synth
New diagnostics::vastai (record/sampler/shipper/logs) + vastai-synth crate for
reproducible synthetic fleet telemetry. Dashboard live_collector unifies collector +
fleet SSE UI; pp example adds vastai_mon, profiles, docker base image, scripts.


Signed-off-by: Zachery Aaron Shores-Chmielewski <zacheryasc@gmail.com>
2026-05-29 12:55:10 +04:00

197 lines
7.2 KiB
Rust

//! Orchestrator "netmap" dashboard plugin — a live connection/bandwidth map.
//!
//! The orchestrator dials every stage, so its egocentric view captures the
//! orchestrator↔stage mesh. This plugin renders that as a directed graph:
//! orchestrator at center, one node per SWIM member, and two arcs per member
//! (orchestrator→peer "sent" and peer→orchestrator "recv"). Each arc is
//! **colored by iroh transport** (direct / relay / mixed / idle) and **weighted
//! by live bandwidth** (bytes/sec, derived client-side from the delta of the
//! cumulative per-peer byte counters between successive snapshots).
//!
//! Two pieces feed it, both **shared** with the existing distribution plugin so
//! we don't install a second diagnostics tap:
//!
//! * [`SharedSnapshot`] + [`MsgCounts`](crate::dist_plugin::MsgCounts) — the
//! cached cluster snapshot (members, names) and the per-peer message/byte
//! tallies the `CountingEmitter` already accumulates.
//! * [`ConnTracker`] — a per-peer transport map kept fresh by
//! [`spawn_conn_poller`], which queries `endpoint.remote_info` directly. It
//! is independent of the diagnostics aggregator (which is only installed
//! under `SWACTOR_DIAG_*`), so the map works even with diagnostics off.
use std::collections::{HashMap, HashSet};
use std::sync::atomic::{AtomicBool, Ordering};
use std::sync::{Arc, Mutex};
use std::time::Duration;
use dashboard::plugin::{DashboardPlugin, PluginResponse};
use distribution::diagnostics::{ConnType, conn_type_of};
use distribution::iroh_driver::IrohDriver;
use iroh::PublicKey;
use crate::dist_plugin::{MsgCounts, SharedSnapshot};
/// Parse a 64-char lowercase-hex node id back into raw key bytes. Inverse of the
/// `node_id_hex` / `hex32` formatting used across the snapshot and tally maps.
fn hex_to_32(hex: &str) -> Option<[u8; 32]> {
if hex.len() != 64 {
return None;
}
let mut out = [0u8; 32];
for (i, byte) in out.iter_mut().enumerate() {
*byte = u8::from_str_radix(hex.get(i * 2..i * 2 + 2)?, 16).ok()?;
}
Some(out)
}
/// One peer's current transport, as observed from this node's iroh endpoint.
#[derive(Clone, serde::Serialize)]
pub struct ConnEntry {
/// `"Direct"` / `"Relay"` / `"Mixed"` / `"None"` (matches [`ConnType`]'s serde form).
pub r#type: String,
/// The relay URL backing this peer, when one is known.
pub relay_url: Option<String>,
}
/// Live per-peer transport map, keyed by peer node-id hex (lowercase).
#[derive(Default)]
pub struct ConnTracker {
map: Mutex<HashMap<String, ConnEntry>>,
}
impl ConnTracker {
pub fn set(&self, hex: String, ty: ConnType, relay_url: Option<String>) {
self.map.lock().unwrap().insert(
hex,
ConnEntry {
r#type: format!("{ty:?}"),
relay_url,
},
);
}
/// Drop entries for peers no longer present (departed members).
pub fn retain_peers(&self, keep: &HashSet<String>) {
self.map.lock().unwrap().retain(|k, _| keep.contains(k));
}
pub fn to_json(&self) -> serde_json::Value {
serde_json::to_value(&*self.map.lock().unwrap()).unwrap_or(serde_json::Value::Null)
}
}
const NETMAP_HTML: &str = include_str!("netmap_page.html");
/// Read-only dashboard plugin backing `/plugin/netmap`.
pub struct NetmapPlugin {
cached: SharedSnapshot,
counts: Arc<MsgCounts>,
conn: Arc<ConnTracker>,
}
impl NetmapPlugin {
pub fn new(cached: SharedSnapshot, counts: Arc<MsgCounts>, conn: Arc<ConnTracker>) -> Self {
Self {
cached,
counts,
conn,
}
}
/// Serialize the cached snapshot with message tallies, the transport map, and
/// a server timestamp the frontend uses as the time base for bytes/sec.
fn rendered_json(&self) -> Option<String> {
let guard = self.cached.lock().unwrap();
let snap = guard.as_ref()?;
let mut v = serde_json::to_value(snap).ok()?;
if let serde_json::Value::Object(ref mut m) = v {
m.insert("msg_counts".to_string(), self.counts.to_json());
m.insert("conn".to_string(), self.conn.to_json());
m.insert(
"conn_ts_ms".to_string(),
serde_json::json!(distribution::diagnostics::wall_ms_now()),
);
}
serde_json::to_string(&v).ok()
}
}
impl DashboardPlugin for NetmapPlugin {
fn name(&self) -> &str {
"netmap"
}
fn snapshot_json(&self) -> Option<String> {
self.rendered_json()
}
fn handle_request(
&self,
method: &str,
path: &str,
_query: &HashMap<String, String>,
_body: &[u8],
) -> PluginResponse {
match (method, path) {
("GET", "") => PluginResponse::json(self.rendered_json().unwrap_or_else(|| "{}".into())),
_ => PluginResponse::not_found(),
}
}
fn html_page(&self) -> Option<&str> {
Some(NETMAP_HTML)
}
}
/// Spawn the transport poller on the driver's tokio runtime. Every ~1s it reads
/// the current member set from `cached`, asks the iroh endpoint for each peer's
/// `remote_info`, derives the [`ConnType`], and updates `conn`. Runs until `stop`
/// is set. Independent of the diagnostics aggregator.
pub fn spawn_conn_poller(
driver: &IrohDriver,
cached: SharedSnapshot,
conn: Arc<ConnTracker>,
stop: Arc<AtomicBool>,
) {
// `Endpoint` is Arc-backed; clone gives the task an owned handle without
// borrowing the driver.
let endpoint = driver.endpoint().clone();
driver.tokio_handle().spawn(async move {
let mut ticker = tokio::time::interval(Duration::from_millis(1000));
ticker.set_missed_tick_behavior(tokio::time::MissedTickBehavior::Skip);
while !stop.load(Ordering::Relaxed) {
ticker.tick().await;
// Clone the member hex set, then drop the guard before any `.await`
// (std Mutex guards must not be held across await points).
let peers: Vec<String> = {
let guard = cached.lock().unwrap();
guard
.as_ref()
.map(|s| s.members.iter().map(|m| m.node_id.clone()).collect())
.unwrap_or_default()
};
let mut seen = HashSet::with_capacity(peers.len());
for hex in peers {
let Some(bytes) = hex_to_32(&hex) else {
continue;
};
let Ok(pk) = PublicKey::from_bytes(&bytes) else {
continue;
};
seen.insert(hex.clone());
match endpoint.remote_info(pk).await {
Some(info) => {
let relay_url = info.addrs().find_map(|ai| match ai.addr() {
iroh::TransportAddr::Relay(url) => Some(url.to_string()),
_ => None,
});
conn.set(hex, conn_type_of(&info), relay_url);
}
// Known member, but iroh has no connection info yet.
None => conn.set(hex, ConnType::None, None),
}
}
conn.retain_peers(&seen);
}
});
}