use alloc::{
string::{String, ToString},
vec::Vec,
};
use core::fmt::Write;
use miden_core::{Felt, operations::Operation};
pub struct OpHistogram {
total_cycles: u128,
counts: [u64; 256],
}
impl Default for OpHistogram {
fn default() -> Self {
Self {
total_cycles: 0,
counts: [0; 256],
}
}
}
impl OpHistogram {
pub fn record(&mut self, op: Operation) {
self.total_cycles += 1;
self.counts[usize::from(op.op_code())] += 1;
}
pub fn total_cycles(&self) -> u128 {
self.total_cycles
}
pub fn sorted_counts(&self) -> SortedCounts {
let mut counts: SortedCounts = ALL_OPERATIONS
.iter()
.map(|&op| (op, self.counts[usize::from(op.op_code())]))
.filter(|&(_, count)| count > 0)
.collect();
counts.sort_by(|a, b| b.1.cmp(&a.1).then_with(|| a.0.op_code().cmp(&b.0.op_code())));
counts
}
pub fn report(&self) -> String {
const OP_COL_WIDTH: usize = 16;
const SHARE_COL_WIDTH: usize = 7;
let total = self.total_cycles;
let mut report = String::new();
writeln!(
report,
"{:<col1$} {:>col2$} {}",
"total_cycles",
"100%",
total,
col1 = OP_COL_WIDTH,
col2 = SHARE_COL_WIDTH,
)
.unwrap();
for (op, count) in self.sorted_counts() {
let share = 100.0 * (count as f64) / (total as f64);
let label = op.to_string();
let label = label.split('(').next().unwrap();
writeln!(
report,
"{:<col1$} {:>col2$} {}",
label,
format!("{share:.2}%"),
count,
col1 = OP_COL_WIDTH,
col2 = SHARE_COL_WIDTH,
)
.unwrap();
}
report
}
}
pub type SortedCounts = Vec<(Operation, u64)>;
const ALL_OPERATIONS: &[Operation] = &[
Operation::Noop,
Operation::Assert(Felt::ZERO),
Operation::SDepth,
Operation::Caller,
Operation::Clk,
Operation::Emit,
Operation::Add,
Operation::Neg,
Operation::Mul,
Operation::Inv,
Operation::Incr,
Operation::And,
Operation::Or,
Operation::Not,
Operation::Eq,
Operation::Eqz,
Operation::Expacc,
Operation::Ext2Mul,
Operation::U32split,
Operation::U32add,
Operation::U32add3,
Operation::U32sub,
Operation::U32mul,
Operation::U32madd,
Operation::U32div,
Operation::U32and,
Operation::U32xor,
Operation::U32assert2(Felt::ZERO),
Operation::Pad,
Operation::Drop,
Operation::Dup0,
Operation::Dup1,
Operation::Dup2,
Operation::Dup3,
Operation::Dup4,
Operation::Dup5,
Operation::Dup6,
Operation::Dup7,
Operation::Dup9,
Operation::Dup11,
Operation::Dup13,
Operation::Dup15,
Operation::Swap,
Operation::SwapW,
Operation::SwapW2,
Operation::SwapW3,
Operation::SwapDW,
Operation::MovUp2,
Operation::MovUp3,
Operation::MovUp4,
Operation::MovUp5,
Operation::MovUp6,
Operation::MovUp7,
Operation::MovUp8,
Operation::MovDn2,
Operation::MovDn3,
Operation::MovDn4,
Operation::MovDn5,
Operation::MovDn6,
Operation::MovDn7,
Operation::MovDn8,
Operation::CSwap,
Operation::CSwapW,
Operation::Push(Felt::ZERO),
Operation::AdvPop,
Operation::AdvPopW,
Operation::MLoadW,
Operation::MStoreW,
Operation::MLoad,
Operation::MStore,
Operation::MStream,
Operation::Pipe,
Operation::CryptoStream,
Operation::HPerm,
Operation::MpVerify(Felt::ZERO),
Operation::MrUpdate,
Operation::FriE2F4,
Operation::HornerBase,
Operation::HornerExt,
Operation::EvalCircuit,
Operation::LogDeferred,
];
#[cfg(test)]
mod tests {
use alloc::{string::String, vec::Vec};
use std::collections::BTreeSet;
use miden_core::{Felt, operations::Operation, serde::Deserializable};
use super::{ALL_OPERATIONS, OpHistogram};
#[test]
fn all_operations_covers_every_valid_opcode() {
let valid: BTreeSet<u8> = (0u8..=u8::MAX)
.filter(|&op| Operation::read_from_bytes(&[op, 0, 0, 0, 0, 0, 0, 0, 0]).is_ok())
.collect();
let ours: BTreeSet<u8> = ALL_OPERATIONS.iter().map(|op| op.op_code()).collect();
let missing_in_ours: Vec<String> = valid
.difference(&ours)
.map(|&op| {
format!(
"{}",
Operation::read_from_bytes(&[op, 0, 0, 0, 0, 0, 0, 0, 0])
.expect("valid opcode deserializes to an Operation")
)
})
.collect();
if !missing_in_ours.is_empty() {
panic!(
"ALL_OPERATIONS is out of sync with miden-core's Operation enum.\n missing from \
ALL_OPERATIONS (add these): {missing_in_ours:?}"
);
}
}
#[test]
fn sorted_counts_orders_by_count_desc_then_opcode() {
let mut hist = OpHistogram::default();
hist.record(Operation::Add);
hist.record(Operation::Add);
hist.record(Operation::Noop);
hist.record(Operation::Eq);
let (ops, counts): (Vec<Operation>, Vec<u64>) = hist.sorted_counts().into_iter().unzip();
assert_eq!(ops[0], Operation::Add);
assert_eq!(counts[0], 2);
assert_eq!(counts[1], 1);
assert_eq!(counts[2], 1);
assert!(ops[1].op_code() < ops[2].op_code());
assert_eq!(hist.total_cycles(), 4);
}
#[test]
fn sorted_counts_excludes_unrecorded_operations() {
let hist = OpHistogram::default();
assert!(hist.sorted_counts().is_empty());
assert_eq!(hist.total_cycles(), 0);
}
#[test]
fn op_histogram_omits_payload_from_mnemonic() {
let mut hist = OpHistogram::default();
hist.record(Operation::Push(Felt::ZERO));
hist.record(Operation::Assert(Felt::ZERO));
hist.record(Operation::MpVerify(Felt::ZERO));
hist.record(Operation::U32assert2(Felt::ZERO));
let report = hist.report();
assert!(
!report.contains("(0)"),
"report must not contain placeholder payloads: {report}"
);
for mnemonic in ["push", "assert", "mpverify", "u32assert2"] {
assert!(report.contains(mnemonic), "report missing `{mnemonic}`: {report}");
}
}
}