Skip to content

Commit 474462f

Browse files
avi-starkwareclaude
andcommitted
add run_with_libfunc_profile + AotWithProgram variant for ContractExecutor
Exposes the libfunc-profiling primitives that downstream consumers (e.g. the blockifier in starkware-libs/sequencer) currently maintain locally. Profile collection is callback-driven so the per-call key (tx hash, etc.) stays out of cairo-native. - metadata::profiler::Profile is now pub (was a private type alias). - AotContractExecutor::run_with_libfunc_profile<H, F> (gated on with-libfunc-profiling, in new file src/executor/libfunc_profile.rs) wraps run: allocates a unique trace ID, points the executor's cairo_native__profiler__profile_id symbol at it, drains the resulting Profile after run returns, and hands it to a caller-supplied FnOnce(Profile). A ProfilerGuard restores the previous trace ID and drops the LIBFUNC_PROFILE slot on both the success and unwind paths. - ContractExecutor::AotWithProgram(AotWithProgram { executor, program }) is a new variant that bundles an AOT executor with the Sierra program it was built from. From<AotWithProgram> is provided. - ContractExecutor::run dispatches the new variant via run_with_libfunc_profile with a no-op profile callback. - ContractExecutor::run_with_profile<H, F> is the profile-capturing counterpart of run; for non-AotWithProgram variants it falls through to run (callback never fires). Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
1 parent 98ed085 commit 474462f

4 files changed

Lines changed: 185 additions & 3 deletions

File tree

src/executor.rs

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,8 @@ pub use self::{
99
};
1010
#[cfg(feature = "sierra-emu")]
1111
pub use self::contract_executor::EmuContractInfo;
12+
#[cfg(feature = "with-libfunc-profiling")]
13+
pub use self::contract_executor::AotWithProgram;
1214
use crate::{
1315
arch::{AbiArgument, ValueWithInfoWrapper},
1416
error::{panic::ToNativeAssertError, Error},
@@ -46,6 +48,8 @@ mod aot;
4648
mod contract;
4749
mod contract_executor;
4850
mod jit;
51+
#[cfg(feature = "with-libfunc-profiling")]
52+
mod libfunc_profile;
4953

5054
#[cfg(target_arch = "aarch64")]
5155
global_asm!(include_str!("arch/aarch64.s"));

src/executor/contract_executor.rs

Lines changed: 68 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -5,19 +5,21 @@
55
//! `H: StarknetSyscallHandler` — sierra-emu and cairo-native re-export the trait from
66
//! `cairo-native-syscalls`, so no adapter is needed.
77
8-
#[cfg(feature = "sierra-emu")]
8+
#[cfg(any(feature = "sierra-emu", feature = "with-libfunc-profiling"))]
99
use cairo_lang_sierra::program::Program;
1010
#[cfg(feature = "sierra-emu")]
1111
use cairo_lang_starknet_classes::compiler_version::VersionId;
1212
#[cfg(feature = "sierra-emu")]
1313
use cairo_lang_starknet_classes::contract_class::ContractEntryPoints;
1414
use starknet_types_core::felt::Felt;
15-
#[cfg(feature = "sierra-emu")]
15+
#[cfg(any(feature = "sierra-emu", feature = "with-libfunc-profiling"))]
1616
use std::sync::Arc;
1717

1818
use crate::error::Result;
1919
use crate::execution_result::ContractExecutionResult;
2020
use crate::executor::AotContractExecutor;
21+
#[cfg(feature = "with-libfunc-profiling")]
22+
use crate::metadata::profiler::Profile;
2123
use crate::starknet::StarknetSyscallHandler;
2224
use crate::utils::BuiltinCosts;
2325

@@ -31,6 +33,8 @@ pub enum ContractExecutor {
3133
Aot(AotContractExecutor),
3234
#[cfg(feature = "sierra-emu")]
3335
Emu(EmuContractInfo),
36+
#[cfg(feature = "with-libfunc-profiling")]
37+
AotWithProgram(AotWithProgram),
3438
}
3539

3640
/// Inputs required to construct a `sierra_emu::VirtualMachine` for the `Emu` variant.
@@ -42,6 +46,16 @@ pub struct EmuContractInfo {
4246
pub sierra_version: VersionId,
4347
}
4448

49+
/// AOT executor paired with the Sierra program it was built from. Required by
50+
/// [`ContractExecutor::run_with_profile`] so libfunc samples can be resolved against
51+
/// the program's declarations.
52+
#[cfg(feature = "with-libfunc-profiling")]
53+
#[derive(Debug)]
54+
pub struct AotWithProgram {
55+
pub executor: AotContractExecutor,
56+
pub program: Arc<Program>,
57+
}
58+
4559
impl From<AotContractExecutor> for ContractExecutor {
4660
fn from(value: AotContractExecutor) -> Self {
4761
Self::Aot(value)
@@ -55,6 +69,13 @@ impl From<EmuContractInfo> for ContractExecutor {
5569
}
5670
}
5771

72+
#[cfg(feature = "with-libfunc-profiling")]
73+
impl From<AotWithProgram> for ContractExecutor {
74+
fn from(value: AotWithProgram) -> Self {
75+
Self::AotWithProgram(value)
76+
}
77+
}
78+
5879
impl ContractExecutor {
5980
/// Run the contract entry point identified by `selector`.
6081
///
@@ -101,6 +122,51 @@ impl ContractExecutor {
101122
builtin_stats: Default::default(),
102123
})
103124
}
125+
#[cfg(feature = "with-libfunc-profiling")]
126+
ContractExecutor::AotWithProgram(AotWithProgram { executor, program }) => executor
127+
.run_with_libfunc_profile(
128+
program,
129+
selector,
130+
args,
131+
gas,
132+
builtin_costs,
133+
syscall_handler,
134+
// Profile is collected and dropped on this path. Use
135+
// `run_with_profile` to capture it.
136+
|_profile| {},
137+
),
138+
}
139+
}
140+
141+
/// Like [`Self::run`] but, for the `AotWithProgram` variant, hands the captured
142+
/// libfunc profile to `on_profile` after the call returns successfully. For other
143+
/// variants this is identical to `run` and `on_profile` is never invoked.
144+
#[cfg(feature = "with-libfunc-profiling")]
145+
pub fn run_with_profile<H, F>(
146+
&self,
147+
selector: Felt,
148+
args: &[Felt],
149+
gas: u64,
150+
builtin_costs: Option<BuiltinCosts>,
151+
syscall_handler: H,
152+
on_profile: F,
153+
) -> Result<ContractExecutionResult>
154+
where
155+
H: StarknetSyscallHandler,
156+
F: FnOnce(Profile),
157+
{
158+
match self {
159+
ContractExecutor::AotWithProgram(AotWithProgram { executor, program }) => executor
160+
.run_with_libfunc_profile(
161+
program,
162+
selector,
163+
args,
164+
gas,
165+
builtin_costs,
166+
syscall_handler,
167+
on_profile,
168+
),
169+
_ => self.run(selector, args, gas, builtin_costs, syscall_handler),
104170
}
105171
}
106172
}

src/executor/libfunc_profile.rs

Lines changed: 112 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,112 @@
1+
//! Profiling-instrumented run wrapper around [`AotContractExecutor::run`].
2+
//!
3+
//! Available under the `with-libfunc-profiling` feature.
4+
5+
#![cfg(feature = "with-libfunc-profiling")]
6+
7+
use std::sync::atomic::{AtomicU64, Ordering};
8+
use std::sync::Arc;
9+
10+
use cairo_lang_sierra::program::Program;
11+
use starknet_types_core::felt::Felt;
12+
13+
use crate::error::Result;
14+
use crate::execution_result::ContractExecutionResult;
15+
use crate::executor::AotContractExecutor;
16+
use crate::metadata::profiler::{Profile, ProfilerBinding, ProfilerImpl, LIBFUNC_PROFILE};
17+
use crate::starknet::StarknetSyscallHandler;
18+
use crate::utils::BuiltinCosts;
19+
20+
impl AotContractExecutor {
21+
/// Run the entrypoint with libfunc-level profiling instrumentation.
22+
///
23+
/// Wraps [`AotContractExecutor::run`] with the bookkeeping the
24+
/// `with-libfunc-profiling` runtime needs:
25+
///
26+
/// 1. Allocates a unique trace ID and inserts an empty `ProfilerImpl` slot in
27+
/// [`LIBFUNC_PROFILE`].
28+
/// 2. Points the executor's `cairo_native__profiler__profile_id` symbol at the new
29+
/// trace ID, saving the previous value.
30+
/// 3. Calls `run`. Per-statement samples accumulate in the slot via the runtime
31+
/// `push_stmt` callback.
32+
/// 4. Drains the slot, calls [`ProfilerImpl::get_profile`] with `program`, and hands
33+
/// the resulting [`Profile`] to `on_profile`.
34+
/// 5. A [`ProfilerGuard`] restores the previous trace ID — and removes the slot if
35+
/// the success path didn't — on both success and unwind paths.
36+
///
37+
/// `program` must be the Sierra program this executor was compiled from; it's used
38+
/// by `get_profile` to map runtime libfunc IDs back to declarations.
39+
///
40+
/// Profiling is intended to run single-threaded; concurrent calls would race on the
41+
/// global `trace_id` symbol.
42+
pub fn run_with_libfunc_profile<H, F>(
43+
&self,
44+
program: &Arc<Program>,
45+
selector: Felt,
46+
args: &[Felt],
47+
gas: u64,
48+
builtin_costs: Option<BuiltinCosts>,
49+
syscall_handler: H,
50+
on_profile: F,
51+
) -> Result<ContractExecutionResult>
52+
where
53+
H: StarknetSyscallHandler,
54+
F: FnOnce(Profile),
55+
{
56+
static COUNTER: AtomicU64 = AtomicU64::new(0);
57+
let counter = COUNTER.fetch_add(1, Ordering::Relaxed);
58+
59+
LIBFUNC_PROFILE
60+
.lock()
61+
.unwrap()
62+
.insert(counter, ProfilerImpl::new());
63+
64+
// The pointer targets a global symbol in the executor's shared library; it lives
65+
// for the executor's lifetime. Single-threaded profiling means no concurrent writer.
66+
let trace_id_ptr = self
67+
.find_symbol_ptr(ProfilerBinding::ProfileId.symbol())
68+
.unwrap()
69+
.cast::<u64>();
70+
// SAFETY: see above. Read/write to a non-null, properly-aligned `*mut u64`.
71+
let old_trace_id = unsafe { *trace_id_ptr };
72+
unsafe {
73+
*trace_id_ptr = counter;
74+
}
75+
76+
// Restore on the success path AND on unwind. On success the caller drains the
77+
// slot below; the guard's `remove` is then a no-op.
78+
let _guard = ProfilerGuard {
79+
trace_id_ptr,
80+
old_trace_id,
81+
counter,
82+
};
83+
84+
let result = self.run(selector, args, gas, builtin_costs, syscall_handler);
85+
86+
let profiler = LIBFUNC_PROFILE.lock().unwrap().remove(&counter).unwrap();
87+
on_profile(profiler.get_profile(program));
88+
89+
result
90+
}
91+
}
92+
93+
/// RAII cleanup for the profiler globals. Restores `*trace_id_ptr` and drops the
94+
/// `LIBFUNC_PROFILE` slot at `counter` if it's still occupied.
95+
struct ProfilerGuard {
96+
trace_id_ptr: *mut u64,
97+
old_trace_id: u64,
98+
counter: u64,
99+
}
100+
101+
impl Drop for ProfilerGuard {
102+
fn drop(&mut self) {
103+
// SAFETY: same provenance as the construction site; single-threaded use.
104+
unsafe {
105+
*self.trace_id_ptr = self.old_trace_id;
106+
}
107+
// Tolerate a poisoned mutex silently — Drop must not panic.
108+
if let Ok(mut profile) = LIBFUNC_PROFILE.lock() {
109+
profile.remove(&self.counter);
110+
}
111+
}
112+
}

src/metadata/profiler.rs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -330,7 +330,7 @@ impl ProfilerMeta {
330330
/// Represents the entire profile of the execution.
331331
///
332332
/// It maps the libfunc ID to a libfunc profile.
333-
type Profile = HashMap<ConcreteLibfuncId, LibfuncProfileData>;
333+
pub type Profile = HashMap<ConcreteLibfuncId, LibfuncProfileData>;
334334

335335
/// Represents the profile data for a particular libfunc.
336336
#[derive(Default)]

0 commit comments

Comments
 (0)