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)]
43pub struct ProfilingInfo {
44 pub sierra_statement_weights: UnorderedHashMap<StatementIdx, usize>,
46
47 pub stack_trace_weights: OrderedHashMap<Vec<usize>, usize>,
54
55 pub scoped_sierra_statement_weights: OrderedHashMap<(Vec<usize>, StatementIdx), usize>,
61}
62
63impl ProfilingInfo {
64 pub fn from_trace(
65 builder: &RunnableBuilder,
66 load_offset: usize,
68 profiling_config: &ProfilingInfoCollectionConfig,
69 trace: &[RelocatedTraceEntry],
70 ) -> Self {
71 let sierra_statement_info = &builder.casm_program().debug_info.sierra_statement_info;
72 let sierra_len = sierra_statement_info.len();
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();
97 let mut scoped_sierra_statement_weights = OrderedHashMap::default();
100 for step in trace {
101 let Some(real_pc) = step.pc.checked_sub(load_offset) else {
103 continue;
104 };
105
106 if real_pc >= bytecode_len {
111 continue;
112 }
113
114 if end_of_program_reached {
115 unreachable!("End of program reached, but trace continues.");
116 }
117
118 cur_weight += 1;
119
120 let sierra_statement_idx = builder.casm_program().sierra_statement_index_by_pc(real_pc);
123 let user_function_idx = user_function_idx_by_sierra_statement_idx(
124 builder.sierra_program(),
125 sierra_statement_idx,
126 );
127
128 *sierra_statement_weights.entry(sierra_statement_idx).or_insert(0) += 1;
129
130 if profiling_config.collect_scoped_sierra_statement_weights {
131 let cur_stack: Vec<usize> =
134 chain!(function_stack.iter().map(|&(idx, _)| idx), [user_function_idx])
135 .dedup()
136 .collect();
137
138 *scoped_sierra_statement_weights
139 .entry((cur_stack, sierra_statement_idx))
140 .or_insert(0) += 1;
141 }
142
143 let Some(gen_statement) =
144 builder.sierra_program().statements.get(sierra_statement_idx.0)
145 else {
146 panic!("Failed fetching statement index {}", sierra_statement_idx.0);
147 };
148
149 match gen_statement {
150 GenStatement::Invocation(invocation) => {
151 if matches!(
152 builder.registry().get_libfunc(&invocation.libfunc_id),
153 Ok(CoreConcreteLibfunc::FunctionCall(_))
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 sierra_statement_weights.remove(&StatementIdx(sierra_len));
186
187 ProfilingInfo {
188 sierra_statement_weights,
189 stack_trace_weights,
190 scoped_sierra_statement_weights,
191 }
192 }
193}
194
195#[derive(Default)]
197pub struct LibfuncWeights {
198 pub concrete_libfunc_weights: Option<OrderedHashMap<String, usize>>,
200 pub generic_libfunc_weights: Option<OrderedHashMap<String, usize>>,
202 pub return_weight: Option<usize>,
204}
205impl Display for LibfuncWeights {
206 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
207 if let Some(concrete_libfunc_weights) = &self.concrete_libfunc_weights {
208 writeln!(f, "Weight by concrete libfunc:")?;
209 for (concrete_name, weight) in concrete_libfunc_weights.iter() {
210 writeln!(f, " libfunc {concrete_name}: {weight}")?;
211 }
212 writeln!(
213 f,
214 " return: {}",
215 self.return_weight.expect(
216 "return_weight should have a value if concrete_libfunc_weights has a value"
217 )
218 )?;
219 }
220 if let Some(generic_libfunc_weights) = &self.generic_libfunc_weights {
221 writeln!(f, "Weight by generic libfunc:")?;
222 for (generic_name, weight) in generic_libfunc_weights.iter() {
223 writeln!(f, " libfunc {generic_name}: {weight}")?;
224 }
225 writeln!(
226 f,
227 " return: {}",
228 self.return_weight.expect(
229 "return_weight should have a value if generic_libfunc_weights has a value"
230 )
231 )?;
232 }
233 Ok(())
234 }
235}
236
237#[derive(Default)]
239pub struct UserFunctionWeights {
240 pub user_function_weights: Option<OrderedHashMap<String, usize>>,
243 pub original_user_function_weights: Option<OrderedHashMap<String, usize>>,
245}
246impl Display for UserFunctionWeights {
247 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
248 if let Some(user_function_weights) = &self.user_function_weights {
249 writeln!(f, "Weight by user function (inc. generated):")?;
250 for (name, weight) in user_function_weights.iter() {
251 writeln!(f, " function {name}: {weight}")?;
252 }
253 }
254 if let Some(original_user_function_weights) = &self.original_user_function_weights {
255 writeln!(f, "Weight by original user function (exc. generated):")?;
256 for (name, weight) in original_user_function_weights.iter() {
257 writeln!(f, " function {name}: {weight}")?;
258 }
259 }
260 Ok(())
261 }
262}
263
264#[derive(Default)]
266pub struct StackTraceWeights {
267 pub sierra_stack_trace_weights: Option<OrderedHashMap<Vec<String>, usize>>,
272 pub cairo_stack_trace_weights: Option<OrderedHashMap<Vec<String>, usize>>,
276}
277impl Display for StackTraceWeights {
278 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
279 if let Some(sierra_stack_trace_weights) = &self.sierra_stack_trace_weights {
280 writeln!(f, "Weight by Sierra stack trace:")?;
281 for (stack_trace, weight) in sierra_stack_trace_weights.iter() {
282 let stack_trace_str = stack_trace.join(" -> ");
283 writeln!(f, " {stack_trace_str}: {weight}")?;
284 }
285 }
286
287 if let Some(cairo_stack_trace_weights) = &self.cairo_stack_trace_weights {
288 writeln!(f, "Weight by Cairo stack trace:")?;
289 for (stack_trace, weight) in cairo_stack_trace_weights.iter() {
290 let stack_trace_str = stack_trace.join(" -> ");
291 writeln!(f, " {stack_trace_str}: {weight}")?;
292 }
293 }
294 Ok(())
295 }
296}
297
298pub struct ProcessedProfilingInfo {
301 pub sierra_statement_weights:
304 Option<OrderedHashMap<StatementIdx, (usize, GenStatement<StatementIdx>)>>,
305
306 pub stack_trace_weights: StackTraceWeights,
308
309 pub libfunc_weights: LibfuncWeights,
311
312 pub user_function_weights: UserFunctionWeights,
314
315 pub cairo_function_weights: Option<OrderedHashMap<String, usize>>,
317
318 pub scoped_sierra_statement_weights: Option<OrderedHashMap<Vec<String>, usize>>,
321}
322impl Display for ProcessedProfilingInfo {
323 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
324 if let Some(sierra_statement_weights) = &self.sierra_statement_weights {
325 writeln!(f, "Weight by sierra statement:")?;
326 for (statement_idx, (weight, gen_statement)) in sierra_statement_weights.iter() {
327 writeln!(f, " statement {statement_idx}: {weight} ({gen_statement})")?;
328 }
329 }
330
331 self.libfunc_weights.fmt(f)?;
333
334 self.user_function_weights.fmt(f)?;
336
337 if let Some(cairo_function_weights) = &self.cairo_function_weights {
338 writeln!(f, "Weight by Cairo function:")?;
339 for (function_identifier, weight) in cairo_function_weights.iter() {
340 writeln!(f, " function {function_identifier}: {weight}")?;
341 }
342 }
343
344 self.stack_trace_weights.fmt(f)?;
345
346 if let Some(weights) = &self.scoped_sierra_statement_weights {
347 format_scoped_sierra_statement_weights(weights, f)?;
348 }
349
350 Ok(())
351 }
352}
353
354pub struct ProfilingInfoProcessorParams {
357 pub min_weight: usize,
361 pub process_by_statement: bool,
363 pub process_by_concrete_libfunc: bool,
365 pub process_by_generic_libfunc: bool,
367 pub process_by_user_function: bool,
369 pub process_by_original_user_function: bool,
371 pub process_by_cairo_function: bool,
374 pub process_by_stack_trace: bool,
377 pub process_by_cairo_stack_trace: bool,
380 pub process_by_scoped_statement: bool,
383}
384impl Default for ProfilingInfoProcessorParams {
385 fn default() -> Self {
386 Self {
387 min_weight: 1,
388 process_by_statement: true,
389 process_by_concrete_libfunc: true,
390 process_by_generic_libfunc: true,
391 process_by_user_function: true,
392 process_by_original_user_function: true,
393 process_by_cairo_function: true,
394 process_by_stack_trace: true,
395 process_by_cairo_stack_trace: true,
396 process_by_scoped_statement: false,
397 }
398 }
399}
400
401impl ProfilingInfoProcessorParams {
402 pub fn from_profiler_config(config: &ProfilerConfig) -> Self {
403 match config {
404 ProfilerConfig::Cairo => Default::default(),
405 ProfilerConfig::Scoped => Self {
406 min_weight: 1,
407 process_by_statement: false,
408 process_by_concrete_libfunc: false,
409 process_by_generic_libfunc: false,
410 process_by_user_function: false,
411 process_by_original_user_function: false,
412 process_by_cairo_function: false,
413 process_by_stack_trace: false,
414 process_by_cairo_stack_trace: false,
415 process_by_scoped_statement: true,
416 },
417 ProfilerConfig::Sierra => Self {
418 process_by_generic_libfunc: false,
419 process_by_cairo_stack_trace: false,
420 process_by_original_user_function: false,
421 process_by_cairo_function: false,
422 ..ProfilingInfoProcessorParams::default()
423 },
424 }
425 }
426}
427
428pub struct ProfilingInfoProcessor<'a> {
431 db: Option<&'a dyn Database>,
432 sierra_program: &'a Program,
433 statements_functions: UnorderedHashMap<StatementIdx, String>,
437}
438impl<'a> ProfilingInfoProcessor<'a> {
439 pub fn new(
440 db: Option<&'a dyn Database>,
441 sierra_program: &'a Program,
442 statements_functions: UnorderedHashMap<StatementIdx, String>,
443 ) -> Self {
444 Self { db, sierra_program, statements_functions }
445 }
446
447 pub fn process(
449 &self,
450 raw_profiling_info: &ProfilingInfo,
451 params: &ProfilingInfoProcessorParams,
452 ) -> ProcessedProfilingInfo {
453 let sierra_statement_weights_iter = raw_profiling_info
454 .sierra_statement_weights
455 .iter_sorted_by_key(|(pc, count)| (usize::MAX - **count, **pc));
456
457 let sierra_statement_weights =
458 self.process_sierra_statement_weights(sierra_statement_weights_iter.clone(), params);
459
460 let stack_trace_weights = self.process_stack_trace_weights(raw_profiling_info, params);
461
462 let libfunc_weights =
463 self.process_libfunc_weights(sierra_statement_weights_iter.clone(), params);
464
465 let user_function_weights =
466 self.process_user_function_weights(sierra_statement_weights_iter.clone(), params);
467
468 let cairo_function_weights =
469 self.process_cairo_function_weights(sierra_statement_weights_iter, params);
470
471 let scoped_sierra_statement_weights =
472 self.process_scoped_sierra_statement_weights(raw_profiling_info, params);
473
474 ProcessedProfilingInfo {
475 sierra_statement_weights,
476 stack_trace_weights,
477 libfunc_weights,
478 user_function_weights,
479 cairo_function_weights,
480 scoped_sierra_statement_weights,
481 }
482 }
483
484 fn process_sierra_statement_weights(
486 &self,
487 sierra_statement_weights_iter: std::vec::IntoIter<(&StatementIdx, &usize)>,
488 params: &ProfilingInfoProcessorParams,
489 ) -> Option<OrderedHashMap<StatementIdx, (usize, GenStatement<StatementIdx>)>> {
490 require(params.process_by_statement)?;
491
492 Some(
493 sierra_statement_weights_iter
494 .filter(|&(_, weight)| *weight >= params.min_weight)
495 .map(|(statement_idx, weight)| {
496 (*statement_idx, (*weight, self.statement_idx_to_gen_statement(statement_idx)))
497 })
498 .collect(),
499 )
500 }
501
502 fn process_stack_trace_weights(
504 &self,
505 raw_profiling_info: &ProfilingInfo,
506 params: &ProfilingInfoProcessorParams,
507 ) -> StackTraceWeights {
508 let resolve_names = |(idx_stack_trace, weight): (&Vec<usize>, &usize)| {
509 (index_stack_trace_to_name_stack_trace(self.sierra_program, idx_stack_trace), *weight)
510 };
511
512 let sierra_stack_trace_weights = params.process_by_stack_trace.then(|| {
513 raw_profiling_info
514 .stack_trace_weights
515 .iter()
516 .sorted_by_key(|&(trace, weight)| (usize::MAX - *weight, trace.clone()))
517 .map(resolve_names)
518 .collect()
519 });
520
521 let cairo_stack_trace_weights = params.process_by_cairo_stack_trace.then(|| {
522 let db = self.db.expect("DB must be set with `process_by_cairo_stack_trace=true`.");
523 raw_profiling_info
524 .stack_trace_weights
525 .iter()
526 .filter(|(trace, _)| is_cairo_trace(db, self.sierra_program, trace))
527 .sorted_by_key(|&(trace, weight)| (usize::MAX - *weight, trace.clone()))
528 .map(resolve_names)
529 .collect()
530 });
531
532 StackTraceWeights { sierra_stack_trace_weights, cairo_stack_trace_weights }
533 }
534
535 fn process_libfunc_weights(
537 &self,
538 sierra_statement_weights: std::vec::IntoIter<(&StatementIdx, &usize)>,
539 params: &ProfilingInfoProcessorParams,
540 ) -> LibfuncWeights {
541 if !params.process_by_concrete_libfunc && !params.process_by_generic_libfunc {
542 return LibfuncWeights::default();
543 }
544
545 let mut return_weight = 0;
546 let mut libfunc_weights = UnorderedHashMap::<ConcreteLibfuncId, usize>::default();
547 for (statement_idx, weight) in sierra_statement_weights {
548 match self.statement_idx_to_gen_statement(statement_idx) {
549 GenStatement::Invocation(invocation) => {
550 *(libfunc_weights.entry(invocation.libfunc_id.clone()).or_insert(0)) += weight;
551 }
552 GenStatement::Return(_) => {
553 return_weight += weight;
554 }
555 }
556 }
557
558 let generic_libfunc_weights = params.process_by_generic_libfunc.then(|| {
559 let db: &dyn Database =
560 self.db.expect("DB must be set with `process_by_generic_libfunc=true`.");
561 libfunc_weights
562 .aggregate_by(
563 |k| db.lookup_concrete_lib_func(k).generic_id.to_string(),
564 |v1: &usize, v2| v1 + v2,
565 &0,
566 )
567 .filter(|_, weight| *weight >= params.min_weight)
568 .into_iter_sorted_by_key(|(generic_name, weight)| {
569 (usize::MAX - *weight, (*generic_name).clone())
570 })
571 .collect()
572 });
573
574 let concrete_libfunc_weights = params.process_by_concrete_libfunc.then(|| {
576 libfunc_weights
577 .filter(|_, weight| *weight >= params.min_weight)
578 .into_iter_sorted_by_key(|(libfunc_id, weight)| {
579 (usize::MAX - *weight, libfunc_id.to_string())
580 })
581 .map(|(libfunc_id, weight)| (libfunc_id.to_string(), weight))
582 .collect()
583 });
584
585 LibfuncWeights {
586 concrete_libfunc_weights,
587 generic_libfunc_weights,
588 return_weight: Some(return_weight),
589 }
590 }
591
592 fn process_user_function_weights(
594 &self,
595 sierra_statement_weights: std::vec::IntoIter<(&StatementIdx, &usize)>,
596 params: &ProfilingInfoProcessorParams,
597 ) -> UserFunctionWeights {
598 if !params.process_by_user_function && !params.process_by_original_user_function {
599 return UserFunctionWeights::default();
600 }
601
602 let mut user_functions = UnorderedHashMap::<usize, usize>::default();
603 for (statement_idx, weight) in sierra_statement_weights {
604 let function_idx: usize =
605 user_function_idx_by_sierra_statement_idx(self.sierra_program, *statement_idx);
606 *(user_functions.entry(function_idx).or_insert(0)) += weight;
607 }
608
609 let original_user_function_weights = params.process_by_original_user_function.then(|| {
610 let db: &dyn Database =
611 self.db.expect("DB must be set with `process_by_original_user_function=true`.");
612 user_functions
613 .aggregate_by(
614 |idx| {
615 let lowering_function_id =
616 db.lookup_sierra_function(&self.sierra_program.funcs[*idx].id);
617 lowering_function_id.semantic_full_path(db)
618 },
619 |x, y| x + y,
620 &0,
621 )
622 .filter(|_, weight| *weight >= params.min_weight)
623 .iter_sorted_by_key(|(orig_name, weight)| {
624 (usize::MAX - **weight, (*orig_name).clone())
625 })
626 .map(|(orig_name, weight)| (orig_name.clone(), *weight))
627 .collect()
628 });
629
630 let user_function_weights = params.process_by_user_function.then(|| {
632 user_functions
633 .filter(|_, weight| *weight >= params.min_weight)
634 .iter_sorted_by_key(|(idx, weight)| {
635 (usize::MAX - **weight, self.sierra_program.funcs[**idx].id.to_string())
636 })
637 .map(|(idx, weight)| {
638 let func: &cairo_lang_sierra::program::GenFunction<StatementIdx> =
639 &self.sierra_program.funcs[*idx];
640 (func.id.to_string(), *weight)
641 })
642 .collect()
643 });
644
645 UserFunctionWeights { user_function_weights, original_user_function_weights }
646 }
647
648 fn process_cairo_function_weights(
650 &self,
651 sierra_statement_weights: std::vec::IntoIter<(&StatementIdx, &usize)>,
652 params: &ProfilingInfoProcessorParams,
653 ) -> Option<OrderedHashMap<String, usize>> {
654 require(params.process_by_cairo_function)?;
655
656 let mut cairo_functions = UnorderedHashMap::<_, _>::default();
657 for (statement_idx, weight) in sierra_statement_weights {
658 let function_identifier = self
660 .statements_functions
661 .get(statement_idx)
662 .unwrap_or(&"unknown".to_string())
663 .clone();
664 *(cairo_functions.entry(function_identifier).or_insert(0)) += weight;
665 }
666
667 Some(
668 cairo_functions
669 .filter(|_, weight| *weight >= params.min_weight)
670 .iter_sorted_by_key(|(function_identifier, weight)| {
671 (usize::MAX - **weight, (*function_identifier).clone())
672 })
673 .map(|(function_identifier, weight)| (function_identifier.clone(), *weight))
674 .collect(),
675 )
676 }
677
678 fn process_scoped_sierra_statement_weights(
680 &self,
681 raw_profiling_info: &ProfilingInfo,
682 params: &ProfilingInfoProcessorParams,
683 ) -> Option<OrderedHashMap<Vec<String>, usize>> {
684 if params.process_by_scoped_statement {
685 let mut scoped_sierra_statement_weights: OrderedHashMap<Vec<String>, usize> =
686 Default::default();
687 for ((idx_stack_trace, statement_idx), weight) in
688 raw_profiling_info.scoped_sierra_statement_weights.iter()
689 {
690 let statement_name = match self.statement_idx_to_gen_statement(statement_idx) {
691 GenStatement::Invocation(invocation) => invocation.libfunc_id.to_string(),
692 GenStatement::Return(_) => "return".into(),
693 };
694 let key: Vec<String> = chain!(
695 index_stack_trace_to_name_stack_trace(self.sierra_program, idx_stack_trace),
696 [statement_name]
697 )
698 .collect();
699 *scoped_sierra_statement_weights.entry(key).or_default() += *weight;
701 }
702 return Some(scoped_sierra_statement_weights);
703 }
704 None
705 }
706
707 fn statement_idx_to_gen_statement(
709 &self,
710 statement_idx: &StatementIdx,
711 ) -> GenStatement<StatementIdx> {
712 self.sierra_program
713 .statements
714 .get(statement_idx.0)
715 .unwrap_or_else(|| panic!("Failed fetching statement index {}", statement_idx.0))
716 .clone()
717 }
718}
719
720fn is_cairo_trace(db: &dyn Database, sierra_program: &Program, sierra_trace: &[usize]) -> bool {
723 sierra_trace.iter().all(|sierra_function_idx| {
724 let sierra_function = &sierra_program.funcs[*sierra_function_idx];
725 let lowering_function_id = db.lookup_sierra_function(&sierra_function.id);
726 matches!(lowering_function_id.long(db), FunctionLongId::Semantic(_))
727 })
728}
729
730pub fn user_function_idx_by_sierra_statement_idx(
737 sierra_program: &Program,
738 statement_idx: StatementIdx,
739) -> usize {
740 sierra_program.funcs.partition_point(|f| f.entry_point.0 <= statement_idx.0) - 1
744}
745
746fn index_stack_trace_to_name_stack_trace(
751 sierra_program: &Program,
752 idx_stack_trace: &[usize],
753) -> Vec<String> {
754 idx_stack_trace.iter().map(|idx| sierra_program.funcs[*idx].id.to_string()).collect()
755}
756
757fn format_scoped_sierra_statement_weights(
759 weights: &OrderedHashMap<Vec<String>, usize>,
760 f: &mut std::fmt::Formatter<'_>,
761) -> std::fmt::Result {
762 for (key, weight) in weights.iter() {
763 f.write_fmt(format_args!("{} {weight}\n", key.join(";")))?;
764 }
765 Ok(())
766}