@@ -3,14 +3,20 @@ use cairo_lang_compiler::{
33 compile_prepared_db, db:: RootDatabase , project:: setup_project, CompilerConfig ,
44} ;
55use cairo_lang_runner:: short_string:: as_cairo_short_string;
6+ #[ cfg( feature = "with-libfunc-profiling" ) ]
7+ use cairo_lang_sierra:: ids:: ConcreteLibfuncId ;
68use cairo_lang_sierra_to_casm:: metadata:: MetadataComputationConfig ;
9+ #[ cfg( feature = "with-libfunc-profiling" ) ]
10+ use cairo_native:: metadata:: profiler:: LibfuncProfileData ;
711use cairo_native:: {
812 context:: NativeContext ,
913 executor:: { AotNativeExecutor , JitNativeExecutor } ,
1014 metadata:: gas:: GasMetadata ,
1115 starknet_stub:: StubSyscallHandler ,
1216} ;
1317use clap:: { Parser , ValueEnum } ;
18+ #[ cfg( feature = "with-libfunc-profiling" ) ]
19+ use std:: collections:: HashMap ;
1420use std:: path:: PathBuf ;
1521use tracing_subscriber:: { EnvFilter , FmtSubscriber } ;
1622use utils:: { find_function, result_to_runresult} ;
@@ -46,6 +52,11 @@ struct Args {
4652 #[ arg( short = 'O' , long, default_value_t = 0 ) ]
4753 opt_level : u8 ,
4854
55+ #[ cfg( feature = "with-libfunc-profiling" ) ]
56+ #[ arg( long) ]
57+ /// The output path for the libfunc profilling results
58+ profiler_output : Option < PathBuf > ,
59+
4960 #[ cfg( feature = "with-trace-dump" ) ]
5061 #[ arg( long) ]
5162 /// The output path for the execution trace
@@ -131,6 +142,18 @@ fn main() -> anyhow::Result<()> {
131142 }
132143 }
133144
145+ #[ cfg( feature = "with-libfunc-profiling" ) ]
146+ {
147+ use cairo_native:: metadata:: profiler:: ProfilerBinding ;
148+
149+ if let Some ( trace_id) =
150+ executor. find_symbol_ptr ( ProfilerBinding :: ProfileId . symbol ( ) )
151+ {
152+ let trace_id = trace_id. cast :: < u64 > ( ) ;
153+ unsafe { * trace_id = 0 } ;
154+ }
155+ }
156+
134157 Box :: new ( move |function_id, args, gas, syscall_handler| {
135158 executor. invoke_dynamic_with_syscall_handler (
136159 function_id,
@@ -153,6 +176,16 @@ fn main() -> anyhow::Result<()> {
153176 ) ;
154177 }
155178
179+ #[ cfg( feature = "with-libfunc-profiling" ) ]
180+ {
181+ use cairo_native:: metadata:: profiler:: { ProfilerImpl , LIBFUNC_PROFILE } ;
182+
183+ LIBFUNC_PROFILE
184+ . lock ( )
185+ . unwrap ( )
186+ . insert ( 0 , ProfilerImpl :: new ( ) ) ;
187+ }
188+
156189 let gas_metadata =
157190 GasMetadata :: new ( & sierra_program, Some ( MetadataComputationConfig :: default ( ) ) ) . unwrap ( ) ;
158191
@@ -188,6 +221,66 @@ fn main() -> anyhow::Result<()> {
188221 println ! ( "Remaining gas: {gas}" ) ;
189222 }
190223
224+ #[ cfg( feature = "with-libfunc-profiling" ) ]
225+ {
226+ use std:: { fs:: File , io:: Write } ;
227+
228+ let profile = cairo_native:: metadata:: profiler:: LIBFUNC_PROFILE
229+ . lock ( )
230+ . unwrap ( ) ;
231+
232+ assert_eq ! ( profile. values( ) . len( ) , 1 ) ;
233+
234+ let profile = profile. values ( ) . next ( ) . unwrap ( ) ;
235+
236+ if let Some ( profiler_output_path) = args. profiler_output {
237+ let mut output = File :: create ( profiler_output_path) ?;
238+
239+ let raw_profile = profile. get_profile ( & sierra_program) ;
240+ let mut processed_profile = process_profile ( raw_profile) ;
241+
242+ processed_profile. sort_by_key ( |LibfuncProfileSummary { libfunc_idx, .. } | {
243+ sierra_program
244+ . libfunc_declarations
245+ . iter ( )
246+ . enumerate ( )
247+ . find_map ( |( i, x) | ( x. id == * libfunc_idx) . then_some ( i) )
248+ . unwrap ( )
249+ } ) ;
250+
251+ for LibfuncProfileSummary {
252+ libfunc_idx,
253+ samples,
254+ total_time,
255+ average_time,
256+ std_deviation,
257+ quartiles,
258+ } in processed_profile
259+ {
260+ writeln ! ( output, "{libfunc_idx}" ) ?;
261+ writeln ! ( output, " Total Samples: {samples}" ) ?;
262+
263+ let ( Some ( total_time) , Some ( average_time) , Some ( std_deviation) , Some ( quartiles) ) =
264+ ( total_time, average_time, std_deviation, quartiles)
265+ else {
266+ writeln ! ( output, " Total Execution Time: none" ) ?;
267+ writeln ! ( output, " Average Execution Time: none" ) ?;
268+ writeln ! ( output, " Standard Deviation: none" ) ?;
269+ writeln ! ( output, " Quartiles: none" ) ?;
270+ writeln ! ( output) ?;
271+
272+ continue ;
273+ } ;
274+
275+ writeln ! ( output, " Total Execution Time: {total_time}" ) ?;
276+ writeln ! ( output, " Average Execution Time: {average_time}" ) ?;
277+ writeln ! ( output, " Standard Deviation: {std_deviation}" ) ?;
278+ writeln ! ( output, " Quartiles: {quartiles:?}" ) ?;
279+ writeln ! ( output) ?;
280+ }
281+ }
282+ }
283+
191284 #[ cfg( feature = "with-trace-dump" ) ]
192285 if let Some ( trace_output) = args. trace_output {
193286 let traces = cairo_native:: metadata:: trace_dump:: trace_dump_runtime:: TRACE_DUMP
@@ -205,3 +298,92 @@ fn main() -> anyhow::Result<()> {
205298
206299 Ok ( ( ) )
207300}
301+
302+ #[ cfg( feature = "with-libfunc-profiling" ) ]
303+ struct LibfuncProfileSummary {
304+ pub libfunc_idx : ConcreteLibfuncId ,
305+ pub samples : u64 ,
306+ pub total_time : Option < u64 > ,
307+ pub average_time : Option < f64 > ,
308+ pub std_deviation : Option < f64 > ,
309+ pub quartiles : Option < [ u64 ; 5 ] > ,
310+ }
311+
312+ #[ cfg( feature = "with-libfunc-profiling" ) ]
313+ fn process_profile (
314+ profiles : HashMap < ConcreteLibfuncId , LibfuncProfileData > ,
315+ ) -> Vec < LibfuncProfileSummary > {
316+ profiles
317+ . into_iter ( )
318+ . map (
319+ |(
320+ libfunc_idx,
321+ LibfuncProfileData {
322+ mut deltas,
323+ extra_counts,
324+ } ,
325+ ) | {
326+ // if no deltas were registered, we only return the libfunc's calls amount
327+ if deltas. is_empty ( ) {
328+ return LibfuncProfileSummary {
329+ libfunc_idx,
330+ samples : extra_counts,
331+ total_time : None ,
332+ average_time : None ,
333+ std_deviation : None ,
334+ quartiles : None ,
335+ } ;
336+ }
337+
338+ deltas. sort ( ) ;
339+
340+ // Drop outliers.
341+ {
342+ let q1 = deltas[ deltas. len ( ) / 4 ] ;
343+ let q3 = deltas[ 3 * deltas. len ( ) / 4 ] ;
344+ let iqr = q3 - q1;
345+
346+ let q1_thr = q1. saturating_sub ( iqr + iqr / 2 ) ;
347+ let q3_thr = q3 + ( iqr + iqr / 2 ) ;
348+
349+ deltas. retain ( |x| * x >= q1_thr && * x <= q3_thr) ;
350+ }
351+
352+ // Compute the quartiles.
353+ let quartiles = [
354+ * deltas. first ( ) . unwrap ( ) ,
355+ deltas[ deltas. len ( ) / 4 ] ,
356+ deltas[ deltas. len ( ) / 2 ] ,
357+ deltas[ 3 * deltas. len ( ) / 4 ] ,
358+ * deltas. last ( ) . unwrap ( ) ,
359+ ] ;
360+
361+ // Compute the average.
362+ let average = deltas. iter ( ) . copied ( ) . sum :: < u64 > ( ) as f64 / deltas. len ( ) as f64 ;
363+
364+ // Compute the standard deviation.
365+ let std_dev = {
366+ let sum = deltas
367+ . iter ( )
368+ . copied ( )
369+ . map ( |x| x as f64 )
370+ . map ( |x| ( x - average) )
371+ . map ( |x| x * x)
372+ . sum :: < f64 > ( ) ;
373+ sum / ( deltas. len ( ) as u64 + extra_counts) as f64
374+ } ;
375+
376+ LibfuncProfileSummary {
377+ libfunc_idx,
378+ samples : deltas. len ( ) as u64 + extra_counts,
379+ total_time : Some (
380+ deltas. iter ( ) . sum :: < u64 > ( ) + ( extra_counts as f64 * average) . round ( ) as u64 ,
381+ ) ,
382+ average_time : Some ( average) ,
383+ std_deviation : Some ( std_dev) ,
384+ quartiles : Some ( quartiles) ,
385+ }
386+ } ,
387+ )
388+ . collect :: < Vec < _ > > ( )
389+ }
0 commit comments