# Actor Panel — v1 Spec (DRAFT) Status: **DRAFT, v1 scope.** Converged scope for the first menu item of the swactor dashboard: live observation of actors, their messages, mailbox, and throughput. North star is `tokio-console`, scoped down to ship fast and iterate. --- ## 1. Goal An **actor-first** roster plus a per-actor **dossier**, focused on identity, lifecycle, mailbox, and message throughput. Streaming-feel, single-runtime in focus with a runtime selector. Replaces the worker page's worker-centric framing with actors as the primary axis; workers become a column and a filter. One sentence: *a live console over the actor population and what each actor is doing with its messages.* ## 2. Non-goals (deferred backlog) Explicitly out of v1, queued for later iterations: - Tree lens (spawn/supervision hierarchy) — needs `parent`. - Flow/message graph (who talks to whom). - All relationship facets: parent, children, senders/receivers, monitors. - Per-actor **arrival** rate (saturation is derived from mailbox growth instead). - Death-event capture with `StopReason` + `ExitValue` (dead actors get a stale badge from the last retained snapshot only). - Rich lifecycle transition timeline (v1 shows current state + age). - SSE streaming transport (v1 polls, matching the existing pages). - Actor state inspection (`GetActorState` — security-gated, tier-3). - Per-actor busy/poll handler timing. ## 3. Roster (primary view) Sortable, filterable live table. Default sort: **mailbox depth descending** (hot actors bubble up). Matches the existing worker page's poll cadence (750 ms) and runtime selector. | Column | Source | Notes | |---------------|-----------------------------------------|-------| | `name · addr` | `LogicalName` + `ActorAddress` (short) | short hex identity + human label | | `type` | `actor_type_name` | **new** — the actor's Rust type | | `state` | derived from lifecycle flags (see §6) | **up from poisoned-only**; colored badge | | `mailbox` | `mailbox_depth` + growth color | trend color: steady / rising / runaway | | `msg/s` | derived from `messages_processed` delta | processed rate | | `processed` | `messages_processed` | lifetime total | | `last msg` | `last_msg_type` | last handled message type | | `worker` | `worker_id` | placement; also a filter | | `age` | from `spawn_time` | **new** — alive duration | Filters: by **state** (e.g. "show all poisoned"), by **type**, by **worker**, by **name/address** substring. ## 4. Dossier (row click) Focus panel with three facets, all about the actor itself — no relationships. ### 4.1 Lifecycle & identity Full address, `actor_type`, the `message_type` it accepts, spawn age, current state. If the actor is dead, show the last-known state as stale (no `StopReason`/`ExitValue` in v1 — deferred). ### 4.2 Mailbox dynamics Depth-over-time chart + growth rate. Saturation signal. Backed by a **per-actor history ring buffer** the view maintains (bounded `VecDeque`, same pattern as the existing `HistorySample` in `worker_view.rs`). No core change — the view folds each incoming snapshot into the buffer. ### 4.3 Message diet *(signature feature)* `message_type_counts` rendered as a sorted bar list (top-N) with counts, plus the last message. Data already exists. This is the column `tokio-console` cannot have (tasks are opaque); swactor actors are message-typed, so "what does this actor do" is answered by what it eats. Lean into it visually. ## 5. Summary strip Throughput at the runtime level, above the roster. Same card shape as the worker view, actor-centric: `actors` · `total msg/s` · `total mailbox` · `poisoned` · `uptime` ## 6. Lifecycle state derivation A single display state derived from the four flags, in priority order: ``` poisoned → "poisoned" (red) else stopping → "stopping" (orange) else suspended → "suspended" (yellow) else !started → "new" (blue) else → "running" (green) ``` swactor's flags *are* the states — cleaner than `tokio-console`'s running/idle. ## 7. Data model — the one core prerequisite The live feed is `runtime.actors`, emitted by `DatastreamStatsHook` (`crates/datastream/src/endpoint.rs`) from `ActorSnapshot` (`src/stats.rs:123`), built in `ActorPool::mailbox_depths_into` (`src/worker.rs:995`). `ActorSlot` (`src/worker.rs:558`) already holds every field v1 needs; the change is purely additive in the snapshot, keeping the read-only push contract. ### Enrich `ActorSnapshot` with | Field | Type | Source on `ActorSlot` / actor | |-----------------|-----------------------|--------------------------------------| | `started` | `bool` | `slot.started` | | `suspended` | `bool` | `slot.suspended` | | `stopping` | `bool` | `slot.stopping` | | `actor_type` | `&'static str` | `slot.actor.metadata().actor_type_name` | | `message_type` | `&'static str` | `slot.actor.metadata().message_type_name` | | `spawn_time` | `Option` | `slot.env` → `SpawnTimestamp` (ms since runtime creation) | `poisoned` already present. Propagate through `RuntimeActorSnapshotRecord` (`endpoint.rs`) so the JSON wire payload carries the new keys. **`parent` is excluded** — only needed for the tree lens (deferred). ### `name` continues via the existing merge `name` is a `swactor-std` registry concern, not core. It already flows through the `runtime.stats` → `actor_details` (`ActorInfo.name`) path and is merged by the existing worker view. v1 reuses that merge; no new core plumbing for names. ## 8. View-side state (dashboard) New view module, mirroring `SwactorWorkerView`'s structure but actor-centric. ### Per-actor view state ```rust struct ActorState { address: String, name: Option, actor_type: Option, message_type: Option, // lifecycle started: bool, suspended: bool, stopping: bool, poisoned: bool, spawn_time: Option, // throughput mailbox_depth: u32, mailbox_growth: f64, // depth/s, derived from history messages_processed: u64, msg_per_sec: f64, // derived via assign_u64_rate last_msg_type: Option, message_type_counts: Vec<(String, u64)>, worker_id: Option, history: VecDeque, // per-actor mailbox chart buffer last_update: Option, } ``` `apply_json` reuses the tolerant multi-alias field helpers already in `worker_view.rs` (`u32_field`, `string_field`, `assign_u64_rate`, `parse_message_type_counts`) so the new keys land gracefully across versions. ### Snapshot JSON contract Same envelope as the worker view: ```jsonc { "runtimes": [ { "stream": { "key": "...", "node": "...", "life": 0 }, "live": true, "last_seen_ms_ago": 42, "summary": { "actors": 0, "msg_per_sec": 0.0, "mailbox_depth": 0, "poisoned": 0, "uptime_ms": 0 }, "actors": [ { /* ActorState fields */ } ] } ] } ``` Keyed by `stream_key = "{node}#{life}"` (one entry per runtime stream), matching the worker view so the runtime selector is shared. ## 9. Transport & feel (kept cheap for v1) - **Poll-first**, 750 ms, matching `worker_page.rs`. SSE is an iteration upgrade. - **State-badge color** carries the alarm; no flash/animation machinery. - **Dead actors**: last snapshot retained with a stale badge (`now - last_seen > LIVE_TTL`), no death-event capture. - `LIVE_TTL` ~8 s (match the worker view). ## 10. File map | File | Change | |------|--------| | `src/stats.rs` | add fields to `ActorSnapshot` (§7) | | `src/worker.rs` (`mailbox_depths_into`) | populate new fields from `ActorSlot` | | `crates/datastream/src/endpoint.rs` (`RuntimeActorSnapshotRecord`) | serialize new fields | | `crates/dashboard/src/swactor/actor_view.rs` | **new** — `ActorPanelView: DashboardView` | | `crates/dashboard/src/swactor/actor_page.rs` | **new** — `ACTOR_HTML` const | | `crates/dashboard/src/swactor/mod.rs` | `pub fn actor_view()`, register | | `crates/dashboard/src/server.rs` / `root_page.rs` | list **first** in the menu | ## 11. Open decisions 1. **Menu first**: render the index dynamically from `ViewRegistry::descriptors()` (today the static root HTML ignores it) vs. hardcode the actor link ahead of workers. Recommend dynamic — scales as views grow. 2. **Type cardinality display**: with many instances per type, do we also offer a type-aggregated rollup (OrleansDashboard-style) in v1, or instance-only? Recommend instance-only for v1; rollup is a fast follow.