Skip to main content

sp1_core_executor/
report.rs

1use std::{
2    fmt::{Display, Formatter, Result as FmtResult},
3    ops::{Add, AddAssign},
4};
5
6use enum_map::{EnumArray, EnumMap};
7use hashbrown::HashMap;
8
9use crate::{
10    events::{generate_execution_report, MemInstrEvent, PrecompileEvent, SyscallEvent},
11    ITypeRecord, Opcode, SyscallCode,
12};
13
14/// This constant is chosen for backwards compatibility with the V4 gas model: with this factor,
15/// the gas costs of op-succinct blocks in V6 will approximately match those in V4.
16const GAS_NORMALIZATION_FACTOR: u64 = 191;
17
18/// An execution report.
19#[derive(Default, Debug, Clone, PartialEq, Eq)]
20pub struct ExecutionReport {
21    /// The opcode counts.
22    pub opcode_counts: Box<EnumMap<Opcode, u64>>,
23    /// The syscall counts.
24    pub syscall_counts: Box<EnumMap<SyscallCode, u64>>,
25    /// The cycle tracker counts.
26    pub cycle_tracker: HashMap<String, u64>,
27    /// Tracker for the number of `cycle-tracker-report-*` invocations for a specific label.
28    pub invocation_tracker: HashMap<String, u64>,
29    /// The unique memory address counts.
30    pub touched_memory_addresses: u64,
31    /// The final exit code of the execution.
32    pub exit_code: u64,
33    /// The unnormalized gas, if it was calculated. Should not be accessed directly. Use `gas()` instead.
34    pub(crate) gas: Option<u64>,
35}
36
37impl ExecutionReport {
38    /// Compute the total number of instructions run during the execution.
39    #[must_use]
40    pub fn total_instruction_count(&self) -> u64 {
41        self.opcode_counts.values().sum()
42    }
43
44    /// Compute the total number of syscalls made during the execution.
45    #[must_use]
46    pub fn total_syscall_count(&self) -> u64 {
47        self.syscall_counts.values().sum()
48    }
49
50    /// The total size expected size (in bytes) of the execution report.
51    #[must_use]
52    pub fn total_record_size(&self) -> u64 {
53        // todo!(n): make this precise.
54
55        // Fix some average bound for each opcode.
56        let avg_opcode_record_size = std::mem::size_of::<(MemInstrEvent, ITypeRecord)>();
57        let total_opcode_records_size_bytes =
58            self.opcode_counts.values().sum::<u64>() * avg_opcode_record_size as u64;
59
60        // Take the maximum size of each precompile + 512 bytes for the vecs
61        // todo: can we fix the array sizes in the precompile events?
62        let syscall_avg_record_size = std::mem::size_of::<(SyscallEvent, PrecompileEvent)>() + 512;
63        let total_syscall_records_size_bytes =
64            self.syscall_counts.values().sum::<u64>() * syscall_avg_record_size as u64;
65
66        total_opcode_records_size_bytes + total_syscall_records_size_bytes
67    }
68
69    /// Normalize the internal gas so that op-succinct blocks have approximately the same gas
70    /// on v4 and v6.
71    #[must_use]
72    pub fn gas(&self) -> Option<u64> {
73        // Using integer arithmetic to avoid f64 precision warnings.
74        self.gas.map(|g| g * 10 / GAS_NORMALIZATION_FACTOR)
75    }
76}
77
78/// Combines two `HashMap`s together. If a key is in both maps, the values are added together.
79fn counts_add_assign<K, V>(lhs: &mut EnumMap<K, V>, rhs: EnumMap<K, V>)
80where
81    K: EnumArray<V>,
82    V: AddAssign,
83{
84    for (k, v) in rhs {
85        lhs[k] += v;
86    }
87}
88
89impl AddAssign for ExecutionReport {
90    fn add_assign(&mut self, rhs: Self) {
91        counts_add_assign(&mut self.opcode_counts, *rhs.opcode_counts);
92        counts_add_assign(&mut self.syscall_counts, *rhs.syscall_counts);
93        self.touched_memory_addresses += rhs.touched_memory_addresses;
94
95        // Merge cycle_tracker and invocation_tracker
96        for (label, count) in rhs.cycle_tracker {
97            *self.cycle_tracker.entry(label).or_insert(0) += count;
98        }
99        for (label, count) in rhs.invocation_tracker {
100            *self.invocation_tracker.entry(label).or_insert(0) += count;
101        }
102
103        // Sum gas costs if both have gas
104        self.gas = match (self.gas, rhs.gas) {
105            (Some(c1), Some(c2)) => Some(c1 + c2),
106            (Some(g), None) | (None, Some(g)) => Some(g),
107            (None, None) => None,
108        };
109
110        // The exit code value must either be `0` or the final exit code, so taking an `OR` works.
111        self.exit_code |= rhs.exit_code;
112    }
113}
114
115impl Add for ExecutionReport {
116    type Output = Self;
117
118    fn add(mut self, rhs: Self) -> Self::Output {
119        self += rhs;
120        self
121    }
122}
123
124impl Display for ExecutionReport {
125    fn fmt(&self, f: &mut Formatter<'_>) -> FmtResult {
126        if let Some(gas) = self.gas() {
127            writeln!(f, "gas: {gas:?}")?;
128        }
129        writeln!(f, "opcode counts ({} total instructions):", self.total_instruction_count())?;
130        for line in generate_execution_report(self.opcode_counts.as_ref()) {
131            writeln!(f, "  {line}")?;
132        }
133
134        writeln!(f, "syscall counts ({} total syscall instructions):", self.total_syscall_count())?;
135        for line in generate_execution_report(self.syscall_counts.as_ref()) {
136            writeln!(f, "  {line}")?;
137        }
138
139        Ok(())
140    }
141}