//! RFC 016 — runtime introspection (Chunk 1: the read primitive). //! //! A synchronous, internal read of the slab that returns *owned* data. This is //! the mechanism the whole RFC hangs off: tests, the future observer //! gen_server (Chunk 4), and a later control plane (RFC 003) are all consumers //! of [`snapshot`] / [`actor_info`], never of the runtime internals directly. //! //! ## Consistency (DECISION D2 — per-slot tearing, `ps` semantics) //! //! [`snapshot`] is point-in-time and mildly racy *across* actors: each slot's //! scheduling state is a lock-free word load, so an actor reported `Running` //! may already be `Parked`, and an actor can die mid-scan. This is the cheap, //! useful model (a coherent stop-the-world cut is expensive and rarely wanted). //! [`actor_info`] is coherent for the single actor it names. //! //! ## Locking //! //! The lock order is **Leaf → Channel, at most one of each** (`raw_mutex.rs`); //! cold locks, the registry, and the free list are all Leaves, so we may never //! hold two at once. The read is therefore phased: first a single registry-leaf //! pass for names and mailbox depth (the per-channel length read is a Channel //! lock taken under that Leaf — legal), released before the slab scan takes any //! per-slot cold Leaf. use crate::pid::Pid; use crate::registry::MailboxInfo; use crate::runtime::{Slot, ROOT_PID}; use crate::scheduler::with_runtime; use crate::slot_state::{ word_gen, word_state, ST_DONE, ST_PARKED, ST_QUEUED, ST_RUNNING, ST_RUNNING_NOTIFIED, }; use std::collections::HashMap; /// Snapshot wire-format version (DECISION D1). [`RuntimeSnapshot`] is treated as /// a stable type from day one: it becomes the observer protocol (Chunk 4) and /// crosses a version boundary the moment a remote observer attaches (RFC 011), /// so the version travels with the data from the start. pub const SNAPSHOT_FORMAT_VERSION: u16 = 1; /// Fine-grained scheduling state, mapped from the packed slot word with no new /// storage. `RunningNotified` collapses into `Notified` — a wake landed while /// the actor was on-CPU and it will re-queue when it yields. #[derive(Debug, Clone, Copy, PartialEq, Eq)] pub enum ActorState { Queued, Running, Notified, Parked, Done, } /// Classify a packed state word. `None` for a Vacant slot (skipped by the scan) /// — the only state that is not an actor. fn classify(w: u64) -> Option { Some(match word_state(w) { ST_QUEUED => ActorState::Queued, ST_RUNNING => ActorState::Running, ST_RUNNING_NOTIFIED => ActorState::Notified, ST_PARKED => ActorState::Parked, ST_DONE => ActorState::Done, _ => return None, // ST_VACANT }) } /// Owned, point-in-time view of one actor — no borrows of runtime internals, so /// it is safe to hand to any consumer. #[derive(Debug, Clone)] pub struct ActorInfo { pub pid: Pid, /// Registered names, inverted from the registry (usually 0 or 1). pub names: Vec<&'static str>, pub state: ActorState, /// Spawn-time parent edge (DECISION D9): `spawn_under` sets it to the /// supervisor, plain `spawn` to the spawning actor — so it is parentage, /// not necessarily a supervision relationship. `ROOT_PID` for the run's /// root actor and for `Done` tombstones (whose `Actor` is already gone). pub supervisor: Pid, pub trap_exit: bool, pub monitors: u32, pub links: u32, pub joiners: u32, /// Queued messages summed over the actor's *published* channels (register / /// install / spawn_addr / gen_server). 0 for an actor that holds only a /// private `channel()` receiver — those are invisible to the registry. pub mailbox_depth: u32, /// Timeslice overruns tallied for this incarnation (RFC 016 Chunk 2): how /// many times the actor was preempted for exceeding its slice. Resets on /// restart (per-incarnation, D7). pub overruns: u64, /// Messages this actor has received (dequeued) this incarnation (RFC 016 /// Chunk 2) — answers "is this actor a hotspot / draining slower than its /// mailbox fills." Counts received, not sent (D4). Per-incarnation (D7). pub messages_received: u64, /// Approximate on-CPU cycles this incarnation has consumed (RFC 016 Chunk 2) /// — a reductions-like work metric for relative comparison. Always 0 unless /// the `budget-accounting` feature is enabled (it costs an RDTSC per resume, /// D6). Per-incarnation (D7). pub budget_cycles: u64, } /// A whole-runtime snapshot. See the module docs for the D2 tearing model. #[derive(Debug, Clone)] pub struct RuntimeSnapshot { pub format_version: u16, pub actors: Vec, } /// Snapshot every live (and `Done`-but-not-yet-reclaimed) actor on the slab. /// O(n) over the slot table, running with preemption disabled (like every /// runtime primitive) but holding no lock across the scan. Panics outside /// `Runtime::run()`; callable from actor code and the run thread. pub fn snapshot() -> RuntimeSnapshot { with_runtime(|inner| { // Phase A: one registry-leaf pass for names + mailbox depth, released // before any cold leaf (no two Leaves at once). let mail = inner.registry.lock().introspect_map(); // Phase B: lock-free slab scan; per-slot cold leaf only to copy cold // fields. Tearing across slots is intentional (D2). let mut actors = Vec::new(); for (idx, slot) in inner.slots.iter().enumerate() { let idx = idx as u32; if let Some(info) = read_slot(slot, idx, mail.get(&idx)) { actors.push(info); } } RuntimeSnapshot { format_version: SNAPSHOT_FORMAT_VERSION, actors } }) } /// Coherent view of a single actor, or `None` if the pid is stale, out of /// range, or names a Vacant slot. pub fn actor_info(pid: Pid) -> Option { with_runtime(|inner| { let slot = inner.slot_at(pid)?; let mail = inner.registry.lock().introspect_one(pid.index()); let info = read_slot(slot, pid.index(), mail.as_ref())?; // read_slot keys on the slab's *current* generation; reject if that // isn't the incarnation the caller asked about. (info.pid.generation() == pid.generation()).then_some(info) }) } /// Build one `ActorInfo` for slot `idx`, or `None` if Vacant or /// racing-reclaimed. State is classified from a lock-free word load (the torn /// read); the cold lock then pins the generation (reclaim bumps it under that /// same lock) so the cold fields are coherent for this incarnation. `mail` is /// this slot's registry entry, if any. fn read_slot(slot: &Slot, idx: u32, mail: Option<&MailboxInfo>) -> Option { let w = slot.state_word(); let state = classify(w)?; let gen = word_gen(w); let pid = Pid::new(idx, gen); let cold = slot.cold.lock(); // If the generation moved between the lock-free load and acquiring the cold // lock, the slot was reclaimed (and maybe reused) — drop it rather than mix // one incarnation's state with another's cold data. (ps semantics: a racing // actor may simply be missed mid-scan.) if word_gen(slot.state_word()) != gen { return None; } let (supervisor, trap_exit) = match cold.actor.as_ref() { // Live incarnation: parent + trap live on the Actor. Some(actor) => (actor.supervisor, actor.trap.is_some()), // Done tombstone: the Actor was taken at finalize and the collections // cleared, so report it root-less with empty counts. None => (ROOT_PID, false), }; let monitors = cold.monitors.len() as u32; let links = cold.links.len() as u32; let joiners = cold.waiters.len() as u32; drop(cold); // Counters are hot-region atomics, read lock-free (RFC 016 Chunk 2). let overruns = slot.overruns(); let messages_received = slot.messages_received(); let budget_cycles = slot.budget_cycles(); // Names + depth belong to this incarnation only if the registry mailbox's // pid matches the slab generation; a stale registry entry (dead prior // occupant, not yet pruned) contributes nothing. let (names, mailbox_depth) = match mail { Some(mi) if mi.pid.generation() == gen => (mi.names.clone(), mi.depth), _ => (Vec::new(), 0), }; Some(ActorInfo { pid, names, state, supervisor, trap_exit, monitors, links, joiners, mailbox_depth, overruns, messages_received, budget_cycles, }) } // --------------------------------------------------------------------------- // Chunk 3 — tree view (pure derivation over a Chunk-1 snapshot) // --------------------------------------------------------------------------- /// One node in the parentage forest. `children` are the actors whose recorded /// parent edge points at this node's pid. #[derive(Debug, Clone)] pub struct TreeNode { pub info: ActorInfo, /// The actor's recorded parent was absent from the snapshot (already /// Done/Vacant, or itself a tombstone), so it was re-rooted under the forest /// sentinel rather than dropped — the tree stays total (DECISION D8). pub orphaned: bool, pub children: Vec, } /// The parentage forest. Roots are actors parented at `ROOT_PID` (genuine /// roots) plus re-rooted orphans. The edge is *spawned-by / parent*, not /// necessarily supervision (DECISION D9) — see [`ActorInfo::supervisor`]. #[derive(Debug, Clone)] pub struct RuntimeTree { pub format_version: u16, pub roots: Vec, } /// Take a live [`snapshot`] and fold it into the parentage forest. pub fn tree() -> RuntimeTree { tree_from(snapshot()) } /// Fold an existing snapshot into a forest by grouping each actor under its /// parent pid — a single O(n) pass, no new reads. Exposed separately so a /// consumer that already holds a snapshot (or a synthetic one, in tests) can /// derive the tree without a second scan. pub fn tree_from(snap: RuntimeSnapshot) -> RuntimeTree { let RuntimeSnapshot { format_version, actors } = snap; let mut index_of: HashMap = HashMap::with_capacity(actors.len()); for (i, a) in actors.iter().enumerate() { index_of.insert(a.pid, i); } // Group children under their present parent; everything else is a root. // Scan order is preserved within each parent's child list. let mut children_of: HashMap> = HashMap::new(); let mut roots: Vec = Vec::new(); let mut orphaned = vec![false; actors.len()]; for (i, a) in actors.iter().enumerate() { let parent = a.supervisor; if parent != ROOT_PID && index_of.contains_key(&parent) { children_of.entry(parent).or_default().push(i); } else { // Parent is the forest sentinel (genuine root) or absent from the // snapshot (orphan, D8) — either way a root of the forest. orphaned[i] = parent != ROOT_PID; roots.push(i); } } // `take()` each actor as it is placed, which also guards against a // (constructionally impossible) parentage cycle re-entering a node. let mut slots: Vec> = actors.into_iter().map(Some).collect(); let root_nodes = roots .into_iter() .filter_map(|i| build_node(i, &children_of, &orphaned, &mut slots)) .collect(); RuntimeTree { format_version, roots: root_nodes } } fn build_node( i: usize, children_of: &HashMap>, orphaned: &[bool], slots: &mut [Option], ) -> Option { let info = slots[i].take()?; // already placed → cycle guard / no double-attach let children = children_of .get(&info.pid) .map(|kids| { kids.iter() .filter_map(|&c| build_node(c, children_of, orphaned, slots)) .collect() }) .unwrap_or_default(); Some(TreeNode { info, orphaned: orphaned[i], children }) }