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 ) {
154 if function_stack_depth < profiling_config.max_stack_trace_depth {
156 function_stack.push((user_function_idx, cur_weight));
157 cur_weight = 0;
158 }
159 function_stack_depth += 1;
160 }
161 }
162 GenStatement::Return(_) => {
163 if function_stack_depth <= profiling_config.max_stack_trace_depth {
165 let cur_stack: Vec<_> =
167 chain!(function_stack.iter().map(|f| f.0), [user_function_idx])
168 .collect();
169 *stack_trace_weights.entry(cur_stack).or_insert(0) += cur_weight;
170
171 let Some(popped) = function_stack.pop() else {
172 end_of_program_reached = true;
174 continue;
175 };
176 cur_weight += popped.1;
177 }
178 function_stack_depth -= 1;
179 }
180 }
181 }
182
183 ProfilingInfo {
184 sierra_statement_weights,
185 stack_trace_weights,
186 scoped_sierra_statement_weights,
187 }
188 }
189}
190
191#[derive(Default)]
193pub struct LibfuncWeights {
194 pub concrete_libfunc_weights: Option<OrderedHashMap<String, usize>>,
196 pub generic_libfunc_weights: Option<OrderedHashMap<String, usize>>,
198 pub return_weight: Option<usize>,
200}
201impl Display for LibfuncWeights {
202 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
203 if let Some(concrete_libfunc_weights) = &self.concrete_libfunc_weights {
204 writeln!(f, "Weight by concrete libfunc:")?;
205 for (concrete_name, weight) in concrete_libfunc_weights.iter() {
206 writeln!(f, " libfunc {concrete_name}: {weight}")?;
207 }
208 writeln!(
209 f,
210 " return: {}",
211 self.return_weight.expect(
212 "return_weight should have a value if concrete_libfunc_weights has a value"
213 )
214 )?;
215 }
216 if let Some(generic_libfunc_weights) = &self.generic_libfunc_weights {
217 writeln!(f, "Weight by generic libfunc:")?;
218 for (generic_name, weight) in generic_libfunc_weights.iter() {
219 writeln!(f, " libfunc {generic_name}: {weight}")?;
220 }
221 writeln!(
222 f,
223 " return: {}",
224 self.return_weight.expect(
225 "return_weight should have a value if generic_libfunc_weights has a value"
226 )
227 )?;
228 }
229 Ok(())
230 }
231}
232
233#[derive(Default)]
235pub struct UserFunctionWeights {
236 pub user_function_weights: Option<OrderedHashMap<String, usize>>,
239 pub original_user_function_weights: Option<OrderedHashMap<String, usize>>,
241}
242impl Display for UserFunctionWeights {
243 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
244 if let Some(user_function_weights) = &self.user_function_weights {
245 writeln!(f, "Weight by user function (inc. generated):")?;
246 for (name, weight) in user_function_weights.iter() {
247 writeln!(f, " function {name}: {weight}")?;
248 }
249 }
250 if let Some(original_user_function_weights) = &self.original_user_function_weights {
251 writeln!(f, "Weight by original user function (exc. generated):")?;
252 for (name, weight) in original_user_function_weights.iter() {
253 writeln!(f, " function {name}: {weight}")?;
254 }
255 }
256 Ok(())
257 }
258}
259
260#[derive(Default)]
262pub struct StackTraceWeights {
263 pub sierra_stack_trace_weights: Option<OrderedHashMap<Vec<String>, usize>>,
268 pub cairo_stack_trace_weights: Option<OrderedHashMap<Vec<String>, usize>>,
272}
273impl Display for StackTraceWeights {
274 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
275 if let Some(sierra_stack_trace_weights) = &self.sierra_stack_trace_weights {
276 writeln!(f, "Weight by Sierra stack trace:")?;
277 for (stack_trace, weight) in sierra_stack_trace_weights.iter() {
278 let stack_trace_str = stack_trace.join(" -> ");
279 writeln!(f, " {stack_trace_str}: {weight}")?;
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 let stack_trace_str = stack_trace.join(" -> ");
287 writeln!(f, " {stack_trace_str}: {weight}")?;
288 }
289 }
290 Ok(())
291 }
292}
293
294pub struct ProcessedProfilingInfo {
297 pub sierra_statement_weights:
300 Option<OrderedHashMap<StatementIdx, (usize, GenStatement<StatementIdx>)>>,
301
302 pub stack_trace_weights: StackTraceWeights,
304
305 pub libfunc_weights: LibfuncWeights,
307
308 pub user_function_weights: UserFunctionWeights,
310
311 pub cairo_function_weights: Option<OrderedHashMap<String, usize>>,
313
314 pub scoped_sierra_statement_weights: Option<OrderedHashMap<Vec<String>, usize>>,
317}
318impl Display for ProcessedProfilingInfo {
319 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
320 if let Some(sierra_statement_weights) = &self.sierra_statement_weights {
321 writeln!(f, "Weight by sierra statement:")?;
322 for (statement_idx, (weight, gen_statement)) in sierra_statement_weights.iter() {
323 writeln!(f, " statement {statement_idx}: {weight} ({gen_statement})")?;
324 }
325 }
326
327 self.libfunc_weights.fmt(f)?;
329
330 self.user_function_weights.fmt(f)?;
332
333 if let Some(cairo_function_weights) = &self.cairo_function_weights {
334 writeln!(f, "Weight by Cairo function:")?;
335 for (function_identifier, weight) in cairo_function_weights.iter() {
336 writeln!(f, " function {function_identifier}: {weight}")?;
337 }
338 }
339
340 self.stack_trace_weights.fmt(f)?;
341
342 if let Some(weights) = &self.scoped_sierra_statement_weights {
343 format_scoped_sierra_statement_weights(weights, f)?;
344 }
345
346 Ok(())
347 }
348}
349
350pub struct ProfilingInfoProcessorParams {
353 pub min_weight: usize,
357 pub process_by_statement: bool,
359 pub process_by_concrete_libfunc: bool,
361 pub process_by_generic_libfunc: bool,
363 pub process_by_user_function: bool,
365 pub process_by_original_user_function: bool,
367 pub process_by_cairo_function: bool,
370 pub process_by_stack_trace: bool,
373 pub process_by_cairo_stack_trace: bool,
376 pub process_by_scoped_statement: bool,
379}
380impl Default for ProfilingInfoProcessorParams {
381 fn default() -> Self {
382 Self {
383 min_weight: 1,
384 process_by_statement: true,
385 process_by_concrete_libfunc: true,
386 process_by_generic_libfunc: true,
387 process_by_user_function: true,
388 process_by_original_user_function: true,
389 process_by_cairo_function: true,
390 process_by_stack_trace: true,
391 process_by_cairo_stack_trace: true,
392 process_by_scoped_statement: false,
393 }
394 }
395}
396
397impl ProfilingInfoProcessorParams {
398 pub fn from_profiler_config(config: &ProfilerConfig) -> Self {
399 match config {
400 ProfilerConfig::Cairo => Default::default(),
401 ProfilerConfig::Scoped => Self {
402 min_weight: 1,
403 process_by_statement: false,
404 process_by_concrete_libfunc: false,
405 process_by_generic_libfunc: false,
406 process_by_user_function: false,
407 process_by_original_user_function: false,
408 process_by_cairo_function: false,
409 process_by_stack_trace: false,
410 process_by_cairo_stack_trace: false,
411 process_by_scoped_statement: true,
412 },
413 ProfilerConfig::Sierra => Self {
414 process_by_generic_libfunc: false,
415 process_by_cairo_stack_trace: false,
416 process_by_original_user_function: false,
417 process_by_cairo_function: false,
418 ..ProfilingInfoProcessorParams::default()
419 },
420 }
421 }
422}
423
424pub struct ProfilingInfoProcessor<'a> {
427 db: Option<&'a dyn Database>,
428 sierra_program: &'a Program,
429 statements_functions: UnorderedHashMap<StatementIdx, String>,
433}
434impl<'a> ProfilingInfoProcessor<'a> {
435 pub fn new(
436 db: Option<&'a dyn Database>,
437 sierra_program: &'a Program,
438 statements_functions: UnorderedHashMap<StatementIdx, String>,
439 ) -> Self {
440 Self { db, sierra_program, statements_functions }
441 }
442
443 pub fn process(
445 &self,
446 raw_profiling_info: &ProfilingInfo,
447 params: &ProfilingInfoProcessorParams,
448 ) -> ProcessedProfilingInfo {
449 let sierra_statement_weights_iter = raw_profiling_info
450 .sierra_statement_weights
451 .iter_sorted_by_key(|(pc, count)| (usize::MAX - **count, **pc));
452
453 let sierra_statement_weights =
454 self.process_sierra_statement_weights(sierra_statement_weights_iter.clone(), params);
455
456 let stack_trace_weights = self.process_stack_trace_weights(raw_profiling_info, params);
457
458 let libfunc_weights =
459 self.process_libfunc_weights(sierra_statement_weights_iter.clone(), params);
460
461 let user_function_weights =
462 self.process_user_function_weights(sierra_statement_weights_iter.clone(), params);
463
464 let cairo_function_weights =
465 self.process_cairo_function_weights(sierra_statement_weights_iter, params);
466
467 let scoped_sierra_statement_weights =
468 self.process_scoped_sierra_statement_weights(raw_profiling_info, params);
469
470 ProcessedProfilingInfo {
471 sierra_statement_weights,
472 stack_trace_weights,
473 libfunc_weights,
474 user_function_weights,
475 cairo_function_weights,
476 scoped_sierra_statement_weights,
477 }
478 }
479
480 fn process_sierra_statement_weights(
482 &self,
483 sierra_statement_weights_iter: std::vec::IntoIter<(&StatementIdx, &usize)>,
484 params: &ProfilingInfoProcessorParams,
485 ) -> Option<OrderedHashMap<StatementIdx, (usize, GenStatement<StatementIdx>)>> {
486 require(params.process_by_statement)?;
487
488 Some(
489 sierra_statement_weights_iter
490 .filter(|&(_, weight)| *weight >= params.min_weight)
491 .map(|(statement_idx, weight)| {
492 (*statement_idx, (*weight, self.statement_idx_to_gen_statement(statement_idx)))
493 })
494 .collect(),
495 )
496 }
497
498 fn process_stack_trace_weights(
500 &self,
501 raw_profiling_info: &ProfilingInfo,
502 params: &ProfilingInfoProcessorParams,
503 ) -> StackTraceWeights {
504 let resolve_names = |(idx_stack_trace, weight): (&Vec<usize>, &usize)| {
505 (index_stack_trace_to_name_stack_trace(self.sierra_program, idx_stack_trace), *weight)
506 };
507
508 let sierra_stack_trace_weights = params.process_by_stack_trace.then(|| {
509 raw_profiling_info
510 .stack_trace_weights
511 .iter()
512 .sorted_by_key(|&(trace, weight)| (usize::MAX - *weight, trace))
513 .map(resolve_names)
514 .collect()
515 });
516
517 let cairo_stack_trace_weights = params.process_by_cairo_stack_trace.then(|| {
518 let db = self.db.expect("DB must be set with `process_by_cairo_stack_trace=true`.");
519 raw_profiling_info
520 .stack_trace_weights
521 .iter()
522 .filter(|(trace, _)| is_cairo_trace(db, self.sierra_program, trace))
523 .sorted_by_key(|&(trace, weight)| (usize::MAX - *weight, trace))
524 .map(resolve_names)
525 .collect()
526 });
527
528 StackTraceWeights { sierra_stack_trace_weights, cairo_stack_trace_weights }
529 }
530
531 fn process_libfunc_weights(
533 &self,
534 sierra_statement_weights: std::vec::IntoIter<(&StatementIdx, &usize)>,
535 params: &ProfilingInfoProcessorParams,
536 ) -> LibfuncWeights {
537 if !params.process_by_concrete_libfunc && !params.process_by_generic_libfunc {
538 return LibfuncWeights::default();
539 }
540
541 let mut return_weight = 0;
542 let mut libfunc_weights = UnorderedHashMap::<ConcreteLibfuncId, usize>::default();
543 for (statement_idx, weight) in sierra_statement_weights {
544 match self.statement_idx_to_gen_statement(statement_idx) {
545 GenStatement::Invocation(invocation) => {
546 *(libfunc_weights.entry(invocation.libfunc_id.clone()).or_insert(0)) += weight;
547 }
548 GenStatement::Return(_) => {
549 return_weight += weight;
550 }
551 }
552 }
553
554 let generic_libfunc_weights = params.process_by_generic_libfunc.then(|| {
555 let db: &dyn Database =
556 self.db.expect("DB must be set with `process_by_generic_libfunc=true`.");
557 libfunc_weights
558 .aggregate_by(
559 |k| db.lookup_concrete_lib_func(k).generic_id.to_string(),
560 |v1: &usize, v2| v1 + v2,
561 &0,
562 )
563 .filter(|_, weight| *weight >= params.min_weight)
564 .into_iter_sorted_by_key(|(generic_name, weight)| {
565 (usize::MAX - *weight, (*generic_name).clone())
566 })
567 .collect()
568 });
569
570 let concrete_libfunc_weights = params.process_by_concrete_libfunc.then(|| {
572 libfunc_weights
573 .filter(|_, weight| *weight >= params.min_weight)
574 .into_iter_sorted_by_key(|(libfunc_id, weight)| {
575 (usize::MAX - *weight, libfunc_id.to_string())
576 })
577 .map(|(libfunc_id, weight)| (libfunc_id.to_string(), weight))
578 .collect()
579 });
580
581 LibfuncWeights {
582 concrete_libfunc_weights,
583 generic_libfunc_weights,
584 return_weight: Some(return_weight),
585 }
586 }
587
588 fn process_user_function_weights(
590 &self,
591 sierra_statement_weights: std::vec::IntoIter<(&StatementIdx, &usize)>,
592 params: &ProfilingInfoProcessorParams,
593 ) -> UserFunctionWeights {
594 if !params.process_by_user_function && !params.process_by_original_user_function {
595 return UserFunctionWeights::default();
596 }
597
598 let mut user_functions = UnorderedHashMap::<usize, usize>::default();
599 for (statement_idx, weight) in sierra_statement_weights {
600 let function_idx: usize =
601 user_function_idx_by_sierra_statement_idx(self.sierra_program, *statement_idx);
602 *(user_functions.entry(function_idx).or_insert(0)) += weight;
603 }
604
605 let original_user_function_weights = params.process_by_original_user_function.then(|| {
606 let db: &dyn Database =
607 self.db.expect("DB must be set with `process_by_original_user_function=true`.");
608 user_functions
609 .aggregate_by(
610 |idx| {
611 let lowering_function_id =
612 db.lookup_sierra_function(&self.sierra_program.funcs[*idx].id);
613 lowering_function_id.semantic_full_path(db)
614 },
615 |x, y| x + y,
616 &0,
617 )
618 .filter(|_, weight| *weight >= params.min_weight)
619 .iter_sorted_by_key(|(orig_name, weight)| {
620 (usize::MAX - **weight, (*orig_name).clone())
621 })
622 .map(|(orig_name, weight)| (orig_name.clone(), *weight))
623 .collect()
624 });
625
626 let user_function_weights = params.process_by_user_function.then(|| {
628 user_functions
629 .filter(|_, weight| *weight >= params.min_weight)
630 .iter_sorted_by_key(|(idx, weight)| {
631 (usize::MAX - **weight, self.sierra_program.funcs[**idx].id.to_string())
632 })
633 .map(|(idx, weight)| {
634 let func: &cairo_lang_sierra::program::GenFunction<StatementIdx> =
635 &self.sierra_program.funcs[*idx];
636 (func.id.to_string(), *weight)
637 })
638 .collect()
639 });
640
641 UserFunctionWeights { user_function_weights, original_user_function_weights }
642 }
643
644 fn process_cairo_function_weights(
646 &self,
647 sierra_statement_weights: std::vec::IntoIter<(&StatementIdx, &usize)>,
648 params: &ProfilingInfoProcessorParams,
649 ) -> Option<OrderedHashMap<String, usize>> {
650 require(params.process_by_cairo_function)?;
651
652 let mut cairo_functions = UnorderedHashMap::<_, _>::default();
653 for (statement_idx, weight) in sierra_statement_weights {
654 let function_identifier = self
656 .statements_functions
657 .get(statement_idx)
658 .cloned()
659 .unwrap_or_else(|| "unknown".to_string());
660
661 *(cairo_functions.entry(function_identifier).or_insert(0)) += weight;
662 }
663
664 Some(
665 cairo_functions
666 .filter(|_, weight| *weight >= params.min_weight)
667 .iter_sorted_by_key(|(function_identifier, weight)| {
668 (usize::MAX - **weight, (*function_identifier).clone())
669 })
670 .map(|(function_identifier, weight)| (function_identifier.clone(), *weight))
671 .collect(),
672 )
673 }
674
675 fn process_scoped_sierra_statement_weights(
677 &self,
678 raw_profiling_info: &ProfilingInfo,
679 params: &ProfilingInfoProcessorParams,
680 ) -> Option<OrderedHashMap<Vec<String>, usize>> {
681 if params.process_by_scoped_statement {
682 let mut scoped_sierra_statement_weights: OrderedHashMap<Vec<String>, usize> =
683 Default::default();
684 for ((idx_stack_trace, statement_idx), weight) in
685 raw_profiling_info.scoped_sierra_statement_weights.iter()
686 {
687 let statement_name = match self.statement_idx_to_gen_statement(statement_idx) {
688 GenStatement::Invocation(invocation) => invocation.libfunc_id.to_string(),
689 GenStatement::Return(_) => "return".into(),
690 };
691 let key: Vec<String> = chain!(
692 index_stack_trace_to_name_stack_trace(self.sierra_program, idx_stack_trace),
693 [statement_name]
694 )
695 .collect();
696 *scoped_sierra_statement_weights.entry(key).or_default() += *weight;
698 }
699 return Some(scoped_sierra_statement_weights);
700 }
701 None
702 }
703
704 fn statement_idx_to_gen_statement(
706 &self,
707 statement_idx: &StatementIdx,
708 ) -> GenStatement<StatementIdx> {
709 self.sierra_program
710 .statements
711 .get(statement_idx.0)
712 .unwrap_or_else(|| panic!("Failed fetching statement index {}", statement_idx.0))
713 .clone()
714 }
715}
716
717fn is_cairo_trace(db: &dyn Database, sierra_program: &Program, sierra_trace: &[usize]) -> bool {
720 sierra_trace.iter().all(|sierra_function_idx| {
721 let sierra_function = &sierra_program.funcs[*sierra_function_idx];
722 let lowering_function_id = db.lookup_sierra_function(&sierra_function.id);
723 matches!(lowering_function_id.long(db), FunctionLongId::Semantic(_))
724 })
725}
726
727pub fn user_function_idx_by_sierra_statement_idx(
734 sierra_program: &Program,
735 statement_idx: StatementIdx,
736) -> usize {
737 sierra_program.funcs.partition_point(|f| f.entry_point.0 <= statement_idx.0) - 1
741}
742
743fn index_stack_trace_to_name_stack_trace(
748 sierra_program: &Program,
749 idx_stack_trace: &[usize],
750) -> Vec<String> {
751 idx_stack_trace.iter().map(|idx| sierra_program.funcs[*idx].id.to_string()).collect()
752}
753
754fn format_scoped_sierra_statement_weights(
756 weights: &OrderedHashMap<Vec<String>, usize>,
757 f: &mut std::fmt::Formatter<'_>,
758) -> std::fmt::Result {
759 for (key, weight) in weights.iter() {
760 f.write_fmt(format_args!("{} {weight}\n", key.join(";")))?;
761 }
762 Ok(())
763}