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                    ) {
154                        // Push to the stack.
155                        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                    // Pop from the stack.
164                    if function_stack_depth <= profiling_config.max_stack_trace_depth {
165                        // The current stack trace, including the current function.
166                        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 the program.
173                            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/// Weights per libfunc.
192#[derive(Default)]
193pub struct LibfuncWeights {
194    /// Weight (in steps in the relevant run) of each concrete libfunc.
195    pub concrete_libfunc_weights: Option<OrderedHashMap<String, usize>>,
196    /// Weight (in steps in the relevant run) of each generic libfunc.
197    pub generic_libfunc_weights: Option<OrderedHashMap<String, usize>>,
198    /// Weight (in steps in the relevant run) of return statements.
199    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/// Weights per user function.
234#[derive(Default)]
235pub struct UserFunctionWeights {
236    /// Weight (in steps in the relevant run) of each user function (including generated
237    /// functions).
238    pub user_function_weights: Option<OrderedHashMap<String, usize>>,
239    /// Weight (in steps in the relevant run) of each original user function.
240    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/// Weights per stack trace.
261#[derive(Default)]
262pub struct StackTraceWeights {
263    /// A map of weights of each Sierra stack trace.
264    /// The key is a function stack trace of an executed function. The stack trace is represented
265    /// as a vector of the function names.
266    /// The value is the weight of the stack trace.
267    pub sierra_stack_trace_weights: Option<OrderedHashMap<Vec<String>, usize>>,
268    /// A map of weights of each stack trace, only for stack traces that are fully semantic
269    /// (equivalent to Cairo traces). That is, none of the trace components is generated.
270    /// This is a filtered map of `sierra_stack_trace_weights`.
271    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
294/// Full profiling info of a single run. This is the processed info which went through additional
295/// processing after collecting the raw data during the run itself.
296pub struct ProcessedProfilingInfo {
297    /// For each Sierra statement: the number of steps in the trace that originated from it, and
298    /// the relevant GenStatement.
299    pub sierra_statement_weights:
300        Option<OrderedHashMap<StatementIdx, (usize, GenStatement<StatementIdx>)>>,
301
302    /// Weights per stack trace.
303    pub stack_trace_weights: StackTraceWeights,
304
305    /// Weights per libfunc.
306    pub libfunc_weights: LibfuncWeights,
307
308    /// Weights per user function.
309    pub user_function_weights: UserFunctionWeights,
310
311    /// Weight (in steps in the relevant run) of each Cairo function.
312    pub cairo_function_weights: Option<OrderedHashMap<String, usize>>,
313
314    /// For each Sierra statement in the scope of a particular call stack with deduplicated frames
315    /// (collapsed recursion): the number of steps in the trace that originated from it.
316    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        // libfunc weights.
328        self.libfunc_weights.fmt(f)?;
329
330        // user functions.
331        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
350/// Parameters controlling what profiling info is processed and how, by the
351/// `ProfilingInfoProcessor`.
352pub struct ProfilingInfoProcessorParams {
353    /// The minimal weight to include in the output. Used for all collected stats. That is - the
354    /// sum of the weights per statement may be smaller than the sum of the weights per concrete
355    /// libfunc, that may be smaller than the sum of the weights per generic libfunc.
356    pub min_weight: usize,
357    /// Whether to process the profiling info by Sierra statement.
358    pub process_by_statement: bool,
359    /// Whether to process the profiling info by concrete libfunc.
360    pub process_by_concrete_libfunc: bool,
361    /// Whether to process the profiling info by generic libfunc.
362    pub process_by_generic_libfunc: bool,
363    /// Whether to process the profiling info by user function, including generated functions.
364    pub process_by_user_function: bool,
365    /// Whether to process the profiling info by original user function only.
366    pub process_by_original_user_function: bool,
367    /// Whether to process the profiling info by Cairo function (computed from the compiler
368    /// StableLocation).
369    pub process_by_cairo_function: bool,
370    /// Whether to process the profiling info by Sierra stack trace (including generated
371    /// functions in the traces).
372    pub process_by_stack_trace: bool,
373    /// Whether to process the profiling info by Cairo stack trace (that is, no generated
374    /// functions in the traces).
375    pub process_by_cairo_stack_trace: bool,
376    /// Process the profiling info by Sierra statement in the scope of a particular
377    /// call stack (recursion collapsed) and output in a format compatible with FlameGraph.
378    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
424/// A processor for profiling info. Used to process the raw profiling info (basic info collected
425/// during the run) into a more detailed profiling info that can also be formatted.
426pub struct ProfilingInfoProcessor<'a> {
427    db: Option<&'a dyn Database>,
428    sierra_program: &'a Program,
429    /// A map between Sierra statement index and the string representation of the Cairo function
430    /// that generated it. The function representation is composed of the function name and the
431    /// path (modules and impls) to the function in the file.
432    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    /// Processes the raw profiling info according to the given params.
444    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    /// Process the weights per Sierra statement.
481    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    /// Process the weights per stack trace.
499    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    /// Process the weights per libfunc.
532    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        // This is done second as .filter() is consuming and to avoid cloning.
571        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    /// Process the weights per user function.
589    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        // This is done second as .filter() is consuming and to avoid cloning.
627        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    /// Process the weights per Cairo function.
645    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            // TODO(Gil): Fill all the `Unknown functions` in the Cairo functions profiling.
655            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    /// Process Sierra statement weights in the scope of a particular call stack
676    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                // Accumulating statements with the same name.
697                *scoped_sierra_statement_weights.entry(key).or_default() += *weight;
698            }
699            return Some(scoped_sierra_statement_weights);
700        }
701        None
702    }
703
704    /// Translates the given Sierra statement index into the actual statement.
705    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
717/// Checks if the given stack trace is fully semantic (so it is equivalent to a Cairo trace). That
718/// is, none of the trace components is generated.
719fn 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
727/// Converts a Sierra statement index to the index of the function that contains it (the index in
728/// the list in the Sierra program).
729///
730/// Assumes that the given `statement_idx` is valid (that it is within range of the given
731/// `sierra_program`) and that the given `sierra_program` is valid, specifically that the first
732/// function's entry point is 0.
733pub fn user_function_idx_by_sierra_statement_idx(
734    sierra_program: &Program,
735    statement_idx: StatementIdx,
736) -> usize {
737    // The `-1` here can't cause an underflow as the first function's entry point is
738    // always 0, so it is always on the left side of the partition, and thus the
739    // partition index is >0.
740    sierra_program.funcs.partition_point(|f| f.entry_point.0 <= statement_idx.0) - 1
741}
742
743/// Converts a stack trace represented as a vector of indices of functions in the Sierra program to
744/// a stack trace represented as a vector of function names.
745/// Assumes that the given `idx_stack_trace` is valid with respect to the given `sierra_program`.
746/// That is, each index in the stack trace is within range of the Sierra program.
747fn 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
754/// Writes scoped Sierra statement weights data in a FlameGraph compatible format.
755fn 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}