Skip to content

Commit c7e5bcc

Browse files
avi-starkwareclaude
andcommitted
address review: ArcProgram alias, profiling deadlock/FFI/instrumentation fixes
Review-driven fixes to the ContractExecutor libfunc-profiling path: - ArcProgram alias (orizi review on src/executor.rs): replace the bare `pub use ...::Program` re-export with `pub type ArcProgram = Arc<Program>`, since profiling/sierra-emu consumers only ever hold the program as a shared handle. Gated on `any(sierra-emu, with-libfunc-profiling)` so EmuContractInfo consumers can name the field type too, and adopted for EmuContractInfo.program, AotWithProgram.program, and the run_with_profile callback. - Self-deadlock: run_with_libfunc_profile held the non-reentrant process-wide PROFILE_LOCK across self.run, which re-enters this function on the same thread whenever a profiled contract calls another contract -- a guaranteed hang. Take the lock only at the outermost profiled frame per thread (tracked via a thread-local PROFILE_DEPTH); nested frames inherit the outer frame's lock, and the existing per-call old_trace_id save/restore keeps the global trace-id symbol correct across nesting. Cross-thread isolation is preserved (the outermost frame holds the lock for the whole run tree). A ProfileDepthGuard decrements the depth on every exit path including unwind. - FFI panic-safety: ProfilerImpl::push_stmt is a `pub extern "C" fn` invoked directly from compiled Cairo code; its `.lock().unwrap()` would unwind across the C ABI on a poisoned mutex. Recover from poison instead, matching the lock handling in run_with_libfunc_profile. - Instrumented compiler binary: add a `with-libfunc-profiling` feature to starknet-native-compile forwarding to `cairo-native/with-libfunc-profiling`, so an instrumented binary (whose emitted .so carries the profiler symbol that run_with_libfunc_profile looks up) can be built. A binary built without it produces .so files with no profiler symbol, making every profiled call fail. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
1 parent 02ac46f commit c7e5bcc

5 files changed

Lines changed: 60 additions & 18 deletions

File tree

binaries/starknet-native-compile/Cargo.toml

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,13 @@ edition.workspace = true
77
license.workspace = true
88
repository.workspace = true
99

10+
[features]
11+
# Builds an instrumented compiler whose emitted `.so` carries the libfunc-profiling
12+
# symbols that `cairo_native::AotContractExecutor::run_with_libfunc_profile` looks up.
13+
# Install with `cargo install ... --features with-libfunc-profiling` to profile native
14+
# execution; a binary built without it produces `.so`s with no profiler symbol.
15+
with-libfunc-profiling = ["cairo-native/with-libfunc-profiling"]
16+
1017
[dependencies]
1118
anyhow.workspace = true
1219
cairo-lang-sierra.workspace = true

src/executor.rs

Lines changed: 5 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -28,10 +28,11 @@ use crate::{
2828
values::Value,
2929
};
3030
use bumpalo::Bump;
31-
// Re-exported so libfunc-profiling consumers (e.g. blockifier) can refer to the
32-
// program type without adding a direct `cairo-lang-sierra` dependency.
33-
#[cfg(feature = "with-libfunc-profiling")]
34-
pub use cairo_lang_sierra::program::Program;
31+
// Profiling and sierra-emu consumers (e.g. blockifier) only ever hold the Sierra
32+
// program as a shared handle. Export the `Arc` alias so they can name it without
33+
// taking a direct `cairo-lang-sierra` dependency.
34+
#[cfg(any(feature = "sierra-emu", feature = "with-libfunc-profiling"))]
35+
pub type ArcProgram = std::sync::Arc<cairo_lang_sierra::program::Program>;
3536
use cairo_lang_sierra::{
3637
extensions::{
3738
circuit::CircuitTypeConcrete,

src/executor/contract_executor.rs

Lines changed: 6 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -5,8 +5,6 @@
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(any(feature = "sierra-emu", feature = "with-libfunc-profiling"))]
9-
use cairo_lang_sierra::program::Program;
108
#[cfg(feature = "sierra-emu")]
119
use cairo_lang_starknet_classes::compiler_version::VersionId;
1210
#[cfg(feature = "sierra-emu")]
@@ -20,6 +18,8 @@ use crate::error::Error;
2018
use crate::error::Result;
2119
use crate::execution_result::ContractExecutionResult;
2220
use crate::executor::AotContractExecutor;
21+
#[cfg(any(feature = "sierra-emu", feature = "with-libfunc-profiling"))]
22+
use crate::executor::ArcProgram;
2323
#[cfg(feature = "with-libfunc-profiling")]
2424
use crate::metadata::profiler::Profile;
2525
use crate::starknet::StarknetSyscallHandler;
@@ -43,7 +43,7 @@ pub enum ContractExecutor {
4343
#[cfg(feature = "sierra-emu")]
4444
#[derive(Debug, Clone)]
4545
pub struct EmuContractInfo {
46-
pub program: Arc<Program>,
46+
pub program: ArcProgram,
4747
pub entry_points: ContractEntryPoints,
4848
pub sierra_version: VersionId,
4949
}
@@ -55,7 +55,7 @@ pub struct EmuContractInfo {
5555
#[derive(Debug)]
5656
pub struct AotWithProgram {
5757
pub executor: AotContractExecutor,
58-
pub program: Arc<Program>,
58+
pub program: ArcProgram,
5959
}
6060

6161
impl From<AotContractExecutor> for ContractExecutor {
@@ -143,7 +143,7 @@ impl ContractExecutor {
143143
}
144144

145145
/// Like [`Self::run`] but, for the `AotWithProgram` variant, hands the captured
146-
/// libfunc profile -- together with the `Arc<Program>` the executor was paired with --
146+
/// libfunc profile -- together with the `ArcProgram` the executor was paired with --
147147
/// to `on_profile` after the call returns successfully. The program is included so
148148
/// callers don't have to keep their own copy around just to resolve libfunc samples.
149149
/// For other variants this is identical to `run` and `on_profile` is never invoked.
@@ -159,7 +159,7 @@ impl ContractExecutor {
159159
) -> Result<ContractExecutionResult>
160160
where
161161
H: StarknetSyscallHandler,
162-
F: FnOnce(Profile, Arc<Program>),
162+
F: FnOnce(Profile, ArcProgram),
163163
{
164164
match self {
165165
ContractExecutor::AotWithProgram(AotWithProgram { executor, program }) => {

src/executor/libfunc_profile.rs

Lines changed: 37 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@
33
//! Available under the `with-libfunc-profiling` feature (gated at the `mod`
44
//! declaration in `src/executor.rs`).
55
6+
use std::cell::Cell;
67
use std::sync::atomic::{AtomicU64, Ordering};
78
use std::sync::{Arc, Mutex};
89

@@ -16,11 +17,23 @@ use crate::metadata::profiler::{Profile, ProfilerBinding, ProfilerImpl, LIBFUNC_
1617
use crate::starknet::StarknetSyscallHandler;
1718
use crate::utils::BuiltinCosts;
1819

19-
/// Process-wide lock that serializes calls into [`AotContractExecutor::run_with_libfunc_profile`].
20-
/// The profiler hot-swaps a process-global symbol (`cairo_native__profiler__profile_id`);
21-
/// concurrent callers would race on that write and on the [`LIBFUNC_PROFILE`] slot bookkeeping.
20+
/// Process-wide lock that serializes *top-level* profiled runs across threads. The
21+
/// profiler hot-swaps a process-global symbol (`cairo_native__profiler__profile_id`);
22+
/// a concurrent thread would race on that write and on the [`LIBFUNC_PROFILE`] slot
23+
/// bookkeeping. It is acquired only by the outermost profiled frame on each thread
24+
/// (see [`PROFILE_DEPTH`]); nested same-thread calls -- a profiled contract invoking
25+
/// another contract -- re-enter without re-locking, which would otherwise deadlock
26+
/// this non-reentrant mutex.
2227
static PROFILE_LOCK: Mutex<()> = Mutex::new(());
2328

29+
thread_local! {
30+
/// Nesting depth of profiled runs on the current thread. Only the outermost frame
31+
/// (depth 0) takes [`PROFILE_LOCK`]; deeper frames rely on the lock the outer frame
32+
/// already holds. The per-call `old_trace_id` save/restore keeps the global trace-id
33+
/// symbol correct across nesting without any additional locking.
34+
static PROFILE_DEPTH: Cell<usize> = const { Cell::new(0) };
35+
}
36+
2437
impl AotContractExecutor {
2538
/// Run the entrypoint with libfunc-level profiling instrumentation.
2639
///
@@ -60,10 +73,17 @@ impl AotContractExecutor {
6073
H: StarknetSyscallHandler,
6174
F: FnOnce(Profile),
6275
{
63-
// Serialize against concurrent profile calls. Recover from a poisoned lock --
64-
// we don't have invariants on the protected state itself; the lock only gates
65-
// access to the global trace-id symbol.
66-
let _profile_lock = PROFILE_LOCK.lock().unwrap_or_else(|e| e.into_inner());
76+
// Acquire the cross-thread lock only at the outermost profiled frame on this
77+
// thread. A profiled contract that calls another contract re-enters this
78+
// function on the same thread; re-locking the non-reentrant `PROFILE_LOCK`
79+
// there would self-deadlock, so nested frames inherit the outer frame's lock.
80+
// Recover from a poisoned lock -- it only gates access to the global trace-id
81+
// symbol, on which we hold no data invariants.
82+
let _profile_lock = PROFILE_DEPTH
83+
.with(|depth| depth.get() == 0)
84+
.then(|| PROFILE_LOCK.lock().unwrap_or_else(|e| e.into_inner()));
85+
PROFILE_DEPTH.with(|depth| depth.set(depth.get() + 1));
86+
let _depth_guard = ProfileDepthGuard;
6787

6888
// Look up the profile-id symbol before touching any global state. If the
6989
// executor wasn't compiled with libfunc-profiling instrumentation, the
@@ -147,3 +167,13 @@ impl Drop for ProfilerGuard {
147167
}
148168
}
149169
}
170+
171+
/// Decrements [`PROFILE_DEPTH`] on every exit path (including unwind) so the
172+
/// outermost-frame lock bookkeeping stays correct even if the profiled run panics.
173+
struct ProfileDepthGuard;
174+
175+
impl Drop for ProfileDepthGuard {
176+
fn drop(&mut self) {
177+
PROFILE_DEPTH.with(|depth| depth.set(depth.get().saturating_sub(1)));
178+
}
179+
}

src/metadata/profiler.rs

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -362,7 +362,11 @@ impl ProfilerImpl {
362362

363363
// Push a profiler frame
364364
pub extern "C" fn push_stmt(profile_id: u64, statement_idx: u64, tick_delta: u64) {
365-
let mut profiler = LIBFUNC_PROFILE.lock().unwrap();
365+
// Invoked directly from compiled/JIT'd Cairo code across the C ABI: a panic
366+
// here (e.g. `.unwrap()` on a poisoned mutex) would unwind into machine code
367+
// that cannot handle it. Recover from poison instead of panicking, matching
368+
// the lock handling in `run_with_libfunc_profile`.
369+
let mut profiler = LIBFUNC_PROFILE.lock().unwrap_or_else(|e| e.into_inner());
366370

367371
let Some(profiler) = profiler.get_mut(&profile_id) else {
368372
eprintln!("Could not find libfunc profiler!");

0 commit comments

Comments
 (0)