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::unordered_hash_map::UnorderedHashMap;
11use cairo_lang_utils::{LookupIntern, require};
12use cairo_vm::vm::trace::trace_entry::RelocatedTraceEntry;
13use itertools::{Itertools, chain};
14use smol_str::SmolStr;
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 Sierra,
29}
30
31#[derive(Debug, Eq, PartialEq, Clone)]
34pub struct ProfilingInfo {
35 pub sierra_statement_weights: UnorderedHashMap<StatementIdx, usize>,
37
38 pub stack_trace_weights: OrderedHashMap<Vec<usize>, usize>,
45
46 pub scoped_sierra_statement_weights: OrderedHashMap<(Vec<usize>, StatementIdx), usize>,
52}
53
54impl ProfilingInfo {
55 pub fn from_trace(
56 builder: &RunnableBuilder,
57 load_offset: usize,
59 profiling_config: &ProfilingInfoCollectionConfig,
60 trace: &[RelocatedTraceEntry],
61 ) -> Self {
62 let sierra_statement_info = &builder.casm_program().debug_info.sierra_statement_info;
63 let sierra_len = sierra_statement_info.len();
64 let bytecode_len = sierra_statement_info.last().unwrap().end_offset;
65
66 let mut function_stack = Vec::new();
72 let mut function_stack_depth = 0;
76 let mut cur_weight = 0;
77 let mut stack_trace_weights = OrderedHashMap::default();
82 let mut end_of_program_reached = false;
83 let mut sierra_statement_weights = UnorderedHashMap::default();
88 let mut scoped_sierra_statement_weights = OrderedHashMap::default();
91 for step in trace {
92 let Some(real_pc) = step.pc.checked_sub(load_offset) else {
94 continue;
95 };
96
97 if real_pc >= bytecode_len {
102 continue;
103 }
104
105 if end_of_program_reached {
106 unreachable!("End of program reached, but trace continues.");
107 }
108
109 cur_weight += 1;
110
111 let sierra_statement_idx = builder.casm_program().sierra_statement_index_by_pc(real_pc);
114 let user_function_idx = user_function_idx_by_sierra_statement_idx(
115 builder.sierra_program(),
116 sierra_statement_idx,
117 );
118
119 *sierra_statement_weights.entry(sierra_statement_idx).or_insert(0) += 1;
120
121 if profiling_config.collect_scoped_sierra_statement_weights {
122 let cur_stack: Vec<usize> =
125 chain!(function_stack.iter().map(|&(idx, _)| idx), [user_function_idx])
126 .dedup()
127 .collect();
128
129 *scoped_sierra_statement_weights
130 .entry((cur_stack, sierra_statement_idx))
131 .or_insert(0) += 1;
132 }
133
134 let Some(gen_statement) =
135 builder.sierra_program().statements.get(sierra_statement_idx.0)
136 else {
137 panic!("Failed fetching statement index {}", sierra_statement_idx.0);
138 };
139
140 match gen_statement {
141 GenStatement::Invocation(invocation) => {
142 if matches!(
143 builder.registry().get_libfunc(&invocation.libfunc_id),
144 Ok(CoreConcreteLibfunc::FunctionCall(_))
145 ) {
146 if function_stack_depth < profiling_config.max_stack_trace_depth {
148 function_stack.push((user_function_idx, cur_weight));
149 cur_weight = 0;
150 }
151 function_stack_depth += 1;
152 }
153 }
154 GenStatement::Return(_) => {
155 if function_stack_depth <= profiling_config.max_stack_trace_depth {
157 let cur_stack: Vec<_> =
159 chain!(function_stack.iter().map(|f| f.0), [user_function_idx])
160 .collect();
161 *stack_trace_weights.entry(cur_stack).or_insert(0) += cur_weight;
162
163 let Some(popped) = function_stack.pop() else {
164 end_of_program_reached = true;
166 continue;
167 };
168 cur_weight += popped.1;
169 }
170 function_stack_depth -= 1;
171 }
172 }
173 }
174
175 sierra_statement_weights.remove(&StatementIdx(sierra_len));
177
178 ProfilingInfo {
179 sierra_statement_weights,
180 stack_trace_weights,
181 scoped_sierra_statement_weights,
182 }
183 }
184}
185
186#[derive(Default)]
188pub struct LibfuncWeights {
189 pub concrete_libfunc_weights: Option<OrderedHashMap<SmolStr, usize>>,
191 pub generic_libfunc_weights: Option<OrderedHashMap<SmolStr, usize>>,
193 pub return_weight: Option<usize>,
195}
196impl Display for LibfuncWeights {
197 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
198 if let Some(concrete_libfunc_weights) = &self.concrete_libfunc_weights {
199 writeln!(f, "Weight by concrete libfunc:")?;
200 for (concrete_name, weight) in concrete_libfunc_weights.iter() {
201 writeln!(f, " libfunc {concrete_name}: {weight}")?;
202 }
203 writeln!(
204 f,
205 " return: {}",
206 self.return_weight.expect(
207 "return_weight should have a value if concrete_libfunc_weights has a value"
208 )
209 )?;
210 }
211 if let Some(generic_libfunc_weights) = &self.generic_libfunc_weights {
212 writeln!(f, "Weight by generic libfunc:")?;
213 for (generic_name, weight) in generic_libfunc_weights.iter() {
214 writeln!(f, " libfunc {generic_name}: {weight}")?;
215 }
216 writeln!(
217 f,
218 " return: {}",
219 self.return_weight.expect(
220 "return_weight should have a value if generic_libfunc_weights has a value"
221 )
222 )?;
223 }
224 Ok(())
225 }
226}
227
228#[derive(Default)]
230pub struct UserFunctionWeights {
231 pub user_function_weights: Option<OrderedHashMap<SmolStr, usize>>,
234 pub original_user_function_weights: Option<OrderedHashMap<String, usize>>,
236}
237impl Display for UserFunctionWeights {
238 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
239 if let Some(user_function_weights) = &self.user_function_weights {
240 writeln!(f, "Weight by user function (inc. generated):")?;
241 for (name, weight) in user_function_weights.iter() {
242 writeln!(f, " function {name}: {weight}")?;
243 }
244 }
245 if let Some(original_user_function_weights) = &self.original_user_function_weights {
246 writeln!(f, "Weight by original user function (exc. generated):")?;
247 for (name, weight) in original_user_function_weights.iter() {
248 writeln!(f, " function {name}: {weight}")?;
249 }
250 }
251 Ok(())
252 }
253}
254
255#[derive(Default)]
257pub struct StackTraceWeights {
258 pub sierra_stack_trace_weights: Option<OrderedHashMap<Vec<String>, usize>>,
263 pub cairo_stack_trace_weights: Option<OrderedHashMap<Vec<String>, usize>>,
267}
268impl Display for StackTraceWeights {
269 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
270 if let Some(sierra_stack_trace_weights) = &self.sierra_stack_trace_weights {
271 writeln!(f, "Weight by Sierra stack trace:")?;
272 for (stack_trace, weight) in sierra_stack_trace_weights.iter() {
273 let stack_trace_str = stack_trace.join(" -> ");
274 writeln!(f, " {stack_trace_str}: {weight}")?;
275 }
276 }
277
278 if let Some(cairo_stack_trace_weights) = &self.cairo_stack_trace_weights {
279 writeln!(f, "Weight by Cairo stack trace:")?;
280 for (stack_trace, weight) in cairo_stack_trace_weights.iter() {
281 let stack_trace_str = stack_trace.join(" -> ");
282 writeln!(f, " {stack_trace_str}: {weight}")?;
283 }
284 }
285 Ok(())
286 }
287}
288
289pub struct ProcessedProfilingInfo {
292 pub sierra_statement_weights:
295 Option<OrderedHashMap<StatementIdx, (usize, GenStatement<StatementIdx>)>>,
296
297 pub stack_trace_weights: StackTraceWeights,
299
300 pub libfunc_weights: LibfuncWeights,
302
303 pub user_function_weights: UserFunctionWeights,
305
306 pub cairo_function_weights: Option<OrderedHashMap<String, usize>>,
308
309 pub scoped_sierra_statement_weights: Option<OrderedHashMap<Vec<String>, usize>>,
312}
313impl Display for ProcessedProfilingInfo {
314 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
315 if let Some(sierra_statement_weights) = &self.sierra_statement_weights {
316 writeln!(f, "Weight by sierra statement:")?;
317 for (statement_idx, (weight, gen_statement)) in sierra_statement_weights.iter() {
318 writeln!(f, " statement {statement_idx}: {weight} ({gen_statement})")?;
319 }
320 }
321
322 self.libfunc_weights.fmt(f)?;
324
325 self.user_function_weights.fmt(f)?;
327
328 if let Some(cairo_function_weights) = &self.cairo_function_weights {
329 writeln!(f, "Weight by Cairo function:")?;
330 for (function_identifier, weight) in cairo_function_weights.iter() {
331 writeln!(f, " function {function_identifier}: {weight}")?;
332 }
333 }
334
335 self.stack_trace_weights.fmt(f)?;
336
337 if let Some(weights) = &self.scoped_sierra_statement_weights {
338 format_scoped_sierra_statement_weights(weights, f)?;
339 }
340
341 Ok(())
342 }
343}
344
345pub struct ProfilingInfoProcessorParams {
348 pub min_weight: usize,
352 pub process_by_statement: bool,
354 pub process_by_concrete_libfunc: bool,
356 pub process_by_generic_libfunc: bool,
358 pub process_by_user_function: bool,
360 pub process_by_original_user_function: bool,
362 pub process_by_cairo_function: bool,
365 pub process_by_stack_trace: bool,
368 pub process_by_cairo_stack_trace: bool,
371 pub process_by_scoped_statement: bool,
374}
375impl Default for ProfilingInfoProcessorParams {
376 fn default() -> Self {
377 Self {
378 min_weight: 1,
379 process_by_statement: true,
380 process_by_concrete_libfunc: true,
381 process_by_generic_libfunc: true,
382 process_by_user_function: true,
383 process_by_original_user_function: true,
384 process_by_cairo_function: true,
385 process_by_stack_trace: true,
386 process_by_cairo_stack_trace: true,
387 process_by_scoped_statement: false,
388 }
389 }
390}
391
392impl ProfilingInfoProcessorParams {
393 pub fn from_profiler_config(config: &ProfilerConfig) -> Self {
394 match config {
395 ProfilerConfig::Cairo => Default::default(),
396 ProfilerConfig::Sierra => Self {
397 process_by_generic_libfunc: false,
398 process_by_cairo_stack_trace: false,
399 process_by_original_user_function: false,
400 process_by_cairo_function: false,
401 ..ProfilingInfoProcessorParams::default()
402 },
403 }
404 }
405}
406
407pub struct ProfilingInfoProcessor<'a> {
410 db: Option<&'a dyn SierraGenGroup>,
411 sierra_program: &'a Program,
412 statements_functions: &'a UnorderedHashMap<StatementIdx, String>,
416}
417impl<'a> ProfilingInfoProcessor<'a> {
418 pub fn new(
419 db: Option<&'a dyn SierraGenGroup>,
420 sierra_program: &'a Program,
421 statements_functions: &'a UnorderedHashMap<StatementIdx, String>,
422 ) -> Self {
423 Self { db, sierra_program, statements_functions }
424 }
425
426 pub fn process(
428 &self,
429 raw_profiling_info: &ProfilingInfo,
430 params: &ProfilingInfoProcessorParams,
431 ) -> ProcessedProfilingInfo {
432 let sierra_statement_weights_iter = raw_profiling_info
433 .sierra_statement_weights
434 .iter_sorted_by_key(|(pc, count)| (usize::MAX - **count, **pc));
435
436 let sierra_statement_weights =
437 self.process_sierra_statement_weights(sierra_statement_weights_iter.clone(), params);
438
439 let stack_trace_weights = self.process_stack_trace_weights(raw_profiling_info, params);
440
441 let libfunc_weights =
442 self.process_libfunc_weights(sierra_statement_weights_iter.clone(), params);
443
444 let user_function_weights =
445 self.process_user_function_weights(sierra_statement_weights_iter.clone(), params);
446
447 let cairo_function_weights =
448 self.process_cairo_function_weights(sierra_statement_weights_iter, params);
449
450 let scoped_sierra_statement_weights =
451 self.process_scoped_sierra_statement_weights(raw_profiling_info, params);
452
453 ProcessedProfilingInfo {
454 sierra_statement_weights,
455 stack_trace_weights,
456 libfunc_weights,
457 user_function_weights,
458 cairo_function_weights,
459 scoped_sierra_statement_weights,
460 }
461 }
462
463 fn process_sierra_statement_weights(
465 &self,
466 sierra_statement_weights_iter: std::vec::IntoIter<(&StatementIdx, &usize)>,
467 params: &ProfilingInfoProcessorParams,
468 ) -> Option<OrderedHashMap<StatementIdx, (usize, GenStatement<StatementIdx>)>> {
469 require(params.process_by_statement)?;
470
471 Some(
472 sierra_statement_weights_iter
473 .filter(|&(_, weight)| *weight >= params.min_weight)
474 .map(|(statement_idx, weight)| {
475 (*statement_idx, (*weight, self.statement_idx_to_gen_statement(statement_idx)))
476 })
477 .collect(),
478 )
479 }
480
481 fn process_stack_trace_weights(
483 &self,
484 raw_profiling_info: &ProfilingInfo,
485 params: &ProfilingInfoProcessorParams,
486 ) -> StackTraceWeights {
487 let resolve_names = |(idx_stack_trace, weight): (&Vec<usize>, &usize)| {
488 (index_stack_trace_to_name_stack_trace(self.sierra_program, idx_stack_trace), *weight)
489 };
490
491 let sierra_stack_trace_weights = params.process_by_stack_trace.then(|| {
492 raw_profiling_info
493 .stack_trace_weights
494 .iter()
495 .sorted_by_key(|&(trace, weight)| (usize::MAX - *weight, trace.clone()))
496 .map(resolve_names)
497 .collect()
498 });
499
500 let cairo_stack_trace_weights = params.process_by_cairo_stack_trace.then(|| {
501 let db = self.db.expect("DB must be set with `process_by_cairo_stack_trace=true`.");
502 raw_profiling_info
503 .stack_trace_weights
504 .iter()
505 .filter(|(trace, _)| is_cairo_trace(db, self.sierra_program, trace))
506 .sorted_by_key(|&(trace, weight)| (usize::MAX - *weight, trace.clone()))
507 .map(resolve_names)
508 .collect()
509 });
510
511 StackTraceWeights { sierra_stack_trace_weights, cairo_stack_trace_weights }
512 }
513
514 fn process_libfunc_weights(
516 &self,
517 sierra_statement_weights: std::vec::IntoIter<(&StatementIdx, &usize)>,
518 params: &ProfilingInfoProcessorParams,
519 ) -> LibfuncWeights {
520 if !params.process_by_concrete_libfunc && !params.process_by_generic_libfunc {
521 return LibfuncWeights::default();
522 }
523
524 let mut return_weight = 0;
525 let mut libfunc_weights = UnorderedHashMap::<ConcreteLibfuncId, usize>::default();
526 for (statement_idx, weight) in sierra_statement_weights {
527 match self.statement_idx_to_gen_statement(statement_idx) {
528 GenStatement::Invocation(invocation) => {
529 *(libfunc_weights.entry(invocation.libfunc_id.clone()).or_insert(0)) += weight;
530 }
531 GenStatement::Return(_) => {
532 return_weight += weight;
533 }
534 }
535 }
536
537 let generic_libfunc_weights = params.process_by_generic_libfunc.then(|| {
538 let db: &dyn SierraGenGroup =
539 self.db.expect("DB must be set with `process_by_generic_libfunc=true`.");
540 libfunc_weights
541 .aggregate_by(
542 |k| -> SmolStr {
543 db.lookup_intern_concrete_lib_func(k.clone()).generic_id.to_string().into()
544 },
545 |v1: &usize, v2| v1 + v2,
546 &0,
547 )
548 .filter(|_, weight| *weight >= params.min_weight)
549 .iter_sorted_by_key(|(generic_name, weight)| {
550 (usize::MAX - **weight, (*generic_name).clone())
551 })
552 .map(|(generic_name, weight)| (generic_name.clone(), *weight))
553 .collect()
554 });
555
556 let concrete_libfunc_weights = params.process_by_concrete_libfunc.then(|| {
558 libfunc_weights
559 .filter(|_, weight| *weight >= params.min_weight)
560 .iter_sorted_by_key(|(libfunc_id, weight)| {
561 (usize::MAX - **weight, (*libfunc_id).to_string())
562 })
563 .map(|(libfunc_id, weight)| (SmolStr::from(libfunc_id.to_string()), *weight))
564 .collect()
565 });
566
567 LibfuncWeights {
568 concrete_libfunc_weights,
569 generic_libfunc_weights,
570 return_weight: Some(return_weight),
571 }
572 }
573
574 fn process_user_function_weights(
576 &self,
577 sierra_statement_weights: std::vec::IntoIter<(&StatementIdx, &usize)>,
578 params: &ProfilingInfoProcessorParams,
579 ) -> UserFunctionWeights {
580 if !params.process_by_user_function && !params.process_by_original_user_function {
581 return UserFunctionWeights::default();
582 }
583
584 let mut user_functions = UnorderedHashMap::<usize, usize>::default();
585 for (statement_idx, weight) in sierra_statement_weights {
586 let function_idx: usize =
587 user_function_idx_by_sierra_statement_idx(self.sierra_program, *statement_idx);
588 *(user_functions.entry(function_idx).or_insert(0)) += weight;
589 }
590
591 let original_user_function_weights = params.process_by_original_user_function.then(|| {
592 let db: &dyn SierraGenGroup =
593 self.db.expect("DB must be set with `process_by_original_user_function=true`.");
594 user_functions
595 .aggregate_by(
596 |idx| {
597 let lowering_function_id =
598 self.sierra_program.funcs[*idx].id.clone().lookup_intern(db);
599 lowering_function_id.semantic_full_path(db.upcast())
600 },
601 |x, y| x + y,
602 &0,
603 )
604 .filter(|_, weight| *weight >= params.min_weight)
605 .iter_sorted_by_key(|(orig_name, weight)| {
606 (usize::MAX - **weight, (*orig_name).clone())
607 })
608 .map(|(orig_name, weight)| (orig_name.clone(), *weight))
609 .collect()
610 });
611
612 let user_function_weights = params.process_by_user_function.then(|| {
614 user_functions
615 .filter(|_, weight| *weight >= params.min_weight)
616 .iter_sorted_by_key(|(idx, weight)| {
617 (usize::MAX - **weight, self.sierra_program.funcs[**idx].id.to_string())
618 })
619 .map(|(idx, weight)| {
620 let func: &cairo_lang_sierra::program::GenFunction<StatementIdx> =
621 &self.sierra_program.funcs[*idx];
622 (func.id.to_string().into(), *weight)
623 })
624 .collect()
625 });
626
627 UserFunctionWeights { user_function_weights, original_user_function_weights }
628 }
629
630 fn process_cairo_function_weights(
632 &self,
633 sierra_statement_weights: std::vec::IntoIter<(&StatementIdx, &usize)>,
634 params: &ProfilingInfoProcessorParams,
635 ) -> Option<OrderedHashMap<String, usize>> {
636 require(params.process_by_cairo_function)?;
637
638 let mut cairo_functions = UnorderedHashMap::<_, _>::default();
639 for (statement_idx, weight) in sierra_statement_weights {
640 let function_identifier = self
642 .statements_functions
643 .get(statement_idx)
644 .unwrap_or(&"unknown".to_string())
645 .clone();
646 *(cairo_functions.entry(function_identifier).or_insert(0)) += weight;
647 }
648
649 Some(
650 cairo_functions
651 .filter(|_, weight| *weight >= params.min_weight)
652 .iter_sorted_by_key(|(function_identifier, weight)| {
653 (usize::MAX - **weight, (*function_identifier).clone())
654 })
655 .map(|(function_identifier, weight)| (function_identifier.clone(), *weight))
656 .collect(),
657 )
658 }
659
660 fn process_scoped_sierra_statement_weights(
662 &self,
663 raw_profiling_info: &ProfilingInfo,
664 params: &ProfilingInfoProcessorParams,
665 ) -> Option<OrderedHashMap<Vec<String>, usize>> {
666 if params.process_by_scoped_statement {
667 let mut scoped_sierra_statement_weights: OrderedHashMap<Vec<String>, usize> =
668 Default::default();
669 for ((idx_stack_trace, statement_idx), weight) in
670 raw_profiling_info.scoped_sierra_statement_weights.iter()
671 {
672 let statement_name = match self.statement_idx_to_gen_statement(statement_idx) {
673 GenStatement::Invocation(invocation) => invocation.libfunc_id.to_string(),
674 GenStatement::Return(_) => "return".into(),
675 };
676 let key: Vec<String> = chain!(
677 index_stack_trace_to_name_stack_trace(self.sierra_program, idx_stack_trace),
678 [statement_name]
679 )
680 .collect();
681 *scoped_sierra_statement_weights.entry(key).or_default() += *weight;
683 }
684 return Some(scoped_sierra_statement_weights);
685 }
686 None
687 }
688
689 fn statement_idx_to_gen_statement(
691 &self,
692 statement_idx: &StatementIdx,
693 ) -> GenStatement<StatementIdx> {
694 self.sierra_program
695 .statements
696 .get(statement_idx.0)
697 .unwrap_or_else(|| panic!("Failed fetching statement index {}", statement_idx.0))
698 .clone()
699 }
700}
701
702fn is_cairo_trace(
705 db: &dyn SierraGenGroup,
706 sierra_program: &Program,
707 sierra_trace: &[usize],
708) -> bool {
709 sierra_trace.iter().all(|sierra_function_idx| {
710 let sierra_function = &sierra_program.funcs[*sierra_function_idx];
711 let lowering_function_id = sierra_function.id.lookup_intern(db);
712 matches!(lowering_function_id.lookup_intern(db), FunctionLongId::Semantic(_))
713 })
714}
715
716pub fn user_function_idx_by_sierra_statement_idx(
723 sierra_program: &Program,
724 statement_idx: StatementIdx,
725) -> usize {
726 sierra_program.funcs.partition_point(|f| f.entry_point.0 <= statement_idx.0) - 1
730}
731
732fn index_stack_trace_to_name_stack_trace(
737 sierra_program: &Program,
738 idx_stack_trace: &[usize],
739) -> Vec<String> {
740 idx_stack_trace.iter().map(|idx| sierra_program.funcs[*idx].id.to_string()).collect()
741}
742
743fn format_scoped_sierra_statement_weights(
745 weights: &OrderedHashMap<Vec<String>, usize>,
746 f: &mut std::fmt::Formatter<'_>,
747) -> std::fmt::Result {
748 for (key, weight) in weights.iter() {
749 f.write_fmt(format_args!("{} {weight}\n", key.join(";")))?;
750 }
751 Ok(())
752}