cairo_lang_runner/
profiling.rs

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/// Profiler configuration.
23#[derive(Clone, Debug, PartialEq, Eq)]
24pub enum ProfilerConfig {
25    /// Profiler with Cairo level debug information.
26    Cairo,
27    /// Similar to Cairo, but stack frames are deduplicated, and the output format is more compact.
28    Scoped,
29    /// Sierra level profiling, no cairo level debug information.
30    Sierra,
31}
32
33impl ProfilerConfig {
34    /// Returns true if the profiling config requires cairo level debug information.
35    pub fn requires_cairo_debug_info(&self) -> bool {
36        matches!(self, ProfilerConfig::Cairo | ProfilerConfig::Scoped)
37    }
38}
39
40/// Profiling into of a single run. This is the raw info - went through minimum processing, as this
41/// is done during the run. To enrich it before viewing/printing, use the `ProfilingInfoProcessor`.
42#[derive(Debug, Eq, PartialEq, Clone)]
43pub struct ProfilingInfo {
44    /// The number of steps in the trace that originated from each sierra statement.
45    pub sierra_statement_weights: UnorderedHashMap<StatementIdx, usize>,
46
47    /// A map of weights of each stack trace.
48    /// The key is a function stack trace of an executed function. The stack trace is represented
49    /// as a vector of indices of the functions in the stack (indices of the functions according to
50    /// the list in the sierra program).
51    /// The value is the weight of the stack trace.
52    /// The stack trace entries are sorted in the order they occur.
53    pub stack_trace_weights: OrderedHashMap<Vec<usize>, usize>,
54
55    /// The number of steps in the trace that originated from each sierra statement
56    /// combined with information about the user function call stack.
57    /// The call stack items are deduplicated to flatten and aggregate recursive calls
58    /// and loops (which are tail recursion).
59    /// The entries are sorted in the order they occur.
60    pub scoped_sierra_statement_weights: OrderedHashMap<(Vec<usize>, StatementIdx), usize>,
61}
62
63impl ProfilingInfo {
64    pub fn from_trace(
65        builder: &RunnableBuilder,
66        // The offset in memory where builder.casm_program() was loaded.
67        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        // The function stack trace of the current function, excluding the current function (that
76        // is, the stack of the caller). Represented as a vector of indices of the functions
77        // in the stack (indices of the functions according to the list in the sierra program).
78        // Limited to depth `max_stack_trace_depth`. Note `function_stack_depth` tracks the real
79        // depth, even if >= `max_stack_trace_depth`.
80        let mut function_stack = Vec::new();
81        // Tracks the depth of the function stack, without limit. This is usually equal to
82        // `function_stack.len()`, but if the actual stack is deeper than `max_stack_trace_depth`,
83        // this remains reliable while `function_stack` does not.
84        let mut function_stack_depth = 0;
85        let mut cur_weight = 0;
86        // The key is a function stack trace (see `function_stack`, but including the current
87        // function).
88        // The value is the weight of the stack trace so far, not including the pending weight being
89        // tracked at the time.
90        let mut stack_trace_weights = OrderedHashMap::default();
91        let mut end_of_program_reached = false;
92        // The total weight of each Sierra statement.
93        // Note the header and footer (CASM instructions added for running the program by the
94        // runner). The header is not counted, and the footer is, but then the relevant
95        // entry is removed.
96        let mut sierra_statement_weights = UnorderedHashMap::default();
97        // Total weight of Sierra statements grouped by the respective (collapsed) user function
98        // call stack.
99        let mut scoped_sierra_statement_weights = OrderedHashMap::default();
100        for step in trace {
101            // Skip the header.
102            let Some(real_pc) = step.pc.checked_sub(load_offset) else {
103                continue;
104            };
105
106            // Skip the footer.
107            // Also if pc is greater or equal the bytecode length it means that it is the outside
108            // ret used for e.g. getting pointer to builtins costs table, const segments
109            // etc.
110            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            // TODO(yuval): Maintain a map of pc to sierra statement index (only for PCs we saw), to
121            // save lookups.
122            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                // The current stack trace, including the current function (recursive calls
132                // collapsed).
133                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                        // Push to the stack.
156                        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                    // Pop from the stack.
165                    if function_stack_depth <= profiling_config.max_stack_trace_depth {
166                        // The current stack trace, including the current function.
167                        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 the program.
174                            end_of_program_reached = true;
175                            continue;
176                        };
177                        cur_weight += popped.1;
178                    }
179                    function_stack_depth -= 1;
180                }
181            }
182        }
183
184        // Remove the footer.
185        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/// Weights per libfunc.
196#[derive(Default)]
197pub struct LibfuncWeights {
198    /// Weight (in steps in the relevant run) of each concrete libfunc.
199    pub concrete_libfunc_weights: Option<OrderedHashMap<String, usize>>,
200    /// Weight (in steps in the relevant run) of each generic libfunc.
201    pub generic_libfunc_weights: Option<OrderedHashMap<String, usize>>,
202    /// Weight (in steps in the relevant run) of return statements.
203    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/// Weights per user function.
238#[derive(Default)]
239pub struct UserFunctionWeights {
240    /// Weight (in steps in the relevant run) of each user function (including generated
241    /// functions).
242    pub user_function_weights: Option<OrderedHashMap<String, usize>>,
243    /// Weight (in steps in the relevant run) of each original user function.
244    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/// Weights per stack trace.
265#[derive(Default)]
266pub struct StackTraceWeights {
267    /// A map of weights of each Sierra stack trace.
268    /// The key is a function stack trace of an executed function. The stack trace is represented
269    /// as a vector of the function names.
270    /// The value is the weight of the stack trace.
271    pub sierra_stack_trace_weights: Option<OrderedHashMap<Vec<String>, usize>>,
272    /// A map of weights of each stack trace, only for stack traces that are fully semantic
273    /// (equivalent to Cairo traces). That is, none of the trace components is generated.
274    /// This is a filtered map of `sierra_stack_trace_weights`.
275    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
298/// Full profiling info of a single run. This is the processed info which went through additional
299/// processing after collecting the raw data during the run itself.
300pub struct ProcessedProfilingInfo {
301    /// For each sierra statement: the number of steps in the trace that originated from it, and
302    /// the relevant GenStatement.
303    pub sierra_statement_weights:
304        Option<OrderedHashMap<StatementIdx, (usize, GenStatement<StatementIdx>)>>,
305
306    /// Weights per stack trace.
307    pub stack_trace_weights: StackTraceWeights,
308
309    /// Weights per libfunc.
310    pub libfunc_weights: LibfuncWeights,
311
312    /// Weights per user function.
313    pub user_function_weights: UserFunctionWeights,
314
315    /// Weight (in steps in the relevant run) of each Cairo function.
316    pub cairo_function_weights: Option<OrderedHashMap<String, usize>>,
317
318    /// For each Sierra statement in the scope of a particular call stack with deduplicated frames
319    /// (collapsed recursion): the number of steps in the trace that originated from it.
320    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        // libfunc weights.
332        self.libfunc_weights.fmt(f)?;
333
334        // user functions.
335        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
354/// Parameters controlling what profiling info is processed and how, by the
355/// `ProfilingInfoProcessor`.
356pub struct ProfilingInfoProcessorParams {
357    /// The minimal weight to include in the output. Used for all collected stats. That is - the
358    /// sum of the weights per statement may be smaller than the sum of the weights per concrete
359    /// libfunc, that may be smaller than the sum of the weights per generic libfunc.
360    pub min_weight: usize,
361    /// Whether to process the profiling info by Sierra statement.
362    pub process_by_statement: bool,
363    /// Whether to process the profiling info by concrete libfunc.
364    pub process_by_concrete_libfunc: bool,
365    /// Whether to process the profiling info by generic libfunc.
366    pub process_by_generic_libfunc: bool,
367    /// Whether to process the profiling info by user function, including generated functions.
368    pub process_by_user_function: bool,
369    /// Whether to process the profiling info by original user function only.
370    pub process_by_original_user_function: bool,
371    /// Whether to process the profiling info by Cairo function (computed from the compiler
372    /// StableLocation).
373    pub process_by_cairo_function: bool,
374    /// Whether to process the profiling info by Sierra stack trace (including generated
375    /// functions in the traces).
376    pub process_by_stack_trace: bool,
377    /// Whether to process the profiling info by Cairo stack trace (that is, no generated
378    /// functions in the traces).
379    pub process_by_cairo_stack_trace: bool,
380    /// Process the profiling info by Sierra statement in the scope of a particular
381    /// call stack (recursion collapsed) and output in a format compatible with Flamegraph.
382    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
428/// A processor for profiling info. Used to process the raw profiling info (basic info collected
429/// during the run) into a more detailed profiling info that can also be formatted.
430pub struct ProfilingInfoProcessor<'a> {
431    db: Option<&'a dyn Database>,
432    sierra_program: &'a Program,
433    /// A map between sierra statement index and the string representation of the Cairo function
434    /// that generated it. The function representation is composed of the function name and the
435    /// path (modules and impls) to the function in the file.
436    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    /// Processes the raw profiling info according to the given params.
448    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    /// Process the weights per Sierra statement.
485    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    /// Process the weights per stack trace.
503    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    /// Process the weights per libfunc.
536    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        // This is done second as .filter() is consuming and to avoid cloning.
575        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    /// Process the weights per user function.
593    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        // This is done second as .filter() is consuming and to avoid cloning.
631        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    /// Process the weights per Cairo function.
649    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            // TODO(Gil): Fill all the `Unknown functions` in the cairo functions profiling.
659            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    /// Process Sierra statement weights in the scope of a particular call stack
679    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                // Accumulating statements with the same name.
700                *scoped_sierra_statement_weights.entry(key).or_default() += *weight;
701            }
702            return Some(scoped_sierra_statement_weights);
703        }
704        None
705    }
706
707    /// Translates the given Sierra statement index into the actual statement.
708    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
720/// Checks if the given stack trace is fully semantic (so it is equivalent to a Cairo trace). That
721/// is, none of the trace components is generated.
722fn 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
730/// Converts a sierra statement index to the index of the function that contains it (the index in
731/// the list in the sierra program).
732///
733/// Assumes that the given `statement_idx` is valid (that is within range of the given
734/// `sierra_program`) and that the given `sierra_program` is valid, specifically that the first
735/// function's entry point is 0.
736pub fn user_function_idx_by_sierra_statement_idx(
737    sierra_program: &Program,
738    statement_idx: StatementIdx,
739) -> usize {
740    // The `-1` here can't cause an underflow as the first function's entry point is
741    // always 0, so it is always on the left side of the partition, and thus the
742    // partition index is >0.
743    sierra_program.funcs.partition_point(|f| f.entry_point.0 <= statement_idx.0) - 1
744}
745
746/// Converts a stack trace represented as a vector of indices of functions in the sierra program to
747/// a stack trace represented as a vector of function names.
748/// Assumes that the given `idx_stack_trace` is valid with respect to the given `sierra_program`.
749/// That is, each index in the stack trace is within range of the sierra program.
750fn 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
757/// Writes scoped Sierra statement weights data in a FlameGraph compatible format.
758fn 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}