use std::{
fmt::{Display, Formatter, Result as FmtResult},
ops::{Add, AddAssign},
};
use enum_map::{EnumArray, EnumMap};
use hashbrown::HashMap;
use serde::{Deserialize, Serialize};
use crate::{
events::{generate_execution_report, MemInstrEvent, PrecompileEvent, SyscallEvent},
ITypeRecord, Opcode, SyscallCode,
};
const GAS_NORMALIZATION_FACTOR: u64 = 191;
#[derive(Default, Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
pub struct ExecutionReport {
pub opcode_counts: Box<EnumMap<Opcode, u64>>,
pub syscall_counts: Box<EnumMap<SyscallCode, u64>>,
pub cycle_tracker: HashMap<String, u64>,
pub invocation_tracker: HashMap<String, u64>,
pub touched_memory_addresses: u64,
pub exit_code: u64,
pub(crate) gas: Option<u64>,
}
impl ExecutionReport {
#[must_use]
pub fn total_instruction_count(&self) -> u64 {
self.opcode_counts.values().sum()
}
#[must_use]
pub fn total_syscall_count(&self) -> u64 {
self.syscall_counts.values().sum()
}
#[must_use]
pub fn total_record_size(&self) -> u64 {
let avg_opcode_record_size = std::mem::size_of::<(MemInstrEvent, ITypeRecord)>();
let total_opcode_records_size_bytes =
self.opcode_counts.values().sum::<u64>() * avg_opcode_record_size as u64;
let syscall_avg_record_size = std::mem::size_of::<(SyscallEvent, PrecompileEvent)>() + 512;
let total_syscall_records_size_bytes =
self.syscall_counts.values().sum::<u64>() * syscall_avg_record_size as u64;
total_opcode_records_size_bytes + total_syscall_records_size_bytes
}
#[must_use]
pub fn gas(&self) -> Option<u64> {
self.gas.map(|g| g * 10 / GAS_NORMALIZATION_FACTOR)
}
}
fn counts_add_assign<K, V>(lhs: &mut EnumMap<K, V>, rhs: EnumMap<K, V>)
where
K: EnumArray<V>,
V: AddAssign,
{
for (k, v) in rhs {
lhs[k] += v;
}
}
impl AddAssign for ExecutionReport {
fn add_assign(&mut self, rhs: Self) {
counts_add_assign(&mut self.opcode_counts, *rhs.opcode_counts);
counts_add_assign(&mut self.syscall_counts, *rhs.syscall_counts);
self.touched_memory_addresses += rhs.touched_memory_addresses;
for (label, count) in rhs.cycle_tracker {
*self.cycle_tracker.entry(label).or_insert(0) += count;
}
for (label, count) in rhs.invocation_tracker {
*self.invocation_tracker.entry(label).or_insert(0) += count;
}
self.gas = match (self.gas, rhs.gas) {
(Some(c1), Some(c2)) => Some(c1 + c2),
(Some(g), None) | (None, Some(g)) => Some(g),
(None, None) => None,
};
self.exit_code |= rhs.exit_code;
}
}
impl Add for ExecutionReport {
type Output = Self;
fn add(mut self, rhs: Self) -> Self::Output {
self += rhs;
self
}
}
impl Display for ExecutionReport {
fn fmt(&self, f: &mut Formatter<'_>) -> FmtResult {
if let Some(gas) = self.gas() {
writeln!(f, "gas: {gas:?}")?;
}
writeln!(f, "opcode counts ({} total instructions):", self.total_instruction_count())?;
for line in generate_execution_report(self.opcode_counts.as_ref()) {
writeln!(f, " {line}")?;
}
writeln!(f, "syscall counts ({} total syscall instructions):", self.total_syscall_count())?;
for line in generate_execution_report(self.syscall_counts.as_ref()) {
writeln!(f, " {line}")?;
}
Ok(())
}
}
#[cfg(test)]
mod tests {
use super::*;
fn populated_report() -> ExecutionReport {
let mut report = ExecutionReport::default();
report.opcode_counts[Opcode::ADD] = 7;
report.opcode_counts[Opcode::SUB] = 3;
report.syscall_counts[SyscallCode::HALT] = 1;
report.cycle_tracker.insert("setup".to_string(), 100);
report.invocation_tracker.insert("setup".to_string(), 2);
report.touched_memory_addresses = 42;
report.exit_code = 0;
report.gas = Some(1_000);
report
}
#[test]
fn execution_report_json_round_trip() {
let report = populated_report();
let json = serde_json::to_string(&report).expect("serialize");
let decoded: ExecutionReport = serde_json::from_str(&json).expect("deserialize");
assert_eq!(report, decoded);
}
#[test]
fn execution_report_bincode_round_trip() {
let report = populated_report();
let bytes = bincode::serialize(&report).expect("serialize");
let decoded: ExecutionReport = bincode::deserialize(&bytes).expect("deserialize");
assert_eq!(report, decoded);
}
#[test]
fn execution_report_default_and_none_gas_round_trip() {
let report = ExecutionReport::default();
assert_eq!(report.gas, None);
let json: ExecutionReport =
serde_json::from_str(&serde_json::to_string(&report).expect("ser")).expect("de");
assert_eq!(report, json);
let bin: ExecutionReport =
bincode::deserialize(&bincode::serialize(&report).expect("ser")).expect("de");
assert_eq!(report, bin);
}
}