From 7133a8556d610d3e9c89faba14bbef02c3b6859a Mon Sep 17 00:00:00 2001 From: Zachery Aaron Shores-Chmielewski Date: Fri, 6 Feb 2026 20:33:55 +0700 Subject: [PATCH] feat: runtime information display Show basic information from the runtime, such as number of actors, worker threads, etc. --- examples/hello_async.py | 2 + src/address_map.rs | 10 ++++ src/python.rs | 102 +++++++++++++++++++++++++++++++++++++++- src/runtime.rs | 11 +++++ 4 files changed, 124 insertions(+), 1 deletion(-) diff --git a/examples/hello_async.py b/examples/hello_async.py index b56aa5e..684a746 100644 --- a/examples/hello_async.py +++ b/examples/hello_async.py @@ -30,6 +30,8 @@ async def main(): reply = await recv(inbox) print(reply) + # show us our actors! + print(handle.stats()) handle.shutdown() handle.join() diff --git a/src/address_map.rs b/src/address_map.rs index 48ac949..6cef070 100644 --- a/src/address_map.rs +++ b/src/address_map.rs @@ -49,6 +49,16 @@ impl AddressMap { pub fn len(&self) -> usize { self.inner.read().unwrap().len() } + + /// Returns a snapshot of all (address, worker) pairs. + pub fn snapshot(&self) -> Vec<(ActorAddress, WorkerId)> { + self.inner + .read() + .unwrap() + .iter() + .map(|(addr, wid)| (*addr, *wid)) + .collect() + } } /// Round-robin actor placement strategy. diff --git a/src/python.rs b/src/python.rs index fc18560..57b5a5e 100644 --- a/src/python.rs +++ b/src/python.rs @@ -176,7 +176,7 @@ impl ActorInterface for PyActor { for effect in effects { match effect { Effect::Send { addr, msg } => { - let _ = ctx.raw_inner().send_via_queue( + let _ = ctx.raw_inner().send_any( addr, Box::new(PyMsg(msg)) as Box, ); @@ -364,6 +364,14 @@ impl PyRuntime { }) } + fn stats(&self) -> PyResult { + let rt = self + .inner + .as_ref() + .ok_or_else(|| pyo3::exceptions::PyRuntimeError::new_err("Runtime consumed by run()"))?; + Ok(build_stats(rt)) + } + fn shutdown(&self) -> PyResult<()> { let rt = self .inner @@ -419,6 +427,16 @@ impl PyRuntimeHandle { Ok(PyInbox { inner: inbox }) } + fn stats(&self) -> PyResult { + let handle = self + .inner + .as_ref() + .ok_or_else(|| { + pyo3::exceptions::PyRuntimeError::new_err("RuntimeHandle consumed by join()") + })?; + Ok(build_stats(&handle.runtime)) + } + fn shutdown(&self) -> PyResult<()> { let handle = self .inner @@ -442,6 +460,86 @@ impl PyRuntimeHandle { } } +// ─── ActorInfo / RuntimeStats ──────────────────────────────────────────────── + +#[pyclass(name = "ActorInfo")] +#[derive(Clone)] +pub struct PyActorInfo { + #[pyo3(get)] + address: PyActorAddress, + #[pyo3(get)] + worker_id: usize, +} + +#[pymethods] +impl PyActorInfo { + fn __repr__(&self) -> String { + let hex = self.address.hex(); + format!("ActorInfo(address={hex}, worker={})", self.worker_id) + } +} + +#[pyclass(name = "RuntimeStats")] +#[derive(Clone)] +pub struct PyRuntimeStats { + #[pyo3(get)] + num_actors: usize, + #[pyo3(get)] + num_workers: usize, + #[pyo3(get)] + actors: Vec, +} + +#[pymethods] +impl PyRuntimeStats { + fn __repr__(&self) -> String { + let mut out = format!( + "RuntimeStats(actors={}, workers={})", + self.num_actors, self.num_workers + ); + + // Group actors by worker + let mut by_worker: std::collections::BTreeMap> = + std::collections::BTreeMap::new(); + for info in &self.actors { + by_worker.entry(info.worker_id).or_default().push(info); + } + + for wid in 0..self.num_workers { + let actors = by_worker.get(&wid); + let count = actors.map_or(0, |v| v.len()); + out.push_str(&format!("\n Worker {wid}: {count} actors")); + if let Some(actors) = actors { + for info in actors { + out.push_str(&format!("\n - {}", info.address.hex())); + } + } + } + + out + } + + fn __str__(&self) -> String { + self.__repr__() + } +} + +fn build_stats(runtime: &Runtime) -> PyRuntimeStats { + let (num_workers, snapshot) = runtime.stats(); + let actors: Vec = snapshot + .into_iter() + .map(|(addr, wid)| PyActorInfo { + address: PyActorAddress::from(addr), + worker_id: wid.as_usize(), + }) + .collect(); + PyRuntimeStats { + num_actors: actors.len(), + num_workers, + actors, + } +} + // ─── Module registration ───────────────────────────────────────────────────── pub fn register(m: &Bound<'_, PyModule>) -> PyResult<()> { @@ -451,5 +549,7 @@ pub fn register(m: &Bound<'_, PyModule>) -> PyResult<()> { m.add_class::()?; m.add_class::()?; m.add_class::()?; + m.add_class::()?; + m.add_class::()?; Ok(()) } diff --git a/src/runtime.rs b/src/runtime.rs index d8e1368..7680899 100644 --- a/src/runtime.rs +++ b/src/runtime.rs @@ -280,6 +280,17 @@ impl Runtime { }) } + /// Returns a snapshot of runtime stats: all actor addresses with their worker assignments, + /// plus the number of workers. + pub(crate) fn stats(&self) -> (usize, Vec<(ActorAddress, WorkerId)>) { + let num_workers = if self.config.num_threads < 2 { + 1 + } else { + self.config.num_threads + }; + (num_workers, self.address_map.snapshot()) + } + /// Signal all workers to stop pub fn shutdown(&self) { self.is_running.store(false, Ordering::Release);