8.8 KiB
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<u64> |
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
struct ActorState {
address: String,
name: Option<String>,
actor_type: Option<String>,
message_type: Option<String>,
// lifecycle
started: bool,
suspended: bool,
stopping: bool,
poisoned: bool,
spawn_time: Option<u64>,
// 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<String>,
message_type_counts: Vec<(String, u64)>,
worker_id: Option<u32>,
history: VecDeque<HistorySample>, // per-actor mailbox chart buffer
last_update: Option<Instant>,
}
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:
{
"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
- 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. - 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.