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.
- 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.

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
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

View File

@ -32,10 +32,18 @@ pub struct ActorSnapshot {
pub incarnation: u64,
pub state: i64,
pub status: Status,
pub execution: Option<ExecutionSnapshot>,
pub queued: Vec<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)]
pub enum Event {
Spawned(ActorId),

View File

@ -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<T> {
@ -24,6 +31,7 @@ type ExecutionFuture<T> = Pin<Box<dyn Future<Output = ExperimentResult<T>>>>;
pub struct Invocation<T> {
future: Option<ExecutionFuture<T>>,
stepped: Rc<Cell<bool>>,
progress: Rc<Cell<InvocationProgress>>,
}
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 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<T: FromValue + 'static> Invocation<T> {
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<T> {
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";
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<T> = Result<T, Box<dyn Error>>;

View File

@ -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<ActorSnapshot> {
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();

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
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

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
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