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 /// Defaults to `false`.
96 pub aot: bool,
97
98 /// Where JIT compilation work runs.
99 ///
100 /// Defaults to [`JitMode::InProcess`].
101 pub jit_mode: JitMode,
102
103 /// Helper executable used when [`jit_mode`](Self::jit_mode)
104 /// is [`JitMode::OutOfProcess`].
105 ///
106 /// When `None`, the runtime spawns `std::env::current_exe()` and expects it
107 /// to call [`super::maybe_run_jit_helper`] during startup.
108 ///
109 /// Defaults to `None`.
110 pub jit_helper_path: Option<PathBuf>,
111
112 /// Blocking mode: every lookup synchronously JIT-compiles on miss and never
113 /// falls back to the interpreter.
114 ///
115 /// When `true`, [`lookup()`](super::JitBackend::lookup) behaves like
116 /// [`lookup_blocking()`](super::JitBackend::lookup_blocking): if the
117 /// compiled function is not already resident, the calling thread blocks
118 /// until JIT compilation completes. This implies `enabled = true` and
119 /// `jit_hot_threshold = 0`.
120 ///
121 /// Intended for debugging and testing only — not for production use.
122 ///
123 /// Defaults to `false`.
124 pub blocking: bool,
125
126 /// Callback invoked after each compilation completes (success or failure).
127 ///
128 /// Defaults to `None`.
129 #[debug(skip)]
130 pub on_compilation: Option<Arc<dyn Fn(CompilationEvent) + Send + Sync>>,
131}
132
133/// Event emitted after a compilation attempt completes.
134#[derive(Clone, Debug)]
135pub struct CompilationEvent {
136 /// The code hash of the compiled bytecode.
137 pub code_hash: B256,
138 /// The hardfork spec the bytecode was compiled for.
139 pub spec_id: SpecId,
140 /// Wall-clock time spent compiling.
141 pub duration: Duration,
142 /// Whether this was a JIT or AOT compilation.
143 pub kind: CompilationKind,
144 /// Whether compilation succeeded.
145 pub success: bool,
146 /// Per-phase timing breakdown (translate, optimize, codegen).
147 pub timings: CompileTimings,
148}
149
150/// The kind of compilation that was performed.
151#[derive(Clone, Copy, Debug, PartialEq, Eq)]
152pub enum CompilationKind {
153 /// JIT compilation (in-memory function pointer).
154 Jit,
155 /// AOT compilation (shared library artifact).
156 Aot,
157}
158
159/// Where JIT compilation work runs.
160#[derive(Clone, Copy, Debug, Default, PartialEq, Eq)]
161pub enum JitMode {
162 /// Compile on background threads in this process.
163 #[default]
164 InProcess,
165 /// Compile in a helper process and link the result into this process.
166 ///
167 /// This is reserved for the out-of-process JIT implementation and is
168 /// disabled by default.
169 OutOfProcess,
170}
171
172impl FromStr for JitMode {
173 type Err = String;
174
175 fn from_str(s: &str) -> Result<Self, Self::Err> {
176 Ok(match s {
177 "in-process" => Self::InProcess,
178 "out-of-process" => Self::OutOfProcess,
179 _ => return Err(format!("unknown JIT mode: {s}")),
180 })
181 }
182}
183
184impl RuntimeConfig {
185 /// Applies runtime environment overrides.
186 ///
187 /// Recognized variables are `REVMC_JIT_MODE`, `REVMC_JIT_HELPER_PATH`,
188 /// `REVMC_JIT_HELPER_MEMORY_LIMIT_BYTES`, and `REVMC_JIT_HELPER_CPU_COUNT`.
189 pub fn with_env_overrides(mut self) -> eyre::Result<Self> {
190 if let Some(mode) = env_var(JIT_MODE_ENV) {
191 self.jit_mode = mode.parse().map_err(|e: String| eyre::eyre!("{JIT_MODE_ENV}: {e}"))?;
192 }
193 if let Some(path) = env_path(JIT_HELPER_PATH_ENV) {
194 self.jit_helper_path = Some(path);
195 }
196 if let Some(limit) = parse_env_u64(JIT_HELPER_MEMORY_LIMIT_ENV)? {
197 self.tuning.jit_helper_memory_limit_bytes = limit;
198 }
199 if let Some(count) = parse_env_usize(JIT_HELPER_CPU_COUNT_ENV)? {
200 self.tuning.jit_helper_cpu_count = count;
201 }
202 Ok(self)
203 }
204}
205
206impl Default for RuntimeConfig {
207 fn default() -> Self {
208 Self {
209 enabled: false,
210 thread_name: "revmc-backend".into(),
211 store: None,
212 tuning: RuntimeTuning::default(),
213 dump_dir: None,
214 debug_assertions: false,
215 single_error: true,
216 no_dedup: false,
217 no_dse: false,
218 gas_params: None,
219 aot: false,
220 jit_mode: JitMode::default(),
221 jit_helper_path: None,
222 blocking: false,
223 on_compilation: None,
224 }
225 }
226}
227
228/// Tuning knobs for the runtime.
229#[derive(Clone, Copy, Debug)]
230pub struct RuntimeTuning {
231 /// Capacity of the channel between API callers and the backend.
232 ///
233 /// Defaults to `4096`.
234 pub channel_capacity: usize,
235
236 /// Maximum lookup events processed per backend wakeup.
237 ///
238 /// Defaults to `4096`.
239 pub max_events_per_drain: usize,
240
241 /// Maximum delay between lookup observation and hotness accounting.
242 ///
243 /// Defaults to `100ms`.
244 pub event_drain_interval: Duration,
245
246 /// Timeout for joining the backend thread during shutdown.
247 ///
248 /// Defaults to `5s`.
249 pub shutdown_timeout: Duration,
250
251 /// Number of observed misses before a key is promoted to JIT compilation.
252 ///
253 /// Defaults to `8`.
254 pub jit_hot_threshold: usize,
255
256 /// Maximum bytecode length eligible for compilation. `0` = no limit.
257 ///
258 /// Defaults to `24 KiB`.
259 pub jit_max_bytecode_len: usize,
260
261 /// Maximum number of JIT compilation jobs in flight.
262 ///
263 /// Defaults to `2048`.
264 pub jit_max_pending_jobs: usize,
265
266 /// Number of JIT compilation worker threads.
267 ///
268 /// Defaults to `min(max(1, cpus/2), 4)`.
269 pub jit_worker_count: usize,
270
271 /// Timeout for a single out-of-process JIT compilation job.
272 ///
273 /// When exceeded, the helper process is killed and a fresh helper is spawned for
274 /// the next job. Only applies to [`JitMode::OutOfProcess`].
275 ///
276 /// Defaults to `5s`.
277 pub jit_timeout: Duration,
278
279 /// Maximum address space for the out-of-process JIT helper, in bytes.
280 ///
281 /// `0` disables the limit. On Unix this is applied with `RLIMIT_AS` before
282 /// the helper process starts executing.
283 ///
284 /// Defaults to `0`.
285 pub jit_helper_memory_limit_bytes: u64,
286
287 /// Maximum CPU count for the out-of-process JIT helper.
288 ///
289 /// `0` disables the limit. On Linux this limits the helper's CPU affinity
290 /// to the first N CPUs from the helper's current affinity mask before the
291 /// helper process starts executing.
292 ///
293 /// Defaults to `0`.
294 pub jit_helper_cpu_count: usize,
295
296 /// Capacity of the per-worker job queue.
297 ///
298 /// Defaults to `64`.
299 pub jit_worker_queue_capacity: usize,
300
301 /// Optimization level for JIT compilation.
302 ///
303 /// Defaults to [`OptimizationLevel::Default`](crate::OptimizationLevel::Default).
304 pub jit_opt_level: crate::OptimizationLevel,
305
306 /// Optimization level for AOT compilation.
307 ///
308 /// Defaults to [`OptimizationLevel::Default`](crate::OptimizationLevel::Default).
309 pub aot_opt_level: crate::OptimizationLevel,
310
311 /// Maximum total resident compiled code size in bytes. `0` = no limit.
312 ///
313 /// When exceeded, least-recently-used entries are evicted.
314 ///
315 /// Defaults to `0`.
316 pub resident_code_cache_bytes: usize,
317
318 /// Duration after which a resident program with no lookup hits is evicted.
319 /// `None` disables idle eviction.
320 ///
321 /// Defaults to `None`.
322 pub idle_evict_duration: Option<Duration>,
323
324 /// How often the backend runs eviction sweeps, if `idle_evict_duration` is set.
325 ///
326 /// Defaults to `60s`.
327 pub eviction_sweep_interval: Duration,
328
329 /// Number of compilations before recycling the compiler to reclaim
330 /// accumulated memory. `0` = never recycle.
331 ///
332 /// Defaults to `1000`.
333 pub compiler_recycle_threshold: usize,
334}
335
336impl RuntimeTuning {
337 /// Returns whether `bytecode` is eligible for JIT/AOT compilation.
338 #[inline]
339 pub fn should_compile(&self, bytecode: &[u8]) -> bool {
340 if bytecode.is_empty() {
341 return false;
342 }
343 if self.jit_max_bytecode_len > 0 && bytecode.len() > self.jit_max_bytecode_len {
344 return false;
345 }
346 true
347 }
348}
349
350impl Default for RuntimeTuning {
351 fn default() -> Self {
352 let cpus = std::thread::available_parallelism().map(|n| n.get()).unwrap_or(1);
353 let worker_count = cpus.div_ceil(2).clamp(1, 4);
354
355 Self {
356 channel_capacity: 4096,
357 max_events_per_drain: 4096,
358 event_drain_interval: Duration::from_millis(100),
359 shutdown_timeout: Duration::from_secs(5),
360 jit_hot_threshold: 8,
361 jit_max_bytecode_len: DEFAULT_JIT_MAX_BYTECODE_LEN,
362 jit_max_pending_jobs: 2048,
363 jit_worker_count: worker_count,
364 jit_timeout: Duration::from_secs(5),
365 jit_helper_memory_limit_bytes: 0,
366 jit_helper_cpu_count: 0,
367 jit_worker_queue_capacity: 64,
368 jit_opt_level: crate::OptimizationLevel::default(),
369 aot_opt_level: crate::OptimizationLevel::default(),
370 resident_code_cache_bytes: 1024 * 1024 * 1024,
371 idle_evict_duration: Some(Duration::from_secs(600)),
372 eviction_sweep_interval: Duration::from_secs(60),
373 compiler_recycle_threshold: 1000,
374 }
375 }
376}
377
378fn parse_env_u64(name: &str) -> eyre::Result<Option<u64>> {
379 let Some(value) = env_var(name) else { return Ok(None) };
380 value.parse().map(Some).map_err(|e| eyre::eyre!("{name}: {e}"))
381}
382
383fn parse_env_usize(name: &str) -> eyre::Result<Option<usize>> {
384 let Some(value) = env_var(name) else { return Ok(None) };
385 value.parse().map(Some).map_err(|e| eyre::eyre!("{name}: {e}"))
386}
387
388fn env_var(name: &str) -> Option<String> {
389 std::env::var(name).ok()
390}
391
392fn env_path(name: &str) -> Option<PathBuf> {
393 std::env::var_os(name).map(|path| {
394 let path = PathBuf::from(path);
395 path.canonicalize().unwrap_or(path)
396 })
397}