1use crate::{
9 EvmCompilerFn,
10 eyre::{self, WrapErr},
11};
12use api::LoadedLibrary;
13use backend::{Command, CompileJitRequest, EventQueue, PrepareAotRequest, ResidentMap};
14use crossbeam_channel as chan;
15use crossbeam_queue::ArrayQueue;
16use revm_primitives::{B256, hardfork::SpecId, hints_util::cold_path};
17use stats::RuntimeStats;
18use std::{
19 ops::ControlFlow,
20 sync::{
21 Arc,
22 atomic::{AtomicBool, AtomicUsize, Ordering},
23 },
24 time::Duration,
25};
26use worker::SyncNotifier;
27
28mod api;
29pub use api::{
30 AotRequest, CompiledProgram, InterpretReason, LookupDecision, LookupRequest, ProgramKind,
31};
32
33mod config;
34pub use config::{CompilationEvent, CompilationKind, JitMode, RuntimeConfig, RuntimeTuning};
35
36mod backend;
37
38mod stats;
39pub use stats::RuntimeStatsSnapshot;
40
41mod storage;
42pub use storage::{
43 ArtifactKey, ArtifactManifest, ArtifactStore, BackendSelection, RuntimeArtifactStore,
44 RuntimeCacheKey, StoredArtifact,
45};
46
47#[cfg(all(feature = "llvm", unix))]
48mod out_of_process;
49
50mod worker;
51
52pub fn maybe_run_jit_helper() -> eyre::Result<ControlFlow<()>> {
58 #[cfg(all(feature = "llvm", unix))]
59 {
60 out_of_process::maybe_run_jit_helper()
61 }
62 #[cfg(not(all(feature = "llvm", unix)))]
63 {
64 if std::env::var_os("REVMC_JIT_HELPER").is_some() {
65 eyre::bail!("out-of-process JIT helper is only available on Unix with LLVM")
66 }
67 Ok(ControlFlow::Continue(()))
68 }
69}
70
71#[cfg(test)]
72mod tests;
73
74#[derive(derive_more::Debug)]
83pub(crate) struct BackendShared {
84 #[debug(skip)]
86 resident: ResidentMap,
87 #[debug(skip)]
89 events: EventQueue,
90 pause_depth: AtomicUsize,
92 #[debug(skip)]
94 stats: Arc<RuntimeStats>,
95}
96
97#[derive(derive_more::Debug)]
99pub(crate) struct BackendInner {
100 shared: Arc<BackendShared>,
102 enabled: AtomicBool,
104 started: AtomicBool,
106 blocking: bool,
108 tuning: crate::runtime::config::RuntimeTuning,
110 #[debug(skip)]
114 tx: chan::Sender<Command>,
115 #[debug(skip)]
117 thread: std::sync::Mutex<Option<BackendThread>>,
118 shutdown_timeout: Duration,
120 #[debug(skip)]
122 lazy_spawn: std::sync::Mutex<Option<LazySpawnState>>,
123}
124
125struct LazySpawnState {
127 rx: chan::Receiver<Command>,
128 config: RuntimeConfig,
129}
130
131struct BackendThread {
133 handle: std::thread::JoinHandle<()>,
134 done_rx: chan::Receiver<()>,
135}
136
137#[derive(Clone, Debug)]
143pub struct JitBackend {
144 inner: Arc<BackendInner>,
145}
146
147impl JitBackend {
148 pub fn disabled() -> Self {
154 Self::new_inner(RuntimeConfig::default()).expect("default config cannot fail")
155 }
156
157 pub fn new(mut config: RuntimeConfig) -> eyre::Result<Self> {
163 config = config.with_env_overrides()?;
164 Self::new_inner(config)
165 }
166
167 fn new_inner(mut config: RuntimeConfig) -> eyre::Result<Self> {
168 if config.blocking {
169 config.enabled = true;
170 config.tuning.jit_hot_threshold = 0;
171 }
172 #[cfg(not(unix))]
173 if config.jit_mode == JitMode::OutOfProcess {
174 eyre::bail!("out-of-process JIT is only available on Unix");
175 }
176 if config.aot && config.store.is_none() {
177 eyre::bail!("AOT mode requires an artifact store");
178 }
179
180 let enabled = config.enabled;
181 let (tx, rx) = chan::bounded::<Command>(config.tuning.channel_capacity);
182 let events = ArrayQueue::new(config.tuning.channel_capacity);
183 let tuning = config.tuning;
184 let shared = Arc::new(BackendShared {
185 resident: ResidentMap::default(),
186 events,
187 pause_depth: AtomicUsize::new(0),
188 stats: Arc::new(RuntimeStats::default()),
189 });
190 let this = Self {
191 inner: Arc::new(BackendInner {
192 shared,
193 enabled: AtomicBool::new(false),
194 started: AtomicBool::new(false),
195 blocking: config.blocking,
196 tx,
197 thread: std::sync::Mutex::new(None),
198 shutdown_timeout: config.tuning.shutdown_timeout,
199 tuning,
200 lazy_spawn: std::sync::Mutex::new(Some(LazySpawnState { rx, config })),
201 }),
202 };
203 this.set_enabled(enabled)?;
204 Ok(this)
205 }
206
207 pub fn lookup(&self, mut req: LookupRequest) -> LookupDecision {
212 let inner = &*self.inner;
213 let shared = &*inner.shared;
214
215 if !inner.enabled.load(Ordering::Relaxed) {
216 cold_path();
217 return LookupDecision::Interpret(InterpretReason::Disabled);
218 }
219 if inner.blocking {
220 cold_path();
221 return self.lookup_blocking(req);
222 }
223 if !inner.tuning.should_compile(&req.code) {
224 cold_path();
225 return LookupDecision::Interpret(InterpretReason::Ineligible);
226 }
227
228 let decision = if let Some(program_ref) = shared.resident.try_get(&req.key).try_unwrap() {
229 let program = Arc::clone(&program_ref);
230 drop(program_ref);
231 req.code.clear();
232 LookupDecision::Compiled(program)
233 } else {
234 LookupDecision::Interpret(InterpretReason::NotReady)
235 };
236
237 if let Err(_v) = shared.events.push(req) {
238 cold_path();
239 shared.stats.events_dropped.fetch_add(1, Ordering::Relaxed);
240 }
241
242 decision
243 }
244
245 pub fn get_compiled(&self, code_hash: B256, spec_id: SpecId) -> Option<Arc<CompiledProgram>> {
247 let key = RuntimeCacheKey { code_hash, spec_id };
248 self.inner.shared.resident.get(&key).map(|entry| Arc::clone(&entry))
249 }
250
251 pub fn get_compiled_tracked(
253 &self,
254 code_hash: B256,
255 spec_id: SpecId,
256 ) -> Option<Arc<CompiledProgram>> {
257 let result = self.get_compiled(code_hash, spec_id);
258 if result.is_some() {
259 self.inner.shared.stats.lookup_hits.fetch_add(1, Ordering::Relaxed);
260 } else {
261 self.inner.shared.stats.lookup_misses.fetch_add(1, Ordering::Relaxed);
262 }
263 result
264 }
265
266 pub fn lookup_blocking(&self, req: LookupRequest) -> LookupDecision {
273 if !self.inner.tuning.should_compile(&req.code) {
274 return LookupDecision::Interpret(InterpretReason::Ineligible);
275 }
276 let code_hash = req.key.code_hash;
277 let spec_id = req.key.spec_id;
278 if let Some(program) = self.get_compiled_tracked(code_hash, spec_id) {
279 return LookupDecision::Compiled(program);
280 }
281 if self.compile_jit_sync(req).is_err() {
282 return LookupDecision::Interpret(InterpretReason::JitFailed);
283 }
284 match self.get_compiled_tracked(code_hash, spec_id) {
285 Some(program) => LookupDecision::Compiled(program),
286 None => LookupDecision::Interpret(InterpretReason::JitFailed),
287 }
288 }
289
290 pub fn compile_jit(&self, req: LookupRequest) {
294 let _ = self.ensure_started();
295 let cmd = Command::CompileJit(CompileJitRequest {
296 key: req.key,
297 bytecode: req.code,
298 sync_notifier: SyncNotifier::none(),
299 });
300 let _ = self.inner.tx.send(cmd);
301 }
302
303 pub fn compile_jit_sync(&self, req: LookupRequest) -> eyre::Result<()> {
309 self.ensure_started()?;
310 let (tx, rx) = chan::bounded(1);
311 let cmd = Command::CompileJit(CompileJitRequest {
312 key: req.key,
313 bytecode: req.code,
314 sync_notifier: SyncNotifier::new(tx),
315 });
316 self.inner.tx.send(cmd).map_err(|_| eyre::eyre!("backend channel closed"))?;
317 rx.recv().map_err(|_| eyre::eyre!("backend shut down before compilation completed"))
318 }
319
320 pub fn prepare_aot(&self, req: AotRequest) {
326 self.prepare_aot_batch(vec![req]);
327 }
328
329 pub fn prepare_aot_batch(&self, reqs: Vec<AotRequest>) {
333 let _ = self.ensure_started();
334 let owned: Vec<PrepareAotRequest> = reqs
335 .into_iter()
336 .map(|r| PrepareAotRequest {
337 key: RuntimeCacheKey { code_hash: r.code_hash, spec_id: r.spec_id },
338 bytecode: r.code,
339 })
340 .collect();
341 let cmd = Command::PrepareAot(owned);
342 let _ = self.inner.tx.send(cmd);
343 }
344
345 pub fn clear_resident(&self) {
350 let _ = self.inner.tx.send(Command::ClearResident);
351 }
352
353 pub fn clear_persisted(&self) {
355 let _ = self.inner.tx.send(Command::ClearPersisted);
356 }
357
358 pub fn clear_all(&self) {
360 let _ = self.inner.tx.send(Command::ClearAll);
361 }
362
363 pub fn enabled(&self) -> bool {
365 self.inner.enabled.load(Ordering::Relaxed)
366 }
367
368 pub fn pause(&self) {
380 if self.inner.shared.pause_depth.fetch_add(1, Ordering::Relaxed) == 0 {
381 self.try_send_control(Command::Pause);
382 }
383 }
384
385 pub fn resume(&self) {
389 if self
390 .inner
391 .shared
392 .pause_depth
393 .try_update(Ordering::Relaxed, Ordering::Relaxed, |depth| Some(depth.saturating_sub(1)))
394 .is_ok_and(|depth| depth == 1)
395 {
396 self.try_send_control(Command::Resume);
397 }
398 }
399
400 fn try_send_control(&self, cmd: Command) {
413 if !self.inner.started.load(Ordering::Relaxed) {
414 return;
415 }
416 if self.inner.tx.try_send(cmd).is_err() {
417 self.inner.shared.stats.commands_dropped.fetch_add(1, Ordering::Relaxed);
418 }
419 }
420
421 pub fn is_paused(&self) -> bool {
423 self.inner.shared.pause_depth.load(Ordering::Relaxed) != 0
424 }
425
426 pub fn set_enabled(&self, enabled: bool) -> eyre::Result<()> {
431 debug!(enabled, "set_enabled");
432 self.inner.enabled.store(enabled, Ordering::Relaxed);
433 if enabled {
434 self.ensure_started()?;
435 }
436 Ok(())
437 }
438
439 pub fn stats(&self) -> RuntimeStatsSnapshot {
441 self.inner.stats()
442 }
443
444 fn ensure_started(&self) -> eyre::Result<()> {
446 let mut guard = self.inner.lazy_spawn.lock().unwrap();
447 let Some(lazy) = guard.take() else {
448 return Ok(());
449 };
450
451 let LazySpawnState { rx, config } = lazy;
452
453 debug!(
454 blocking = self.inner.blocking,
455 workers = config.tuning.jit_worker_count,
456 jit_mode = ?config.jit_mode,
457 hot_threshold = config.tuning.jit_hot_threshold,
458 channel_capacity = config.tuning.channel_capacity,
459 "spawning backend thread",
460 );
461
462 match Self::preload_aot(config.store.as_deref()) {
464 Ok(entries) => {
465 for (key, prog) in entries {
466 self.inner.shared.resident.insert(key, prog);
467 }
468 }
469 Err(e) => {
470 *guard = Some(LazySpawnState { rx, config });
472 return Err(e);
473 }
474 }
475
476 drop(guard);
477
478 let (done_tx, done_rx) = chan::bounded::<()>(1);
479 let shared = Arc::clone(&self.inner.shared);
480
481 let thread = std::thread::Builder::new()
482 .name(config.thread_name.clone())
483 .spawn(move || {
484 backend::run(shared, rx, config);
485 let _ = done_tx.send(());
486 })
487 .wrap_err("failed to spawn backend thread")?;
488
489 *self.inner.thread.lock().unwrap() = Some(BackendThread { handle: thread, done_rx });
490 self.inner.started.store(true, Ordering::Relaxed);
491 Ok(())
492 }
493
494 fn preload_aot(
496 store: Option<&dyn ArtifactStore>,
497 ) -> eyre::Result<Vec<(RuntimeCacheKey, Arc<CompiledProgram>)>> {
498 let Some(store) = store else {
499 debug!("no artifact store configured, skipping AOT preload");
500 return Ok(Vec::new());
501 };
502
503 let span = info_span!("aot_preload");
504 let _enter = span.enter();
505
506 let artifacts = store.load_all()?;
507 info!(count = artifacts.len(), "loading AOT artifacts");
508
509 let mut out: Vec<(RuntimeCacheKey, Arc<CompiledProgram>)> =
510 Vec::with_capacity(artifacts.len());
511 let mut seen = alloy_primitives::map::HashSet::<RuntimeCacheKey>::default();
512 let mut loaded = 0u64;
513 let mut failed = 0u64;
514
515 for (artifact_key, stored) in artifacts {
516 match Self::load_artifact(&artifact_key, &stored) {
517 Ok(program) => {
518 let key = artifact_key.runtime;
519 if !seen.insert(key) {
520 warn!(
521 code_hash = %key.code_hash,
522 spec_id = ?key.spec_id,
523 "duplicate artifact key, keeping first",
524 );
525 continue;
526 }
527 out.push((key, Arc::new(program)));
528 loaded += 1;
529 }
530 Err(e) => {
531 warn!(
532 code_hash = %artifact_key.runtime.code_hash,
533 error = %e,
534 "failed to load artifact, skipping",
535 );
536 failed += 1;
537 }
538 }
539 }
540
541 info!(loaded, failed, "AOT preload complete");
542 Ok(out)
543 }
544
545 fn load_artifact(key: &ArtifactKey, stored: &StoredArtifact) -> eyre::Result<CompiledProgram> {
547 let library = unsafe { libloading::Library::new(&stored.dylib_path) }
548 .wrap_err_with(|| format!("dlopen {:?}", stored.dylib_path))?;
549
550 let func: EvmCompilerFn = unsafe {
551 let sym: libloading::Symbol<'_, EvmCompilerFn> = library
552 .get(stored.manifest.symbol_name.as_bytes())
553 .wrap_err_with(|| format!("symbol '{}'", stored.manifest.symbol_name))?;
554 *sym
555 };
556
557 let library = Arc::new(LoadedLibrary::new(library));
558 Ok(CompiledProgram::new_aot(key.runtime, func, library))
559 }
560}
561
562impl BackendShared {
563 pub(crate) fn stats(&self) -> RuntimeStatsSnapshot {
568 self.stats.snapshot(stats::RuntimeStatsGauges {
569 resident_entries: self.resident.len() as u64,
570 events_queued: self.events.len() as u64,
571 command_queue_len: 0,
572 })
573 }
574}
575
576impl BackendInner {
577 pub(crate) fn stats(&self) -> RuntimeStatsSnapshot {
579 let mut snap = self.shared.stats();
580 snap.command_queue_len = self.tx.len() as u64;
581 snap
582 }
583
584 fn shutdown(&self) -> eyre::Result<()> {
585 debug!("shutting down JIT backend");
586 if let Some(ct) = self.thread.lock().unwrap().take() {
587 let _ = self.tx.send(Command::Shutdown);
589
590 match ct.done_rx.recv_timeout(self.shutdown_timeout) {
592 Ok(()) | Err(chan::RecvTimeoutError::Disconnected) => {}
593 Err(chan::RecvTimeoutError::Timeout) => {
594 eyre::bail!(
595 "backend thread did not exit within timeout ({:?})",
596 self.shutdown_timeout
597 );
598 }
599 }
600
601 ct.handle.join().map_err(|_| eyre::eyre!("backend thread panicked"))?;
603 }
604 Ok(())
605 }
606}
607
608impl Drop for BackendInner {
609 fn drop(&mut self) {
610 if let Err(err) = self.shutdown() {
611 warn!(%err, "failed to shutdown JIT backend");
612 }
613 }
614}