Skip to content

Commit

Permalink
Add execution trace support to the interpreter
Browse files Browse the repository at this point in the history
  • Loading branch information
Dentosal committed Dec 9, 2024
1 parent a80f82e commit 0c79e90
Show file tree
Hide file tree
Showing 5 changed files with 114 additions and 2 deletions.
4 changes: 4 additions & 0 deletions fuel-vm/src/interpreter.rs
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@ use core::{
mem,
ops::Index,
};
use trace::ExecutionTrace;

use fuel_asm::{
Flags,
Expand Down Expand Up @@ -67,6 +68,7 @@ mod memory;
mod metadata;
mod post_execution;
mod receipts;
mod trace;

mod debug;
mod ecal;
Expand Down Expand Up @@ -132,6 +134,8 @@ pub struct Interpreter<M, S, Tx = (), Ecal = NotSupportedEcal> {
context: Context,
balances: RuntimeBalances,
profiler: Profiler,
/// `None` if the excution trace is not enabled.
trace: Option<ExecutionTrace<M>>,
interpreter_params: InterpreterParams,
/// `PanicContext` after the latest execution. It is consumed by
/// `append_panic_receipt` and is `PanicContext::None` after consumption.
Expand Down
1 change: 1 addition & 0 deletions fuel-vm/src/interpreter/constructors.rs
Original file line number Diff line number Diff line change
Expand Up @@ -81,6 +81,7 @@ where
context: Context::default(),
balances: RuntimeBalances::default(),
profiler: Profiler::default(),
trace: None,
interpreter_params,
panic_context: PanicContext::None,
ecal_state,
Expand Down
2 changes: 2 additions & 0 deletions fuel-vm/src/interpreter/diff/storage.rs
Original file line number Diff line number Diff line change
Expand Up @@ -109,6 +109,7 @@ where
balances: self.balances,
panic_context: self.panic_context,
profiler: self.profiler,
trace: self.trace,
interpreter_params: self.interpreter_params,
ecal_state: self.ecal_state,
}
Expand Down Expand Up @@ -199,6 +200,7 @@ where
balances: self.balances,
panic_context: self.panic_context,
profiler: self.profiler,
trace: self.trace,
interpreter_params: self.interpreter_params,
ecal_state: self.ecal_state,
}
Expand Down
11 changes: 9 additions & 2 deletions fuel-vm/src/interpreter/executors/instruction.rs
Original file line number Diff line number Diff line change
Expand Up @@ -80,8 +80,15 @@ where
}
}

self.instruction_inner(raw.into())
.map_err(|e| InterpreterError::from_runtime(e, raw.into()))
let result = self
.instruction_inner(raw.into())
.map_err(|e| InterpreterError::from_runtime(e, raw.into()));

if !matches!(result, Err(_) | Ok(ExecuteState::DebugEvent(_))) {
self.record_trace_after_instruction();
}

result
}

fn instruction_inner(
Expand Down
98 changes: 98 additions & 0 deletions fuel-vm/src/interpreter/trace.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,98 @@
//! Execution traces
use fuel_asm::Word;

use super::{
memory::MemoryRollbackData,
Interpreter,
Memory,
VM_REGISTER_COUNT,
};

#[derive(Debug, Clone, Copy)]
pub enum Trigger {
/// Capture state after an instruction adds a new receipt
OnReceipt,
/// Capture state after each instruction
OnInstruction,
}

/// Used to capture an execution trace
#[derive(Debug, Clone)]
pub struct ExecutionTrace<M> {
/// When should we take a new snapshot, i.e. insert a frame?
trigger: Trigger,
/// Append-only set of frames
frames: Vec<Frame>,
/// Memory at the time of the previous snapshot
previous_memory: M,
}

/// Snapshot of the execution state, with some delta compression applied
#[derive(Debug, Clone)]
pub struct Frame {
/// Registers at this point
registers: [Word; VM_REGISTER_COUNT],
/// Memory delta from the previous snapshot
memory_diff: Option<MemoryRollbackData>,
/// How many of the receipts have been added by now
receipt_count: usize,
}

impl<M, S, Tx, Ecal> Interpreter<M, S, Tx, Ecal>
where
M: Memory,
{
/// This is called after each instruction, and it should record a new snapshot
/// if `trigger` condition is met.
pub fn with_trace_recording(mut self, trigger: Trigger, memory: M) -> Self {
self.trace = Some(ExecutionTrace {
trigger,
frames: Vec::new(),
previous_memory: memory,
});
self
}

/// This is called after each instruction, and it should record a new snapshot
/// if `trigger` condition is met.
pub(crate) fn record_trace_after_instruction(&mut self) {
let Some(trace) = self.trace.as_mut() else {
return; // Trace disabled
};

let take_snapshot = match trace.trigger {
Trigger::OnReceipt => {
trace.frames.last().map(|s| s.receipt_count).unwrap_or(0)
< self.receipts.len()
}
Trigger::OnInstruction => true,
};

if take_snapshot {
let memory_diff = trace
.previous_memory
.as_ref()
.collect_rollback_data(self.memory.as_ref());
if let Some(diff) = memory_diff.as_ref() {
trace.previous_memory.as_mut().rollback(&diff);
}

trace.frames.push(Frame {
memory_diff,
registers: self.registers,
receipt_count: self.receipts.len(),
})
}
}

/// Get trace frames at the current moment.
/// Mostly useful after the execution.
pub fn trace_frames(&self) -> &[Frame] {
if let Some(trace) = self.trace.as_ref() {
&trace.frames
} else {
&[]
}
}
}

0 comments on commit 0c79e90

Please sign in to comment.