Skip to content

Commit 26f1f91

Browse files
Implement libfunc profiling (squash) (#1253)
This commit is a squash of the whole #946 PR (#946)
1 parent b92123c commit 26f1f91

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

42 files changed

+1000
-403
lines changed

.github/workflows/starknet-blocks.yml

Lines changed: 2 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -32,7 +32,7 @@ jobs:
3232
with:
3333
repository: lambdaclass/starknet-replay
3434
path: starknet-replay
35-
ref: 1b8e2e0be21a8df9f5f6b8f8514d1a40b456ef58
35+
ref: d36491aa5fca3f48b4d7fb25eba599603ff48225
3636
# We need native to use the linux deps ci action
3737
- name: Checkout Native
3838
uses: actions/checkout@v4
@@ -43,8 +43,7 @@ jobs:
4343
with:
4444
repository: lambdaclass/sequencer
4545
path: sequencer
46-
ref: 40331042c1149f5cb84b27f9dd8d47994a010bbe
47-
46+
ref: 14be65ca995ac702bad26ac20f2a522d9515f70a
4847
- name: Cache RPC Calls
4948
uses: actions/cache@v4.2.0
5049
with:

Cargo.toml

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -60,6 +60,7 @@ scarb = ["build-cli", "dep:scarb-ui", "dep:scarb-metadata"]
6060
with-cheatcode = []
6161
with-debug-utils = []
6262
with-mem-tracing = []
63+
with-libfunc-profiling = []
6364
with-segfault-catcher = []
6465
with-trace-dump = ["dep:sierra-emu"]
6566

src/bin/cairo-native-run.rs

Lines changed: 182 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,14 +3,20 @@ use cairo_lang_compiler::{
33
compile_prepared_db, db::RootDatabase, project::setup_project, CompilerConfig,
44
};
55
use cairo_lang_runner::short_string::as_cairo_short_string;
6+
#[cfg(feature = "with-libfunc-profiling")]
7+
use cairo_lang_sierra::ids::ConcreteLibfuncId;
68
use cairo_lang_sierra_to_casm::metadata::MetadataComputationConfig;
9+
#[cfg(feature = "with-libfunc-profiling")]
10+
use cairo_native::metadata::profiler::LibfuncProfileData;
711
use cairo_native::{
812
context::NativeContext,
913
executor::{AotNativeExecutor, JitNativeExecutor},
1014
metadata::gas::GasMetadata,
1115
starknet_stub::StubSyscallHandler,
1216
};
1317
use clap::{Parser, ValueEnum};
18+
#[cfg(feature = "with-libfunc-profiling")]
19+
use std::collections::HashMap;
1420
use std::path::PathBuf;
1521
use tracing_subscriber::{EnvFilter, FmtSubscriber};
1622
use 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+
}

src/compiler.rs

Lines changed: 45 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -550,26 +550,6 @@ fn compile_func(
550550

551551
let (state, _) = edit_state::take_args(state, invocation.args.iter())?;
552552

553-
let helper = LibfuncHelper {
554-
module,
555-
init_block: &pre_entry_block,
556-
region: &region,
557-
blocks_arena: &blocks_arena,
558-
last_block: Cell::new(block),
559-
branches: generate_branching_targets(
560-
&blocks,
561-
statements,
562-
statement_idx,
563-
invocation,
564-
&state,
565-
),
566-
results: invocation
567-
.branches
568-
.iter()
569-
.map(|x| vec![Cell::new(None); x.results.len()])
570-
.collect::<Vec<_>>(),
571-
};
572-
573553
let libfunc = registry.get_libfunc(&invocation.libfunc_id)?;
574554
if is_recursive {
575555
if let Some(target) = libfunc.is_function_call() {
@@ -620,6 +600,46 @@ fn compile_func(
620600
}
621601
}
622602

603+
#[allow(unused_mut)]
604+
let mut helper = LibfuncHelper {
605+
module,
606+
init_block: &pre_entry_block,
607+
region: &region,
608+
blocks_arena: &blocks_arena,
609+
last_block: Cell::new(block),
610+
branches: generate_branching_targets(
611+
&blocks,
612+
statements,
613+
statement_idx,
614+
invocation,
615+
&state,
616+
),
617+
results: invocation
618+
.branches
619+
.iter()
620+
.map(|x| vec![Cell::new(None); x.results.len()])
621+
.collect::<Vec<_>>(),
622+
623+
#[cfg(feature = "with-libfunc-profiling")]
624+
profiler: match libfunc {
625+
CoreConcreteLibfunc::FunctionCall(_) => {
626+
// Tail-recursive function calls are broken beacuse a stack of timestamps is required,
627+
// which would invalidate tail recursion. Also, since each libfunc is measured individually,
628+
// it doesn't make sense to take function calls into account, therefore it's ignored on purpose.
629+
None
630+
}
631+
_ => match metadata.remove::<crate::metadata::profiler::ProfilerMeta>()
632+
{
633+
Some(profiler_meta) => {
634+
let t0 = profiler_meta
635+
.measure_timestamp(context, block, location)?;
636+
Some((profiler_meta, statement_idx, t0))
637+
}
638+
None => None,
639+
},
640+
},
641+
};
642+
623643
libfunc.build(
624644
context,
625645
registry,
@@ -651,6 +671,11 @@ fn compile_func(
651671
libfunc_name
652672
);
653673

674+
#[cfg(feature = "with-libfunc-profiling")]
675+
if let Some((profiler_meta, _, _)) = helper.profiler.take() {
676+
metadata.insert(profiler_meta);
677+
}
678+
654679
if let Some(tailrec_meta) = metadata.remove::<TailRecursionMeta>() {
655680
if let Some(return_block) = tailrec_meta.return_target() {
656681
tailrec_state = Some((tailrec_meta.depth_counter(), return_block));

src/context.rs

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -163,6 +163,9 @@ impl NativeContext {
163163
// already some metadata of the same type.
164164
metadata.insert(gas_metadata);
165165

166+
#[cfg(feature = "with-libfunc-profiling")]
167+
metadata.insert(crate::metadata::profiler::ProfilerMeta::new());
168+
166169
// Create the Sierra program registry
167170
let registry = ProgramRegistry::<CoreType, CoreLibfunc>::new(program)?;
168171

src/executor/aot.rs

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -60,6 +60,9 @@ impl AotNativeExecutor {
6060
#[cfg(feature = "with-trace-dump")]
6161
crate::metadata::trace_dump::setup_runtime(|name| executor.find_symbol_ptr(name));
6262

63+
#[cfg(feature = "with-libfunc-profiling")]
64+
crate::metadata::profiler::setup_runtime(|name| executor.find_symbol_ptr(name));
65+
6366
executor
6467
}
6568

src/executor/contract.rs

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -330,6 +330,9 @@ impl AotContractExecutor {
330330
#[cfg(feature = "with-trace-dump")]
331331
crate::metadata::trace_dump::setup_runtime(|name| executor.find_symbol_ptr(name));
332332

333+
#[cfg(feature = "with-libfunc-profiling")]
334+
crate::metadata::profiler::setup_runtime(|name| executor.find_symbol_ptr(name));
335+
333336
Ok(Some(executor))
334337
}
335338

src/executor/jit.rs

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -71,6 +71,9 @@ impl<'m> JitNativeExecutor<'m> {
7171
#[cfg(feature = "with-trace-dump")]
7272
crate::metadata::trace_dump::setup_runtime(|name| executor.find_symbol_ptr(name));
7373

74+
#[cfg(feature = "with-libfunc-profiling")]
75+
crate::metadata::profiler::setup_runtime(|name| executor.find_symbol_ptr(name));
76+
7477
Ok(executor)
7578
}
7679

0 commit comments

Comments
 (0)