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