From 8d3496c621137ad385099153ee5ca50e505d622a Mon Sep 17 00:00:00 2001 From: Steve Biedermann Date: Mon, 2 Nov 2026 13:11:11 +0100 Subject: [PATCH] feat: expose active execution telemetry Expose the latest completed Rune step count, instruction pointer, and call-frame depth on active actor snapshots. Preserve telemetry while paused and clear it when execution ends or is cancelled. Document that this is not source-level debugging. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- CHANGELOG.md | 2 ++ README.md | 8 +++-- crates/runa-actor/src/lib.rs | 8 +++++ crates/runa-rune/src/execution.rs | 26 ++++++++++++++- crates/runa-rune/src/lib.rs | 2 +- crates/runa-runtime/src/lib.rs | 54 ++++++++++++++++++++++++++++++- docs/architecture.md | 6 ++-- docs/runtime.md | 6 ++++ 8 files changed, 104 insertions(+), 8 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 284fe29..c303be9 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -29,6 +29,8 @@ All notable changes to this project will be documented in this file. atomic failure behavior and migration events. - Explicit invocation cancellation that requeues the reserved message for retry, exposed in the CLI and documented with native side-effect caveats. +- Active actor execution telemetry in snapshots: completed VM steps, + instruction pointer, and call-frame depth. - Execution-model decision and Phase 1 runtime documentation. - Lifecycle tests proving failed-message retry from committed state and stop/restart preservation of reserved-first FIFO order. diff --git a/README.md b/README.md index 9b5032a..58d1b7d 100644 --- a/README.md +++ b/README.md @@ -16,11 +16,13 @@ effects. Source and compiled implementation identities are BLAKE3 content addresses with an in-memory compile cache. Persistence, interactive behavior editing, canonicalization, the graphical world, and development API are not implemented yet. The code library now supports an in-memory versioned behavior -registry, and the runtime can install an implementation at an idle handler -boundary; these operations are not yet exposed in the CLI. +registry, and the CLI exposes publishing, history, rollback, safe-boundary +installation, pause/resume, and cancellation. The prototype uses worker-local invocation stacks and cooperative instruction -slices. Native callbacks are not preemptible. +slices, and actor snapshots report basic active-execution progress. Native +callbacks are not preemptible; source-level debugging and persistent storage +are not implemented. ## Design goals diff --git a/crates/runa-actor/src/lib.rs b/crates/runa-actor/src/lib.rs index 523e26b..94ced20 100644 --- a/crates/runa-actor/src/lib.rs +++ b/crates/runa-actor/src/lib.rs @@ -32,10 +32,18 @@ pub struct ActorSnapshot { pub incarnation: u64, pub state: i64, pub status: Status, + pub execution: Option, pub queued: Vec, pub reserved: Option, } +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +pub struct ExecutionSnapshot { + pub vm_steps: u64, + pub instruction_pointer: usize, + pub call_depth: usize, +} + #[derive(Debug, Clone)] pub enum Event { Spawned(ActorId), diff --git a/crates/runa-rune/src/execution.rs b/crates/runa-rune/src/execution.rs index 1a2b8e1..b877507 100644 --- a/crates/runa-rune/src/execution.rs +++ b/crates/runa-rune/src/execution.rs @@ -10,6 +10,13 @@ use rune::{FromValue, Vm}; use crate::ExperimentResult; +#[derive(Debug, Default, Clone, Copy, PartialEq, Eq)] +pub struct InvocationProgress { + pub vm_steps: u64, + pub instruction_pointer: usize, + pub call_depth: usize, +} + /// A worker-local execution slice; native callbacks must remain nonblocking. #[derive(Debug)] pub enum Slice { @@ -24,6 +31,7 @@ type ExecutionFuture = Pin>>>; pub struct Invocation { future: Option>, stepped: Rc>, + progress: Rc>, } impl Invocation { @@ -31,9 +39,19 @@ impl Invocation { let mut execution = vm.execute([name], args)?.into_owned(); let stepped = Rc::new(Cell::new(false)); let marker = Rc::clone(&stepped); + let progress = Rc::new(Cell::new(InvocationProgress::default())); + let inspector = Rc::clone(&progress); let future = Box::pin(async move { loop { - if let Some(value) = execution.async_step().await.into_result()? { + let result = execution.async_step().await; + let vm = execution.vm(); + let previous = inspector.get(); + inspector.set(InvocationProgress { + vm_steps: previous.vm_steps.saturating_add(1), + instruction_pointer: vm.ip(), + call_depth: vm.call_frames().len(), + }); + if let Some(value) = result.into_result()? { return Ok(rune::from_value(value)?); } marker.set(true); @@ -53,9 +71,15 @@ impl Invocation { Ok(Self { future: Some(future), stepped, + progress, }) } + /// Get instruction telemetry from the most recently completed VM step. + pub fn progress(&self) -> InvocationProgress { + self.progress.get() + } + /// Bound VM steps per call, preserving the exact pending async step. pub fn poll_slice(&mut self, cx: &mut Context<'_>, budget: NonZeroUsize) -> Slice { let Some(future) = self.future.as_mut() else { diff --git a/crates/runa-rune/src/lib.rs b/crates/runa-rune/src/lib.rs index 52ae601..124fdae 100644 --- a/crates/runa-rune/src/lib.rs +++ b/crates/runa-rune/src/lib.rs @@ -17,7 +17,7 @@ pub const COMPILER_CONFIGURATION_ID: &str = "default-modules; native-functions:v1:native_double,native_suspend"; mod execution; -pub use execution::{Invocation, Slice}; +pub use execution::{Invocation, InvocationProgress, Slice}; /// Errors remain visible to the caller; compilation also emits source diagnostics. pub type ExperimentResult = Result>; diff --git a/crates/runa-runtime/src/lib.rs b/crates/runa-runtime/src/lib.rs index 54a2249..a9df515 100644 --- a/crates/runa-runtime/src/lib.rs +++ b/crates/runa-runtime/src/lib.rs @@ -6,7 +6,7 @@ use std::sync::Arc; use std::sync::atomic::{AtomicBool, Ordering}; use std::task::{Context, Wake, Waker}; -use runa_actor::{ActorId, ActorSnapshot, Event, MailboxId, Message, Status}; +use runa_actor::{ActorId, ActorSnapshot, Event, ExecutionSnapshot, MailboxId, Message, Status}; use runa_code::CompiledArtifact; use runa_core::ImplementationId; use runa_rune::{ExperimentResult, Invocation, Slice}; @@ -82,6 +82,7 @@ impl Runtime { incarnation: 1, state, status: Status::Idle, + execution: None, queued: Vec::new(), reserved: None, }, @@ -113,6 +114,14 @@ impl Runtime { pub fn inspect(&self, id: ActorId) -> ExperimentResult { let actor = self.actors.get(&id).ok_or("unknown actor")?; let mut snapshot = actor.snapshot.clone(); + snapshot.execution = actor.active.as_ref().map(|invocation| { + let progress = invocation.progress(); + ExecutionSnapshot { + vm_steps: progress.vm_steps, + instruction_pointer: progress.instruction_pointer, + call_depth: progress.call_depth, + } + }); snapshot.queued = actor.mailbox.iter().copied().collect(); Ok(snapshot) } @@ -122,6 +131,14 @@ impl Runtime { .values() .map(|actor| { let mut snapshot = actor.snapshot.clone(); + snapshot.execution = actor.active.as_ref().map(|invocation| { + let progress = invocation.progress(); + ExecutionSnapshot { + vm_steps: progress.vm_steps, + instruction_pointer: progress.instruction_pointer, + call_depth: progress.call_depth, + } + }); snapshot.queued = actor.mailbox.iter().copied().collect(); snapshot }) @@ -419,6 +436,41 @@ mod tests { Ok(()) } + #[test] + fn execution_snapshot_reports_live_progress_and_is_stable_while_paused() -> ExperimentResult<()> + { + let mut runtime = Runtime::default(); + let implementation = publish( + &mut runtime, + "pub fn handle(state, kind, value) { loop {} }", + )?; + let actor = runtime.spawn(implementation, 0, capacity(2))?; + runtime.send(actor, Message { kind: 0, value: 1 })?; + runtime.tick(capacity(3)); + + let running = runtime.inspect(actor)?; + assert_eq!(running.status, Status::Running); + let progress = running + .execution + .ok_or("running actor has no execution snapshot")?; + assert!(progress.vm_steps > 0); + assert!(running.reserved.is_some()); + + runtime.pause(actor)?; + for _ in 0..5 { + runtime.tick(capacity(10)); + } + let paused = runtime.inspect(actor)?; + assert_eq!(paused.status, Status::Paused); + assert_eq!(paused.execution, Some(progress)); + + runtime.cancel(actor)?; + let cancelled = runtime.inspect(actor)?; + assert_eq!(cancelled.execution, None); + assert_eq!(cancelled.queued, vec![Message { kind: 0, value: 1 }]); + Ok(()) + } + #[test] fn invalid_effect_does_not_commit_state_or_partial_sends() -> ExperimentResult<()> { let mut runtime = Runtime::default(); diff --git a/docs/architecture.md b/docs/architecture.md index 5a1add0..f5068ac 100644 --- a/docs/architecture.md +++ b/docs/architecture.md @@ -411,8 +411,10 @@ This is a **target layout**, not a request to generate empty crates. For Phase 0 The current runtime prototype supports implementation installation only at an idle handler boundary, cooperative pause/resume between scheduler ticks, and an explicit host-provided migration closure for its temporary `i64` actor state. -Structured state schemas, version compatibility checks, and Rune-authored -migration functions remain future work. +Actor snapshots expose active VM instruction progress, instruction pointer, +and call-frame depth. Structured state schemas, version compatibility checks, +Rune-authored migration functions, source mapping, and local-variable +inspection remain future work. ### Phase 5 — Image API diff --git a/docs/runtime.md b/docs/runtime.md index 338a909..2c29782 100644 --- a/docs/runtime.md +++ b/docs/runtime.md @@ -88,6 +88,12 @@ only if its future has been woken. These operations take effect between calls to `tick` and cannot interrupt a currently executing Rune slice or native callback. They provide cooperative scheduling control, not preemption. +Actor snapshots include basic execution telemetry while an invocation is +active: completed VM instruction-step count, Rune instruction pointer, and +call-frame depth. Paused invocations retain the same telemetry. These values +are execution-local diagnostics only; they do not map to source locations or +expose locals, and disappear when the invocation completes or is cancelled. + `cancel` is distinct from `stop`: it drops an actor's active invocation, moves its reserved message to the front of the mailbox, and returns the actor to `Idle`. The input is therefore eligible for retry on the next tick, ahead of