Skip to main content

revmc_runtime/runtime/
config.rs

1//! Runtime configuration.
2
3use crate::{CompileTimings, eyre, runtime::storage::ArtifactStore};
4use alloy_primitives::B256;
5use revm_context_interface::cfg::GasParams;
6use revm_primitives::hardfork::SpecId;
7use std::{path::PathBuf, str::FromStr, sync::Arc, time::Duration};
8
9const JIT_MODE_ENV: &str = "REVMC_JIT_MODE";
10const JIT_HELPER_PATH_ENV: &str = "REVMC_JIT_HELPER_PATH";
11const JIT_HELPER_MEMORY_LIMIT_ENV: &str = "REVMC_JIT_HELPER_MEMORY_LIMIT_BYTES";
12const JIT_HELPER_CPU_COUNT_ENV: &str = "REVMC_JIT_HELPER_CPU_COUNT";
13const DEFAULT_JIT_MAX_BYTECODE_LEN: usize = 24 * 1024;
14
15/// Runtime configuration.
16#[derive(Clone, derive_more::Debug)]
17pub struct RuntimeConfig {
18    /// Whether compiled-code lookup is enabled.
19    ///
20    /// Defaults to `false` (safe rollout default).
21    pub enabled: bool,
22
23    /// Name for the backend thread.
24    ///
25    /// Defaults to `"revmc-backend"`.
26    pub thread_name: String,
27
28    /// Artifact store for loading precompiled AOT artifacts.
29    ///
30    /// `None` means no AOT preload—only JIT will populate the map (in later phases).
31    #[debug(skip)]
32    pub store: Option<Arc<dyn ArtifactStore>>,
33
34    /// Tuning knobs.
35    pub tuning: RuntimeTuning,
36
37    /// Base directory for compiler debug dumps.
38    ///
39    /// When set, the compiler dumps IR, assembly, and bytecode for each compiled contract
40    /// to `{dump_dir}/{spec_id}/{code_hash}/`.
41    ///
42    /// Defaults to `None` (no dumps).
43    pub dump_dir: Option<PathBuf>,
44
45    /// Enable debug assertions in compiled code.
46    ///
47    /// When `true`, the compiler inserts runtime checks (e.g. stack bounds)
48    /// that `panic!` on violation. Useful for diagnosing JIT correctness bugs.
49    ///
50    /// Defaults to `false`.
51    pub debug_assertions: bool,
52
53    /// Collapse every JIT failure path to a single
54    /// [`OutOfGas`](revm_interpreter::InstructionResult::OutOfGas) constant.
55    ///
56    /// Failures (stack under/overflow, invalid jump, real OOG, invalid opcode, etc.) are
57    /// semantically interchangeable for callers that only branch on success vs failure, so
58    /// this lets LLVM DCE the per-failure-site materialization and the failure-block phi.
59    /// Successful exits (`STOP`/`RETURN`/`REVERT`) keep their original codes.
60    ///
61    /// Useful for benchmarking the cost of failure-result materialization.
62    ///
63    /// Defaults to `true`.
64    pub single_error: bool,
65
66    /// Disable the block deduplication pass.
67    ///
68    /// When `true`, the dedup pass that merges identical basic blocks is skipped.
69    /// Useful for isolating dedup-related JIT correctness bugs.
70    ///
71    /// Defaults to `false`.
72    pub no_dedup: bool,
73
74    /// Disable the dead store elimination pass.
75    ///
76    /// When `true`, DSE is skipped. Useful for debugging JIT correctness issues
77    /// where DSE incorrectly eliminates live stack operations.
78    ///
79    /// Defaults to `false`.
80    pub no_dse: bool,
81
82    /// Custom gas parameters for compile-time gas folding.
83    ///
84    /// Overrides the default gas schedule derived from `spec_id` when compiling
85    /// bytecode. Useful for custom chains with non-standard gas costs (e.g.
86    /// modified SSTORE, CREATE, or EXP costs).
87    ///
88    /// When `None`, the compiler uses `GasParams::new_spec(spec_id)`.
89    ///
90    /// Defaults to `None`.
91    pub gas_params: Option<GasParams>,
92
93    /// AOT mode: observed misses are promoted to AOT compilation instead of JIT.
94    ///
95    /// Requires [`store`](Self::store) to be set.
96    ///
97    /// Defaults to `false`.
98    pub aot: bool,
99
100    /// Where JIT compilation work runs.
101    ///
102    /// Defaults to [`JitMode::InProcess`].
103    pub jit_mode: JitMode,
104
105    /// Helper executable used when [`jit_mode`](Self::jit_mode)
106    /// is [`JitMode::OutOfProcess`].
107    ///
108    /// When `None`, the runtime spawns `std::env::current_exe()` and expects it
109    /// to call [`super::maybe_run_jit_helper`] during startup.
110    ///
111    /// Defaults to `None`.
112    pub jit_helper_path: Option<PathBuf>,
113
114    /// Blocking mode: every lookup synchronously JIT-compiles on miss and never
115    /// falls back to the interpreter.
116    ///
117    /// When `true`, [`lookup()`](super::JitBackend::lookup) behaves like
118    /// [`lookup_blocking()`](super::JitBackend::lookup_blocking): if the
119    /// compiled function is not already resident, the calling thread blocks
120    /// until JIT compilation completes. This implies `enabled = true` and
121    /// `jit_hot_threshold = 0`.
122    ///
123    /// Intended for debugging and testing only — not for production use.
124    ///
125    /// Defaults to `false`.
126    pub blocking: bool,
127
128    /// Callback invoked after each compilation completes (success or failure).
129    ///
130    /// Defaults to `None`.
131    #[debug(skip)]
132    pub on_compilation: Option<Arc<dyn Fn(CompilationEvent) + Send + Sync>>,
133}
134
135/// Event emitted after a compilation attempt completes.
136#[derive(Clone, Debug)]
137pub struct CompilationEvent {
138    /// The code hash of the compiled bytecode.
139    pub code_hash: B256,
140    /// The hardfork spec the bytecode was compiled for.
141    pub spec_id: SpecId,
142    /// Wall-clock time spent compiling.
143    pub duration: Duration,
144    /// Whether this was a JIT or AOT compilation.
145    pub kind: CompilationKind,
146    /// Whether compilation succeeded.
147    pub success: bool,
148    /// Per-phase timing breakdown (translate, optimize, codegen).
149    pub timings: CompileTimings,
150}
151
152/// The kind of compilation that was performed.
153#[derive(Clone, Copy, Debug, PartialEq, Eq)]
154pub enum CompilationKind {
155    /// JIT compilation (in-memory function pointer).
156    Jit,
157    /// AOT compilation (shared library artifact).
158    Aot,
159}
160
161/// Where JIT compilation work runs.
162#[derive(Clone, Copy, Debug, Default, PartialEq, Eq)]
163pub enum JitMode {
164    /// Compile on background threads in this process.
165    #[default]
166    InProcess,
167    /// Compile in a helper process and link the result into this process.
168    ///
169    /// This is reserved for the out-of-process JIT implementation and is
170    /// disabled by default.
171    OutOfProcess,
172}
173
174impl FromStr for JitMode {
175    type Err = String;
176
177    fn from_str(s: &str) -> Result<Self, Self::Err> {
178        Ok(match s {
179            "in-process" => Self::InProcess,
180            "out-of-process" => Self::OutOfProcess,
181            _ => return Err(format!("unknown JIT mode: {s}")),
182        })
183    }
184}
185
186impl RuntimeConfig {
187    /// Applies runtime environment overrides.
188    ///
189    /// Recognized variables are `REVMC_JIT_MODE`, `REVMC_JIT_HELPER_PATH`,
190    /// `REVMC_JIT_HELPER_MEMORY_LIMIT_BYTES`, and `REVMC_JIT_HELPER_CPU_COUNT`.
191    pub fn with_env_overrides(mut self) -> eyre::Result<Self> {
192        if let Some(mode) = env_var(JIT_MODE_ENV) {
193            self.jit_mode = mode.parse().map_err(|e: String| eyre::eyre!("{JIT_MODE_ENV}: {e}"))?;
194        }
195        if let Some(path) = env_path(JIT_HELPER_PATH_ENV) {
196            self.jit_helper_path = Some(path);
197        }
198        if let Some(limit) = parse_env_u64(JIT_HELPER_MEMORY_LIMIT_ENV)? {
199            self.tuning.jit_helper_memory_limit_bytes = limit;
200        }
201        if let Some(count) = parse_env_usize(JIT_HELPER_CPU_COUNT_ENV)? {
202            self.tuning.jit_helper_cpu_count = count;
203        }
204        Ok(self)
205    }
206}
207
208impl Default for RuntimeConfig {
209    fn default() -> Self {
210        Self {
211            enabled: false,
212            thread_name: "revmc-backend".into(),
213            store: None,
214            tuning: RuntimeTuning::default(),
215            dump_dir: None,
216            debug_assertions: false,
217            single_error: true,
218            no_dedup: false,
219            no_dse: false,
220            gas_params: None,
221            aot: false,
222            jit_mode: JitMode::default(),
223            jit_helper_path: None,
224            blocking: false,
225            on_compilation: None,
226        }
227    }
228}
229
230/// Tuning knobs for the runtime.
231#[derive(Clone, Copy, Debug)]
232pub struct RuntimeTuning {
233    /// Capacity of the channel between API callers and the backend.
234    ///
235    /// Defaults to `4096`.
236    pub channel_capacity: usize,
237
238    /// Maximum lookup events processed per backend wakeup.
239    ///
240    /// Defaults to `4096`.
241    pub max_events_per_drain: usize,
242
243    /// Maximum delay between lookup observation and hotness accounting.
244    ///
245    /// Defaults to `100ms`.
246    pub event_drain_interval: Duration,
247
248    /// Timeout for joining the backend thread during shutdown.
249    ///
250    /// Defaults to `5s`.
251    pub shutdown_timeout: Duration,
252
253    /// Number of observed misses before a key is promoted to JIT compilation.
254    ///
255    /// Defaults to `8`.
256    pub jit_hot_threshold: usize,
257
258    /// Maximum bytecode length eligible for compilation. `0` = no limit.
259    ///
260    /// Defaults to `24 KiB`.
261    pub jit_max_bytecode_len: usize,
262
263    /// Maximum number of JIT compilation jobs in flight.
264    ///
265    /// Defaults to `2048`.
266    pub jit_max_pending_jobs: usize,
267
268    /// Number of JIT compilation worker threads.
269    ///
270    /// Defaults to `min(max(1, cpus/2), 4)`.
271    pub jit_worker_count: usize,
272
273    /// Timeout for a single out-of-process JIT compilation job.
274    ///
275    /// When exceeded, the helper process is killed and a fresh helper is spawned for
276    /// the next job. Only applies to [`JitMode::OutOfProcess`].
277    ///
278    /// Defaults to `5s`.
279    pub jit_timeout: Duration,
280
281    /// Maximum address space for the out-of-process JIT helper, in bytes.
282    ///
283    /// `0` disables the limit. On Unix this is applied with `RLIMIT_AS` before
284    /// the helper process starts executing.
285    ///
286    /// Defaults to `0`.
287    pub jit_helper_memory_limit_bytes: u64,
288
289    /// Maximum CPU count for the out-of-process JIT helper.
290    ///
291    /// `0` disables the limit. On Linux this limits the helper's CPU affinity
292    /// to the first N CPUs from the helper's current affinity mask before the
293    /// helper process starts executing.
294    ///
295    /// Defaults to `0`.
296    pub jit_helper_cpu_count: usize,
297
298    /// Capacity of the per-worker job queue.
299    ///
300    /// Defaults to `64`.
301    pub jit_worker_queue_capacity: usize,
302
303    /// Optimization level for JIT compilation.
304    ///
305    /// Defaults to [`OptimizationLevel::Default`](crate::OptimizationLevel::Default).
306    pub jit_opt_level: crate::OptimizationLevel,
307
308    /// Optimization level for AOT compilation.
309    ///
310    /// Defaults to [`OptimizationLevel::Default`](crate::OptimizationLevel::Default).
311    pub aot_opt_level: crate::OptimizationLevel,
312
313    /// Maximum total resident compiled code size in bytes. `0` = no limit.
314    ///
315    /// When exceeded, least-recently-used entries are evicted.
316    ///
317    /// Defaults to 1 GiB.
318    pub resident_code_cache_bytes: usize,
319
320    /// Duration after which a resident program with no lookup hits is evicted.
321    /// `None` disables idle eviction.
322    ///
323    /// Defaults to 600 seconds.
324    pub idle_evict_duration: Option<Duration>,
325
326    /// How often the backend runs eviction sweeps, if `idle_evict_duration` is set.
327    ///
328    /// Defaults to `60s`.
329    pub eviction_sweep_interval: Duration,
330
331    /// Number of compilations before recycling the compiler to reclaim
332    /// accumulated memory. `0` = never recycle.
333    ///
334    /// Defaults to `1000`.
335    pub compiler_recycle_threshold: usize,
336}
337
338impl RuntimeTuning {
339    /// Returns whether `bytecode` is eligible for JIT/AOT compilation.
340    #[inline]
341    pub fn should_compile(&self, bytecode: &[u8]) -> bool {
342        if bytecode.is_empty() {
343            return false;
344        }
345        if self.jit_max_bytecode_len > 0 && bytecode.len() > self.jit_max_bytecode_len {
346            return false;
347        }
348        true
349    }
350}
351
352impl Default for RuntimeTuning {
353    fn default() -> Self {
354        let cpus = std::thread::available_parallelism().map(|n| n.get()).unwrap_or(1);
355        let worker_count = cpus.div_ceil(2).clamp(1, 4);
356
357        Self {
358            channel_capacity: 4096,
359            max_events_per_drain: 4096,
360            event_drain_interval: Duration::from_millis(100),
361            shutdown_timeout: Duration::from_secs(5),
362            jit_hot_threshold: 8,
363            jit_max_bytecode_len: DEFAULT_JIT_MAX_BYTECODE_LEN,
364            jit_max_pending_jobs: 2048,
365            jit_worker_count: worker_count,
366            jit_timeout: Duration::from_secs(5),
367            jit_helper_memory_limit_bytes: 0,
368            jit_helper_cpu_count: 0,
369            jit_worker_queue_capacity: 64,
370            jit_opt_level: crate::OptimizationLevel::default(),
371            aot_opt_level: crate::OptimizationLevel::default(),
372            resident_code_cache_bytes: 1024 * 1024 * 1024,
373            idle_evict_duration: Some(Duration::from_secs(600)),
374            eviction_sweep_interval: Duration::from_secs(60),
375            compiler_recycle_threshold: 1000,
376        }
377    }
378}
379
380fn parse_env_u64(name: &str) -> eyre::Result<Option<u64>> {
381    let Some(value) = env_var(name) else { return Ok(None) };
382    value.parse().map(Some).map_err(|e| eyre::eyre!("{name}: {e}"))
383}
384
385fn parse_env_usize(name: &str) -> eyre::Result<Option<usize>> {
386    let Some(value) = env_var(name) else { return Ok(None) };
387    value.parse().map(Some).map_err(|e| eyre::eyre!("{name}: {e}"))
388}
389
390fn env_var(name: &str) -> Option<String> {
391    std::env::var(name).ok()
392}
393
394fn env_path(name: &str) -> Option<PathBuf> {
395    std::env::var_os(name).map(|path| {
396        let path = PathBuf::from(path);
397        path.canonicalize().unwrap_or(path)
398    })
399}