Skip to main content

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 info of a single run. This is the raw info — it went through minimal processing, as
41/// this is done during the run. To enrich it before viewing/printing, use the
42/// `ProfilingInfoProcessor`.
43#[derive(Debug, Eq, PartialEq, Clone)]
44pub struct ProfilingInfo {
45    /// The number of steps in the trace that originated from each Sierra statement.
46    pub sierra_statement_weights: UnorderedHashMap<StatementIdx, usize>,
47
48    /// A map of weights of each stack trace.
49    /// The key is a function stack trace of an executed function. The stack trace is represented
50    /// as a vector of indices of the functions in the stack (indices of the functions according to
51    /// the list in the Sierra program).
52    /// The value is the weight of the stack trace.
53    /// The stack trace entries are sorted in the order they occur.
54    pub stack_trace_weights: OrderedHashMap<Vec<usize>, usize>,
55
56    /// The number of steps in the trace that originated from each Sierra statement
57    /// combined with information about the user function call stack.
58    /// The call stack items are deduplicated to flatten and aggregate recursive calls
59    /// and loops (which are tail recursion).
60    /// The entries are sorted in the order they occur.
61    pub scoped_sierra_statement_weights: OrderedHashMap<(Vec<usize>, StatementIdx), usize>,
62}
63
64impl ProfilingInfo {
65    pub fn from_trace(
66        builder: &RunnableBuilder,
67        // The offset in memory where builder.casm_program() was loaded.
68        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        // 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). Both header and footer are not counted when collecting the weights.
95        let mut sierra_statement_weights = UnorderedHashMap::default();
96        // Total weight of Sierra statements grouped by the respective (collapsed) user function
97        // call stack.
98        let mut scoped_sierra_statement_weights = OrderedHashMap::default();
99        for step in trace {
100            // Skip the header.
101            let Some(real_pc) = step.pc.checked_sub(load_offset) else {
102                continue;
103            };
104
105            // Skip the footer.
106            // Also if pc is greater or equal the bytecode length it means that it is the outside
107            // ret used for, e.g., getting pointer to builtins costs table, const segments
108            // etc.
109            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            // TODO(yuval): Maintain a map of pc to sierra statement index (only for PCs we saw), to
120            // save lookups.
121            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                // The current stack trace, including the current function (recursive calls
131                // collapsed).
132                let cur_stack: Vec<usize> =
133                    chain!(function_stack.iter().map(|&(idx, _)| idx), [user_function_idx])
134                        .dedup()
135                        .collect();
136
137                *scoped_sierra_statement_weights
138                    .entry((cur_stack, sierra_statement_idx))
139                    .or_insert(0) += 1;
140            }
141
142            let Some(gen_statement) =
143                builder.sierra_program().statements.get(sierra_statement_idx.0)
144            else {
145                panic!("Failed fetching statement index {}", sierra_statement_idx.0);
146            };
147
148            match gen_statement {
149                GenStatement::Invocation(invocation) => {
150                    if matches!(
151                        builder.registry().get_libfunc(&invocation.libfunc_id),
152                        Ok(CoreConcreteLibfunc::FunctionCall(_)
153                            | CoreConcreteLibfunc::CouponCall(_))
154                    ) {
155                        // 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        ProfilingInfo {
185            sierra_statement_weights,
186            stack_trace_weights,
187            scoped_sierra_statement_weights,
188        }
189    }
190}
191
192/// Weights per libfunc.
193#[derive(Default)]
194pub struct LibfuncWeights {
195    /// Weight (in steps in the relevant run) of each concrete libfunc.
196    pub concrete_libfunc_weights: Option<OrderedHashMap<String, usize>>,
197    /// Weight (in steps in the relevant run) of each generic libfunc.
198    pub generic_libfunc_weights: Option<OrderedHashMap<String, usize>>,
199    /// Weight (in steps in the relevant run) of return statements.
200    pub return_weight: Option<usize>,
201}
202impl Display for LibfuncWeights {
203    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
204        if let Some(concrete_libfunc_weights) = &self.concrete_libfunc_weights {
205            writeln!(f, "Weight by concrete libfunc:")?;
206            for (concrete_name, weight) in concrete_libfunc_weights.iter() {
207                writeln!(f, "  libfunc {concrete_name}: {weight}")?;
208            }
209            writeln!(
210                f,
211                "  return: {}",
212                self.return_weight.expect(
213                    "return_weight should have a value if concrete_libfunc_weights has a value"
214                )
215            )?;
216        }
217        if let Some(generic_libfunc_weights) = &self.generic_libfunc_weights {
218            writeln!(f, "Weight by generic libfunc:")?;
219            for (generic_name, weight) in generic_libfunc_weights.iter() {
220                writeln!(f, "  libfunc {generic_name}: {weight}")?;
221            }
222            writeln!(
223                f,
224                "  return: {}",
225                self.return_weight.expect(
226                    "return_weight should have a value if generic_libfunc_weights has a value"
227                )
228            )?;
229        }
230        Ok(())
231    }
232}
233
234/// Weights per user function.
235#[derive(Default)]
236pub struct UserFunctionWeights {
237    /// Weight (in steps in the relevant run) of each user function (including generated
238    /// functions).
239    pub user_function_weights: Option<OrderedHashMap<String, usize>>,
240    /// Weight (in steps in the relevant run) of each original user function.
241    pub original_user_function_weights: Option<OrderedHashMap<String, usize>>,
242}
243impl Display for UserFunctionWeights {
244    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
245        if let Some(user_function_weights) = &self.user_function_weights {
246            writeln!(f, "Weight by user function (inc. generated):")?;
247            for (name, weight) in user_function_weights.iter() {
248                writeln!(f, "  function {name}: {weight}")?;
249            }
250        }
251        if let Some(original_user_function_weights) = &self.original_user_function_weights {
252            writeln!(f, "Weight by original user function (exc. generated):")?;
253            for (name, weight) in original_user_function_weights.iter() {
254                writeln!(f, "  function {name}: {weight}")?;
255            }
256        }
257        Ok(())
258    }
259}
260
261/// Weights per stack trace.
262#[derive(Default)]
263pub struct StackTraceWeights {
264    /// A map of weights of each Sierra stack trace.
265    /// The key is a function stack trace of an executed function. The stack trace is represented
266    /// as a vector of the function names.
267    /// The value is the weight of the stack trace.
268    pub sierra_stack_trace_weights: Option<OrderedHashMap<Vec<String>, usize>>,
269    /// A map of weights of each stack trace, only for stack traces that are fully semantic
270    /// (equivalent to Cairo traces). That is, none of the trace components is generated.
271    /// This is a filtered map of `sierra_stack_trace_weights`.
272    pub cairo_stack_trace_weights: Option<OrderedHashMap<Vec<String>, usize>>,
273}
274impl Display for StackTraceWeights {
275    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
276        if let Some(sierra_stack_trace_weights) = &self.sierra_stack_trace_weights {
277            writeln!(f, "Weight by Sierra stack trace:")?;
278            for (stack_trace, weight) in sierra_stack_trace_weights.iter() {
279                writeln!(f, "  {}: {weight}", stack_trace.iter().format(" -> "))?;
280            }
281        }
282
283        if let Some(cairo_stack_trace_weights) = &self.cairo_stack_trace_weights {
284            writeln!(f, "Weight by Cairo stack trace:")?;
285            for (stack_trace, weight) in cairo_stack_trace_weights.iter() {
286                writeln!(f, "  {}: {weight}", stack_trace.iter().format(" -> "))?;
287            }
288        }
289        Ok(())
290    }
291}
292
293/// Full profiling info of a single run. This is the processed info which went through additional
294/// processing after collecting the raw data during the run itself.
295pub struct ProcessedProfilingInfo {
296    /// For each Sierra statement: the number of steps in the trace that originated from it, and
297    /// the relevant GenStatement.
298    pub sierra_statement_weights:
299        Option<OrderedHashMap<StatementIdx, (usize, GenStatement<StatementIdx>)>>,
300
301    /// Weights per stack trace.
302    pub stack_trace_weights: StackTraceWeights,
303
304    /// Weights per libfunc.
305    pub libfunc_weights: LibfuncWeights,
306
307    /// Weights per user function.
308    pub user_function_weights: UserFunctionWeights,
309
310    /// Weight (in steps in the relevant run) of each Cairo function.
311    pub cairo_function_weights: Option<OrderedHashMap<String, usize>>,
312
313    /// For each Sierra statement in the scope of a particular call stack with deduplicated frames
314    /// (collapsed recursion): the number of steps in the trace that originated from it.
315    pub scoped_sierra_statement_weights: Option<OrderedHashMap<Vec<String>, usize>>,
316}
317impl Display for ProcessedProfilingInfo {
318    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
319        if let Some(sierra_statement_weights) = &self.sierra_statement_weights {
320            writeln!(f, "Weight by sierra statement:")?;
321            for (statement_idx, (weight, gen_statement)) in sierra_statement_weights.iter() {
322                writeln!(f, "  statement {statement_idx}: {weight} ({gen_statement})")?;
323            }
324        }
325
326        // libfunc weights.
327        self.libfunc_weights.fmt(f)?;
328
329        // user functions.
330        self.user_function_weights.fmt(f)?;
331
332        if let Some(cairo_function_weights) = &self.cairo_function_weights {
333            writeln!(f, "Weight by Cairo function:")?;
334            for (function_identifier, weight) in cairo_function_weights.iter() {
335                writeln!(f, "  function {function_identifier}: {weight}")?;
336            }
337        }
338
339        self.stack_trace_weights.fmt(f)?;
340
341        if let Some(weights) = &self.scoped_sierra_statement_weights {
342            format_scoped_sierra_statement_weights(weights, f)?;
343        }
344
345        Ok(())
346    }
347}
348
349/// Parameters controlling what profiling info is processed and how, by the
350/// `ProfilingInfoProcessor`.
351pub struct ProfilingInfoProcessorParams {
352    /// The minimal weight to include in the output. Used for all collected stats. That is - the
353    /// sum of the weights per statement may be smaller than the sum of the weights per concrete
354    /// libfunc, that may be smaller than the sum of the weights per generic libfunc.
355    pub min_weight: usize,
356    /// Whether to process the profiling info by Sierra statement.
357    pub process_by_statement: bool,
358    /// Whether to process the profiling info by concrete libfunc.
359    pub process_by_concrete_libfunc: bool,
360    /// Whether to process the profiling info by generic libfunc.
361    pub process_by_generic_libfunc: bool,
362    /// Whether to process the profiling info by user function, including generated functions.
363    pub process_by_user_function: bool,
364    /// Whether to process the profiling info by original user function only.
365    pub process_by_original_user_function: bool,
366    /// Whether to process the profiling info by Cairo function (computed from the compiler
367    /// StableLocation).
368    pub process_by_cairo_function: bool,
369    /// Whether to process the profiling info by Sierra stack trace (including generated
370    /// functions in the traces).
371    pub process_by_stack_trace: bool,
372    /// Whether to process the profiling info by Cairo stack trace (that is, no generated
373    /// functions in the traces).
374    pub process_by_cairo_stack_trace: bool,
375    /// Process the profiling info by Sierra statement in the scope of a particular
376    /// call stack (recursion collapsed) and output in a format compatible with FlameGraph.
377    pub process_by_scoped_statement: bool,
378}
379impl Default for ProfilingInfoProcessorParams {
380    fn default() -> Self {
381        Self {
382            min_weight: 1,
383            process_by_statement: true,
384            process_by_concrete_libfunc: true,
385            process_by_generic_libfunc: true,
386            process_by_user_function: true,
387            process_by_original_user_function: true,
388            process_by_cairo_function: true,
389            process_by_stack_trace: true,
390            process_by_cairo_stack_trace: true,
391            process_by_scoped_statement: false,
392        }
393    }
394}
395
396impl ProfilingInfoProcessorParams {
397    pub fn from_profiler_config(config: &ProfilerConfig) -> Self {
398        match config {
399            ProfilerConfig::Cairo => Default::default(),
400            ProfilerConfig::Scoped => Self {
401                min_weight: 1,
402                process_by_statement: false,
403                process_by_concrete_libfunc: false,
404                process_by_generic_libfunc: false,
405                process_by_user_function: false,
406                process_by_original_user_function: false,
407                process_by_cairo_function: false,
408                process_by_stack_trace: false,
409                process_by_cairo_stack_trace: false,
410                process_by_scoped_statement: true,
411            },
412            ProfilerConfig::Sierra => Self {
413                process_by_generic_libfunc: false,
414                process_by_cairo_stack_trace: false,
415                process_by_original_user_function: false,
416                process_by_cairo_function: false,
417                ..ProfilingInfoProcessorParams::default()
418            },
419        }
420    }
421}
422
423/// A processor for profiling info. Used to process the raw profiling info (basic info collected
424/// during the run) into a more detailed profiling info that can also be formatted.
425pub struct ProfilingInfoProcessor<'a> {
426    db: Option<&'a dyn Database>,
427    sierra_program: &'a Program,
428    /// A map between Sierra statement index and the string representation of the Cairo function
429    /// that generated it. The function representation is composed of the function name and the
430    /// path (modules and impls) to the function in the file.
431    statements_functions: UnorderedHashMap<StatementIdx, String>,
432}
433impl<'a> ProfilingInfoProcessor<'a> {
434    pub fn new(
435        db: Option<&'a dyn Database>,
436        sierra_program: &'a Program,
437        statements_functions: UnorderedHashMap<StatementIdx, String>,
438    ) -> Self {
439        Self { db, sierra_program, statements_functions }
440    }
441
442    /// Processes the raw profiling info according to the given params.
443    pub fn process(
444        &self,
445        raw_profiling_info: &ProfilingInfo,
446        params: &ProfilingInfoProcessorParams,
447    ) -> ProcessedProfilingInfo {
448        let sierra_statement_weights_iter = raw_profiling_info
449            .sierra_statement_weights
450            .iter_sorted_by_key(|(pc, count)| (usize::MAX - **count, **pc));
451
452        let sierra_statement_weights =
453            self.process_sierra_statement_weights(sierra_statement_weights_iter.clone(), params);
454
455        let stack_trace_weights = self.process_stack_trace_weights(raw_profiling_info, params);
456
457        let libfunc_weights =
458            self.process_libfunc_weights(sierra_statement_weights_iter.clone(), params);
459
460        let user_function_weights =
461            self.process_user_function_weights(sierra_statement_weights_iter.clone(), params);
462
463        let cairo_function_weights =
464            self.process_cairo_function_weights(sierra_statement_weights_iter, params);
465
466        let scoped_sierra_statement_weights =
467            self.process_scoped_sierra_statement_weights(raw_profiling_info, params);
468
469        ProcessedProfilingInfo {
470            sierra_statement_weights,
471            stack_trace_weights,
472            libfunc_weights,
473            user_function_weights,
474            cairo_function_weights,
475            scoped_sierra_statement_weights,
476        }
477    }
478
479    /// Process the weights per Sierra statement.
480    fn process_sierra_statement_weights(
481        &self,
482        sierra_statement_weights_iter: std::vec::IntoIter<(&StatementIdx, &usize)>,
483        params: &ProfilingInfoProcessorParams,
484    ) -> Option<OrderedHashMap<StatementIdx, (usize, GenStatement<StatementIdx>)>> {
485        require(params.process_by_statement)?;
486
487        Some(
488            sierra_statement_weights_iter
489                .filter(|&(_, weight)| *weight >= params.min_weight)
490                .map(|(statement_idx, weight)| {
491                    (*statement_idx, (*weight, self.statement_idx_to_gen_statement(statement_idx)))
492                })
493                .collect(),
494        )
495    }
496
497    /// Process the weights per stack trace.
498    fn process_stack_trace_weights(
499        &self,
500        raw_profiling_info: &ProfilingInfo,
501        params: &ProfilingInfoProcessorParams,
502    ) -> StackTraceWeights {
503        let resolve_names = |(idx_stack_trace, weight): (&Vec<usize>, &usize)| {
504            (index_stack_trace_to_name_stack_trace(self.sierra_program, idx_stack_trace), *weight)
505        };
506
507        let sierra_stack_trace_weights = params.process_by_stack_trace.then(|| {
508            raw_profiling_info
509                .stack_trace_weights
510                .iter()
511                .sorted_by_key(|&(trace, weight)| (usize::MAX - *weight, trace))
512                .map(resolve_names)
513                .collect()
514        });
515
516        let cairo_stack_trace_weights = params.process_by_cairo_stack_trace.then(|| {
517            let db = self.db.expect("DB must be set with `process_by_cairo_stack_trace=true`.");
518            raw_profiling_info
519                .stack_trace_weights
520                .iter()
521                .filter(|(trace, _)| is_cairo_trace(db, self.sierra_program, trace))
522                .sorted_by_key(|&(trace, weight)| (usize::MAX - *weight, trace))
523                .map(resolve_names)
524                .collect()
525        });
526
527        StackTraceWeights { sierra_stack_trace_weights, cairo_stack_trace_weights }
528    }
529
530    /// Process the weights per libfunc.
531    fn process_libfunc_weights(
532        &self,
533        sierra_statement_weights: std::vec::IntoIter<(&StatementIdx, &usize)>,
534        params: &ProfilingInfoProcessorParams,
535    ) -> LibfuncWeights {
536        if !params.process_by_concrete_libfunc && !params.process_by_generic_libfunc {
537            return LibfuncWeights::default();
538        }
539
540        let mut return_weight = 0;
541        let mut libfunc_weights = UnorderedHashMap::<ConcreteLibfuncId, usize>::default();
542        for (statement_idx, weight) in sierra_statement_weights {
543            match self.statement_idx_to_gen_statement(statement_idx) {
544                GenStatement::Invocation(invocation) => {
545                    *(libfunc_weights.entry(invocation.libfunc_id.clone()).or_insert(0)) += weight;
546                }
547                GenStatement::Return(_) => {
548                    return_weight += weight;
549                }
550            }
551        }
552
553        let generic_libfunc_weights = params.process_by_generic_libfunc.then(|| {
554            let db: &dyn Database =
555                self.db.expect("DB must be set with `process_by_generic_libfunc=true`.");
556            libfunc_weights
557                .aggregate_by(
558                    |k| db.lookup_concrete_lib_func(k).generic_id.to_string(),
559                    |v1: &usize, v2| v1 + v2,
560                    &0,
561                )
562                .filter(|_, weight| *weight >= params.min_weight)
563                .into_iter_sorted_by_key(|(generic_name, weight)| {
564                    (usize::MAX - *weight, (*generic_name).clone())
565                })
566                .collect()
567        });
568
569        // This is done second as .filter() is consuming and to avoid cloning.
570        let concrete_libfunc_weights = params.process_by_concrete_libfunc.then(|| {
571            libfunc_weights
572                .filter(|_, weight| *weight >= params.min_weight)
573                .into_iter_sorted_by_key(|(libfunc_id, weight)| {
574                    (usize::MAX - *weight, libfunc_id.to_string())
575                })
576                .map(|(libfunc_id, weight)| (libfunc_id.to_string(), weight))
577                .collect()
578        });
579
580        LibfuncWeights {
581            concrete_libfunc_weights,
582            generic_libfunc_weights,
583            return_weight: Some(return_weight),
584        }
585    }
586
587    /// Process the weights per user function.
588    fn process_user_function_weights(
589        &self,
590        sierra_statement_weights: std::vec::IntoIter<(&StatementIdx, &usize)>,
591        params: &ProfilingInfoProcessorParams,
592    ) -> UserFunctionWeights {
593        if !params.process_by_user_function && !params.process_by_original_user_function {
594            return UserFunctionWeights::default();
595        }
596
597        let mut user_functions = UnorderedHashMap::<usize, usize>::default();
598        for (statement_idx, weight) in sierra_statement_weights {
599            let function_idx: usize =
600                user_function_idx_by_sierra_statement_idx(self.sierra_program, *statement_idx);
601            *(user_functions.entry(function_idx).or_insert(0)) += weight;
602        }
603
604        let original_user_function_weights = params.process_by_original_user_function.then(|| {
605            let db: &dyn Database =
606                self.db.expect("DB must be set with `process_by_original_user_function=true`.");
607            user_functions
608                .aggregate_by(
609                    |idx| {
610                        let lowering_function_id =
611                            db.lookup_sierra_function(&self.sierra_program.funcs[*idx].id);
612                        lowering_function_id.semantic_full_path(db)
613                    },
614                    |x, y| x + y,
615                    &0,
616                )
617                .filter(|_, weight| *weight >= params.min_weight)
618                .iter_sorted_by_key(|(orig_name, weight)| {
619                    (usize::MAX - **weight, (*orig_name).clone())
620                })
621                .map(|(orig_name, weight)| (orig_name.clone(), *weight))
622                .collect()
623        });
624
625        // This is done second as .filter() is consuming and to avoid cloning.
626        let user_function_weights = params.process_by_user_function.then(|| {
627            user_functions
628                .filter(|_, weight| *weight >= params.min_weight)
629                .iter_sorted_by_key(|(idx, weight)| {
630                    (usize::MAX - **weight, self.sierra_program.funcs[**idx].id.to_string())
631                })
632                .map(|(idx, weight)| {
633                    let func: &cairo_lang_sierra::program::GenFunction<StatementIdx> =
634                        &self.sierra_program.funcs[*idx];
635                    (func.id.to_string(), *weight)
636                })
637                .collect()
638        });
639
640        UserFunctionWeights { user_function_weights, original_user_function_weights }
641    }
642
643    /// Process the weights per Cairo function.
644    fn process_cairo_function_weights(
645        &self,
646        sierra_statement_weights: std::vec::IntoIter<(&StatementIdx, &usize)>,
647        params: &ProfilingInfoProcessorParams,
648    ) -> Option<OrderedHashMap<String, usize>> {
649        require(params.process_by_cairo_function)?;
650
651        let mut cairo_functions = UnorderedHashMap::<_, _>::default();
652        for (statement_idx, weight) in sierra_statement_weights {
653            // TODO(Gil): Fill all the `Unknown functions` in the Cairo functions profiling.
654            let function_identifier = self
655                .statements_functions
656                .get(statement_idx)
657                .cloned()
658                .unwrap_or_else(|| "unknown".to_string());
659
660            *(cairo_functions.entry(function_identifier).or_insert(0)) += weight;
661        }
662
663        Some(
664            cairo_functions
665                .filter(|_, weight| *weight >= params.min_weight)
666                .iter_sorted_by_key(|(function_identifier, weight)| {
667                    (usize::MAX - **weight, (*function_identifier).clone())
668                })
669                .map(|(function_identifier, weight)| (function_identifier.clone(), *weight))
670                .collect(),
671        )
672    }
673
674    /// Process Sierra statement weights in the scope of a particular call stack.
675    fn process_scoped_sierra_statement_weights(
676        &self,
677        raw_profiling_info: &ProfilingInfo,
678        params: &ProfilingInfoProcessorParams,
679    ) -> Option<OrderedHashMap<Vec<String>, usize>> {
680        if params.process_by_scoped_statement {
681            let mut scoped_sierra_statement_weights: OrderedHashMap<Vec<String>, usize> =
682                Default::default();
683            for ((idx_stack_trace, statement_idx), weight) in
684                raw_profiling_info.scoped_sierra_statement_weights.iter()
685            {
686                let statement_name = match self.statement_idx_to_gen_statement(statement_idx) {
687                    GenStatement::Invocation(invocation) => invocation.libfunc_id.to_string(),
688                    GenStatement::Return(_) => "return".into(),
689                };
690                let key: Vec<String> = chain!(
691                    index_stack_trace_to_name_stack_trace(self.sierra_program, idx_stack_trace),
692                    [statement_name]
693                )
694                .collect();
695                // Accumulating statements with the same name.
696                *scoped_sierra_statement_weights.entry(key).or_default() += *weight;
697            }
698            return Some(scoped_sierra_statement_weights);
699        }
700        None
701    }
702
703    /// Translates the given Sierra statement index into the actual statement.
704    fn statement_idx_to_gen_statement(
705        &self,
706        statement_idx: &StatementIdx,
707    ) -> GenStatement<StatementIdx> {
708        self.sierra_program
709            .statements
710            .get(statement_idx.0)
711            .unwrap_or_else(|| panic!("Failed fetching statement index {}", statement_idx.0))
712            .clone()
713    }
714}
715
716/// Checks if the given stack trace is fully semantic (so it is equivalent to a Cairo trace). That
717/// is, none of the trace components is generated.
718fn is_cairo_trace(db: &dyn Database, sierra_program: &Program, sierra_trace: &[usize]) -> bool {
719    sierra_trace.iter().all(|sierra_function_idx| {
720        let sierra_function = &sierra_program.funcs[*sierra_function_idx];
721        let lowering_function_id = db.lookup_sierra_function(&sierra_function.id);
722        matches!(lowering_function_id.long(db), FunctionLongId::Semantic(_))
723    })
724}
725
726/// Converts a Sierra statement index to the index of the function that contains it (the index in
727/// the list in the Sierra program).
728///
729/// Assumes that the given `statement_idx` is valid (that it is within range of the given
730/// `sierra_program`) and that the given `sierra_program` is valid, specifically that the first
731/// function's entry point is 0.
732pub fn user_function_idx_by_sierra_statement_idx(
733    sierra_program: &Program,
734    statement_idx: StatementIdx,
735) -> usize {
736    // The `-1` here can't cause an underflow as the first function's entry point is
737    // always 0, so it is always on the left side of the partition, and thus the
738    // partition index is >0.
739    sierra_program.funcs.partition_point(|f| f.entry_point.0 <= statement_idx.0) - 1
740}
741
742/// Converts a stack trace represented as a vector of indices of functions in the Sierra program to
743/// a stack trace represented as a vector of function names.
744/// Assumes that the given `idx_stack_trace` is valid with respect to the given `sierra_program`.
745/// That is, each index in the stack trace is within range of the Sierra program.
746fn index_stack_trace_to_name_stack_trace(
747    sierra_program: &Program,
748    idx_stack_trace: &[usize],
749) -> Vec<String> {
750    idx_stack_trace.iter().map(|idx| sierra_program.funcs[*idx].id.to_string()).collect()
751}
752
753/// Writes scoped Sierra statement weights data in a FlameGraph compatible format.
754fn format_scoped_sierra_statement_weights(
755    weights: &OrderedHashMap<Vec<String>, usize>,
756    f: &mut std::fmt::Formatter<'_>,
757) -> std::fmt::Result {
758    for (key, weight) in weights.iter() {
759        f.write_fmt(format_args!("{} {weight}\n", key.iter().format(";")))?;
760    }
761    Ok(())
762}