1use std::fmt::{Debug, Display};
2
3use cairo_lang_lowering::ids::FunctionLongId;
4use cairo_lang_runnable_utils::builder::RunnableBuilder;
5use cairo_lang_sierra::extensions::core::CoreConcreteLibfunc;
6use cairo_lang_sierra::ids::ConcreteLibfuncId;
7use cairo_lang_sierra::program::{GenStatement, Program, StatementIdx};
8use cairo_lang_sierra_generator::db::SierraGenGroup;
9use cairo_lang_utils::ordered_hash_map::OrderedHashMap;
10use cairo_lang_utils::require;
11use cairo_lang_utils::unordered_hash_map::UnorderedHashMap;
12use cairo_vm::vm::trace::trace_entry::RelocatedTraceEntry;
13use itertools::{Itertools, chain};
14use salsa::Database;
15
16use crate::ProfilingInfoCollectionConfig;
17
18#[cfg(test)]
19#[path = "profiling_test.rs"]
20mod test;
21
22#[derive(Clone, Debug, PartialEq, Eq)]
24pub enum ProfilerConfig {
25 Cairo,
27 Scoped,
29 Sierra,
31}
32
33impl ProfilerConfig {
34 pub fn requires_cairo_debug_info(&self) -> bool {
36 matches!(self, ProfilerConfig::Cairo | ProfilerConfig::Scoped)
37 }
38}
39
40#[derive(Debug, Eq, PartialEq, Clone)]
44pub struct ProfilingInfo {
45 pub sierra_statement_weights: UnorderedHashMap<StatementIdx, usize>,
47
48 pub stack_trace_weights: OrderedHashMap<Vec<usize>, usize>,
55
56 pub scoped_sierra_statement_weights: OrderedHashMap<(Vec<usize>, StatementIdx), usize>,
62}
63
64impl ProfilingInfo {
65 pub fn from_trace(
66 builder: &RunnableBuilder,
67 load_offset: usize,
69 profiling_config: &ProfilingInfoCollectionConfig,
70 trace: &[RelocatedTraceEntry],
71 ) -> Self {
72 let sierra_statement_info = &builder.casm_program().debug_info.sierra_statement_info;
73 let sierra_len = sierra_statement_info.len();
74 let bytecode_len = sierra_statement_info.last().unwrap().end_offset;
75
76 let mut function_stack = Vec::new();
82 let mut function_stack_depth = 0;
86 let mut cur_weight = 0;
87 let mut stack_trace_weights = OrderedHashMap::default();
92 let mut end_of_program_reached = false;
93 let mut sierra_statement_weights = UnorderedHashMap::default();
98 let mut scoped_sierra_statement_weights = OrderedHashMap::default();
101 for step in trace {
102 let Some(real_pc) = step.pc.checked_sub(load_offset) else {
104 continue;
105 };
106
107 if real_pc >= bytecode_len {
112 continue;
113 }
114
115 if end_of_program_reached {
116 unreachable!("End of program reached, but trace continues.");
117 }
118
119 cur_weight += 1;
120
121 let sierra_statement_idx = builder.casm_program().sierra_statement_index_by_pc(real_pc);
124 let user_function_idx = user_function_idx_by_sierra_statement_idx(
125 builder.sierra_program(),
126 sierra_statement_idx,
127 );
128
129 *sierra_statement_weights.entry(sierra_statement_idx).or_insert(0) += 1;
130
131 if profiling_config.collect_scoped_sierra_statement_weights {
132 let cur_stack: Vec<usize> =
135 chain!(function_stack.iter().map(|&(idx, _)| idx), [user_function_idx])
136 .dedup()
137 .collect();
138
139 *scoped_sierra_statement_weights
140 .entry((cur_stack, sierra_statement_idx))
141 .or_insert(0) += 1;
142 }
143
144 let Some(gen_statement) =
145 builder.sierra_program().statements.get(sierra_statement_idx.0)
146 else {
147 panic!("Failed fetching statement index {}", sierra_statement_idx.0);
148 };
149
150 match gen_statement {
151 GenStatement::Invocation(invocation) => {
152 if matches!(
153 builder.registry().get_libfunc(&invocation.libfunc_id),
154 Ok(CoreConcreteLibfunc::FunctionCall(_))
155 ) {
156 if function_stack_depth < profiling_config.max_stack_trace_depth {
158 function_stack.push((user_function_idx, cur_weight));
159 cur_weight = 0;
160 }
161 function_stack_depth += 1;
162 }
163 }
164 GenStatement::Return(_) => {
165 if function_stack_depth <= profiling_config.max_stack_trace_depth {
167 let cur_stack: Vec<_> =
169 chain!(function_stack.iter().map(|f| f.0), [user_function_idx])
170 .collect();
171 *stack_trace_weights.entry(cur_stack).or_insert(0) += cur_weight;
172
173 let Some(popped) = function_stack.pop() else {
174 end_of_program_reached = true;
176 continue;
177 };
178 cur_weight += popped.1;
179 }
180 function_stack_depth -= 1;
181 }
182 }
183 }
184
185 sierra_statement_weights.remove(&StatementIdx(sierra_len));
187
188 ProfilingInfo {
189 sierra_statement_weights,
190 stack_trace_weights,
191 scoped_sierra_statement_weights,
192 }
193 }
194}
195
196#[derive(Default)]
198pub struct LibfuncWeights {
199 pub concrete_libfunc_weights: Option<OrderedHashMap<String, usize>>,
201 pub generic_libfunc_weights: Option<OrderedHashMap<String, usize>>,
203 pub return_weight: Option<usize>,
205}
206impl Display for LibfuncWeights {
207 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
208 if let Some(concrete_libfunc_weights) = &self.concrete_libfunc_weights {
209 writeln!(f, "Weight by concrete libfunc:")?;
210 for (concrete_name, weight) in concrete_libfunc_weights.iter() {
211 writeln!(f, " libfunc {concrete_name}: {weight}")?;
212 }
213 writeln!(
214 f,
215 " return: {}",
216 self.return_weight.expect(
217 "return_weight should have a value if concrete_libfunc_weights has a value"
218 )
219 )?;
220 }
221 if let Some(generic_libfunc_weights) = &self.generic_libfunc_weights {
222 writeln!(f, "Weight by generic libfunc:")?;
223 for (generic_name, weight) in generic_libfunc_weights.iter() {
224 writeln!(f, " libfunc {generic_name}: {weight}")?;
225 }
226 writeln!(
227 f,
228 " return: {}",
229 self.return_weight.expect(
230 "return_weight should have a value if generic_libfunc_weights has a value"
231 )
232 )?;
233 }
234 Ok(())
235 }
236}
237
238#[derive(Default)]
240pub struct UserFunctionWeights {
241 pub user_function_weights: Option<OrderedHashMap<String, usize>>,
244 pub original_user_function_weights: Option<OrderedHashMap<String, usize>>,
246}
247impl Display for UserFunctionWeights {
248 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
249 if let Some(user_function_weights) = &self.user_function_weights {
250 writeln!(f, "Weight by user function (inc. generated):")?;
251 for (name, weight) in user_function_weights.iter() {
252 writeln!(f, " function {name}: {weight}")?;
253 }
254 }
255 if let Some(original_user_function_weights) = &self.original_user_function_weights {
256 writeln!(f, "Weight by original user function (exc. generated):")?;
257 for (name, weight) in original_user_function_weights.iter() {
258 writeln!(f, " function {name}: {weight}")?;
259 }
260 }
261 Ok(())
262 }
263}
264
265#[derive(Default)]
267pub struct StackTraceWeights {
268 pub sierra_stack_trace_weights: Option<OrderedHashMap<Vec<String>, usize>>,
273 pub cairo_stack_trace_weights: Option<OrderedHashMap<Vec<String>, usize>>,
277}
278impl Display for StackTraceWeights {
279 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
280 if let Some(sierra_stack_trace_weights) = &self.sierra_stack_trace_weights {
281 writeln!(f, "Weight by Sierra stack trace:")?;
282 for (stack_trace, weight) in sierra_stack_trace_weights.iter() {
283 let stack_trace_str = stack_trace.join(" -> ");
284 writeln!(f, " {stack_trace_str}: {weight}")?;
285 }
286 }
287
288 if let Some(cairo_stack_trace_weights) = &self.cairo_stack_trace_weights {
289 writeln!(f, "Weight by Cairo stack trace:")?;
290 for (stack_trace, weight) in cairo_stack_trace_weights.iter() {
291 let stack_trace_str = stack_trace.join(" -> ");
292 writeln!(f, " {stack_trace_str}: {weight}")?;
293 }
294 }
295 Ok(())
296 }
297}
298
299pub struct ProcessedProfilingInfo {
302 pub sierra_statement_weights:
305 Option<OrderedHashMap<StatementIdx, (usize, GenStatement<StatementIdx>)>>,
306
307 pub stack_trace_weights: StackTraceWeights,
309
310 pub libfunc_weights: LibfuncWeights,
312
313 pub user_function_weights: UserFunctionWeights,
315
316 pub cairo_function_weights: Option<OrderedHashMap<String, usize>>,
318
319 pub scoped_sierra_statement_weights: Option<OrderedHashMap<Vec<String>, usize>>,
322}
323impl Display for ProcessedProfilingInfo {
324 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
325 if let Some(sierra_statement_weights) = &self.sierra_statement_weights {
326 writeln!(f, "Weight by sierra statement:")?;
327 for (statement_idx, (weight, gen_statement)) in sierra_statement_weights.iter() {
328 writeln!(f, " statement {statement_idx}: {weight} ({gen_statement})")?;
329 }
330 }
331
332 self.libfunc_weights.fmt(f)?;
334
335 self.user_function_weights.fmt(f)?;
337
338 if let Some(cairo_function_weights) = &self.cairo_function_weights {
339 writeln!(f, "Weight by Cairo function:")?;
340 for (function_identifier, weight) in cairo_function_weights.iter() {
341 writeln!(f, " function {function_identifier}: {weight}")?;
342 }
343 }
344
345 self.stack_trace_weights.fmt(f)?;
346
347 if let Some(weights) = &self.scoped_sierra_statement_weights {
348 format_scoped_sierra_statement_weights(weights, f)?;
349 }
350
351 Ok(())
352 }
353}
354
355pub struct ProfilingInfoProcessorParams {
358 pub min_weight: usize,
362 pub process_by_statement: bool,
364 pub process_by_concrete_libfunc: bool,
366 pub process_by_generic_libfunc: bool,
368 pub process_by_user_function: bool,
370 pub process_by_original_user_function: bool,
372 pub process_by_cairo_function: bool,
375 pub process_by_stack_trace: bool,
378 pub process_by_cairo_stack_trace: bool,
381 pub process_by_scoped_statement: bool,
384}
385impl Default for ProfilingInfoProcessorParams {
386 fn default() -> Self {
387 Self {
388 min_weight: 1,
389 process_by_statement: true,
390 process_by_concrete_libfunc: true,
391 process_by_generic_libfunc: true,
392 process_by_user_function: true,
393 process_by_original_user_function: true,
394 process_by_cairo_function: true,
395 process_by_stack_trace: true,
396 process_by_cairo_stack_trace: true,
397 process_by_scoped_statement: false,
398 }
399 }
400}
401
402impl ProfilingInfoProcessorParams {
403 pub fn from_profiler_config(config: &ProfilerConfig) -> Self {
404 match config {
405 ProfilerConfig::Cairo => Default::default(),
406 ProfilerConfig::Scoped => Self {
407 min_weight: 1,
408 process_by_statement: false,
409 process_by_concrete_libfunc: false,
410 process_by_generic_libfunc: false,
411 process_by_user_function: false,
412 process_by_original_user_function: false,
413 process_by_cairo_function: false,
414 process_by_stack_trace: false,
415 process_by_cairo_stack_trace: false,
416 process_by_scoped_statement: true,
417 },
418 ProfilerConfig::Sierra => Self {
419 process_by_generic_libfunc: false,
420 process_by_cairo_stack_trace: false,
421 process_by_original_user_function: false,
422 process_by_cairo_function: false,
423 ..ProfilingInfoProcessorParams::default()
424 },
425 }
426 }
427}
428
429pub struct ProfilingInfoProcessor<'a> {
432 db: Option<&'a dyn Database>,
433 sierra_program: &'a Program,
434 statements_functions: UnorderedHashMap<StatementIdx, String>,
438}
439impl<'a> ProfilingInfoProcessor<'a> {
440 pub fn new(
441 db: Option<&'a dyn Database>,
442 sierra_program: &'a Program,
443 statements_functions: UnorderedHashMap<StatementIdx, String>,
444 ) -> Self {
445 Self { db, sierra_program, statements_functions }
446 }
447
448 pub fn process(
450 &self,
451 raw_profiling_info: &ProfilingInfo,
452 params: &ProfilingInfoProcessorParams,
453 ) -> ProcessedProfilingInfo {
454 let sierra_statement_weights_iter = raw_profiling_info
455 .sierra_statement_weights
456 .iter_sorted_by_key(|(pc, count)| (usize::MAX - **count, **pc));
457
458 let sierra_statement_weights =
459 self.process_sierra_statement_weights(sierra_statement_weights_iter.clone(), params);
460
461 let stack_trace_weights = self.process_stack_trace_weights(raw_profiling_info, params);
462
463 let libfunc_weights =
464 self.process_libfunc_weights(sierra_statement_weights_iter.clone(), params);
465
466 let user_function_weights =
467 self.process_user_function_weights(sierra_statement_weights_iter.clone(), params);
468
469 let cairo_function_weights =
470 self.process_cairo_function_weights(sierra_statement_weights_iter, params);
471
472 let scoped_sierra_statement_weights =
473 self.process_scoped_sierra_statement_weights(raw_profiling_info, params);
474
475 ProcessedProfilingInfo {
476 sierra_statement_weights,
477 stack_trace_weights,
478 libfunc_weights,
479 user_function_weights,
480 cairo_function_weights,
481 scoped_sierra_statement_weights,
482 }
483 }
484
485 fn process_sierra_statement_weights(
487 &self,
488 sierra_statement_weights_iter: std::vec::IntoIter<(&StatementIdx, &usize)>,
489 params: &ProfilingInfoProcessorParams,
490 ) -> Option<OrderedHashMap<StatementIdx, (usize, GenStatement<StatementIdx>)>> {
491 require(params.process_by_statement)?;
492
493 Some(
494 sierra_statement_weights_iter
495 .filter(|&(_, weight)| *weight >= params.min_weight)
496 .map(|(statement_idx, weight)| {
497 (*statement_idx, (*weight, self.statement_idx_to_gen_statement(statement_idx)))
498 })
499 .collect(),
500 )
501 }
502
503 fn process_stack_trace_weights(
505 &self,
506 raw_profiling_info: &ProfilingInfo,
507 params: &ProfilingInfoProcessorParams,
508 ) -> StackTraceWeights {
509 let resolve_names = |(idx_stack_trace, weight): (&Vec<usize>, &usize)| {
510 (index_stack_trace_to_name_stack_trace(self.sierra_program, idx_stack_trace), *weight)
511 };
512
513 let sierra_stack_trace_weights = params.process_by_stack_trace.then(|| {
514 raw_profiling_info
515 .stack_trace_weights
516 .iter()
517 .sorted_by_key(|&(trace, weight)| (usize::MAX - *weight, trace.clone()))
518 .map(resolve_names)
519 .collect()
520 });
521
522 let cairo_stack_trace_weights = params.process_by_cairo_stack_trace.then(|| {
523 let db = self.db.expect("DB must be set with `process_by_cairo_stack_trace=true`.");
524 raw_profiling_info
525 .stack_trace_weights
526 .iter()
527 .filter(|(trace, _)| is_cairo_trace(db, self.sierra_program, trace))
528 .sorted_by_key(|&(trace, weight)| (usize::MAX - *weight, trace.clone()))
529 .map(resolve_names)
530 .collect()
531 });
532
533 StackTraceWeights { sierra_stack_trace_weights, cairo_stack_trace_weights }
534 }
535
536 fn process_libfunc_weights(
538 &self,
539 sierra_statement_weights: std::vec::IntoIter<(&StatementIdx, &usize)>,
540 params: &ProfilingInfoProcessorParams,
541 ) -> LibfuncWeights {
542 if !params.process_by_concrete_libfunc && !params.process_by_generic_libfunc {
543 return LibfuncWeights::default();
544 }
545
546 let mut return_weight = 0;
547 let mut libfunc_weights = UnorderedHashMap::<ConcreteLibfuncId, usize>::default();
548 for (statement_idx, weight) in sierra_statement_weights {
549 match self.statement_idx_to_gen_statement(statement_idx) {
550 GenStatement::Invocation(invocation) => {
551 *(libfunc_weights.entry(invocation.libfunc_id.clone()).or_insert(0)) += weight;
552 }
553 GenStatement::Return(_) => {
554 return_weight += weight;
555 }
556 }
557 }
558
559 let generic_libfunc_weights = params.process_by_generic_libfunc.then(|| {
560 let db: &dyn Database =
561 self.db.expect("DB must be set with `process_by_generic_libfunc=true`.");
562 libfunc_weights
563 .aggregate_by(
564 |k| db.lookup_concrete_lib_func(k).generic_id.to_string(),
565 |v1: &usize, v2| v1 + v2,
566 &0,
567 )
568 .filter(|_, weight| *weight >= params.min_weight)
569 .into_iter_sorted_by_key(|(generic_name, weight)| {
570 (usize::MAX - *weight, (*generic_name).clone())
571 })
572 .collect()
573 });
574
575 let concrete_libfunc_weights = params.process_by_concrete_libfunc.then(|| {
577 libfunc_weights
578 .filter(|_, weight| *weight >= params.min_weight)
579 .into_iter_sorted_by_key(|(libfunc_id, weight)| {
580 (usize::MAX - *weight, libfunc_id.to_string())
581 })
582 .map(|(libfunc_id, weight)| (libfunc_id.to_string(), weight))
583 .collect()
584 });
585
586 LibfuncWeights {
587 concrete_libfunc_weights,
588 generic_libfunc_weights,
589 return_weight: Some(return_weight),
590 }
591 }
592
593 fn process_user_function_weights(
595 &self,
596 sierra_statement_weights: std::vec::IntoIter<(&StatementIdx, &usize)>,
597 params: &ProfilingInfoProcessorParams,
598 ) -> UserFunctionWeights {
599 if !params.process_by_user_function && !params.process_by_original_user_function {
600 return UserFunctionWeights::default();
601 }
602
603 let mut user_functions = UnorderedHashMap::<usize, usize>::default();
604 for (statement_idx, weight) in sierra_statement_weights {
605 let function_idx: usize =
606 user_function_idx_by_sierra_statement_idx(self.sierra_program, *statement_idx);
607 *(user_functions.entry(function_idx).or_insert(0)) += weight;
608 }
609
610 let original_user_function_weights = params.process_by_original_user_function.then(|| {
611 let db: &dyn Database =
612 self.db.expect("DB must be set with `process_by_original_user_function=true`.");
613 user_functions
614 .aggregate_by(
615 |idx| {
616 let lowering_function_id =
617 db.lookup_sierra_function(&self.sierra_program.funcs[*idx].id);
618 lowering_function_id.semantic_full_path(db)
619 },
620 |x, y| x + y,
621 &0,
622 )
623 .filter(|_, weight| *weight >= params.min_weight)
624 .iter_sorted_by_key(|(orig_name, weight)| {
625 (usize::MAX - **weight, (*orig_name).clone())
626 })
627 .map(|(orig_name, weight)| (orig_name.clone(), *weight))
628 .collect()
629 });
630
631 let user_function_weights = params.process_by_user_function.then(|| {
633 user_functions
634 .filter(|_, weight| *weight >= params.min_weight)
635 .iter_sorted_by_key(|(idx, weight)| {
636 (usize::MAX - **weight, self.sierra_program.funcs[**idx].id.to_string())
637 })
638 .map(|(idx, weight)| {
639 let func: &cairo_lang_sierra::program::GenFunction<StatementIdx> =
640 &self.sierra_program.funcs[*idx];
641 (func.id.to_string(), *weight)
642 })
643 .collect()
644 });
645
646 UserFunctionWeights { user_function_weights, original_user_function_weights }
647 }
648
649 fn process_cairo_function_weights(
651 &self,
652 sierra_statement_weights: std::vec::IntoIter<(&StatementIdx, &usize)>,
653 params: &ProfilingInfoProcessorParams,
654 ) -> Option<OrderedHashMap<String, usize>> {
655 require(params.process_by_cairo_function)?;
656
657 let mut cairo_functions = UnorderedHashMap::<_, _>::default();
658 for (statement_idx, weight) in sierra_statement_weights {
659 let function_identifier = self
661 .statements_functions
662 .get(statement_idx)
663 .unwrap_or(&"unknown".to_string())
664 .clone();
665 *(cairo_functions.entry(function_identifier).or_insert(0)) += weight;
666 }
667
668 Some(
669 cairo_functions
670 .filter(|_, weight| *weight >= params.min_weight)
671 .iter_sorted_by_key(|(function_identifier, weight)| {
672 (usize::MAX - **weight, (*function_identifier).clone())
673 })
674 .map(|(function_identifier, weight)| (function_identifier.clone(), *weight))
675 .collect(),
676 )
677 }
678
679 fn process_scoped_sierra_statement_weights(
681 &self,
682 raw_profiling_info: &ProfilingInfo,
683 params: &ProfilingInfoProcessorParams,
684 ) -> Option<OrderedHashMap<Vec<String>, usize>> {
685 if params.process_by_scoped_statement {
686 let mut scoped_sierra_statement_weights: OrderedHashMap<Vec<String>, usize> =
687 Default::default();
688 for ((idx_stack_trace, statement_idx), weight) in
689 raw_profiling_info.scoped_sierra_statement_weights.iter()
690 {
691 let statement_name = match self.statement_idx_to_gen_statement(statement_idx) {
692 GenStatement::Invocation(invocation) => invocation.libfunc_id.to_string(),
693 GenStatement::Return(_) => "return".into(),
694 };
695 let key: Vec<String> = chain!(
696 index_stack_trace_to_name_stack_trace(self.sierra_program, idx_stack_trace),
697 [statement_name]
698 )
699 .collect();
700 *scoped_sierra_statement_weights.entry(key).or_default() += *weight;
702 }
703 return Some(scoped_sierra_statement_weights);
704 }
705 None
706 }
707
708 fn statement_idx_to_gen_statement(
710 &self,
711 statement_idx: &StatementIdx,
712 ) -> GenStatement<StatementIdx> {
713 self.sierra_program
714 .statements
715 .get(statement_idx.0)
716 .unwrap_or_else(|| panic!("Failed fetching statement index {}", statement_idx.0))
717 .clone()
718 }
719}
720
721fn is_cairo_trace(db: &dyn Database, sierra_program: &Program, sierra_trace: &[usize]) -> bool {
724 sierra_trace.iter().all(|sierra_function_idx| {
725 let sierra_function = &sierra_program.funcs[*sierra_function_idx];
726 let lowering_function_id = db.lookup_sierra_function(&sierra_function.id);
727 matches!(lowering_function_id.long(db), FunctionLongId::Semantic(_))
728 })
729}
730
731pub fn user_function_idx_by_sierra_statement_idx(
738 sierra_program: &Program,
739 statement_idx: StatementIdx,
740) -> usize {
741 sierra_program.funcs.partition_point(|f| f.entry_point.0 <= statement_idx.0) - 1
745}
746
747fn index_stack_trace_to_name_stack_trace(
752 sierra_program: &Program,
753 idx_stack_trace: &[usize],
754) -> Vec<String> {
755 idx_stack_trace.iter().map(|idx| sierra_program.funcs[*idx].id.to_string()).collect()
756}
757
758fn format_scoped_sierra_statement_weights(
760 weights: &OrderedHashMap<Vec<String>, usize>,
761 f: &mut std::fmt::Formatter<'_>,
762) -> std::fmt::Result {
763 for (key, weight) in weights.iter() {
764 f.write_fmt(format_args!("{} {weight}\n", key.join(";")))?;
765 }
766 Ok(())
767}