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 bytecode_len = sierra_statement_info.last().unwrap().end_offset;
74
75 let mut function_stack = Vec::new();
81 let mut function_stack_depth = 0;
85 let mut cur_weight = 0;
86 let mut stack_trace_weights = OrderedHashMap::default();
91 let mut end_of_program_reached = false;
92 let mut sierra_statement_weights = UnorderedHashMap::default();
96 let mut scoped_sierra_statement_weights = OrderedHashMap::default();
99 for step in trace {
100 let Some(real_pc) = step.pc.checked_sub(load_offset) else {
102 continue;
103 };
104
105 if real_pc >= bytecode_len {
110 continue;
111 }
112
113 if end_of_program_reached {
114 unreachable!("End of program reached, but trace continues.");
115 }
116
117 cur_weight += 1;
118
119 let sierra_statement_idx = builder.casm_program().sierra_statement_index_by_pc(real_pc);
122 let user_function_idx = user_function_idx_by_sierra_statement_idx(
123 builder.sierra_program(),
124 sierra_statement_idx,
125 );
126
127 *sierra_statement_weights.entry(sierra_statement_idx).or_insert(0) += 1;
128
129 if profiling_config.collect_scoped_sierra_statement_weights {
130 let cur_stack: Vec<usize> =
133 chain!(function_stack.iter().map(|&(idx, _)| idx), [user_function_idx])
134 .dedup()
135 .collect();
136
137 *scoped_sierra_statement_weights
138 .entry((cur_stack, sierra_statement_idx))
139 .or_insert(0) += 1;
140 }
141
142 let Some(gen_statement) =
143 builder.sierra_program().statements.get(sierra_statement_idx.0)
144 else {
145 panic!("Failed fetching statement index {}", sierra_statement_idx.0);
146 };
147
148 match gen_statement {
149 GenStatement::Invocation(invocation) => {
150 if matches!(
151 builder.registry().get_libfunc(&invocation.libfunc_id),
152 Ok(CoreConcreteLibfunc::FunctionCall(_)
153 | CoreConcreteLibfunc::CouponCall(_))
154 ) {
155 if function_stack_depth < profiling_config.max_stack_trace_depth {
157 function_stack.push((user_function_idx, cur_weight));
158 cur_weight = 0;
159 }
160 function_stack_depth += 1;
161 }
162 }
163 GenStatement::Return(_) => {
164 if function_stack_depth <= profiling_config.max_stack_trace_depth {
166 let cur_stack: Vec<_> =
168 chain!(function_stack.iter().map(|f| f.0), [user_function_idx])
169 .collect();
170 *stack_trace_weights.entry(cur_stack).or_insert(0) += cur_weight;
171
172 let Some(popped) = function_stack.pop() else {
173 end_of_program_reached = true;
175 continue;
176 };
177 cur_weight += popped.1;
178 }
179 function_stack_depth -= 1;
180 }
181 }
182 }
183
184 ProfilingInfo {
185 sierra_statement_weights,
186 stack_trace_weights,
187 scoped_sierra_statement_weights,
188 }
189 }
190}
191
192#[derive(Default)]
194pub struct LibfuncWeights {
195 pub concrete_libfunc_weights: Option<OrderedHashMap<String, usize>>,
197 pub generic_libfunc_weights: Option<OrderedHashMap<String, usize>>,
199 pub return_weight: Option<usize>,
201}
202impl Display for LibfuncWeights {
203 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
204 if let Some(concrete_libfunc_weights) = &self.concrete_libfunc_weights {
205 writeln!(f, "Weight by concrete libfunc:")?;
206 for (concrete_name, weight) in concrete_libfunc_weights.iter() {
207 writeln!(f, " libfunc {concrete_name}: {weight}")?;
208 }
209 writeln!(
210 f,
211 " return: {}",
212 self.return_weight.expect(
213 "return_weight should have a value if concrete_libfunc_weights has a value"
214 )
215 )?;
216 }
217 if let Some(generic_libfunc_weights) = &self.generic_libfunc_weights {
218 writeln!(f, "Weight by generic libfunc:")?;
219 for (generic_name, weight) in generic_libfunc_weights.iter() {
220 writeln!(f, " libfunc {generic_name}: {weight}")?;
221 }
222 writeln!(
223 f,
224 " return: {}",
225 self.return_weight.expect(
226 "return_weight should have a value if generic_libfunc_weights has a value"
227 )
228 )?;
229 }
230 Ok(())
231 }
232}
233
234#[derive(Default)]
236pub struct UserFunctionWeights {
237 pub user_function_weights: Option<OrderedHashMap<String, usize>>,
240 pub original_user_function_weights: Option<OrderedHashMap<String, usize>>,
242}
243impl Display for UserFunctionWeights {
244 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
245 if let Some(user_function_weights) = &self.user_function_weights {
246 writeln!(f, "Weight by user function (inc. generated):")?;
247 for (name, weight) in user_function_weights.iter() {
248 writeln!(f, " function {name}: {weight}")?;
249 }
250 }
251 if let Some(original_user_function_weights) = &self.original_user_function_weights {
252 writeln!(f, "Weight by original user function (exc. generated):")?;
253 for (name, weight) in original_user_function_weights.iter() {
254 writeln!(f, " function {name}: {weight}")?;
255 }
256 }
257 Ok(())
258 }
259}
260
261#[derive(Default)]
263pub struct StackTraceWeights {
264 pub sierra_stack_trace_weights: Option<OrderedHashMap<Vec<String>, usize>>,
269 pub cairo_stack_trace_weights: Option<OrderedHashMap<Vec<String>, usize>>,
273}
274impl Display for StackTraceWeights {
275 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
276 if let Some(sierra_stack_trace_weights) = &self.sierra_stack_trace_weights {
277 writeln!(f, "Weight by Sierra stack trace:")?;
278 for (stack_trace, weight) in sierra_stack_trace_weights.iter() {
279 writeln!(f, " {}: {weight}", stack_trace.iter().format(" -> "))?;
280 }
281 }
282
283 if let Some(cairo_stack_trace_weights) = &self.cairo_stack_trace_weights {
284 writeln!(f, "Weight by Cairo stack trace:")?;
285 for (stack_trace, weight) in cairo_stack_trace_weights.iter() {
286 writeln!(f, " {}: {weight}", stack_trace.iter().format(" -> "))?;
287 }
288 }
289 Ok(())
290 }
291}
292
293pub struct ProcessedProfilingInfo {
296 pub sierra_statement_weights:
299 Option<OrderedHashMap<StatementIdx, (usize, GenStatement<StatementIdx>)>>,
300
301 pub stack_trace_weights: StackTraceWeights,
303
304 pub libfunc_weights: LibfuncWeights,
306
307 pub user_function_weights: UserFunctionWeights,
309
310 pub cairo_function_weights: Option<OrderedHashMap<String, usize>>,
312
313 pub scoped_sierra_statement_weights: Option<OrderedHashMap<Vec<String>, usize>>,
316}
317impl Display for ProcessedProfilingInfo {
318 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
319 if let Some(sierra_statement_weights) = &self.sierra_statement_weights {
320 writeln!(f, "Weight by sierra statement:")?;
321 for (statement_idx, (weight, gen_statement)) in sierra_statement_weights.iter() {
322 writeln!(f, " statement {statement_idx}: {weight} ({gen_statement})")?;
323 }
324 }
325
326 self.libfunc_weights.fmt(f)?;
328
329 self.user_function_weights.fmt(f)?;
331
332 if let Some(cairo_function_weights) = &self.cairo_function_weights {
333 writeln!(f, "Weight by Cairo function:")?;
334 for (function_identifier, weight) in cairo_function_weights.iter() {
335 writeln!(f, " function {function_identifier}: {weight}")?;
336 }
337 }
338
339 self.stack_trace_weights.fmt(f)?;
340
341 if let Some(weights) = &self.scoped_sierra_statement_weights {
342 format_scoped_sierra_statement_weights(weights, f)?;
343 }
344
345 Ok(())
346 }
347}
348
349pub struct ProfilingInfoProcessorParams {
352 pub min_weight: usize,
356 pub process_by_statement: bool,
358 pub process_by_concrete_libfunc: bool,
360 pub process_by_generic_libfunc: bool,
362 pub process_by_user_function: bool,
364 pub process_by_original_user_function: bool,
366 pub process_by_cairo_function: bool,
369 pub process_by_stack_trace: bool,
372 pub process_by_cairo_stack_trace: bool,
375 pub process_by_scoped_statement: bool,
378}
379impl Default for ProfilingInfoProcessorParams {
380 fn default() -> Self {
381 Self {
382 min_weight: 1,
383 process_by_statement: true,
384 process_by_concrete_libfunc: true,
385 process_by_generic_libfunc: true,
386 process_by_user_function: true,
387 process_by_original_user_function: true,
388 process_by_cairo_function: true,
389 process_by_stack_trace: true,
390 process_by_cairo_stack_trace: true,
391 process_by_scoped_statement: false,
392 }
393 }
394}
395
396impl ProfilingInfoProcessorParams {
397 pub fn from_profiler_config(config: &ProfilerConfig) -> Self {
398 match config {
399 ProfilerConfig::Cairo => Default::default(),
400 ProfilerConfig::Scoped => Self {
401 min_weight: 1,
402 process_by_statement: false,
403 process_by_concrete_libfunc: false,
404 process_by_generic_libfunc: false,
405 process_by_user_function: false,
406 process_by_original_user_function: false,
407 process_by_cairo_function: false,
408 process_by_stack_trace: false,
409 process_by_cairo_stack_trace: false,
410 process_by_scoped_statement: true,
411 },
412 ProfilerConfig::Sierra => Self {
413 process_by_generic_libfunc: false,
414 process_by_cairo_stack_trace: false,
415 process_by_original_user_function: false,
416 process_by_cairo_function: false,
417 ..ProfilingInfoProcessorParams::default()
418 },
419 }
420 }
421}
422
423pub struct ProfilingInfoProcessor<'a> {
426 db: Option<&'a dyn Database>,
427 sierra_program: &'a Program,
428 statements_functions: UnorderedHashMap<StatementIdx, String>,
432}
433impl<'a> ProfilingInfoProcessor<'a> {
434 pub fn new(
435 db: Option<&'a dyn Database>,
436 sierra_program: &'a Program,
437 statements_functions: UnorderedHashMap<StatementIdx, String>,
438 ) -> Self {
439 Self { db, sierra_program, statements_functions }
440 }
441
442 pub fn process(
444 &self,
445 raw_profiling_info: &ProfilingInfo,
446 params: &ProfilingInfoProcessorParams,
447 ) -> ProcessedProfilingInfo {
448 let sierra_statement_weights_iter = raw_profiling_info
449 .sierra_statement_weights
450 .iter_sorted_by_key(|(pc, count)| (usize::MAX - **count, **pc));
451
452 let sierra_statement_weights =
453 self.process_sierra_statement_weights(sierra_statement_weights_iter.clone(), params);
454
455 let stack_trace_weights = self.process_stack_trace_weights(raw_profiling_info, params);
456
457 let libfunc_weights =
458 self.process_libfunc_weights(sierra_statement_weights_iter.clone(), params);
459
460 let user_function_weights =
461 self.process_user_function_weights(sierra_statement_weights_iter.clone(), params);
462
463 let cairo_function_weights =
464 self.process_cairo_function_weights(sierra_statement_weights_iter, params);
465
466 let scoped_sierra_statement_weights =
467 self.process_scoped_sierra_statement_weights(raw_profiling_info, params);
468
469 ProcessedProfilingInfo {
470 sierra_statement_weights,
471 stack_trace_weights,
472 libfunc_weights,
473 user_function_weights,
474 cairo_function_weights,
475 scoped_sierra_statement_weights,
476 }
477 }
478
479 fn process_sierra_statement_weights(
481 &self,
482 sierra_statement_weights_iter: std::vec::IntoIter<(&StatementIdx, &usize)>,
483 params: &ProfilingInfoProcessorParams,
484 ) -> Option<OrderedHashMap<StatementIdx, (usize, GenStatement<StatementIdx>)>> {
485 require(params.process_by_statement)?;
486
487 Some(
488 sierra_statement_weights_iter
489 .filter(|&(_, weight)| *weight >= params.min_weight)
490 .map(|(statement_idx, weight)| {
491 (*statement_idx, (*weight, self.statement_idx_to_gen_statement(statement_idx)))
492 })
493 .collect(),
494 )
495 }
496
497 fn process_stack_trace_weights(
499 &self,
500 raw_profiling_info: &ProfilingInfo,
501 params: &ProfilingInfoProcessorParams,
502 ) -> StackTraceWeights {
503 let resolve_names = |(idx_stack_trace, weight): (&Vec<usize>, &usize)| {
504 (index_stack_trace_to_name_stack_trace(self.sierra_program, idx_stack_trace), *weight)
505 };
506
507 let sierra_stack_trace_weights = params.process_by_stack_trace.then(|| {
508 raw_profiling_info
509 .stack_trace_weights
510 .iter()
511 .sorted_by_key(|&(trace, weight)| (usize::MAX - *weight, trace))
512 .map(resolve_names)
513 .collect()
514 });
515
516 let cairo_stack_trace_weights = params.process_by_cairo_stack_trace.then(|| {
517 let db = self.db.expect("DB must be set with `process_by_cairo_stack_trace=true`.");
518 raw_profiling_info
519 .stack_trace_weights
520 .iter()
521 .filter(|(trace, _)| is_cairo_trace(db, self.sierra_program, trace))
522 .sorted_by_key(|&(trace, weight)| (usize::MAX - *weight, trace))
523 .map(resolve_names)
524 .collect()
525 });
526
527 StackTraceWeights { sierra_stack_trace_weights, cairo_stack_trace_weights }
528 }
529
530 fn process_libfunc_weights(
532 &self,
533 sierra_statement_weights: std::vec::IntoIter<(&StatementIdx, &usize)>,
534 params: &ProfilingInfoProcessorParams,
535 ) -> LibfuncWeights {
536 if !params.process_by_concrete_libfunc && !params.process_by_generic_libfunc {
537 return LibfuncWeights::default();
538 }
539
540 let mut return_weight = 0;
541 let mut libfunc_weights = UnorderedHashMap::<ConcreteLibfuncId, usize>::default();
542 for (statement_idx, weight) in sierra_statement_weights {
543 match self.statement_idx_to_gen_statement(statement_idx) {
544 GenStatement::Invocation(invocation) => {
545 *(libfunc_weights.entry(invocation.libfunc_id.clone()).or_insert(0)) += weight;
546 }
547 GenStatement::Return(_) => {
548 return_weight += weight;
549 }
550 }
551 }
552
553 let generic_libfunc_weights = params.process_by_generic_libfunc.then(|| {
554 let db: &dyn Database =
555 self.db.expect("DB must be set with `process_by_generic_libfunc=true`.");
556 libfunc_weights
557 .aggregate_by(
558 |k| db.lookup_concrete_lib_func(k).generic_id.to_string(),
559 |v1: &usize, v2| v1 + v2,
560 &0,
561 )
562 .filter(|_, weight| *weight >= params.min_weight)
563 .into_iter_sorted_by_key(|(generic_name, weight)| {
564 (usize::MAX - *weight, (*generic_name).clone())
565 })
566 .collect()
567 });
568
569 let concrete_libfunc_weights = params.process_by_concrete_libfunc.then(|| {
571 libfunc_weights
572 .filter(|_, weight| *weight >= params.min_weight)
573 .into_iter_sorted_by_key(|(libfunc_id, weight)| {
574 (usize::MAX - *weight, libfunc_id.to_string())
575 })
576 .map(|(libfunc_id, weight)| (libfunc_id.to_string(), weight))
577 .collect()
578 });
579
580 LibfuncWeights {
581 concrete_libfunc_weights,
582 generic_libfunc_weights,
583 return_weight: Some(return_weight),
584 }
585 }
586
587 fn process_user_function_weights(
589 &self,
590 sierra_statement_weights: std::vec::IntoIter<(&StatementIdx, &usize)>,
591 params: &ProfilingInfoProcessorParams,
592 ) -> UserFunctionWeights {
593 if !params.process_by_user_function && !params.process_by_original_user_function {
594 return UserFunctionWeights::default();
595 }
596
597 let mut user_functions = UnorderedHashMap::<usize, usize>::default();
598 for (statement_idx, weight) in sierra_statement_weights {
599 let function_idx: usize =
600 user_function_idx_by_sierra_statement_idx(self.sierra_program, *statement_idx);
601 *(user_functions.entry(function_idx).or_insert(0)) += weight;
602 }
603
604 let original_user_function_weights = params.process_by_original_user_function.then(|| {
605 let db: &dyn Database =
606 self.db.expect("DB must be set with `process_by_original_user_function=true`.");
607 user_functions
608 .aggregate_by(
609 |idx| {
610 let lowering_function_id =
611 db.lookup_sierra_function(&self.sierra_program.funcs[*idx].id);
612 lowering_function_id.semantic_full_path(db)
613 },
614 |x, y| x + y,
615 &0,
616 )
617 .filter(|_, weight| *weight >= params.min_weight)
618 .iter_sorted_by_key(|(orig_name, weight)| {
619 (usize::MAX - **weight, (*orig_name).clone())
620 })
621 .map(|(orig_name, weight)| (orig_name.clone(), *weight))
622 .collect()
623 });
624
625 let user_function_weights = params.process_by_user_function.then(|| {
627 user_functions
628 .filter(|_, weight| *weight >= params.min_weight)
629 .iter_sorted_by_key(|(idx, weight)| {
630 (usize::MAX - **weight, self.sierra_program.funcs[**idx].id.to_string())
631 })
632 .map(|(idx, weight)| {
633 let func: &cairo_lang_sierra::program::GenFunction<StatementIdx> =
634 &self.sierra_program.funcs[*idx];
635 (func.id.to_string(), *weight)
636 })
637 .collect()
638 });
639
640 UserFunctionWeights { user_function_weights, original_user_function_weights }
641 }
642
643 fn process_cairo_function_weights(
645 &self,
646 sierra_statement_weights: std::vec::IntoIter<(&StatementIdx, &usize)>,
647 params: &ProfilingInfoProcessorParams,
648 ) -> Option<OrderedHashMap<String, usize>> {
649 require(params.process_by_cairo_function)?;
650
651 let mut cairo_functions = UnorderedHashMap::<_, _>::default();
652 for (statement_idx, weight) in sierra_statement_weights {
653 let function_identifier = self
655 .statements_functions
656 .get(statement_idx)
657 .cloned()
658 .unwrap_or_else(|| "unknown".to_string());
659
660 *(cairo_functions.entry(function_identifier).or_insert(0)) += weight;
661 }
662
663 Some(
664 cairo_functions
665 .filter(|_, weight| *weight >= params.min_weight)
666 .iter_sorted_by_key(|(function_identifier, weight)| {
667 (usize::MAX - **weight, (*function_identifier).clone())
668 })
669 .map(|(function_identifier, weight)| (function_identifier.clone(), *weight))
670 .collect(),
671 )
672 }
673
674 fn process_scoped_sierra_statement_weights(
676 &self,
677 raw_profiling_info: &ProfilingInfo,
678 params: &ProfilingInfoProcessorParams,
679 ) -> Option<OrderedHashMap<Vec<String>, usize>> {
680 if params.process_by_scoped_statement {
681 let mut scoped_sierra_statement_weights: OrderedHashMap<Vec<String>, usize> =
682 Default::default();
683 for ((idx_stack_trace, statement_idx), weight) in
684 raw_profiling_info.scoped_sierra_statement_weights.iter()
685 {
686 let statement_name = match self.statement_idx_to_gen_statement(statement_idx) {
687 GenStatement::Invocation(invocation) => invocation.libfunc_id.to_string(),
688 GenStatement::Return(_) => "return".into(),
689 };
690 let key: Vec<String> = chain!(
691 index_stack_trace_to_name_stack_trace(self.sierra_program, idx_stack_trace),
692 [statement_name]
693 )
694 .collect();
695 *scoped_sierra_statement_weights.entry(key).or_default() += *weight;
697 }
698 return Some(scoped_sierra_statement_weights);
699 }
700 None
701 }
702
703 fn statement_idx_to_gen_statement(
705 &self,
706 statement_idx: &StatementIdx,
707 ) -> GenStatement<StatementIdx> {
708 self.sierra_program
709 .statements
710 .get(statement_idx.0)
711 .unwrap_or_else(|| panic!("Failed fetching statement index {}", statement_idx.0))
712 .clone()
713 }
714}
715
716fn is_cairo_trace(db: &dyn Database, sierra_program: &Program, sierra_trace: &[usize]) -> bool {
719 sierra_trace.iter().all(|sierra_function_idx| {
720 let sierra_function = &sierra_program.funcs[*sierra_function_idx];
721 let lowering_function_id = db.lookup_sierra_function(&sierra_function.id);
722 matches!(lowering_function_id.long(db), FunctionLongId::Semantic(_))
723 })
724}
725
726pub fn user_function_idx_by_sierra_statement_idx(
733 sierra_program: &Program,
734 statement_idx: StatementIdx,
735) -> usize {
736 sierra_program.funcs.partition_point(|f| f.entry_point.0 <= statement_idx.0) - 1
740}
741
742fn index_stack_trace_to_name_stack_trace(
747 sierra_program: &Program,
748 idx_stack_trace: &[usize],
749) -> Vec<String> {
750 idx_stack_trace.iter().map(|idx| sierra_program.funcs[*idx].id.to_string()).collect()
751}
752
753fn format_scoped_sierra_statement_weights(
755 weights: &OrderedHashMap<Vec<String>, usize>,
756 f: &mut std::fmt::Formatter<'_>,
757) -> std::fmt::Result {
758 for (key, weight) in weights.iter() {
759 f.write_fmt(format_args!("{} {weight}\n", key.iter().format(";")))?;
760 }
761 Ok(())
762}