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>
This commit is contained in:
Steve Biedermann 2026-11-02 13:11:11 +01:00
parent 1916814c2e
commit 8d3496c621
8 changed files with 104 additions and 8 deletions

View File

@ -29,6 +29,8 @@ All notable changes to this project will be documented in this file.
atomic failure behavior and migration events. atomic failure behavior and migration events.
- Explicit invocation cancellation that requeues the reserved message for - Explicit invocation cancellation that requeues the reserved message for
retry, exposed in the CLI and documented with native side-effect caveats. 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. - Execution-model decision and Phase 1 runtime documentation.
- Lifecycle tests proving failed-message retry from committed state and - Lifecycle tests proving failed-message retry from committed state and
stop/restart preservation of reserved-first FIFO order. stop/restart preservation of reserved-first FIFO order.

View File

@ -16,11 +16,13 @@ effects. Source and compiled implementation identities are BLAKE3 content
addresses with an in-memory compile cache. Persistence, interactive behavior addresses with an in-memory compile cache. Persistence, interactive behavior
editing, canonicalization, the graphical world, and development API are not editing, canonicalization, the graphical world, and development API are not
implemented yet. The code library now supports an in-memory versioned behavior implemented yet. The code library now supports an in-memory versioned behavior
registry, and the runtime can install an implementation at an idle handler registry, and the CLI exposes publishing, history, rollback, safe-boundary
boundary; these operations are not yet exposed in the CLI. installation, pause/resume, and cancellation.
The prototype uses worker-local invocation stacks and cooperative instruction 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 ## Design goals

View File

@ -32,10 +32,18 @@ pub struct ActorSnapshot {
pub incarnation: u64, pub incarnation: u64,
pub state: i64, pub state: i64,
pub status: Status, pub status: Status,
pub execution: Option<ExecutionSnapshot>,
pub queued: Vec<Message>, pub queued: Vec<Message>,
pub reserved: Option<Message>, pub reserved: Option<Message>,
} }
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub struct ExecutionSnapshot {
pub vm_steps: u64,
pub instruction_pointer: usize,
pub call_depth: usize,
}
#[derive(Debug, Clone)] #[derive(Debug, Clone)]
pub enum Event { pub enum Event {
Spawned(ActorId), Spawned(ActorId),

View File

@ -10,6 +10,13 @@ use rune::{FromValue, Vm};
use crate::ExperimentResult; 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. /// A worker-local execution slice; native callbacks must remain nonblocking.
#[derive(Debug)] #[derive(Debug)]
pub enum Slice<T> { pub enum Slice<T> {
@ -24,6 +31,7 @@ type ExecutionFuture<T> = Pin<Box<dyn Future<Output = ExperimentResult<T>>>>;
pub struct Invocation<T> { pub struct Invocation<T> {
future: Option<ExecutionFuture<T>>, future: Option<ExecutionFuture<T>>,
stepped: Rc<Cell<bool>>, stepped: Rc<Cell<bool>>,
progress: Rc<Cell<InvocationProgress>>,
} }
impl<T: FromValue + 'static> Invocation<T> { impl<T: FromValue + 'static> Invocation<T> {
@ -31,9 +39,19 @@ impl<T: FromValue + 'static> Invocation<T> {
let mut execution = vm.execute([name], args)?.into_owned(); let mut execution = vm.execute([name], args)?.into_owned();
let stepped = Rc::new(Cell::new(false)); let stepped = Rc::new(Cell::new(false));
let marker = Rc::clone(&stepped); let marker = Rc::clone(&stepped);
let progress = Rc::new(Cell::new(InvocationProgress::default()));
let inspector = Rc::clone(&progress);
let future = Box::pin(async move { let future = Box::pin(async move {
loop { 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)?); return Ok(rune::from_value(value)?);
} }
marker.set(true); marker.set(true);
@ -53,9 +71,15 @@ impl<T: FromValue + 'static> Invocation<T> {
Ok(Self { Ok(Self {
future: Some(future), future: Some(future),
stepped, 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. /// Bound VM steps per call, preserving the exact pending async step.
pub fn poll_slice(&mut self, cx: &mut Context<'_>, budget: NonZeroUsize) -> Slice<T> { pub fn poll_slice(&mut self, cx: &mut Context<'_>, budget: NonZeroUsize) -> Slice<T> {
let Some(future) = self.future.as_mut() else { let Some(future) = self.future.as_mut() else {

View File

@ -17,7 +17,7 @@ pub const COMPILER_CONFIGURATION_ID: &str =
"default-modules; native-functions:v1:native_double,native_suspend"; "default-modules; native-functions:v1:native_double,native_suspend";
mod execution; mod execution;
pub use execution::{Invocation, Slice}; pub use execution::{Invocation, InvocationProgress, Slice};
/// Errors remain visible to the caller; compilation also emits source diagnostics. /// Errors remain visible to the caller; compilation also emits source diagnostics.
pub type ExperimentResult<T> = Result<T, Box<dyn Error>>; pub type ExperimentResult<T> = Result<T, Box<dyn Error>>;

View File

@ -6,7 +6,7 @@ use std::sync::Arc;
use std::sync::atomic::{AtomicBool, Ordering}; use std::sync::atomic::{AtomicBool, Ordering};
use std::task::{Context, Wake, Waker}; 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_code::CompiledArtifact;
use runa_core::ImplementationId; use runa_core::ImplementationId;
use runa_rune::{ExperimentResult, Invocation, Slice}; use runa_rune::{ExperimentResult, Invocation, Slice};
@ -82,6 +82,7 @@ impl Runtime {
incarnation: 1, incarnation: 1,
state, state,
status: Status::Idle, status: Status::Idle,
execution: None,
queued: Vec::new(), queued: Vec::new(),
reserved: None, reserved: None,
}, },
@ -113,6 +114,14 @@ impl Runtime {
pub fn inspect(&self, id: ActorId) -> ExperimentResult<ActorSnapshot> { pub fn inspect(&self, id: ActorId) -> ExperimentResult<ActorSnapshot> {
let actor = self.actors.get(&id).ok_or("unknown actor")?; let actor = self.actors.get(&id).ok_or("unknown actor")?;
let mut snapshot = actor.snapshot.clone(); 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.queued = actor.mailbox.iter().copied().collect();
Ok(snapshot) Ok(snapshot)
} }
@ -122,6 +131,14 @@ impl Runtime {
.values() .values()
.map(|actor| { .map(|actor| {
let mut snapshot = actor.snapshot.clone(); 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.queued = actor.mailbox.iter().copied().collect();
snapshot snapshot
}) })
@ -419,6 +436,41 @@ mod tests {
Ok(()) 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] #[test]
fn invalid_effect_does_not_commit_state_or_partial_sends() -> ExperimentResult<()> { fn invalid_effect_does_not_commit_state_or_partial_sends() -> ExperimentResult<()> {
let mut runtime = Runtime::default(); let mut runtime = Runtime::default();

View File

@ -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 The current runtime prototype supports implementation installation only at an
idle handler boundary, cooperative pause/resume between scheduler ticks, and an idle handler boundary, cooperative pause/resume between scheduler ticks, and an
explicit host-provided migration closure for its temporary `i64` actor state. explicit host-provided migration closure for its temporary `i64` actor state.
Structured state schemas, version compatibility checks, and Rune-authored Actor snapshots expose active VM instruction progress, instruction pointer,
migration functions remain future work. 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 ### Phase 5 — Image API

View File

@ -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 to `tick` and cannot interrupt a currently executing Rune slice or native
callback. They provide cooperative scheduling control, not preemption. 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 `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 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 `Idle`. The input is therefore eligible for retry on the next tick, ahead of