use std::fmt::{Debug, Display};
use cairo_lang_lowering::ids::FunctionLongId;
use cairo_lang_runnable_utils::builder::RunnableBuilder;
use cairo_lang_sierra::extensions::core::CoreConcreteLibfunc;
use cairo_lang_sierra::ids::ConcreteLibfuncId;
use cairo_lang_sierra::program::{GenStatement, Program, StatementIdx};
use cairo_lang_sierra_generator::db::SierraGenGroup;
use cairo_lang_utils::ordered_hash_map::OrderedHashMap;
use cairo_lang_utils::require;
use cairo_lang_utils::unordered_hash_map::UnorderedHashMap;
use cairo_vm::vm::trace::trace_entry::RelocatedTraceEntry;
use itertools::{Itertools, chain};
use salsa::Database;
use crate::ProfilingInfoCollectionConfig;
#[cfg(test)]
#[path = "profiling_test.rs"]
mod test;
#[derive(Clone, Debug, PartialEq, Eq)]
pub enum ProfilerConfig {
Cairo,
Scoped,
Sierra,
}
impl ProfilerConfig {
pub fn requires_cairo_debug_info(&self) -> bool {
matches!(self, ProfilerConfig::Cairo | ProfilerConfig::Scoped)
}
}
#[derive(Debug, Eq, PartialEq, Clone)]
pub struct ProfilingInfo {
pub sierra_statement_weights: UnorderedHashMap<StatementIdx, usize>,
pub stack_trace_weights: OrderedHashMap<Vec<usize>, usize>,
pub scoped_sierra_statement_weights: OrderedHashMap<(Vec<usize>, StatementIdx), usize>,
}
impl ProfilingInfo {
pub fn from_trace(
builder: &RunnableBuilder,
load_offset: usize,
profiling_config: &ProfilingInfoCollectionConfig,
trace: &[RelocatedTraceEntry],
) -> Self {
let sierra_statement_info = &builder.casm_program().debug_info.sierra_statement_info;
let bytecode_len = sierra_statement_info.last().unwrap().end_offset;
let mut function_stack = Vec::new();
let mut function_stack_depth = 0;
let mut cur_weight = 0;
let mut stack_trace_weights = OrderedHashMap::default();
let mut end_of_program_reached = false;
let mut sierra_statement_weights = UnorderedHashMap::default();
let mut scoped_sierra_statement_weights = OrderedHashMap::default();
for step in trace {
let Some(real_pc) = step.pc.checked_sub(load_offset) else {
continue;
};
if real_pc >= bytecode_len {
continue;
}
if end_of_program_reached {
unreachable!("End of program reached, but trace continues.");
}
cur_weight += 1;
let sierra_statement_idx = builder.casm_program().sierra_statement_index_by_pc(real_pc);
let user_function_idx = user_function_idx_by_sierra_statement_idx(
builder.sierra_program(),
sierra_statement_idx,
);
*sierra_statement_weights.entry(sierra_statement_idx).or_insert(0) += 1;
if profiling_config.collect_scoped_sierra_statement_weights {
let cur_stack: Vec<usize> =
chain!(function_stack.iter().map(|&(idx, _)| idx), [user_function_idx])
.dedup()
.collect();
*scoped_sierra_statement_weights
.entry((cur_stack, sierra_statement_idx))
.or_insert(0) += 1;
}
let Some(gen_statement) =
builder.sierra_program().statements.get(sierra_statement_idx.0)
else {
panic!("Failed fetching statement index {}", sierra_statement_idx.0);
};
match gen_statement {
GenStatement::Invocation(invocation) => {
if matches!(
builder.registry().get_libfunc(&invocation.libfunc_id),
Ok(CoreConcreteLibfunc::FunctionCall(_))
) {
if function_stack_depth < profiling_config.max_stack_trace_depth {
function_stack.push((user_function_idx, cur_weight));
cur_weight = 0;
}
function_stack_depth += 1;
}
}
GenStatement::Return(_) => {
if function_stack_depth <= profiling_config.max_stack_trace_depth {
let cur_stack: Vec<_> =
chain!(function_stack.iter().map(|f| f.0), [user_function_idx])
.collect();
*stack_trace_weights.entry(cur_stack).or_insert(0) += cur_weight;
let Some(popped) = function_stack.pop() else {
end_of_program_reached = true;
continue;
};
cur_weight += popped.1;
}
function_stack_depth -= 1;
}
}
}
ProfilingInfo {
sierra_statement_weights,
stack_trace_weights,
scoped_sierra_statement_weights,
}
}
}
#[derive(Default)]
pub struct LibfuncWeights {
pub concrete_libfunc_weights: Option<OrderedHashMap<String, usize>>,
pub generic_libfunc_weights: Option<OrderedHashMap<String, usize>>,
pub return_weight: Option<usize>,
}
impl Display for LibfuncWeights {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
if let Some(concrete_libfunc_weights) = &self.concrete_libfunc_weights {
writeln!(f, "Weight by concrete libfunc:")?;
for (concrete_name, weight) in concrete_libfunc_weights.iter() {
writeln!(f, " libfunc {concrete_name}: {weight}")?;
}
writeln!(
f,
" return: {}",
self.return_weight.expect(
"return_weight should have a value if concrete_libfunc_weights has a value"
)
)?;
}
if let Some(generic_libfunc_weights) = &self.generic_libfunc_weights {
writeln!(f, "Weight by generic libfunc:")?;
for (generic_name, weight) in generic_libfunc_weights.iter() {
writeln!(f, " libfunc {generic_name}: {weight}")?;
}
writeln!(
f,
" return: {}",
self.return_weight.expect(
"return_weight should have a value if generic_libfunc_weights has a value"
)
)?;
}
Ok(())
}
}
#[derive(Default)]
pub struct UserFunctionWeights {
pub user_function_weights: Option<OrderedHashMap<String, usize>>,
pub original_user_function_weights: Option<OrderedHashMap<String, usize>>,
}
impl Display for UserFunctionWeights {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
if let Some(user_function_weights) = &self.user_function_weights {
writeln!(f, "Weight by user function (inc. generated):")?;
for (name, weight) in user_function_weights.iter() {
writeln!(f, " function {name}: {weight}")?;
}
}
if let Some(original_user_function_weights) = &self.original_user_function_weights {
writeln!(f, "Weight by original user function (exc. generated):")?;
for (name, weight) in original_user_function_weights.iter() {
writeln!(f, " function {name}: {weight}")?;
}
}
Ok(())
}
}
#[derive(Default)]
pub struct StackTraceWeights {
pub sierra_stack_trace_weights: Option<OrderedHashMap<Vec<String>, usize>>,
pub cairo_stack_trace_weights: Option<OrderedHashMap<Vec<String>, usize>>,
}
impl Display for StackTraceWeights {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
if let Some(sierra_stack_trace_weights) = &self.sierra_stack_trace_weights {
writeln!(f, "Weight by Sierra stack trace:")?;
for (stack_trace, weight) in sierra_stack_trace_weights.iter() {
let stack_trace_str = stack_trace.join(" -> ");
writeln!(f, " {stack_trace_str}: {weight}")?;
}
}
if let Some(cairo_stack_trace_weights) = &self.cairo_stack_trace_weights {
writeln!(f, "Weight by Cairo stack trace:")?;
for (stack_trace, weight) in cairo_stack_trace_weights.iter() {
let stack_trace_str = stack_trace.join(" -> ");
writeln!(f, " {stack_trace_str}: {weight}")?;
}
}
Ok(())
}
}
pub struct ProcessedProfilingInfo {
pub sierra_statement_weights:
Option<OrderedHashMap<StatementIdx, (usize, GenStatement<StatementIdx>)>>,
pub stack_trace_weights: StackTraceWeights,
pub libfunc_weights: LibfuncWeights,
pub user_function_weights: UserFunctionWeights,
pub cairo_function_weights: Option<OrderedHashMap<String, usize>>,
pub scoped_sierra_statement_weights: Option<OrderedHashMap<Vec<String>, usize>>,
}
impl Display for ProcessedProfilingInfo {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
if let Some(sierra_statement_weights) = &self.sierra_statement_weights {
writeln!(f, "Weight by sierra statement:")?;
for (statement_idx, (weight, gen_statement)) in sierra_statement_weights.iter() {
writeln!(f, " statement {statement_idx}: {weight} ({gen_statement})")?;
}
}
self.libfunc_weights.fmt(f)?;
self.user_function_weights.fmt(f)?;
if let Some(cairo_function_weights) = &self.cairo_function_weights {
writeln!(f, "Weight by Cairo function:")?;
for (function_identifier, weight) in cairo_function_weights.iter() {
writeln!(f, " function {function_identifier}: {weight}")?;
}
}
self.stack_trace_weights.fmt(f)?;
if let Some(weights) = &self.scoped_sierra_statement_weights {
format_scoped_sierra_statement_weights(weights, f)?;
}
Ok(())
}
}
pub struct ProfilingInfoProcessorParams {
pub min_weight: usize,
pub process_by_statement: bool,
pub process_by_concrete_libfunc: bool,
pub process_by_generic_libfunc: bool,
pub process_by_user_function: bool,
pub process_by_original_user_function: bool,
pub process_by_cairo_function: bool,
pub process_by_stack_trace: bool,
pub process_by_cairo_stack_trace: bool,
pub process_by_scoped_statement: bool,
}
impl Default for ProfilingInfoProcessorParams {
fn default() -> Self {
Self {
min_weight: 1,
process_by_statement: true,
process_by_concrete_libfunc: true,
process_by_generic_libfunc: true,
process_by_user_function: true,
process_by_original_user_function: true,
process_by_cairo_function: true,
process_by_stack_trace: true,
process_by_cairo_stack_trace: true,
process_by_scoped_statement: false,
}
}
}
impl ProfilingInfoProcessorParams {
pub fn from_profiler_config(config: &ProfilerConfig) -> Self {
match config {
ProfilerConfig::Cairo => Default::default(),
ProfilerConfig::Scoped => Self {
min_weight: 1,
process_by_statement: false,
process_by_concrete_libfunc: false,
process_by_generic_libfunc: false,
process_by_user_function: false,
process_by_original_user_function: false,
process_by_cairo_function: false,
process_by_stack_trace: false,
process_by_cairo_stack_trace: false,
process_by_scoped_statement: true,
},
ProfilerConfig::Sierra => Self {
process_by_generic_libfunc: false,
process_by_cairo_stack_trace: false,
process_by_original_user_function: false,
process_by_cairo_function: false,
..ProfilingInfoProcessorParams::default()
},
}
}
}
pub struct ProfilingInfoProcessor<'a> {
db: Option<&'a dyn Database>,
sierra_program: &'a Program,
statements_functions: UnorderedHashMap<StatementIdx, String>,
}
impl<'a> ProfilingInfoProcessor<'a> {
pub fn new(
db: Option<&'a dyn Database>,
sierra_program: &'a Program,
statements_functions: UnorderedHashMap<StatementIdx, String>,
) -> Self {
Self { db, sierra_program, statements_functions }
}
pub fn process(
&self,
raw_profiling_info: &ProfilingInfo,
params: &ProfilingInfoProcessorParams,
) -> ProcessedProfilingInfo {
let sierra_statement_weights_iter = raw_profiling_info
.sierra_statement_weights
.iter_sorted_by_key(|(pc, count)| (usize::MAX - **count, **pc));
let sierra_statement_weights =
self.process_sierra_statement_weights(sierra_statement_weights_iter.clone(), params);
let stack_trace_weights = self.process_stack_trace_weights(raw_profiling_info, params);
let libfunc_weights =
self.process_libfunc_weights(sierra_statement_weights_iter.clone(), params);
let user_function_weights =
self.process_user_function_weights(sierra_statement_weights_iter.clone(), params);
let cairo_function_weights =
self.process_cairo_function_weights(sierra_statement_weights_iter, params);
let scoped_sierra_statement_weights =
self.process_scoped_sierra_statement_weights(raw_profiling_info, params);
ProcessedProfilingInfo {
sierra_statement_weights,
stack_trace_weights,
libfunc_weights,
user_function_weights,
cairo_function_weights,
scoped_sierra_statement_weights,
}
}
fn process_sierra_statement_weights(
&self,
sierra_statement_weights_iter: std::vec::IntoIter<(&StatementIdx, &usize)>,
params: &ProfilingInfoProcessorParams,
) -> Option<OrderedHashMap<StatementIdx, (usize, GenStatement<StatementIdx>)>> {
require(params.process_by_statement)?;
Some(
sierra_statement_weights_iter
.filter(|&(_, weight)| *weight >= params.min_weight)
.map(|(statement_idx, weight)| {
(*statement_idx, (*weight, self.statement_idx_to_gen_statement(statement_idx)))
})
.collect(),
)
}
fn process_stack_trace_weights(
&self,
raw_profiling_info: &ProfilingInfo,
params: &ProfilingInfoProcessorParams,
) -> StackTraceWeights {
let resolve_names = |(idx_stack_trace, weight): (&Vec<usize>, &usize)| {
(index_stack_trace_to_name_stack_trace(self.sierra_program, idx_stack_trace), *weight)
};
let sierra_stack_trace_weights = params.process_by_stack_trace.then(|| {
raw_profiling_info
.stack_trace_weights
.iter()
.sorted_by_key(|&(trace, weight)| (usize::MAX - *weight, trace))
.map(resolve_names)
.collect()
});
let cairo_stack_trace_weights = params.process_by_cairo_stack_trace.then(|| {
let db = self.db.expect("DB must be set with `process_by_cairo_stack_trace=true`.");
raw_profiling_info
.stack_trace_weights
.iter()
.filter(|(trace, _)| is_cairo_trace(db, self.sierra_program, trace))
.sorted_by_key(|&(trace, weight)| (usize::MAX - *weight, trace))
.map(resolve_names)
.collect()
});
StackTraceWeights { sierra_stack_trace_weights, cairo_stack_trace_weights }
}
fn process_libfunc_weights(
&self,
sierra_statement_weights: std::vec::IntoIter<(&StatementIdx, &usize)>,
params: &ProfilingInfoProcessorParams,
) -> LibfuncWeights {
if !params.process_by_concrete_libfunc && !params.process_by_generic_libfunc {
return LibfuncWeights::default();
}
let mut return_weight = 0;
let mut libfunc_weights = UnorderedHashMap::<ConcreteLibfuncId, usize>::default();
for (statement_idx, weight) in sierra_statement_weights {
match self.statement_idx_to_gen_statement(statement_idx) {
GenStatement::Invocation(invocation) => {
*(libfunc_weights.entry(invocation.libfunc_id.clone()).or_insert(0)) += weight;
}
GenStatement::Return(_) => {
return_weight += weight;
}
}
}
let generic_libfunc_weights = params.process_by_generic_libfunc.then(|| {
let db: &dyn Database =
self.db.expect("DB must be set with `process_by_generic_libfunc=true`.");
libfunc_weights
.aggregate_by(
|k| db.lookup_concrete_lib_func(k).generic_id.to_string(),
|v1: &usize, v2| v1 + v2,
&0,
)
.filter(|_, weight| *weight >= params.min_weight)
.into_iter_sorted_by_key(|(generic_name, weight)| {
(usize::MAX - *weight, (*generic_name).clone())
})
.collect()
});
let concrete_libfunc_weights = params.process_by_concrete_libfunc.then(|| {
libfunc_weights
.filter(|_, weight| *weight >= params.min_weight)
.into_iter_sorted_by_key(|(libfunc_id, weight)| {
(usize::MAX - *weight, libfunc_id.to_string())
})
.map(|(libfunc_id, weight)| (libfunc_id.to_string(), weight))
.collect()
});
LibfuncWeights {
concrete_libfunc_weights,
generic_libfunc_weights,
return_weight: Some(return_weight),
}
}
fn process_user_function_weights(
&self,
sierra_statement_weights: std::vec::IntoIter<(&StatementIdx, &usize)>,
params: &ProfilingInfoProcessorParams,
) -> UserFunctionWeights {
if !params.process_by_user_function && !params.process_by_original_user_function {
return UserFunctionWeights::default();
}
let mut user_functions = UnorderedHashMap::<usize, usize>::default();
for (statement_idx, weight) in sierra_statement_weights {
let function_idx: usize =
user_function_idx_by_sierra_statement_idx(self.sierra_program, *statement_idx);
*(user_functions.entry(function_idx).or_insert(0)) += weight;
}
let original_user_function_weights = params.process_by_original_user_function.then(|| {
let db: &dyn Database =
self.db.expect("DB must be set with `process_by_original_user_function=true`.");
user_functions
.aggregate_by(
|idx| {
let lowering_function_id =
db.lookup_sierra_function(&self.sierra_program.funcs[*idx].id);
lowering_function_id.semantic_full_path(db)
},
|x, y| x + y,
&0,
)
.filter(|_, weight| *weight >= params.min_weight)
.iter_sorted_by_key(|(orig_name, weight)| {
(usize::MAX - **weight, (*orig_name).clone())
})
.map(|(orig_name, weight)| (orig_name.clone(), *weight))
.collect()
});
let user_function_weights = params.process_by_user_function.then(|| {
user_functions
.filter(|_, weight| *weight >= params.min_weight)
.iter_sorted_by_key(|(idx, weight)| {
(usize::MAX - **weight, self.sierra_program.funcs[**idx].id.to_string())
})
.map(|(idx, weight)| {
let func: &cairo_lang_sierra::program::GenFunction<StatementIdx> =
&self.sierra_program.funcs[*idx];
(func.id.to_string(), *weight)
})
.collect()
});
UserFunctionWeights { user_function_weights, original_user_function_weights }
}
fn process_cairo_function_weights(
&self,
sierra_statement_weights: std::vec::IntoIter<(&StatementIdx, &usize)>,
params: &ProfilingInfoProcessorParams,
) -> Option<OrderedHashMap<String, usize>> {
require(params.process_by_cairo_function)?;
let mut cairo_functions = UnorderedHashMap::<_, _>::default();
for (statement_idx, weight) in sierra_statement_weights {
let function_identifier = self
.statements_functions
.get(statement_idx)
.cloned()
.unwrap_or_else(|| "unknown".to_string());
*(cairo_functions.entry(function_identifier).or_insert(0)) += weight;
}
Some(
cairo_functions
.filter(|_, weight| *weight >= params.min_weight)
.iter_sorted_by_key(|(function_identifier, weight)| {
(usize::MAX - **weight, (*function_identifier).clone())
})
.map(|(function_identifier, weight)| (function_identifier.clone(), *weight))
.collect(),
)
}
fn process_scoped_sierra_statement_weights(
&self,
raw_profiling_info: &ProfilingInfo,
params: &ProfilingInfoProcessorParams,
) -> Option<OrderedHashMap<Vec<String>, usize>> {
if params.process_by_scoped_statement {
let mut scoped_sierra_statement_weights: OrderedHashMap<Vec<String>, usize> =
Default::default();
for ((idx_stack_trace, statement_idx), weight) in
raw_profiling_info.scoped_sierra_statement_weights.iter()
{
let statement_name = match self.statement_idx_to_gen_statement(statement_idx) {
GenStatement::Invocation(invocation) => invocation.libfunc_id.to_string(),
GenStatement::Return(_) => "return".into(),
};
let key: Vec<String> = chain!(
index_stack_trace_to_name_stack_trace(self.sierra_program, idx_stack_trace),
[statement_name]
)
.collect();
*scoped_sierra_statement_weights.entry(key).or_default() += *weight;
}
return Some(scoped_sierra_statement_weights);
}
None
}
fn statement_idx_to_gen_statement(
&self,
statement_idx: &StatementIdx,
) -> GenStatement<StatementIdx> {
self.sierra_program
.statements
.get(statement_idx.0)
.unwrap_or_else(|| panic!("Failed fetching statement index {}", statement_idx.0))
.clone()
}
}
fn is_cairo_trace(db: &dyn Database, sierra_program: &Program, sierra_trace: &[usize]) -> bool {
sierra_trace.iter().all(|sierra_function_idx| {
let sierra_function = &sierra_program.funcs[*sierra_function_idx];
let lowering_function_id = db.lookup_sierra_function(&sierra_function.id);
matches!(lowering_function_id.long(db), FunctionLongId::Semantic(_))
})
}
pub fn user_function_idx_by_sierra_statement_idx(
sierra_program: &Program,
statement_idx: StatementIdx,
) -> usize {
sierra_program.funcs.partition_point(|f| f.entry_point.0 <= statement_idx.0) - 1
}
fn index_stack_trace_to_name_stack_trace(
sierra_program: &Program,
idx_stack_trace: &[usize],
) -> Vec<String> {
idx_stack_trace.iter().map(|idx| sierra_program.funcs[*idx].id.to_string()).collect()
}
fn format_scoped_sierra_statement_weights(
weights: &OrderedHashMap<Vec<String>, usize>,
f: &mut std::fmt::Formatter<'_>,
) -> std::fmt::Result {
for (key, weight) in weights.iter() {
f.write_fmt(format_args!("{} {weight}\n", key.join(";")))?;
}
Ok(())
}