#[cfg(feature = "metrics")]
use std::collections::BTreeMap;
use std::{iter::repeat_n, sync::Arc};
#[cfg(not(feature = "parallel"))]
use itertools::Itertools;
use openvm_instructions::{instruction::Instruction, program::Program, LocalOpcode, SystemOpcode};
use openvm_stark_backend::{
p3_field::{Field, PrimeField32},
p3_maybe_rayon::prelude::*,
};
use crate::{
arch::{
execution_mode::PreflightCtx, interpreter::get_pc_index, Arena, ExecutionError, ExecutorId,
ExecutorInventory, PreflightExecutor, StaticProgramError, VmExecState,
},
system::memory::online::TracingMemory,
};
pub struct PreflightInterpretedInstance<F, E> {
inventory: Arc<ExecutorInventory<E>>,
pc_handler: Vec<PcEntry<F>>,
execution_frequencies: Vec<u32>,
pc_base: u32,
pub(super) executor_idx_to_air_idx: Vec<usize>,
}
#[repr(C)]
#[derive(Clone)]
pub struct PcEntry<F> {
pub insn: Instruction<F>,
pub executor_idx: ExecutorId,
}
impl<F: Field, E> PreflightInterpretedInstance<F, E> {
pub fn new(
program: &Program<F>,
inventory: Arc<ExecutorInventory<E>>,
executor_idx_to_air_idx: Vec<usize>,
) -> Result<Self, StaticProgramError> {
if inventory.executors().len() > u32::MAX as usize {
return Err(StaticProgramError::TooManyExecutors);
}
let len = program.instructions_and_debug_infos.len();
let pc_base = program.pc_base;
let base_idx = get_pc_index(pc_base);
let mut pc_handler = Vec::with_capacity(base_idx + len);
pc_handler.extend(repeat_n(PcEntry::undefined(), base_idx));
for insn_and_debug_info in &program.instructions_and_debug_infos {
if let Some((insn, _)) = insn_and_debug_info {
let insn = insn.clone();
let executor_idx = if insn.opcode == SystemOpcode::TERMINATE.global_opcode() {
0
} else {
*inventory.instruction_lookup.get(&insn.opcode).ok_or(
StaticProgramError::ExecutorNotFound {
opcode: insn.opcode,
},
)?
};
assert!(
(executor_idx as usize) < inventory.executors.len(),
"ExecutorInventory ensures executor_idx is in bounds"
);
let pc_entry = PcEntry { insn, executor_idx };
pc_handler.push(pc_entry);
} else {
pc_handler.push(PcEntry::undefined());
}
}
Ok(Self {
inventory,
execution_frequencies: vec![0u32; base_idx + len],
pc_base,
pc_handler,
executor_idx_to_air_idx,
})
}
pub fn executors(&self) -> &[E] {
&self.inventory.executors
}
pub fn filtered_execution_frequencies(&self) -> Vec<u32> {
let base_idx = get_pc_index(self.pc_base);
self.pc_handler
.par_iter()
.zip_eq(&self.execution_frequencies)
.skip(base_idx)
.filter_map(|(entry, freq)| entry.is_some().then_some(*freq))
.collect()
}
pub fn reset_execution_frequencies(&mut self) {
self.execution_frequencies.fill(0);
}
}
impl<F: PrimeField32, E> PreflightInterpretedInstance<F, E> {
#[cfg(feature = "metrics")]
pub fn opcode_counts_by_air<RA>(&self) -> BTreeMap<(usize, String), u64>
where
RA: Arena,
E: PreflightExecutor<F, RA>,
{
let mut counts = BTreeMap::new();
for (entry, &freq) in self.pc_handler.iter().zip(&self.execution_frequencies) {
if freq == 0
|| !entry.is_some()
|| entry.insn.opcode == SystemOpcode::TERMINATE.global_opcode()
{
continue;
}
let executor_idx = entry.executor_idx as usize;
let air_idx = unsafe {
*self.executor_idx_to_air_idx.get_unchecked(executor_idx)
};
let executor = unsafe {
self.inventory.executors.get_unchecked(executor_idx)
};
let opcode = executor.get_opcode_name(entry.insn.opcode.as_usize());
*counts.entry((air_idx, opcode)).or_insert(0) += freq as u64;
}
counts
}
pub fn execute_from_state<RA>(
&mut self,
state: &mut VmExecState<F, TracingMemory, PreflightCtx<RA>>,
) -> Result<(), ExecutionError>
where
RA: Arena,
E: PreflightExecutor<F, RA>,
{
loop {
if let Ok(Some(_)) = state.exit_code {
break;
}
if state.ctx.instret_left == 0 {
break;
}
self.execute_instruction(state)?;
state.ctx.instret_left -= 1;
}
Ok(())
}
#[inline(always)]
fn execute_instruction<RA>(
&mut self,
state: &mut VmExecState<F, TracingMemory, PreflightCtx<RA>>,
) -> Result<(), ExecutionError>
where
RA: Arena,
E: PreflightExecutor<F, RA>,
{
let pc = state.pc();
let pc_idx = get_pc_index(pc);
let pc_entry = self
.pc_handler
.get(pc_idx)
.ok_or_else(|| ExecutionError::PcOutOfBounds(pc))?;
unsafe {
*self.execution_frequencies.get_unchecked_mut(pc_idx) += 1;
};
tracing::trace!("pc: {pc:#x} | {:?}", pc_entry.insn);
if !pc_entry.is_some() {
return Err(ExecutionError::Unreachable(pc));
}
let opcode = pc_entry.insn.opcode;
let c = pc_entry.insn.c;
if opcode == SystemOpcode::TERMINATE.global_opcode() {
state.exit_code = Ok(Some(c.as_canonical_u32()));
return Ok(());
}
let executor = unsafe {
self.inventory
.executors
.get_unchecked(pc_entry.executor_idx as usize)
};
tracing::trace!(
"opcode: {} | timestamp: {}",
executor.get_opcode_name(pc_entry.insn.opcode.as_usize()),
state.memory.timestamp()
);
let arena = unsafe {
let air_idx = *self
.executor_idx_to_air_idx
.get_unchecked(pc_entry.executor_idx as usize);
state.ctx.arenas.get_unchecked_mut(air_idx)
};
let vm_state_mut = state.vm_state.into_mut(arena);
executor.execute(vm_state_mut, &pc_entry.insn)?;
#[cfg(feature = "metrics")]
{
crate::metrics::update_instruction_metrics(state, executor, pc, pc_entry);
}
Ok(())
}
}
impl<F> PcEntry<F> {
pub fn is_some(&self) -> bool {
self.executor_idx != u32::MAX
}
}
impl<F: Default> PcEntry<F> {
fn undefined() -> Self {
Self {
insn: Instruction::default(),
executor_idx: u32::MAX,
}
}
}
#[macro_export]
macro_rules! execute_spanned {
($name:literal, $executor:expr, $state:expr) => {{
#[cfg(feature = "metrics")]
let start = std::time::Instant::now();
#[cfg(feature = "metrics")]
let start_instret_left = $state.ctx.instret_left;
let result = $executor.execute_from_state($state);
#[cfg(feature = "metrics")]
{
let elapsed = start.elapsed();
let insns = start_instret_left - $state.ctx.instret_left;
tracing::info!("instructions_executed={insns}");
metrics::counter!(concat!($name, "_insns")).absolute(insns);
metrics::gauge!(concat!($name, "_insn_mi/s"))
.set(insns as f64 / elapsed.as_micros() as f64);
}
result
}};
}