From 80ce780028b191e3a334c4af06059fc3ada08aa3 Mon Sep 17 00:00:00 2001 From: DaniPopes <57450786+DaniPopes@users.noreply.github.com> Date: Wed, 29 Apr 2026 14:03:55 +0200 Subject: [PATCH 01/43] feat(runtime): add JIT process mode --- crates/revmc-runtime/src/runtime/config.rs | 19 +++++++++++ crates/revmc-runtime/src/runtime/mod.rs | 10 ++++-- docs/out-of-process-jit.md | 38 ++++++++++++++++++++++ 3 files changed, 65 insertions(+), 2 deletions(-) create mode 100644 docs/out-of-process-jit.md diff --git a/crates/revmc-runtime/src/runtime/config.rs b/crates/revmc-runtime/src/runtime/config.rs index 449756f32..90527869a 100644 --- a/crates/revmc-runtime/src/runtime/config.rs +++ b/crates/revmc-runtime/src/runtime/config.rs @@ -76,6 +76,11 @@ pub struct RuntimeConfig { /// Defaults to `false`. pub aot: bool, + /// Where JIT compilation work runs. + /// + /// Defaults to [`JitProcessMode::InProcess`]. + pub jit_process_mode: JitProcessMode, + /// Blocking mode: every lookup synchronously JIT-compiles on miss and never /// falls back to the interpreter. /// @@ -123,6 +128,19 @@ pub enum CompilationKind { Aot, } +/// Where JIT compilation work runs. +#[derive(Clone, Copy, Debug, Default, PartialEq, Eq)] +pub enum JitProcessMode { + /// Compile on background threads in this process. + #[default] + InProcess, + /// Compile in a helper process and link the result into this process. + /// + /// This is reserved for the out-of-process JIT implementation and is + /// disabled by default. + OutOfProcess, +} + impl Default for RuntimeConfig { fn default() -> Self { Self { @@ -136,6 +154,7 @@ impl Default for RuntimeConfig { no_dse: false, gas_params: None, aot: false, + jit_process_mode: JitProcessMode::default(), blocking: false, on_compilation: None, } diff --git a/crates/revmc-runtime/src/runtime/mod.rs b/crates/revmc-runtime/src/runtime/mod.rs index efa4842a2..42f5d86e6 100644 --- a/crates/revmc-runtime/src/runtime/mod.rs +++ b/crates/revmc-runtime/src/runtime/mod.rs @@ -30,7 +30,7 @@ pub use api::{ }; mod config; -pub use config::{CompilationEvent, CompilationKind, RuntimeConfig, RuntimeTuning}; +pub use config::{CompilationEvent, CompilationKind, JitProcessMode, RuntimeConfig, RuntimeTuning}; mod backend; @@ -137,7 +137,6 @@ impl JitBackend { config.enabled = true; config.tuning.jit_hot_threshold = 0; } - let enabled = config.enabled; let (tx, rx) = chan::bounded::(config.tuning.channel_capacity); let events = ArrayQueue::new(config.tuning.channel_capacity); @@ -350,10 +349,17 @@ impl JitBackend { }; let LazySpawnState { rx, config } = lazy; + if config.jit_process_mode == JitProcessMode::OutOfProcess { + *guard = Some(LazySpawnState { rx, config }); + eyre::bail!( + "out-of-process JIT is not implemented yet; see docs/out-of-process-jit.md" + ); + } debug!( blocking = self.inner.blocking, workers = config.tuning.jit_worker_count, + jit_process_mode = ?config.jit_process_mode, hot_threshold = config.tuning.jit_hot_threshold, channel_capacity = config.tuning.channel_capacity, "spawning backend thread", diff --git a/docs/out-of-process-jit.md b/docs/out-of-process-jit.md new file mode 100644 index 000000000..df6da15c8 --- /dev/null +++ b/docs/out-of-process-jit.md @@ -0,0 +1,38 @@ +# Out-of-process JIT exploration + +`RuntimeConfig::jit_process_mode` now has an `OutOfProcess` variant, disabled by default. It currently returns an error if selected; this document records the implementation work needed to make it transparent. + +## Recommended architecture + +Keep execution and ORC linking in the parent process. Move only translation, optimization, and object emission to a helper process that owns the background worker pool. + +The helper process receives `CompileJob`s over IPC and returns relocatable object bytes plus timings/errors. The parent process then adds the object to its existing ORC `LLJIT`, resolves the symbol to a local `EvmCompilerFn`, and owns the `ResourceTracker`/`JitDylibGuard` exactly as it does today. This preserves the current runtime API and keeps compiled function pointers valid in the caller process. + +Using LLVM ORC's remote executor APIs (`ExecutorProcessControl`, `SimpleRemoteEPC`, etc.) would put the executable code in the child process. That is not transparent for the current `EvmCompilerFn` API, because callers need a local function pointer and direct calls into `revm` state. + +## LLVM ORC support needed + +- Add C/Rust bindings to add an already-compiled object buffer to a specific `JITDylib` with a `ResourceTracker`: + - `LLJIT::add_object_file_with_rt` via `LLVMOrcLLJITAddObjectFileWithRT` if available, or a small C++ wrapper around `ObjectLayer::add` / `object::ObjectFile` materialization. + - Continue to perform `lookup_in(jd, symbol)` in the parent after linking. +- Keep builtin absolute symbols, process symbol generators, perf/debug plugins, and memory accounting in the parent ORC instance. The child only emits relocatable objects with unresolved external symbols. +- Preserve per-entry eviction by creating the `ResourceTracker` in the parent before adding the object and returning it through `JitCodeBacking`. +- Replace the current object capture TLS path for out-of-process jobs: object bytes are already returned by the helper, so disassembly/debug dumping should consume those bytes directly. + +## Runtime/IPC work + +- Add a helper-process entrypoint, preferably the same binary invoked with a hidden argument or environment variable. +- Spawn one helper process from the backend thread when `jit_process_mode == OutOfProcess`. +- The helper owns the existing Rayon worker pool and thread-local `EvmCompiler` instances. +- Define a framed IPC protocol for `CompileJob` and `WorkerResult` data: key, bytecode, symbol name, spec id, optimization level, gas params, debug flags, dedup/DSE flags, dump settings, generation, timings, object bytes, and errors. +- In the parent, turn a successful JIT worker result into a resident program by linking object bytes into ORC, looking up the symbol, and constructing `JitCodeBacking`. +- Keep AOT jobs either in the helper too or explicitly route them through the existing in-process AOT path; the first option gives consistent isolation. +- Define shutdown semantics: close IPC, let the helper drain or cancel queued jobs, then kill on timeout. +- Treat helper crash as worker-pool failure: fail pending synchronous jobs, drop pending async jobs, and optionally respawn. + +## Open questions + +- Serialization crate and stability requirements for the private IPC protocol. +- Whether debug dumps should be written by the child, the parent, or both. +- Whether compiler recycling is still needed per helper worker once the whole helper can be restarted. +- How to expose helper process configuration such as executable path, environment, and restart policy without making the default API noisy. From d4ffc0b4e1444dac8a2cb757dbff69c4fe1bebed Mon Sep 17 00:00:00 2001 From: DaniPopes <57450786+DaniPopes@users.noreply.github.com> Date: Wed, 29 Apr 2026 14:06:04 +0200 Subject: [PATCH 02/43] feat(llvm): link JIT object buffers --- crates/revmc-llvm/src/lib.rs | 20 +++++++++++++++++++- crates/revmc-llvm/src/orc.rs | 22 +++++++++++++++++++++- 2 files changed, 40 insertions(+), 2 deletions(-) diff --git a/crates/revmc-llvm/src/lib.rs b/crates/revmc-llvm/src/lib.rs index 259b72535..c9f27b3b6 100644 --- a/crates/revmc-llvm/src/lib.rs +++ b/crates/revmc-llvm/src/lib.rs @@ -36,7 +36,7 @@ use revmc_backend::{ }; use std::{ cell::Cell, - ffi::CString, + ffi::{CStr, CString}, fmt::{self, Write}, iter, mem::ManuallyDrop, @@ -738,6 +738,24 @@ impl EvmLlvmBackend { Ok(()) } + /// Links a relocatable object into this backend's JITDylib and returns the function address + /// and resource tracker that owns the linked code. + pub fn link_jit_object( + &mut self, + symbol_name: &CStr, + object: &[u8], + symbols: &[(CString, usize)], + ) -> Result<(usize, orc::ResourceTracker)> { + self.ensure_orc()?; + let orc = self.orc.as_mut().unwrap(); + orc.global.define_builtins(symbols); + let tracker = orc.jd().create_resource_tracker(); + orc.global.jit.add_object_with_rt(object, &tracker).map_err(error_msg)?; + let addr = orc.global.jit.lookup_in(orc.jd(), symbol_name).map_err(error_msg)?; + orc.loaded_trackers.push(tracker); + Ok((addr, orc.loaded_trackers.pop().unwrap())) + } + /// Pops and returns the [`ResourceTracker`](orc::ResourceTracker) for the last committed /// JIT module. /// diff --git a/crates/revmc-llvm/src/orc.rs b/crates/revmc-llvm/src/orc.rs index 49e14cc27..997ceb58c 100644 --- a/crates/revmc-llvm/src/orc.rs +++ b/crates/revmc-llvm/src/orc.rs @@ -17,7 +17,10 @@ use crate::llvm_string; use inkwell::{ context::Context, llvm_sys::{ - core::{LLVMContextCreate, LLVMModuleCreateWithNameInContext}, + core::{ + LLVMContextCreate, LLVMCreateMemoryBufferWithMemoryRangeCopy, + LLVMModuleCreateWithNameInContext, + }, error::*, orc2::{lljit::*, *}, prelude::*, @@ -1398,6 +1401,23 @@ impl LLJIT { }) } + /// Add a relocatable object file to the given ResourceTracker's JITDylib. + pub fn add_object_with_rt( + &self, + object: &[u8], + rt: &ResourceTracker, + ) -> Result<(), LLVMString> { + let name = c"revmc-object"; + let buf = unsafe { + LLVMCreateMemoryBufferWithMemoryRangeCopy( + object.as_ptr().cast(), + object.len(), + name.as_ptr(), + ) + }; + cvt(unsafe { LLVMOrcLLJITAddObjectFileWithRT(self.as_inner(), rt.as_inner(), buf) }) + } + /// Gets the execution session. pub fn get_execution_session(&self) -> ExecutionSessionRef<'_> { unsafe { ExecutionSessionRef::from_inner(LLVMOrcLLJITGetExecutionSession(self.as_inner())) } From c01b57c2864798bd0db5779aa6edc06e41f6ab93 Mon Sep 17 00:00:00 2001 From: DaniPopes <57450786+DaniPopes@users.noreply.github.com> Date: Wed, 29 Apr 2026 14:06:59 +0200 Subject: [PATCH 03/43] feat(codegen): emit objects from JIT modules --- crates/revmc-codegen/src/compiler/mod.rs | 3 +-- crates/revmc-llvm/src/lib.rs | 8 ++++++++ 2 files changed, 9 insertions(+), 2 deletions(-) diff --git a/crates/revmc-codegen/src/compiler/mod.rs b/crates/revmc-codegen/src/compiler/mod.rs index b8a4c1fee..efc565737 100644 --- a/crates/revmc-codegen/src/compiler/mod.rs +++ b/crates/revmc-codegen/src/compiler/mod.rs @@ -502,9 +502,8 @@ impl EvmCompiler { Ok(()) } - /// (AOT) Finalizes the module and writes the compiled object to the given writer. + /// Finalizes the module and writes the compiled object to the given writer. pub fn write_object(&mut self, w: W) -> Result<()> { - ensure!(self.is_aot(), "cannot write AOT object during JIT compilation"); self.finalize()?; { let _t = self.remarks.time(|r| &r.codegen); diff --git a/crates/revmc-llvm/src/lib.rs b/crates/revmc-llvm/src/lib.rs index c9f27b3b6..c3869c5cc 100644 --- a/crates/revmc-llvm/src/lib.rs +++ b/crates/revmc-llvm/src/lib.rs @@ -738,6 +738,14 @@ impl EvmLlvmBackend { Ok(()) } + /// Returns pending absolute symbols collected while translating the current JIT module. + pub fn pending_symbol_names(&self) -> Vec { + self.orc + .as_ref() + .map(|orc| orc.pending_symbols.iter().map(|(name, _)| name.clone()).collect()) + .unwrap_or_default() + } + /// Links a relocatable object into this backend's JITDylib and returns the function address /// and resource tracker that owns the linked code. pub fn link_jit_object( From 3ad367ad77319b58d581292e61a3989ee402001a Mon Sep 17 00:00:00 2001 From: DaniPopes <57450786+DaniPopes@users.noreply.github.com> Date: Wed, 29 Apr 2026 14:08:59 +0200 Subject: [PATCH 04/43] feat(runtime): link worker-emitted JIT objects --- Cargo.lock | 1 + crates/revmc-builtins/src/ir.rs | 7 ++ crates/revmc-runtime/Cargo.toml | 1 + crates/revmc-runtime/src/runtime/backend.rs | 75 ++++++++++++++++++++- crates/revmc-runtime/src/runtime/mod.rs | 6 -- crates/revmc-runtime/src/runtime/worker.rs | 48 ++++++++++++- 6 files changed, 130 insertions(+), 8 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index f8d3fa0f6..8944a45d6 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -3339,6 +3339,7 @@ dependencies = [ "revm-primitives", "revm-state", "revmc-backend", + "revmc-builtins", "revmc-codegen", "revmc-context", "revmc-llvm", diff --git a/crates/revmc-builtins/src/ir.rs b/crates/revmc-builtins/src/ir.rs index 4333a323e..75b241c93 100644 --- a/crates/revmc-builtins/src/ir.rs +++ b/crates/revmc-builtins/src/ir.rs @@ -132,6 +132,13 @@ macro_rules! builtins { } } + pub fn addr_by_name(name: &str) -> Option { + match name { + $(stringify!($name) => Some(crate::$name as *const () as usize),)* + _ => None, + } + } + pub const fn call_conv(self) -> CallConv { match self { Self::Mresize => CallConv::PreserveMost, diff --git a/crates/revmc-runtime/Cargo.toml b/crates/revmc-runtime/Cargo.toml index 8ac038c5f..3857b86f4 100644 --- a/crates/revmc-runtime/Cargo.toml +++ b/crates/revmc-runtime/Cargo.toml @@ -25,6 +25,7 @@ revmc-backend.workspace = true revmc-codegen.workspace = true revmc-context = { workspace = true, features = ["evm"] } revmc-llvm = { workspace = true, optional = true } +revmc-builtins = { workspace = true, features = ["ir"] } alloy-primitives = { workspace = true, features = ["std", "map-fxhash"] } alloy-evm = { workspace = true, optional = true } diff --git a/crates/revmc-runtime/src/runtime/backend.rs b/crates/revmc-runtime/src/runtime/backend.rs index bae35e0a2..e7ddb0cd8 100644 --- a/crates/revmc-runtime/src/runtime/backend.rs +++ b/crates/revmc-runtime/src/runtime/backend.rs @@ -7,7 +7,10 @@ use crate::{ storage::{ ArtifactKey, ArtifactManifest, ArtifactStore, BackendSelection, RuntimeCacheKey, }, - worker::{AotSuccess, CompileJob, SyncNotifier, WorkerPool, WorkerResult, WorkerSuccess}, + worker::{ + AotSuccess, CompileJob, JitCodeBacking, JitObjectSuccess, SyncNotifier, WorkerPool, + WorkerResult, WorkerSuccess, + }, }, }; use alloy_primitives::{ @@ -19,6 +22,7 @@ use crossbeam_queue::ArrayQueue; use dashmap::DashMap; use quanta::Instant; use std::{ + ffi::CString, mem, ops::ControlFlow, sync::{Arc, atomic::Ordering}, @@ -27,6 +31,8 @@ use std::{ #[cfg(feature = "llvm")] use crate::llvm::jit_memory_usage; +#[cfg(feature = "llvm")] +use revmc_context::RawEvmCompilerFn; /// The resident map type: code_hash+spec_id → compiled program. pub(crate) type ResidentMap = DashMap, DefaultHashBuilder>; @@ -56,6 +62,35 @@ fn jit_total_bytes() -> usize { } } +#[cfg(feature = "llvm")] +fn link_jit_object( + success: &JitObjectSuccess, +) -> eyre::Result<(EvmCompilerFn, Arc)> { + let mut backend = crate::EvmLlvmBackend::new(false)?; + let symbol_name = CString::new(success.symbol_name.clone())?; + let builtin_symbols = success + .builtin_symbols + .iter() + .map(|name| { + let addr = revmc_builtins::Builtin::addr_by_name(name) + .ok_or_else(|| eyre::eyre!("unknown builtin symbol: {name}"))?; + Ok((CString::new(name.as_str())?, addr)) + }) + .collect::>>()?; + let (addr, tracker) = + backend.link_jit_object(&symbol_name, &success.object_bytes, &builtin_symbols)?; + let jd_guard = backend.jit_dylib_guard(); + let func = EvmCompilerFn::new(unsafe { std::mem::transmute::(addr) }); + Ok((func, Arc::new(JitCodeBacking::new(tracker, jd_guard)))) +} + +#[cfg(not(feature = "llvm"))] +fn link_jit_object( + _success: &JitObjectSuccess, +) -> eyre::Result<(EvmCompilerFn, Arc)> { + eyre::bail!("LLVM backend not available") +} + /// Commands sent to the backend thread on the bounded command channel. /// /// Lookup-observed events are NOT carried here — they go through the @@ -474,6 +509,9 @@ impl BackendState { Ok(WorkerSuccess::Aot(success)) => { self.handle_aot_success(result.key, success); } + Ok(WorkerSuccess::JitObject(success)) => { + self.handle_jit_object_success(result.key, success, result.compile_duration); + } Err(err) => { self.entries.remove(&result.key); self.inner.stats.compilations_failed.fetch_add(1, Ordering::Relaxed); @@ -490,6 +528,41 @@ impl BackendState { notify(); } + fn handle_jit_object_success( + &mut self, + key: RuntimeCacheKey, + success: JitObjectSuccess, + compile_duration: std::time::Duration, + ) { + match link_jit_object(&success) { + Ok((func, backing)) => { + let program = Arc::new(CompiledProgram::new_jit(key, func, backing)); + self.insert_resident(key, program); + self.entries.remove(&key); + self.inner.stats.compilations_succeeded.fetch_add(1, Ordering::Relaxed); + + debug!( + code_hash = %key.code_hash, + spec_id = ?key.spec_id, + compile_time = ?compile_duration, + object_len = success.object_bytes.len(), + "JIT object linked and published to resident map", + ); + } + Err(err) => { + self.entries.remove(&key); + self.inner.stats.compilations_failed.fetch_add(1, Ordering::Relaxed); + + warn!( + code_hash = %key.code_hash, + error = %err, + compile_time = ?compile_duration, + "failed to link JIT object", + ); + } + } + } + fn handle_aot_success(&mut self, key: RuntimeCacheKey, success: AotSuccess) { let artifact_key = ArtifactKey { runtime: key, diff --git a/crates/revmc-runtime/src/runtime/mod.rs b/crates/revmc-runtime/src/runtime/mod.rs index 42f5d86e6..4db0495f8 100644 --- a/crates/revmc-runtime/src/runtime/mod.rs +++ b/crates/revmc-runtime/src/runtime/mod.rs @@ -349,12 +349,6 @@ impl JitBackend { }; let LazySpawnState { rx, config } = lazy; - if config.jit_process_mode == JitProcessMode::OutOfProcess { - *guard = Some(LazySpawnState { rx, config }); - eyre::bail!( - "out-of-process JIT is not implemented yet; see docs/out-of-process-jit.md" - ); - } debug!( blocking = self.inner.blocking, diff --git a/crates/revmc-runtime/src/runtime/worker.rs b/crates/revmc-runtime/src/runtime/worker.rs index 727c219e2..6197514b9 100644 --- a/crates/revmc-runtime/src/runtime/worker.rs +++ b/crates/revmc-runtime/src/runtime/worker.rs @@ -12,7 +12,7 @@ use crate::{ CompileTimings, EvmCompilerFn, OptimizationLevel, runtime::{ - config::{CompilationKind, RuntimeConfig}, + config::{CompilationKind, JitProcessMode, RuntimeConfig}, storage::RuntimeCacheKey, }, }; @@ -102,6 +102,8 @@ pub(crate) enum WorkerSuccess { Jit(JitSuccess), /// AOT compilation produced shared-library bytes. Aot(AotSuccess), + /// JIT compilation produced relocatable object bytes to link in the parent. + JitObject(JitObjectSuccess), } /// Successful JIT compilation output. @@ -114,6 +116,15 @@ pub(crate) struct JitSuccess { } /// Successful AOT compilation output. +pub(crate) struct JitObjectSuccess { + /// The symbol name in the object file. + pub(crate) symbol_name: String, + /// The raw relocatable object bytes. + pub(crate) object_bytes: Vec, + /// Builtin absolute symbols referenced by the object. + pub(crate) builtin_symbols: Vec, +} + pub(crate) struct AotSuccess { /// The symbol name in the shared library. pub(crate) symbol_name: String, @@ -323,6 +334,9 @@ fn compile_with_state( compiler.set_opt_level(job.opt_level); let outcome = match job.kind { + CompilationKind::Jit if config.jit_process_mode == JitProcessMode::OutOfProcess => { + compile_jit_object_artifact(&job, compiler) + } CompilationKind::Jit => compile_jit_artifact(&job, compiler), CompilationKind::Aot => compile_aot_artifact(&job, compiler), }; @@ -429,6 +443,38 @@ fn compile_jit_artifact( /// Compiles a single bytecode to a shared library and returns the raw bytes. #[cfg(feature = "llvm")] +fn compile_jit_object_artifact( + job: &CompileJob, + compiler: &mut EvmCompiler, +) -> Result { + compiler + .translate(&job.symbol_name, &job.bytecode[..], job.key.spec_id) + .map_err(|e| format!("JIT object translate failed: {e}"))?; + + let mut object_bytes = Vec::new(); + compiler + .write_object(&mut object_bytes) + .map_err(|e| format!("JIT object write failed: {e}"))?; + let builtin_symbols = compiler + .backend() + .pending_symbol_names() + .into_iter() + .map(|name| name.to_string_lossy().into_owned()) + .collect(); + + debug!( + bytecode_len = job.bytecode.len(), + object_len = object_bytes.len(), + "JIT object compilation succeeded", + ); + + Ok(WorkerSuccess::JitObject(JitObjectSuccess { + symbol_name: job.symbol_name.clone(), + object_bytes, + builtin_symbols, + })) +} + fn compile_aot_artifact( job: &CompileJob, compiler: &mut EvmCompiler, From ca6f8bc9f7fc8d4b23309644da8ffb1a57f16913 Mon Sep 17 00:00:00 2001 From: DaniPopes <57450786+DaniPopes@users.noreply.github.com> Date: Wed, 29 Apr 2026 14:10:50 +0200 Subject: [PATCH 05/43] feat(runtime): spawn JIT helper processes --- crates/revmc-cli/src/main.rs | 3 + crates/revmc-runtime/src/runtime/mod.rs | 20 ++ crates/revmc-runtime/src/runtime/worker.rs | 213 ++++++++++++++++++++- 3 files changed, 233 insertions(+), 3 deletions(-) diff --git a/crates/revmc-cli/src/main.rs b/crates/revmc-cli/src/main.rs index 700ede0d6..23416626c 100644 --- a/crates/revmc-cli/src/main.rs +++ b/crates/revmc-cli/src/main.rs @@ -21,6 +21,9 @@ enum Command { } fn main() -> Result<()> { + if revmc::runtime::maybe_run_jit_helper()? { + return Ok(()); + } if std::env::var_os("RUST_BACKTRACE").is_none() { // SAFETY: This is called at the very beginning of main, before any other threads are // spawned. diff --git a/crates/revmc-runtime/src/runtime/mod.rs b/crates/revmc-runtime/src/runtime/mod.rs index 4db0495f8..2eebb253f 100644 --- a/crates/revmc-runtime/src/runtime/mod.rs +++ b/crates/revmc-runtime/src/runtime/mod.rs @@ -45,6 +45,26 @@ pub use storage::{ mod worker; +/// Runs the out-of-process JIT helper if this process was launched as one. +/// +/// Returns `Ok(true)` after the helper request has been handled and the caller +/// should exit immediately. Normal application startup should continue on +/// `Ok(false)`. +pub fn maybe_run_jit_helper() -> eyre::Result { + if std::env::var_os("REVMC_JIT_HELPER").is_none() { + return Ok(false); + } + #[cfg(feature = "llvm")] + { + worker::run_jit_helper_stdio()?; + Ok(true) + } + #[cfg(not(feature = "llvm"))] + { + eyre::bail!("LLVM backend not available") + } +} + #[cfg(test)] mod tests; diff --git a/crates/revmc-runtime/src/runtime/worker.rs b/crates/revmc-runtime/src/runtime/worker.rs index 6197514b9..b95c378cf 100644 --- a/crates/revmc-runtime/src/runtime/worker.rs +++ b/crates/revmc-runtime/src/runtime/worker.rs @@ -10,17 +10,23 @@ //! as the job channel closes. use crate::{ - CompileTimings, EvmCompilerFn, OptimizationLevel, + CompileTimings, EvmCompilerFn, OptimizationLevel, eyre, runtime::{ config::{CompilationKind, JitProcessMode, RuntimeConfig}, storage::RuntimeCacheKey, }, }; -use alloy_primitives::Bytes; +use alloy_primitives::{B256, Bytes}; use crossbeam_channel as chan; use rayon::{ThreadPool, ThreadPoolBuilder}; #[cfg(feature = "llvm")] -use std::{cell::RefCell, fs::File, io::Read, time::Instant}; +use std::{ + cell::RefCell, + fs::File, + io::{Read, Write}, + process::{Command, Stdio}, + time::Instant, +}; use std::{ sync::{ Arc, @@ -34,6 +40,8 @@ use crate::{ EvmCompiler, EvmLlvmBackend, Linker, llvm::{JitDylibGuard, orc::ResourceTracker}, }; +#[cfg(feature = "llvm")] +use revm_primitives::hardfork::SpecId; /// Notifier for synchronous compilation requests. /// @@ -278,6 +286,9 @@ fn clear_thread_local_compilers() {} fn compile_job(job: CompileJob, config: &RuntimeConfig) -> WorkerResult { trace!(?job, "received job"); match job.kind { + CompilationKind::Jit if config.jit_process_mode == JitProcessMode::OutOfProcess => { + compile_job_out_of_process(job, config) + } CompilationKind::Jit => { JIT_COMPILER.with_borrow_mut(|state| compile_with_state(job, config, state)) } @@ -287,6 +298,202 @@ fn compile_job(job: CompileJob, config: &RuntimeConfig) -> WorkerResult { } } +#[cfg(feature = "llvm")] +fn compile_job_out_of_process(job: CompileJob, config: &RuntimeConfig) -> WorkerResult { + let t0 = Instant::now(); + let outcome = run_helper_job(&job, config); + WorkerResult { + key: job.key, + outcome, + kind: job.kind, + sync_notifier: job.sync_notifier, + generation: job.generation, + compile_duration: t0.elapsed(), + timings: CompileTimings::default(), + } +} + +const HELPER_ENV: &str = "REVMC_JIT_HELPER"; + +#[cfg(feature = "llvm")] +fn run_helper_job(job: &CompileJob, config: &RuntimeConfig) -> Result { + let exe = std::env::current_exe().map_err(|e| format!("failed to locate current exe: {e}"))?; + let mut child = Command::new(exe) + .env(HELPER_ENV, "1") + .stdin(Stdio::piped()) + .stdout(Stdio::piped()) + .stderr(Stdio::inherit()) + .spawn() + .map_err(|e| format!("failed to spawn JIT helper: {e}"))?; + + { + let stdin = child.stdin.as_mut().ok_or("helper stdin unavailable")?; + write_job(stdin, job, config).map_err(|e| format!("failed to write helper job: {e}"))?; + } + + let mut stdout = child.stdout.take().ok_or("helper stdout unavailable")?; + let result = read_helper_result(&mut stdout); + let status = child.wait().map_err(|e| format!("failed to wait for JIT helper: {e}"))?; + if !status.success() { + return Err(format!("JIT helper exited with {status}")); + } + result +} + +#[cfg(feature = "llvm")] +fn write_job(mut w: impl Write, job: &CompileJob, config: &RuntimeConfig) -> std::io::Result<()> { + w.write_all(b"RJIT\0")?; + w.write_all(&job.key.code_hash.0)?; + w.write_all(&[job.key.spec_id as u8, opt_level_to_u8(job.opt_level)])?; + w.write_all(&[ + u8::from(config.debug_assertions), + u8::from(config.no_dedup), + u8::from(config.no_dse), + ])?; + write_bytes(&mut w, job.symbol_name.as_bytes())?; + write_bytes(&mut w, &job.bytecode)?; + Ok(()) +} + +#[cfg(feature = "llvm")] +fn read_helper_result(mut r: impl Read) -> Result { + let mut tag = [0u8; 1]; + r.read_exact(&mut tag).map_err(|e| format!("failed to read helper result: {e}"))?; + match tag[0] { + 0 => { + let msg = + read_string(&mut r).map_err(|e| format!("failed to read helper error: {e}"))?; + Err(msg) + } + 1 => { + let symbol_name = + read_string(&mut r).map_err(|e| format!("failed to read symbol name: {e}"))?; + let object_bytes = + read_vec(&mut r).map_err(|e| format!("failed to read object bytes: {e}"))?; + let count = + read_u32(&mut r).map_err(|e| format!("failed to read symbol count: {e}"))?; + let mut builtin_symbols = Vec::with_capacity(count as usize); + for _ in 0..count { + builtin_symbols.push( + read_string(&mut r) + .map_err(|e| format!("failed to read builtin symbol: {e}"))?, + ); + } + Ok(WorkerSuccess::JitObject(JitObjectSuccess { + symbol_name, + object_bytes, + builtin_symbols, + })) + } + tag => Err(format!("unknown helper result tag {tag}")), + } +} + +#[cfg(feature = "llvm")] +pub(super) fn run_jit_helper_stdio() -> eyre::Result<()> { + let mut stdin = std::io::stdin().lock(); + let mut magic = [0u8; 5]; + stdin.read_exact(&mut magic)?; + eyre::ensure!(&magic == b"RJIT\0", "invalid JIT helper request"); + + let mut hash = [0u8; 32]; + stdin.read_exact(&mut hash)?; + let mut fixed = [0u8; 5]; + stdin.read_exact(&mut fixed)?; + let spec_id = SpecId::try_from_u8(fixed[0]).ok_or_else(|| eyre::eyre!("invalid spec id"))?; + let opt_level = opt_level_from_u8(fixed[1])?; + let symbol_name = read_string(&mut stdin)?; + let bytecode = Bytes::from(read_vec(&mut stdin)?); + + let config = RuntimeConfig { + debug_assertions: fixed[2] != 0, + no_dedup: fixed[3] != 0, + no_dse: fixed[4] != 0, + ..Default::default() + }; + let job = CompileJob { + kind: CompilationKind::Jit, + key: RuntimeCacheKey { code_hash: B256::from(hash), spec_id }, + bytecode, + symbol_name, + opt_level, + sync_notifier: SyncNotifier::none(), + generation: 0, + }; + + let mut compiler = create_compiler(&config, false).map_err(|e| eyre::eyre!(e))?; + compiler.set_opt_level(opt_level); + let result = compile_jit_object_artifact(&job, &mut compiler); + + let mut stdout = std::io::stdout().lock(); + match result { + Ok(WorkerSuccess::JitObject(success)) => { + stdout.write_all(&[1])?; + write_bytes(&mut stdout, success.symbol_name.as_bytes())?; + write_bytes(&mut stdout, &success.object_bytes)?; + stdout.write_all(&(success.builtin_symbols.len() as u32).to_le_bytes())?; + for symbol in success.builtin_symbols { + write_bytes(&mut stdout, symbol.as_bytes())?; + } + } + Ok(_) => unreachable!(), + Err(err) => { + stdout.write_all(&[0])?; + write_bytes(&mut stdout, err.as_bytes())?; + } + } + stdout.flush()?; + Ok(()) +} + +#[cfg(feature = "llvm")] +fn write_bytes(mut w: impl Write, bytes: &[u8]) -> std::io::Result<()> { + w.write_all(&(bytes.len() as u32).to_le_bytes())?; + w.write_all(bytes) +} + +#[cfg(feature = "llvm")] +fn read_string(r: impl Read) -> std::io::Result { + String::from_utf8(read_vec(r)?) + .map_err(|e| std::io::Error::new(std::io::ErrorKind::InvalidData, e)) +} + +#[cfg(feature = "llvm")] +fn read_vec(mut r: impl Read) -> std::io::Result> { + let len = read_u32(&mut r)? as usize; + let mut bytes = vec![0; len]; + r.read_exact(&mut bytes)?; + Ok(bytes) +} + +#[cfg(feature = "llvm")] +fn read_u32(mut r: impl Read) -> std::io::Result { + let mut bytes = [0u8; 4]; + r.read_exact(&mut bytes)?; + Ok(u32::from_le_bytes(bytes)) +} + +#[cfg(feature = "llvm")] +fn opt_level_to_u8(level: OptimizationLevel) -> u8 { + match level { + OptimizationLevel::None => 0, + OptimizationLevel::Less => 1, + OptimizationLevel::Default => 2, + OptimizationLevel::Aggressive => 3, + } +} + +#[cfg(feature = "llvm")] +fn opt_level_from_u8(level: u8) -> eyre::Result { + Ok(match level { + 0 => OptimizationLevel::None, + 1 => OptimizationLevel::Less, + 2 => OptimizationLevel::Default, + 3 => OptimizationLevel::Aggressive, + _ => eyre::bail!("invalid optimization level"), + }) +} + #[cfg(feature = "llvm")] fn compile_with_state( job: CompileJob, From 567674d41a4a29f1f76972a5fe6871bf6891ce53 Mon Sep 17 00:00:00 2001 From: DaniPopes <57450786+DaniPopes@users.noreply.github.com> Date: Wed, 29 Apr 2026 14:11:29 +0200 Subject: [PATCH 06/43] docs: update out-of-process JIT status --- docs/out-of-process-jit.md | 17 ++++++++++++----- 1 file changed, 12 insertions(+), 5 deletions(-) diff --git a/docs/out-of-process-jit.md b/docs/out-of-process-jit.md index df6da15c8..88f53fddb 100644 --- a/docs/out-of-process-jit.md +++ b/docs/out-of-process-jit.md @@ -21,11 +21,18 @@ Using LLVM ORC's remote executor APIs (`ExecutorProcessControl`, `SimpleRemoteEP ## Runtime/IPC work -- Add a helper-process entrypoint, preferably the same binary invoked with a hidden argument or environment variable. -- Spawn one helper process from the backend thread when `jit_process_mode == OutOfProcess`. -- The helper owns the existing Rayon worker pool and thread-local `EvmCompiler` instances. -- Define a framed IPC protocol for `CompileJob` and `WorkerResult` data: key, bytecode, symbol name, spec id, optimization level, gas params, debug flags, dedup/DSE flags, dump settings, generation, timings, object bytes, and errors. -- In the parent, turn a successful JIT worker result into a resident program by linking object bytes into ORC, looking up the symbol, and constructing `JitCodeBacking`. +Current prototype: + +- `RuntimeConfig::jit_process_mode = JitProcessMode::OutOfProcess` makes JIT workers spawn a helper process via `std::env::current_exe()`. +- Binaries must call `revmc::runtime::maybe_run_jit_helper()` at process startup. `revmc-cli` does this already. +- The helper compiles one JIT object request from stdin and writes one framed response to stdout. +- The parent links returned object bytes into its local ORC instance, resolves the symbol, and constructs `JitCodeBacking` with a parent-owned `ResourceTracker`. + +Still needed: + +- Keep one long-lived helper process instead of spawning per job. +- Move the worker pool into the helper process; the parent should only enqueue IPC requests. +- Define a versioned framed IPC protocol for `CompileJob` and `WorkerResult` data: key, bytecode, symbol name, spec id, optimization level, gas params, debug flags, dedup/DSE flags, dump settings, generation, timings, object bytes, and errors. - Keep AOT jobs either in the helper too or explicitly route them through the existing in-process AOT path; the first option gives consistent isolation. - Define shutdown semantics: close IPC, let the helper drain or cancel queued jobs, then kill on timeout. - Treat helper crash as worker-pool failure: fail pending synchronous jobs, drop pending async jobs, and optionally respawn. From e193770302783da920eb2dc9c34652b37a7d2c4e Mon Sep 17 00:00:00 2001 From: DaniPopes <57450786+DaniPopes@users.noreply.github.com> Date: Wed, 29 Apr 2026 14:12:25 +0200 Subject: [PATCH 07/43] feat(runtime): configure JIT helper path --- crates/revmc-runtime/src/runtime/config.rs | 8 ++++++++ crates/revmc-runtime/src/runtime/worker.rs | 7 ++++++- docs/out-of-process-jit.md | 4 ++-- 3 files changed, 16 insertions(+), 3 deletions(-) diff --git a/crates/revmc-runtime/src/runtime/config.rs b/crates/revmc-runtime/src/runtime/config.rs index 90527869a..c9e28ea1f 100644 --- a/crates/revmc-runtime/src/runtime/config.rs +++ b/crates/revmc-runtime/src/runtime/config.rs @@ -81,6 +81,13 @@ pub struct RuntimeConfig { /// Defaults to [`JitProcessMode::InProcess`]. pub jit_process_mode: JitProcessMode, + /// Helper executable used when [`jit_process_mode`](Self::jit_process_mode) + /// is [`JitProcessMode::OutOfProcess`]. + /// + /// When `None`, the runtime spawns `std::env::current_exe()` and expects it + /// to call [`super::maybe_run_jit_helper`] during startup. + pub jit_helper_path: Option, + /// Blocking mode: every lookup synchronously JIT-compiles on miss and never /// falls back to the interpreter. /// @@ -155,6 +162,7 @@ impl Default for RuntimeConfig { gas_params: None, aot: false, jit_process_mode: JitProcessMode::default(), + jit_helper_path: None, blocking: false, on_compilation: None, } diff --git a/crates/revmc-runtime/src/runtime/worker.rs b/crates/revmc-runtime/src/runtime/worker.rs index b95c378cf..0957321f9 100644 --- a/crates/revmc-runtime/src/runtime/worker.rs +++ b/crates/revmc-runtime/src/runtime/worker.rs @@ -317,7 +317,12 @@ const HELPER_ENV: &str = "REVMC_JIT_HELPER"; #[cfg(feature = "llvm")] fn run_helper_job(job: &CompileJob, config: &RuntimeConfig) -> Result { - let exe = std::env::current_exe().map_err(|e| format!("failed to locate current exe: {e}"))?; + let exe = match &config.jit_helper_path { + Some(path) => path.clone(), + None => { + std::env::current_exe().map_err(|e| format!("failed to locate current exe: {e}"))? + } + }; let mut child = Command::new(exe) .env(HELPER_ENV, "1") .stdin(Stdio::piped()) diff --git a/docs/out-of-process-jit.md b/docs/out-of-process-jit.md index 88f53fddb..ee8418b1f 100644 --- a/docs/out-of-process-jit.md +++ b/docs/out-of-process-jit.md @@ -23,8 +23,8 @@ Using LLVM ORC's remote executor APIs (`ExecutorProcessControl`, `SimpleRemoteEP Current prototype: -- `RuntimeConfig::jit_process_mode = JitProcessMode::OutOfProcess` makes JIT workers spawn a helper process via `std::env::current_exe()`. -- Binaries must call `revmc::runtime::maybe_run_jit_helper()` at process startup. `revmc-cli` does this already. +- `RuntimeConfig::jit_process_mode = JitProcessMode::OutOfProcess` makes JIT workers spawn a helper process via `RuntimeConfig::jit_helper_path`, or `std::env::current_exe()` when unset. +- Helper binaries must call `revmc::runtime::maybe_run_jit_helper()` at process startup. `revmc-cli` does this already. - The helper compiles one JIT object request from stdin and writes one framed response to stdout. - The parent links returned object bytes into its local ORC instance, resolves the symbol, and constructs `JitCodeBacking` with a parent-owned `ResourceTracker`. From 158ccb56782263a7c7f85f5a9834cf9ccacf5ffd Mon Sep 17 00:00:00 2001 From: DaniPopes <57450786+DaniPopes@users.noreply.github.com> Date: Wed, 29 Apr 2026 14:12:57 +0200 Subject: [PATCH 08/43] fix(runtime): reject unsupported helper options --- crates/revmc-runtime/src/runtime/worker.rs | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/crates/revmc-runtime/src/runtime/worker.rs b/crates/revmc-runtime/src/runtime/worker.rs index 0957321f9..2f6d13434 100644 --- a/crates/revmc-runtime/src/runtime/worker.rs +++ b/crates/revmc-runtime/src/runtime/worker.rs @@ -317,6 +317,13 @@ const HELPER_ENV: &str = "REVMC_JIT_HELPER"; #[cfg(feature = "llvm")] fn run_helper_job(job: &CompileJob, config: &RuntimeConfig) -> Result { + if config.gas_params.is_some() { + return Err("out-of-process JIT does not support custom gas params yet".into()); + } + if config.dump_dir.is_some() { + return Err("out-of-process JIT does not support debug dumps yet".into()); + } + let exe = match &config.jit_helper_path { Some(path) => path.clone(), None => { From 7228bda66d93f5c5b3cad7148b56d494ced342ea Mon Sep 17 00:00:00 2001 From: DaniPopes <57450786+DaniPopes@users.noreply.github.com> Date: Wed, 29 Apr 2026 14:47:55 +0200 Subject: [PATCH 09/43] feat(runtime): keep persistent JIT helpers --- crates/revmc-runtime/src/runtime/config.rs | 9 ++ crates/revmc-runtime/src/runtime/worker.rs | 165 +++++++++++++++++---- docs/out-of-process-jit.md | 8 +- 3 files changed, 149 insertions(+), 33 deletions(-) diff --git a/crates/revmc-runtime/src/runtime/config.rs b/crates/revmc-runtime/src/runtime/config.rs index c9e28ea1f..936f33ffb 100644 --- a/crates/revmc-runtime/src/runtime/config.rs +++ b/crates/revmc-runtime/src/runtime/config.rs @@ -212,6 +212,14 @@ pub struct RuntimeTuning { /// Defaults to `min(max(1, cpus/2), 4)`. pub jit_worker_count: usize, + /// Timeout for a single out-of-process JIT compilation job. + /// + /// When exceeded, the helper process is killed and a fresh helper is spawned for + /// the next job. Only applies to [`JitProcessMode::OutOfProcess`]. + /// + /// Defaults to `5s`. + pub jit_helper_timeout: Duration, + /// Capacity of the per-worker job queue. /// /// Defaults to `64`. @@ -280,6 +288,7 @@ impl Default for RuntimeTuning { jit_max_bytecode_len: 0, jit_max_pending_jobs: 2048, jit_worker_count: worker_count, + jit_helper_timeout: Duration::from_secs(5), jit_worker_queue_capacity: 64, jit_opt_level: crate::OptimizationLevel::default(), aot_opt_level: crate::OptimizationLevel::default(), diff --git a/crates/revmc-runtime/src/runtime/worker.rs b/crates/revmc-runtime/src/runtime/worker.rs index 2f6d13434..d290e3ad6 100644 --- a/crates/revmc-runtime/src/runtime/worker.rs +++ b/crates/revmc-runtime/src/runtime/worker.rs @@ -24,7 +24,9 @@ use std::{ cell::RefCell, fs::File, io::{Read, Write}, - process::{Command, Stdio}, + path::PathBuf, + process::{Child, ChildStdin, Command, Stdio}, + thread::JoinHandle, time::Instant, }; use std::{ @@ -277,6 +279,7 @@ impl Drop for WorkerPool { fn clear_thread_local_compilers() { JIT_COMPILER.with_borrow_mut(Option::take); AOT_COMPILER.with_borrow_mut(Option::take); + JIT_HELPER.with_borrow_mut(Option::take); } #[cfg(not(feature = "llvm"))] @@ -324,32 +327,104 @@ fn run_helper_job(job: &CompileJob, config: &RuntimeConfig) -> Result path.clone(), - None => { - std::env::current_exe().map_err(|e| format!("failed to locate current exe: {e}"))? + JIT_HELPER.with_borrow_mut(|slot| { + if slot.as_ref().is_none_or(|helper| !helper.matches_config(config)) { + *slot = Some(HelperProcess::spawn(config)?); } - }; - let mut child = Command::new(exe) - .env(HELPER_ENV, "1") - .stdin(Stdio::piped()) - .stdout(Stdio::piped()) - .stderr(Stdio::inherit()) - .spawn() - .map_err(|e| format!("failed to spawn JIT helper: {e}"))?; - { - let stdin = child.stdin.as_mut().ok_or("helper stdin unavailable")?; - write_job(stdin, job, config).map_err(|e| format!("failed to write helper job: {e}"))?; + let helper = slot.as_mut().unwrap(); + match helper.compile(job, config) { + Ok(result) => Ok(result), + Err(err) => { + *slot = None; + Err(err) + } + } + }) +} + +#[cfg(feature = "llvm")] +struct HelperProcess { + path: PathBuf, + child: Child, + stdin: ChildStdin, + result_rx: chan::Receiver>, + reader: Option>, +} + +#[cfg(feature = "llvm")] +impl HelperProcess { + fn spawn(config: &RuntimeConfig) -> Result { + let path = match &config.jit_helper_path { + Some(path) => path.clone(), + None => { + std::env::current_exe().map_err(|e| format!("failed to locate current exe: {e}"))? + } + }; + let mut child = Command::new(&path) + .env(HELPER_ENV, "1") + .stdin(Stdio::piped()) + .stdout(Stdio::piped()) + .stderr(Stdio::inherit()) + .spawn() + .map_err(|e| format!("failed to spawn JIT helper: {e}"))?; + let stdin = child.stdin.take().ok_or("helper stdin unavailable")?; + let stdout = child.stdout.take().ok_or("helper stdout unavailable")?; + let (result_tx, result_rx) = chan::bounded(1); + let reader = std::thread::spawn(move || { + let mut stdout = stdout; + loop { + let result = read_helper_result(&mut stdout); + if result_tx.send(result).is_err() { + break; + } + } + }); + Ok(Self { path, child, stdin, result_rx, reader: Some(reader) }) + } + + fn matches_config(&self, config: &RuntimeConfig) -> bool { + match &config.jit_helper_path { + Some(path) => self.path == *path, + None => std::env::current_exe().map(|path| self.path == path).unwrap_or(false), + } } - let mut stdout = child.stdout.take().ok_or("helper stdout unavailable")?; - let result = read_helper_result(&mut stdout); - let status = child.wait().map_err(|e| format!("failed to wait for JIT helper: {e}"))?; - if !status.success() { - return Err(format!("JIT helper exited with {status}")); + fn compile( + &mut self, + job: &CompileJob, + config: &RuntimeConfig, + ) -> Result { + write_job(&mut self.stdin, job, config) + .map_err(|e| format!("failed to write helper job: {e}"))?; + self.stdin.flush().map_err(|e| format!("failed to flush helper job: {e}"))?; + + match self.result_rx.recv_timeout(config.tuning.jit_helper_timeout) { + Ok(result) => result, + Err(chan::RecvTimeoutError::Timeout) => { + let _ = self.child.kill(); + Err(format!("JIT helper timed out after {:?}", config.tuning.jit_helper_timeout)) + } + Err(chan::RecvTimeoutError::Disconnected) => { + let status = self.child.try_wait().ok().flatten(); + Err(match status { + Some(status) => format!("JIT helper exited with {status}"), + None => "JIT helper disconnected".into(), + }) + } + } + } +} + +#[cfg(feature = "llvm")] +impl Drop for HelperProcess { + fn drop(&mut self) { + let _ = self.child.kill(); + let _ = self.child.wait(); + if let Some(reader) = self.reader.take() { + let _ = reader.join(); + } } - result } #[cfg(feature = "llvm")] @@ -404,8 +479,38 @@ fn read_helper_result(mut r: impl Read) -> Result { #[cfg(feature = "llvm")] pub(super) fn run_jit_helper_stdio() -> eyre::Result<()> { let mut stdin = std::io::stdin().lock(); + let mut stdout = std::io::stdout().lock(); + let mut compiler: Option> = None; + + while let Some((job, config)) = read_helper_job(&mut stdin)? { + if compiler.is_none() { + compiler = Some(create_compiler(&config, false).map_err(|e| eyre::eyre!(e))?); + } + let compiler = compiler.as_mut().unwrap(); + compiler.set_opt_level(job.opt_level); + compiler.debug_assertions(config.debug_assertions); + compiler.set_dedup(!config.no_dedup); + compiler.set_dse(!config.no_dse); + + let result = compile_jit_object_artifact(&job, compiler); + if let Err(err) = compiler.clear_ir() { + warn!(%err, "clear_ir failed"); + } + write_helper_result(&mut stdout, result)?; + stdout.flush()?; + } + + Ok(()) +} + +#[cfg(feature = "llvm")] +fn read_helper_job(mut stdin: impl Read) -> eyre::Result> { let mut magic = [0u8; 5]; - stdin.read_exact(&mut magic)?; + match stdin.read_exact(&mut magic) { + Ok(()) => {} + Err(e) if e.kind() == std::io::ErrorKind::UnexpectedEof => return Ok(None), + Err(e) => return Err(e.into()), + } eyre::ensure!(&magic == b"RJIT\0", "invalid JIT helper request"); let mut hash = [0u8; 32]; @@ -432,12 +537,14 @@ pub(super) fn run_jit_helper_stdio() -> eyre::Result<()> { sync_notifier: SyncNotifier::none(), generation: 0, }; + Ok(Some((job, config))) +} - let mut compiler = create_compiler(&config, false).map_err(|e| eyre::eyre!(e))?; - compiler.set_opt_level(opt_level); - let result = compile_jit_object_artifact(&job, &mut compiler); - - let mut stdout = std::io::stdout().lock(); +#[cfg(feature = "llvm")] +fn write_helper_result( + mut stdout: impl Write, + result: Result, +) -> eyre::Result<()> { match result { Ok(WorkerSuccess::JitObject(success)) => { stdout.write_all(&[1])?; @@ -454,7 +561,6 @@ pub(super) fn run_jit_helper_stdio() -> eyre::Result<()> { write_bytes(&mut stdout, err.as_bytes())?; } } - stdout.flush()?; Ok(()) } @@ -613,6 +719,7 @@ impl CompilerState { thread_local! { static JIT_COMPILER: RefCell> = const { RefCell::new(None) }; static AOT_COMPILER: RefCell> = const { RefCell::new(None) }; + static JIT_HELPER: RefCell> = const { RefCell::new(None) }; } #[cfg(feature = "llvm")] diff --git a/docs/out-of-process-jit.md b/docs/out-of-process-jit.md index ee8418b1f..5faa5202f 100644 --- a/docs/out-of-process-jit.md +++ b/docs/out-of-process-jit.md @@ -23,15 +23,15 @@ Using LLVM ORC's remote executor APIs (`ExecutorProcessControl`, `SimpleRemoteEP Current prototype: -- `RuntimeConfig::jit_process_mode = JitProcessMode::OutOfProcess` makes JIT workers spawn a helper process via `RuntimeConfig::jit_helper_path`, or `std::env::current_exe()` when unset. +- `RuntimeConfig::jit_process_mode = JitProcessMode::OutOfProcess` makes each JIT worker keep a persistent helper process spawned via `RuntimeConfig::jit_helper_path`, or `std::env::current_exe()` when unset. - Helper binaries must call `revmc::runtime::maybe_run_jit_helper()` at process startup. `revmc-cli` does this already. -- The helper compiles one JIT object request from stdin and writes one framed response to stdout. +- Each worker sends a stream of JIT object requests to its helper over stdin and receives framed responses from stdout. - The parent links returned object bytes into its local ORC instance, resolves the symbol, and constructs `JitCodeBacking` with a parent-owned `ResourceTracker`. +- `RuntimeTuning::jit_helper_timeout` bounds each helper compilation; timed-out helpers are killed and replaced on the next job. Still needed: -- Keep one long-lived helper process instead of spawning per job. -- Move the worker pool into the helper process; the parent should only enqueue IPC requests. +- Move the worker pool into a single helper process; the parent should only enqueue IPC requests. - Define a versioned framed IPC protocol for `CompileJob` and `WorkerResult` data: key, bytecode, symbol name, spec id, optimization level, gas params, debug flags, dedup/DSE flags, dump settings, generation, timings, object bytes, and errors. - Keep AOT jobs either in the helper too or explicitly route them through the existing in-process AOT path; the first option gives consistent isolation. - Define shutdown semantics: close IPC, let the helper drain or cancel queued jobs, then kill on timeout. From fac7b9febe21a50072fdc476c9bd79aea7889615 Mon Sep 17 00:00:00 2001 From: DaniPopes <57450786+DaniPopes@users.noreply.github.com> Date: Wed, 29 Apr 2026 14:58:19 +0200 Subject: [PATCH 10/43] refactor(runtime): rename JIT mode config --- crates/revmc-builtins/src/ir.rs | 4 ++-- crates/revmc-runtime/src/runtime/backend.rs | 5 +++-- crates/revmc-runtime/src/runtime/config.rs | 14 +++++++------- crates/revmc-runtime/src/runtime/mod.rs | 5 +++-- crates/revmc-runtime/src/runtime/worker.rs | 6 +++--- docs/out-of-process-jit.md | 4 ++-- 6 files changed, 20 insertions(+), 18 deletions(-) diff --git a/crates/revmc-builtins/src/ir.rs b/crates/revmc-builtins/src/ir.rs index 75b241c93..1134dddad 100644 --- a/crates/revmc-builtins/src/ir.rs +++ b/crates/revmc-builtins/src/ir.rs @@ -132,9 +132,9 @@ macro_rules! builtins { } } - pub fn addr_by_name(name: &str) -> Option { + pub fn parse(name: &str) -> Option { match name { - $(stringify!($name) => Some(crate::$name as *const () as usize),)* + $(stringify!($name) => Some(Self::$ident),)* _ => None, } } diff --git a/crates/revmc-runtime/src/runtime/backend.rs b/crates/revmc-runtime/src/runtime/backend.rs index e7ddb0cd8..10b9ee20f 100644 --- a/crates/revmc-runtime/src/runtime/backend.rs +++ b/crates/revmc-runtime/src/runtime/backend.rs @@ -72,8 +72,9 @@ fn link_jit_object( .builtin_symbols .iter() .map(|name| { - let addr = revmc_builtins::Builtin::addr_by_name(name) - .ok_or_else(|| eyre::eyre!("unknown builtin symbol: {name}"))?; + let addr = revmc_builtins::Builtin::parse(name) + .ok_or_else(|| eyre::eyre!("unknown builtin symbol: {name}"))? + .addr(); Ok((CString::new(name.as_str())?, addr)) }) .collect::>>()?; diff --git a/crates/revmc-runtime/src/runtime/config.rs b/crates/revmc-runtime/src/runtime/config.rs index 936f33ffb..2f8750c36 100644 --- a/crates/revmc-runtime/src/runtime/config.rs +++ b/crates/revmc-runtime/src/runtime/config.rs @@ -78,11 +78,11 @@ pub struct RuntimeConfig { /// Where JIT compilation work runs. /// - /// Defaults to [`JitProcessMode::InProcess`]. - pub jit_process_mode: JitProcessMode, + /// Defaults to [`JitMode::InProcess`]. + pub jit_mode: JitMode, - /// Helper executable used when [`jit_process_mode`](Self::jit_process_mode) - /// is [`JitProcessMode::OutOfProcess`]. + /// Helper executable used when [`jit_mode`](Self::jit_mode) + /// is [`JitMode::OutOfProcess`]. /// /// When `None`, the runtime spawns `std::env::current_exe()` and expects it /// to call [`super::maybe_run_jit_helper`] during startup. @@ -137,7 +137,7 @@ pub enum CompilationKind { /// Where JIT compilation work runs. #[derive(Clone, Copy, Debug, Default, PartialEq, Eq)] -pub enum JitProcessMode { +pub enum JitMode { /// Compile on background threads in this process. #[default] InProcess, @@ -161,7 +161,7 @@ impl Default for RuntimeConfig { no_dse: false, gas_params: None, aot: false, - jit_process_mode: JitProcessMode::default(), + jit_mode: JitMode::default(), jit_helper_path: None, blocking: false, on_compilation: None, @@ -215,7 +215,7 @@ pub struct RuntimeTuning { /// Timeout for a single out-of-process JIT compilation job. /// /// When exceeded, the helper process is killed and a fresh helper is spawned for - /// the next job. Only applies to [`JitProcessMode::OutOfProcess`]. + /// the next job. Only applies to [`JitMode::OutOfProcess`]. /// /// Defaults to `5s`. pub jit_helper_timeout: Duration, diff --git a/crates/revmc-runtime/src/runtime/mod.rs b/crates/revmc-runtime/src/runtime/mod.rs index 2eebb253f..fdbba0f7a 100644 --- a/crates/revmc-runtime/src/runtime/mod.rs +++ b/crates/revmc-runtime/src/runtime/mod.rs @@ -30,7 +30,7 @@ pub use api::{ }; mod config; -pub use config::{CompilationEvent, CompilationKind, JitProcessMode, RuntimeConfig, RuntimeTuning}; +pub use config::{CompilationEvent, CompilationKind, JitMode, RuntimeConfig, RuntimeTuning}; mod backend; @@ -157,6 +157,7 @@ impl JitBackend { config.enabled = true; config.tuning.jit_hot_threshold = 0; } + let enabled = config.enabled; let (tx, rx) = chan::bounded::(config.tuning.channel_capacity); let events = ArrayQueue::new(config.tuning.channel_capacity); @@ -373,7 +374,7 @@ impl JitBackend { debug!( blocking = self.inner.blocking, workers = config.tuning.jit_worker_count, - jit_process_mode = ?config.jit_process_mode, + jit_mode = ?config.jit_mode, hot_threshold = config.tuning.jit_hot_threshold, channel_capacity = config.tuning.channel_capacity, "spawning backend thread", diff --git a/crates/revmc-runtime/src/runtime/worker.rs b/crates/revmc-runtime/src/runtime/worker.rs index d290e3ad6..4eb054aee 100644 --- a/crates/revmc-runtime/src/runtime/worker.rs +++ b/crates/revmc-runtime/src/runtime/worker.rs @@ -12,7 +12,7 @@ use crate::{ CompileTimings, EvmCompilerFn, OptimizationLevel, eyre, runtime::{ - config::{CompilationKind, JitProcessMode, RuntimeConfig}, + config::{CompilationKind, JitMode, RuntimeConfig}, storage::RuntimeCacheKey, }, }; @@ -289,7 +289,7 @@ fn clear_thread_local_compilers() {} fn compile_job(job: CompileJob, config: &RuntimeConfig) -> WorkerResult { trace!(?job, "received job"); match job.kind { - CompilationKind::Jit if config.jit_process_mode == JitProcessMode::OutOfProcess => { + CompilationKind::Jit if config.jit_mode == JitMode::OutOfProcess => { compile_job_out_of_process(job, config) } CompilationKind::Jit => { @@ -659,7 +659,7 @@ fn compile_with_state( compiler.set_opt_level(job.opt_level); let outcome = match job.kind { - CompilationKind::Jit if config.jit_process_mode == JitProcessMode::OutOfProcess => { + CompilationKind::Jit if config.jit_mode == JitMode::OutOfProcess => { compile_jit_object_artifact(&job, compiler) } CompilationKind::Jit => compile_jit_artifact(&job, compiler), diff --git a/docs/out-of-process-jit.md b/docs/out-of-process-jit.md index 5faa5202f..306c6ff00 100644 --- a/docs/out-of-process-jit.md +++ b/docs/out-of-process-jit.md @@ -1,6 +1,6 @@ # Out-of-process JIT exploration -`RuntimeConfig::jit_process_mode` now has an `OutOfProcess` variant, disabled by default. It currently returns an error if selected; this document records the implementation work needed to make it transparent. +`RuntimeConfig::jit_mode` now has an `OutOfProcess` variant, disabled by default. This document records the current prototype and the remaining work needed to make it production-ready. ## Recommended architecture @@ -23,7 +23,7 @@ Using LLVM ORC's remote executor APIs (`ExecutorProcessControl`, `SimpleRemoteEP Current prototype: -- `RuntimeConfig::jit_process_mode = JitProcessMode::OutOfProcess` makes each JIT worker keep a persistent helper process spawned via `RuntimeConfig::jit_helper_path`, or `std::env::current_exe()` when unset. +- `RuntimeConfig::jit_mode = JitMode::OutOfProcess` makes each JIT worker keep a persistent helper process spawned via `RuntimeConfig::jit_helper_path`, or `std::env::current_exe()` when unset. - Helper binaries must call `revmc::runtime::maybe_run_jit_helper()` at process startup. `revmc-cli` does this already. - Each worker sends a stream of JIT object requests to its helper over stdin and receives framed responses from stdout. - The parent links returned object bytes into its local ORC instance, resolves the symbol, and constructs `JitCodeBacking` with a parent-owned `ResourceTracker`. From 18a9ff12bf789c3f55568421da63f5972f79745e Mon Sep 17 00:00:00 2001 From: DaniPopes <57450786+DaniPopes@users.noreply.github.com> Date: Wed, 29 Apr 2026 15:00:01 +0200 Subject: [PATCH 11/43] refactor(runtime): share JIT helper process --- crates/revmc-runtime/src/runtime/config.rs | 4 +-- crates/revmc-runtime/src/runtime/worker.rs | 37 ++++++++++++---------- docs/out-of-process-jit.md | 6 ++-- 3 files changed, 25 insertions(+), 22 deletions(-) diff --git a/crates/revmc-runtime/src/runtime/config.rs b/crates/revmc-runtime/src/runtime/config.rs index 2f8750c36..d09f5dc25 100644 --- a/crates/revmc-runtime/src/runtime/config.rs +++ b/crates/revmc-runtime/src/runtime/config.rs @@ -218,7 +218,7 @@ pub struct RuntimeTuning { /// the next job. Only applies to [`JitMode::OutOfProcess`]. /// /// Defaults to `5s`. - pub jit_helper_timeout: Duration, + pub jit_timeout: Duration, /// Capacity of the per-worker job queue. /// @@ -288,7 +288,7 @@ impl Default for RuntimeTuning { jit_max_bytecode_len: 0, jit_max_pending_jobs: 2048, jit_worker_count: worker_count, - jit_helper_timeout: Duration::from_secs(5), + jit_timeout: Duration::from_secs(5), jit_worker_queue_capacity: 64, jit_opt_level: crate::OptimizationLevel::default(), aot_opt_level: crate::OptimizationLevel::default(), diff --git a/crates/revmc-runtime/src/runtime/worker.rs b/crates/revmc-runtime/src/runtime/worker.rs index 4eb054aee..7600437ca 100644 --- a/crates/revmc-runtime/src/runtime/worker.rs +++ b/crates/revmc-runtime/src/runtime/worker.rs @@ -31,7 +31,7 @@ use std::{ }; use std::{ sync::{ - Arc, + Arc, Mutex, OnceLock, atomic::{AtomicBool, AtomicUsize, Ordering}, }, time::Duration, @@ -279,7 +279,6 @@ impl Drop for WorkerPool { fn clear_thread_local_compilers() { JIT_COMPILER.with_borrow_mut(Option::take); AOT_COMPILER.with_borrow_mut(Option::take); - JIT_HELPER.with_borrow_mut(Option::take); } #[cfg(not(feature = "llvm"))] @@ -327,20 +326,25 @@ fn run_helper_job(job: &CompileJob, config: &RuntimeConfig) -> Result Ok(result), - Err(err) => { - *slot = None; - Err(err) - } + let helper = slot.as_mut().unwrap(); + match helper.compile(job, config) { + Ok(result) => Ok(result), + Err(err) => { + *slot = None; + Err(err) } - }) + } +} + +#[cfg(feature = "llvm")] +fn helper_process() -> &'static Mutex> { + static HELPER: OnceLock>> = OnceLock::new(); + HELPER.get_or_init(|| Mutex::new(None)) } #[cfg(feature = "llvm")] @@ -399,11 +403,11 @@ impl HelperProcess { .map_err(|e| format!("failed to write helper job: {e}"))?; self.stdin.flush().map_err(|e| format!("failed to flush helper job: {e}"))?; - match self.result_rx.recv_timeout(config.tuning.jit_helper_timeout) { + match self.result_rx.recv_timeout(config.tuning.jit_timeout) { Ok(result) => result, Err(chan::RecvTimeoutError::Timeout) => { let _ = self.child.kill(); - Err(format!("JIT helper timed out after {:?}", config.tuning.jit_helper_timeout)) + Err(format!("JIT helper timed out after {:?}", config.tuning.jit_timeout)) } Err(chan::RecvTimeoutError::Disconnected) => { let status = self.child.try_wait().ok().flatten(); @@ -719,7 +723,6 @@ impl CompilerState { thread_local! { static JIT_COMPILER: RefCell> = const { RefCell::new(None) }; static AOT_COMPILER: RefCell> = const { RefCell::new(None) }; - static JIT_HELPER: RefCell> = const { RefCell::new(None) }; } #[cfg(feature = "llvm")] diff --git a/docs/out-of-process-jit.md b/docs/out-of-process-jit.md index 306c6ff00..03782aa93 100644 --- a/docs/out-of-process-jit.md +++ b/docs/out-of-process-jit.md @@ -23,11 +23,11 @@ Using LLVM ORC's remote executor APIs (`ExecutorProcessControl`, `SimpleRemoteEP Current prototype: -- `RuntimeConfig::jit_mode = JitMode::OutOfProcess` makes each JIT worker keep a persistent helper process spawned via `RuntimeConfig::jit_helper_path`, or `std::env::current_exe()` when unset. +- `RuntimeConfig::jit_mode = JitMode::OutOfProcess` makes the runtime keep a global persistent helper process spawned via `RuntimeConfig::jit_helper_path`, or `std::env::current_exe()` when unset. - Helper binaries must call `revmc::runtime::maybe_run_jit_helper()` at process startup. `revmc-cli` does this already. -- Each worker sends a stream of JIT object requests to its helper over stdin and receives framed responses from stdout. +- Workers send a stream of JIT object requests to the helper over stdin and receive framed responses from stdout. - The parent links returned object bytes into its local ORC instance, resolves the symbol, and constructs `JitCodeBacking` with a parent-owned `ResourceTracker`. -- `RuntimeTuning::jit_helper_timeout` bounds each helper compilation; timed-out helpers are killed and replaced on the next job. +- `RuntimeTuning::jit_timeout` bounds each helper compilation; timed-out helpers are killed and replaced on the next job. Still needed: From e541a980d5926f9cabcb021dc554ddcebf005016 Mon Sep 17 00:00:00 2001 From: DaniPopes <57450786+DaniPopes@users.noreply.github.com> Date: Wed, 29 Apr 2026 15:00:16 +0200 Subject: [PATCH 12/43] refactor(llvm): reuse ensured ORC state --- crates/revmc-llvm/src/lib.rs | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/crates/revmc-llvm/src/lib.rs b/crates/revmc-llvm/src/lib.rs index c3869c5cc..62a8e7581 100644 --- a/crates/revmc-llvm/src/lib.rs +++ b/crates/revmc-llvm/src/lib.rs @@ -754,8 +754,7 @@ impl EvmLlvmBackend { object: &[u8], symbols: &[(CString, usize)], ) -> Result<(usize, orc::ResourceTracker)> { - self.ensure_orc()?; - let orc = self.orc.as_mut().unwrap(); + let orc = self.ensure_orc()?; orc.global.define_builtins(symbols); let tracker = orc.jd().create_resource_tracker(); orc.global.jit.add_object_with_rt(object, &tracker).map_err(error_msg)?; From 8fd53edfa3840931acdbded624b02763ae5e66d4 Mon Sep 17 00:00:00 2001 From: DaniPopes <57450786+DaniPopes@users.noreply.github.com> Date: Wed, 29 Apr 2026 15:01:10 +0200 Subject: [PATCH 13/43] refactor(runtime): encapsulate JIT helper state --- crates/revmc-runtime/src/runtime/worker.rs | 49 ++++++++++++++-------- 1 file changed, 31 insertions(+), 18 deletions(-) diff --git a/crates/revmc-runtime/src/runtime/worker.rs b/crates/revmc-runtime/src/runtime/worker.rs index 7600437ca..2367f6b06 100644 --- a/crates/revmc-runtime/src/runtime/worker.rs +++ b/crates/revmc-runtime/src/runtime/worker.rs @@ -326,29 +326,42 @@ fn run_helper_job(job: &CompileJob, config: &RuntimeConfig) -> Result Ok(result), - Err(err) => { - *slot = None; - Err(err) - } - } + helper_process().compile(job, config) } #[cfg(feature = "llvm")] -fn helper_process() -> &'static Mutex> { - static HELPER: OnceLock>> = OnceLock::new(); - HELPER.get_or_init(|| Mutex::new(None)) +fn helper_process() -> &'static HelperProcess { + static HELPER: OnceLock = OnceLock::new(); + HELPER.get_or_init(HelperProcess::default) } #[cfg(feature = "llvm")] +#[derive(Default)] struct HelperProcess { + inner: Mutex>, +} + +#[cfg(feature = "llvm")] +impl HelperProcess { + fn compile(&self, job: &CompileJob, config: &RuntimeConfig) -> Result { + let mut slot = self.inner.lock().unwrap(); + if slot.as_ref().is_none_or(|helper| !helper.matches_config(config)) { + *slot = Some(HelperProcessInner::spawn(config)?); + } + + let helper = slot.as_mut().unwrap(); + match helper.compile(job, config) { + Ok(result) => Ok(result), + Err(err) => { + *slot = None; + Err(err) + } + } + } +} + +#[cfg(feature = "llvm")] +struct HelperProcessInner { path: PathBuf, child: Child, stdin: ChildStdin, @@ -357,7 +370,7 @@ struct HelperProcess { } #[cfg(feature = "llvm")] -impl HelperProcess { +impl HelperProcessInner { fn spawn(config: &RuntimeConfig) -> Result { let path = match &config.jit_helper_path { Some(path) => path.clone(), @@ -421,7 +434,7 @@ impl HelperProcess { } #[cfg(feature = "llvm")] -impl Drop for HelperProcess { +impl Drop for HelperProcessInner { fn drop(&mut self) { let _ = self.child.kill(); let _ = self.child.wait(); From 4913491a1a59535ff4bcba6cec091c11f2fde39f Mon Sep 17 00:00:00 2001 From: DaniPopes <57450786+DaniPopes@users.noreply.github.com> Date: Wed, 29 Apr 2026 15:07:45 +0200 Subject: [PATCH 14/43] refactor(runtime): use JSON helper IPC --- Cargo.lock | 2 + crates/revmc-runtime/Cargo.toml | 2 + crates/revmc-runtime/src/runtime/worker.rs | 174 +++++++++------------ docs/out-of-process-jit.md | 4 +- 4 files changed, 78 insertions(+), 104 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index 8944a45d6..f26770f4a 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -3344,6 +3344,8 @@ dependencies = [ "revmc-context", "revmc-llvm", "revmc-statetest", + "serde", + "serde_json", "similar-asserts", "tempfile", "tracing", diff --git a/crates/revmc-runtime/Cargo.toml b/crates/revmc-runtime/Cargo.toml index 3857b86f4..e2f74d44c 100644 --- a/crates/revmc-runtime/Cargo.toml +++ b/crates/revmc-runtime/Cargo.toml @@ -50,6 +50,8 @@ libloading = "0.9" tempfile = "3.10" quanta.workspace = true rayon.workspace = true +serde = { version = "1", features = ["derive"] } +serde_json = "1" tracing.workspace = true [dev-dependencies] diff --git a/crates/revmc-runtime/src/runtime/worker.rs b/crates/revmc-runtime/src/runtime/worker.rs index 2367f6b06..e8e3d8b86 100644 --- a/crates/revmc-runtime/src/runtime/worker.rs +++ b/crates/revmc-runtime/src/runtime/worker.rs @@ -23,7 +23,7 @@ use rayon::{ThreadPool, ThreadPoolBuilder}; use std::{ cell::RefCell, fs::File, - io::{Read, Write}, + io::{BufRead, BufReader, Read, Write}, path::PathBuf, process::{Child, ChildStdin, Command, Stdio}, thread::JoinHandle, @@ -44,6 +44,8 @@ use crate::{ }; #[cfg(feature = "llvm")] use revm_primitives::hardfork::SpecId; +#[cfg(feature = "llvm")] +use serde::{Deserialize, Serialize}; /// Notifier for synchronous compilation requests. /// @@ -389,7 +391,7 @@ impl HelperProcessInner { let stdout = child.stdout.take().ok_or("helper stdout unavailable")?; let (result_tx, result_rx) = chan::bounded(1); let reader = std::thread::spawn(move || { - let mut stdout = stdout; + let mut stdout = BufReader::new(stdout); loop { let result = read_helper_result(&mut stdout); if result_tx.send(result).is_err() { @@ -444,52 +446,61 @@ impl Drop for HelperProcessInner { } } +#[cfg(feature = "llvm")] +#[derive(Serialize, Deserialize)] +struct HelperRequest { + code_hash: [u8; 32], + spec_id: u8, + opt_level: u8, + debug_assertions: bool, + no_dedup: bool, + no_dse: bool, + symbol_name: String, + bytecode: Vec, +} + +#[cfg(feature = "llvm")] +#[derive(Serialize, Deserialize)] +#[serde(tag = "status", rename_all = "snake_case")] +enum HelperResponse { + Ok { symbol_name: String, object_bytes: Vec, builtin_symbols: Vec }, + Err { error: String }, +} + #[cfg(feature = "llvm")] fn write_job(mut w: impl Write, job: &CompileJob, config: &RuntimeConfig) -> std::io::Result<()> { - w.write_all(b"RJIT\0")?; - w.write_all(&job.key.code_hash.0)?; - w.write_all(&[job.key.spec_id as u8, opt_level_to_u8(job.opt_level)])?; - w.write_all(&[ - u8::from(config.debug_assertions), - u8::from(config.no_dedup), - u8::from(config.no_dse), - ])?; - write_bytes(&mut w, job.symbol_name.as_bytes())?; - write_bytes(&mut w, &job.bytecode)?; - Ok(()) + let req = HelperRequest { + code_hash: job.key.code_hash.0, + spec_id: job.key.spec_id as u8, + opt_level: opt_level_to_u8(job.opt_level), + debug_assertions: config.debug_assertions, + no_dedup: config.no_dedup, + no_dse: config.no_dse, + symbol_name: job.symbol_name.clone(), + bytecode: job.bytecode.to_vec(), + }; + serde_json::to_writer(&mut w, &req)?; + w.write_all(b"\n") } #[cfg(feature = "llvm")] -fn read_helper_result(mut r: impl Read) -> Result { - let mut tag = [0u8; 1]; - r.read_exact(&mut tag).map_err(|e| format!("failed to read helper result: {e}"))?; - match tag[0] { - 0 => { - let msg = - read_string(&mut r).map_err(|e| format!("failed to read helper error: {e}"))?; - Err(msg) - } - 1 => { - let symbol_name = - read_string(&mut r).map_err(|e| format!("failed to read symbol name: {e}"))?; - let object_bytes = - read_vec(&mut r).map_err(|e| format!("failed to read object bytes: {e}"))?; - let count = - read_u32(&mut r).map_err(|e| format!("failed to read symbol count: {e}"))?; - let mut builtin_symbols = Vec::with_capacity(count as usize); - for _ in 0..count { - builtin_symbols.push( - read_string(&mut r) - .map_err(|e| format!("failed to read builtin symbol: {e}"))?, - ); - } +fn read_helper_result(r: &mut impl BufRead) -> Result { + let mut line = String::new(); + let n = r.read_line(&mut line).map_err(|e| format!("failed to read helper result: {e}"))?; + if n == 0 { + return Err("JIT helper closed stdout".into()); + } + match serde_json::from_str::(&line) + .map_err(|e| format!("failed to decode helper result: {e}"))? + { + HelperResponse::Ok { symbol_name, object_bytes, builtin_symbols } => { Ok(WorkerSuccess::JitObject(JitObjectSuccess { symbol_name, object_bytes, builtin_symbols, })) } - tag => Err(format!("unknown helper result tag {tag}")), + HelperResponse::Err { error } => Err(error), } } @@ -521,35 +532,26 @@ pub(super) fn run_jit_helper_stdio() -> eyre::Result<()> { } #[cfg(feature = "llvm")] -fn read_helper_job(mut stdin: impl Read) -> eyre::Result> { - let mut magic = [0u8; 5]; - match stdin.read_exact(&mut magic) { - Ok(()) => {} - Err(e) if e.kind() == std::io::ErrorKind::UnexpectedEof => return Ok(None), - Err(e) => return Err(e.into()), - } - eyre::ensure!(&magic == b"RJIT\0", "invalid JIT helper request"); - - let mut hash = [0u8; 32]; - stdin.read_exact(&mut hash)?; - let mut fixed = [0u8; 5]; - stdin.read_exact(&mut fixed)?; - let spec_id = SpecId::try_from_u8(fixed[0]).ok_or_else(|| eyre::eyre!("invalid spec id"))?; - let opt_level = opt_level_from_u8(fixed[1])?; - let symbol_name = read_string(&mut stdin)?; - let bytecode = Bytes::from(read_vec(&mut stdin)?); +fn read_helper_job(stdin: &mut impl BufRead) -> eyre::Result> { + let mut line = String::new(); + if stdin.read_line(&mut line)? == 0 { + return Ok(None); + } + let req: HelperRequest = serde_json::from_str(&line)?; + let spec_id = SpecId::try_from_u8(req.spec_id).ok_or_else(|| eyre::eyre!("invalid spec id"))?; + let opt_level = opt_level_from_u8(req.opt_level)?; let config = RuntimeConfig { - debug_assertions: fixed[2] != 0, - no_dedup: fixed[3] != 0, - no_dse: fixed[4] != 0, + debug_assertions: req.debug_assertions, + no_dedup: req.no_dedup, + no_dse: req.no_dse, ..Default::default() }; let job = CompileJob { kind: CompilationKind::Jit, - key: RuntimeCacheKey { code_hash: B256::from(hash), spec_id }, - bytecode, - symbol_name, + key: RuntimeCacheKey { code_hash: B256::from(req.code_hash), spec_id }, + bytecode: Bytes::from(req.bytecode), + symbol_name: req.symbol_name, opt_level, sync_notifier: SyncNotifier::none(), generation: 0, @@ -562,52 +564,20 @@ fn write_helper_result( mut stdout: impl Write, result: Result, ) -> eyre::Result<()> { - match result { - Ok(WorkerSuccess::JitObject(success)) => { - stdout.write_all(&[1])?; - write_bytes(&mut stdout, success.symbol_name.as_bytes())?; - write_bytes(&mut stdout, &success.object_bytes)?; - stdout.write_all(&(success.builtin_symbols.len() as u32).to_le_bytes())?; - for symbol in success.builtin_symbols { - write_bytes(&mut stdout, symbol.as_bytes())?; - } - } + let response = match result { + Ok(WorkerSuccess::JitObject(success)) => HelperResponse::Ok { + symbol_name: success.symbol_name, + object_bytes: success.object_bytes, + builtin_symbols: success.builtin_symbols, + }, Ok(_) => unreachable!(), - Err(err) => { - stdout.write_all(&[0])?; - write_bytes(&mut stdout, err.as_bytes())?; - } - } + Err(error) => HelperResponse::Err { error }, + }; + serde_json::to_writer(&mut stdout, &response)?; + stdout.write_all(b"\n")?; Ok(()) } -#[cfg(feature = "llvm")] -fn write_bytes(mut w: impl Write, bytes: &[u8]) -> std::io::Result<()> { - w.write_all(&(bytes.len() as u32).to_le_bytes())?; - w.write_all(bytes) -} - -#[cfg(feature = "llvm")] -fn read_string(r: impl Read) -> std::io::Result { - String::from_utf8(read_vec(r)?) - .map_err(|e| std::io::Error::new(std::io::ErrorKind::InvalidData, e)) -} - -#[cfg(feature = "llvm")] -fn read_vec(mut r: impl Read) -> std::io::Result> { - let len = read_u32(&mut r)? as usize; - let mut bytes = vec![0; len]; - r.read_exact(&mut bytes)?; - Ok(bytes) -} - -#[cfg(feature = "llvm")] -fn read_u32(mut r: impl Read) -> std::io::Result { - let mut bytes = [0u8; 4]; - r.read_exact(&mut bytes)?; - Ok(u32::from_le_bytes(bytes)) -} - #[cfg(feature = "llvm")] fn opt_level_to_u8(level: OptimizationLevel) -> u8 { match level { diff --git a/docs/out-of-process-jit.md b/docs/out-of-process-jit.md index 03782aa93..60f195b22 100644 --- a/docs/out-of-process-jit.md +++ b/docs/out-of-process-jit.md @@ -25,14 +25,14 @@ Current prototype: - `RuntimeConfig::jit_mode = JitMode::OutOfProcess` makes the runtime keep a global persistent helper process spawned via `RuntimeConfig::jit_helper_path`, or `std::env::current_exe()` when unset. - Helper binaries must call `revmc::runtime::maybe_run_jit_helper()` at process startup. `revmc-cli` does this already. -- Workers send a stream of JIT object requests to the helper over stdin and receive framed responses from stdout. +- Workers send newline-delimited JSON JIT object requests to the helper over stdin and receive newline-delimited JSON responses from stdout. - The parent links returned object bytes into its local ORC instance, resolves the symbol, and constructs `JitCodeBacking` with a parent-owned `ResourceTracker`. - `RuntimeTuning::jit_timeout` bounds each helper compilation; timed-out helpers are killed and replaced on the next job. Still needed: - Move the worker pool into a single helper process; the parent should only enqueue IPC requests. -- Define a versioned framed IPC protocol for `CompileJob` and `WorkerResult` data: key, bytecode, symbol name, spec id, optimization level, gas params, debug flags, dedup/DSE flags, dump settings, generation, timings, object bytes, and errors. +- Add protocol versioning to the JSON IPC payloads and carry the remaining data: gas params, dump settings, generation, timings, object bytes, and errors. - Keep AOT jobs either in the helper too or explicitly route them through the existing in-process AOT path; the first option gives consistent isolation. - Define shutdown semantics: close IPC, let the helper drain or cancel queued jobs, then kill on timeout. - Treat helper crash as worker-pool failure: fail pending synchronous jobs, drop pending async jobs, and optionally respawn. From fd6df05209af633ac8780062036c4bbbc300b96f Mon Sep 17 00:00:00 2001 From: DaniPopes <57450786+DaniPopes@users.noreply.github.com> Date: Wed, 29 Apr 2026 15:20:25 +0200 Subject: [PATCH 15/43] refactor(runtime): use domain types in JIT IPC --- crates/revmc-runtime/src/runtime/worker.rs | 18 +++++++++--------- 1 file changed, 9 insertions(+), 9 deletions(-) diff --git a/crates/revmc-runtime/src/runtime/worker.rs b/crates/revmc-runtime/src/runtime/worker.rs index e8e3d8b86..606acb84c 100644 --- a/crates/revmc-runtime/src/runtime/worker.rs +++ b/crates/revmc-runtime/src/runtime/worker.rs @@ -132,7 +132,7 @@ pub(crate) struct JitObjectSuccess { /// The symbol name in the object file. pub(crate) symbol_name: String, /// The raw relocatable object bytes. - pub(crate) object_bytes: Vec, + pub(crate) object_bytes: Bytes, /// Builtin absolute symbols referenced by the object. pub(crate) builtin_symbols: Vec, } @@ -449,35 +449,35 @@ impl Drop for HelperProcessInner { #[cfg(feature = "llvm")] #[derive(Serialize, Deserialize)] struct HelperRequest { - code_hash: [u8; 32], + code_hash: B256, spec_id: u8, opt_level: u8, debug_assertions: bool, no_dedup: bool, no_dse: bool, symbol_name: String, - bytecode: Vec, + bytecode: Bytes, } #[cfg(feature = "llvm")] #[derive(Serialize, Deserialize)] #[serde(tag = "status", rename_all = "snake_case")] enum HelperResponse { - Ok { symbol_name: String, object_bytes: Vec, builtin_symbols: Vec }, + Ok { symbol_name: String, object_bytes: Bytes, builtin_symbols: Vec }, Err { error: String }, } #[cfg(feature = "llvm")] fn write_job(mut w: impl Write, job: &CompileJob, config: &RuntimeConfig) -> std::io::Result<()> { let req = HelperRequest { - code_hash: job.key.code_hash.0, + code_hash: job.key.code_hash, spec_id: job.key.spec_id as u8, opt_level: opt_level_to_u8(job.opt_level), debug_assertions: config.debug_assertions, no_dedup: config.no_dedup, no_dse: config.no_dse, symbol_name: job.symbol_name.clone(), - bytecode: job.bytecode.to_vec(), + bytecode: job.bytecode.clone(), }; serde_json::to_writer(&mut w, &req)?; w.write_all(b"\n") @@ -549,8 +549,8 @@ fn read_helper_job(stdin: &mut impl BufRead) -> eyre::Result Date: Sat, 16 May 2026 05:21:39 +0200 Subject: [PATCH 16/43] fix(runtime): wire out-of-process jit env --- Cargo.lock | 1 + Cargo.toml | 1 + crates/revmc-builtins/src/ir.rs | 6 -- crates/revmc-runtime/Cargo.toml | 1 + crates/revmc-runtime/src/runtime/config.rs | 63 ++++++++++++++++++-- crates/revmc-runtime/src/runtime/worker.rs | 67 +++++++++++++++++----- docs/out-of-process-jit.md | 5 +- 7 files changed, 119 insertions(+), 25 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index 75dd141dc..1420e3e26 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -3327,6 +3327,7 @@ dependencies = [ "dashmap", "derive_more", "eyre", + "libc", "libloading", "paste", "quanta", diff --git a/Cargo.toml b/Cargo.toml index b0d6a6d06..3b035a94b 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -63,6 +63,7 @@ revm-statetest-types = { version = "17.0.0", default-features = false } crossbeam-channel = "0.5" crossbeam-queue = "0.3" +libc = "0.2" rayon = "1.10" color-eyre = "0.6" criterion = { package = "codspeed-criterion-compat", version = "4", default-features = false, features = ["cargo_bench_support"] } diff --git a/crates/revmc-builtins/src/ir.rs b/crates/revmc-builtins/src/ir.rs index bf9de1167..1d9603f68 100644 --- a/crates/revmc-builtins/src/ir.rs +++ b/crates/revmc-builtins/src/ir.rs @@ -146,12 +146,6 @@ macro_rules! builtins { } } - pub const fn call_conv(self) -> CallConv { - match self { - Self::Mresize => CallConv::PreserveMost, - _ => CallConv::Default, - } - } pub fn ret(self, $bcx: &mut B) -> Option { $($types_init)* match self { diff --git a/crates/revmc-runtime/Cargo.toml b/crates/revmc-runtime/Cargo.toml index e2f74d44c..0fc5e6cbc 100644 --- a/crates/revmc-runtime/Cargo.toml +++ b/crates/revmc-runtime/Cargo.toml @@ -46,6 +46,7 @@ crossbeam-queue.workspace = true dashmap = "6" derive_more = { version = "2", default-features = false, features = ["debug"] } eyre.workspace = true +libc.workspace = true libloading = "0.9" tempfile = "3.10" quanta.workspace = true diff --git a/crates/revmc-runtime/src/runtime/config.rs b/crates/revmc-runtime/src/runtime/config.rs index d09f5dc25..cc1e4bc8a 100644 --- a/crates/revmc-runtime/src/runtime/config.rs +++ b/crates/revmc-runtime/src/runtime/config.rs @@ -4,7 +4,12 @@ use crate::{CompileTimings, runtime::storage::ArtifactStore}; use alloy_primitives::B256; use revm_context_interface::cfg::GasParams; use revm_primitives::hardfork::SpecId; -use std::{path::PathBuf, sync::Arc, time::Duration}; +use std::{env, path::PathBuf, str::FromStr, sync::Arc, time::Duration}; + +const JIT_MODE_ENV: &str = "REVMC_JIT_MODE"; +const JIT_HELPER_PATH_ENV: &str = "REVMC_JIT_HELPER_PATH"; +const JIT_HELPER_MEMORY_LIMIT_ENV: &str = "REVMC_JIT_HELPER_MEMORY_LIMIT_BYTES"; +const JIT_HELPER_CPU_SECONDS_ENV: &str = "REVMC_JIT_HELPER_CPU_SECONDS"; /// Runtime configuration. #[derive(Clone, derive_more::Debug)] @@ -78,14 +83,15 @@ pub struct RuntimeConfig { /// Where JIT compilation work runs. /// - /// Defaults to [`JitMode::InProcess`]. + /// Defaults to [`JitMode::InProcess`], or `REVMC_JIT_MODE` when set. pub jit_mode: JitMode, /// Helper executable used when [`jit_mode`](Self::jit_mode) /// is [`JitMode::OutOfProcess`]. /// /// When `None`, the runtime spawns `std::env::current_exe()` and expects it - /// to call [`super::maybe_run_jit_helper`] during startup. + /// to call [`super::maybe_run_jit_helper`] during startup. Defaults to + /// `REVMC_JIT_HELPER_PATH` when set. pub jit_helper_path: Option, /// Blocking mode: every lookup synchronously JIT-compiles on miss and never @@ -148,6 +154,21 @@ pub enum JitMode { OutOfProcess, } +impl FromStr for JitMode { + type Err = String; + + fn from_str(s: &str) -> Result { + let mode = s.trim().to_ascii_lowercase(); + Ok(match mode.as_str() { + "" | "in-process" | "in_process" | "inprocess" | "in" | "0" => Self::InProcess, + "out-of-process" | "out_of_process" | "outofprocess" | "out" | "oop" | "1" => { + Self::OutOfProcess + } + _ => return Err(format!("unknown JIT mode: {s}")), + }) + } +} + impl Default for RuntimeConfig { fn default() -> Self { Self { @@ -161,8 +182,8 @@ impl Default for RuntimeConfig { no_dse: false, gas_params: None, aot: false, - jit_mode: JitMode::default(), - jit_helper_path: None, + jit_mode: env::var(JIT_MODE_ENV).ok().and_then(|s| s.parse().ok()).unwrap_or_default(), + jit_helper_path: env_path(JIT_HELPER_PATH_ENV), blocking: false, on_compilation: None, } @@ -220,6 +241,23 @@ pub struct RuntimeTuning { /// Defaults to `5s`. pub jit_timeout: Duration, + /// Maximum address space for the out-of-process JIT helper, in bytes. + /// + /// `0` disables the limit. On Unix this is applied with `RLIMIT_AS` before + /// the helper process starts executing. + /// + /// Defaults to `0`, or `REVMC_JIT_HELPER_MEMORY_LIMIT_BYTES` when set. + pub jit_helper_memory_limit_bytes: u64, + + /// Maximum CPU time for the out-of-process JIT helper. + /// + /// `None` disables the limit. On Unix this is applied with `RLIMIT_CPU` + /// before the helper process starts executing. + /// + /// Defaults to `None`, or `REVMC_JIT_HELPER_CPU_SECONDS` when set to a + /// non-zero integer. + pub jit_helper_cpu_time: Option, + /// Capacity of the per-worker job queue. /// /// Defaults to `64`. @@ -289,6 +327,10 @@ impl Default for RuntimeTuning { jit_max_pending_jobs: 2048, jit_worker_count: worker_count, jit_timeout: Duration::from_secs(5), + jit_helper_memory_limit_bytes: parse_env_u64(JIT_HELPER_MEMORY_LIMIT_ENV).unwrap_or(0), + jit_helper_cpu_time: parse_env_u64(JIT_HELPER_CPU_SECONDS_ENV) + .filter(|secs| *secs > 0) + .map(Duration::from_secs), jit_worker_queue_capacity: 64, jit_opt_level: crate::OptimizationLevel::default(), aot_opt_level: crate::OptimizationLevel::default(), @@ -299,3 +341,14 @@ impl Default for RuntimeTuning { } } } + +fn parse_env_u64(name: &str) -> Option { + env::var(name).ok().and_then(|s| s.parse().ok()) +} + +fn env_path(name: &str) -> Option { + env::var_os(name).map(|path| { + let path = PathBuf::from(path); + path.canonicalize().unwrap_or(path) + }) +} diff --git a/crates/revmc-runtime/src/runtime/worker.rs b/crates/revmc-runtime/src/runtime/worker.rs index 606acb84c..7729ce86f 100644 --- a/crates/revmc-runtime/src/runtime/worker.rs +++ b/crates/revmc-runtime/src/runtime/worker.rs @@ -319,6 +319,45 @@ fn compile_job_out_of_process(job: CompileJob, config: &RuntimeConfig) -> Worker const HELPER_ENV: &str = "REVMC_JIT_HELPER"; +#[cfg(all(feature = "llvm", unix))] +fn apply_helper_limits(command: &mut Command, config: &RuntimeConfig) { + use std::os::unix::process::CommandExt; + + let memory_limit = config.tuning.jit_helper_memory_limit_bytes; + let cpu_time = config.tuning.jit_helper_cpu_time; + if memory_limit == 0 && cpu_time.is_none() { + return; + } + + // SAFETY: `pre_exec` runs in the child after fork and before exec. The closure only calls + // async-signal-safe libc `setrlimit` and constructs an `io::Error` if it fails. + unsafe { + command.pre_exec(move || { + if memory_limit > 0 { + set_rlimit(libc::RLIMIT_AS as _, memory_limit)?; + } + if let Some(cpu_time) = cpu_time { + let seconds = cpu_time.as_secs().max(1); + set_rlimit(libc::RLIMIT_CPU as _, seconds)?; + } + Ok(()) + }); + } +} + +#[cfg(all(feature = "llvm", unix))] +fn set_rlimit(resource: libc::c_int, value: u64) -> std::io::Result<()> { + let value = libc::rlim_t::try_from(value).unwrap_or(libc::rlim_t::MAX); + let limit = libc::rlimit { rlim_cur: value, rlim_max: value }; + if unsafe { libc::setrlimit(resource as _, &limit) } != 0 { + return Err(std::io::Error::last_os_error()); + } + Ok(()) +} + +#[cfg(all(feature = "llvm", not(unix)))] +fn apply_helper_limits(_command: &mut Command, _config: &RuntimeConfig) {} + #[cfg(feature = "llvm")] fn run_helper_job(job: &CompileJob, config: &RuntimeConfig) -> Result { if config.gas_params.is_some() { @@ -380,13 +419,15 @@ impl HelperProcessInner { std::env::current_exe().map_err(|e| format!("failed to locate current exe: {e}"))? } }; - let mut child = Command::new(&path) + let mut command = Command::new(&path); + command .env(HELPER_ENV, "1") .stdin(Stdio::piped()) .stdout(Stdio::piped()) - .stderr(Stdio::inherit()) - .spawn() - .map_err(|e| format!("failed to spawn JIT helper: {e}"))?; + .stderr(Stdio::inherit()); + apply_helper_limits(&mut command, config); + + let mut child = command.spawn().map_err(|e| format!("failed to spawn JIT helper: {e}"))?; let stdin = child.stdin.take().ok_or("helper stdin unavailable")?; let stdout = child.stdout.take().ok_or("helper stdout unavailable")?; let (result_tx, result_rx) = chan::bounded(1); @@ -449,35 +490,35 @@ impl Drop for HelperProcessInner { #[cfg(feature = "llvm")] #[derive(Serialize, Deserialize)] struct HelperRequest { - code_hash: B256, + code_hash: [u8; 32], spec_id: u8, opt_level: u8, debug_assertions: bool, no_dedup: bool, no_dse: bool, symbol_name: String, - bytecode: Bytes, + bytecode: Vec, } #[cfg(feature = "llvm")] #[derive(Serialize, Deserialize)] #[serde(tag = "status", rename_all = "snake_case")] enum HelperResponse { - Ok { symbol_name: String, object_bytes: Bytes, builtin_symbols: Vec }, + Ok { symbol_name: String, object_bytes: Vec, builtin_symbols: Vec }, Err { error: String }, } #[cfg(feature = "llvm")] fn write_job(mut w: impl Write, job: &CompileJob, config: &RuntimeConfig) -> std::io::Result<()> { let req = HelperRequest { - code_hash: job.key.code_hash, + code_hash: job.key.code_hash.0, spec_id: job.key.spec_id as u8, opt_level: opt_level_to_u8(job.opt_level), debug_assertions: config.debug_assertions, no_dedup: config.no_dedup, no_dse: config.no_dse, symbol_name: job.symbol_name.clone(), - bytecode: job.bytecode.clone(), + bytecode: job.bytecode.to_vec(), }; serde_json::to_writer(&mut w, &req)?; w.write_all(b"\n") @@ -496,7 +537,7 @@ fn read_helper_result(r: &mut impl BufRead) -> Result { HelperResponse::Ok { symbol_name, object_bytes, builtin_symbols } => { Ok(WorkerSuccess::JitObject(JitObjectSuccess { symbol_name, - object_bytes, + object_bytes: Bytes::from(object_bytes), builtin_symbols, })) } @@ -549,8 +590,8 @@ fn read_helper_job(stdin: &mut impl BufRead) -> eyre::Result HelperResponse::Ok { symbol_name: success.symbol_name, - object_bytes: success.object_bytes, + object_bytes: success.object_bytes.to_vec(), builtin_symbols: success.builtin_symbols, }, Ok(_) => unreachable!(), diff --git a/docs/out-of-process-jit.md b/docs/out-of-process-jit.md index 60f195b22..5c0929fe5 100644 --- a/docs/out-of-process-jit.md +++ b/docs/out-of-process-jit.md @@ -24,6 +24,8 @@ Using LLVM ORC's remote executor APIs (`ExecutorProcessControl`, `SimpleRemoteEP Current prototype: - `RuntimeConfig::jit_mode = JitMode::OutOfProcess` makes the runtime keep a global persistent helper process spawned via `RuntimeConfig::jit_helper_path`, or `std::env::current_exe()` when unset. +- `REVMC_JIT_MODE=out-of-process` switches default runtime configs to out-of-process JIT. `REVMC_JIT_HELPER_PATH` overrides the helper executable path. Test harnesses should point this at a binary that calls `revmc::runtime::maybe_run_jit_helper()` at startup, such as `target/debug/revmc`. +- `REVMC_JIT_HELPER_MEMORY_LIMIT_BYTES` and `REVMC_JIT_HELPER_CPU_SECONDS` apply Unix `RLIMIT_AS` and `RLIMIT_CPU` limits to helper processes before `exec`. - Helper binaries must call `revmc::runtime::maybe_run_jit_helper()` at process startup. `revmc-cli` does this already. - Workers send newline-delimited JSON JIT object requests to the helper over stdin and receive newline-delimited JSON responses from stdout. - The parent links returned object bytes into its local ORC instance, resolves the symbol, and constructs `JitCodeBacking` with a parent-owned `ResourceTracker`. @@ -32,7 +34,8 @@ Current prototype: Still needed: - Move the worker pool into a single helper process; the parent should only enqueue IPC requests. -- Add protocol versioning to the JSON IPC payloads and carry the remaining data: gas params, dump settings, generation, timings, object bytes, and errors. +- Replace newline-delimited JSON with a length-prefixed binary protocol once the payload shape settles. The hot payload is bytecode in and object bytes out, so binary framing avoids JSON's array/base64 overhead and gives the parent a single `read_exact` per response. +- Add protocol versioning to the IPC payloads and carry the remaining data: gas params, dump settings, generation, timings, object bytes, and errors. - Keep AOT jobs either in the helper too or explicitly route them through the existing in-process AOT path; the first option gives consistent isolation. - Define shutdown semantics: close IPC, let the helper drain or cancel queued jobs, then kill on timeout. - Treat helper crash as worker-pool failure: fail pending synchronous jobs, drop pending async jobs, and optionally respawn. From 1361e41c364e919044ad91ff652753551803461a Mon Sep 17 00:00:00 2001 From: DaniPopes <57450786+DaniPopes@users.noreply.github.com> Date: Sat, 16 May 2026 05:29:14 +0200 Subject: [PATCH 17/43] fix(runtime): link out-of-process jit objects --- Cargo.lock | 2 - crates/revmc-builtins/src/ir.rs | 1 + crates/revmc-runtime/Cargo.toml | 2 - crates/revmc-runtime/src/runtime/worker.rs | 252 +++++++++++++++++---- docs/out-of-process-jit.md | 5 +- 5 files changed, 217 insertions(+), 45 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index 1420e3e26..5cc9fc668 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -3348,8 +3348,6 @@ dependencies = [ "revmc-context", "revmc-llvm", "revmc-statetest", - "serde", - "serde_json", "similar-asserts", "tempfile", "tracing", diff --git a/crates/revmc-builtins/src/ir.rs b/crates/revmc-builtins/src/ir.rs index 1d9603f68..a88bef999 100644 --- a/crates/revmc-builtins/src/ir.rs +++ b/crates/revmc-builtins/src/ir.rs @@ -126,6 +126,7 @@ macro_rules! builtins { #[allow(unused_variables)] impl Builtin { pub const COUNT: usize = builtins!(@count $($ident),*); + pub const ALL: [Self; Self::COUNT] = [$(Self::$ident),*]; pub const fn name(self) -> &'static str { match self { diff --git a/crates/revmc-runtime/Cargo.toml b/crates/revmc-runtime/Cargo.toml index 0fc5e6cbc..4b5983af8 100644 --- a/crates/revmc-runtime/Cargo.toml +++ b/crates/revmc-runtime/Cargo.toml @@ -51,8 +51,6 @@ libloading = "0.9" tempfile = "3.10" quanta.workspace = true rayon.workspace = true -serde = { version = "1", features = ["derive"] } -serde_json = "1" tracing.workspace = true [dev-dependencies] diff --git a/crates/revmc-runtime/src/runtime/worker.rs b/crates/revmc-runtime/src/runtime/worker.rs index 7729ce86f..2d3b9f008 100644 --- a/crates/revmc-runtime/src/runtime/worker.rs +++ b/crates/revmc-runtime/src/runtime/worker.rs @@ -23,7 +23,7 @@ use rayon::{ThreadPool, ThreadPoolBuilder}; use std::{ cell::RefCell, fs::File, - io::{BufRead, BufReader, Read, Write}, + io::{BufReader, Cursor, Read, Write}, path::PathBuf, process::{Child, ChildStdin, Command, Stdio}, thread::JoinHandle, @@ -44,8 +44,6 @@ use crate::{ }; #[cfg(feature = "llvm")] use revm_primitives::hardfork::SpecId; -#[cfg(feature = "llvm")] -use serde::{Deserialize, Serialize}; /// Notifier for synchronous compilation requests. /// @@ -487,8 +485,11 @@ impl Drop for HelperProcessInner { } } +const HELPER_PROTOCOL_VERSION: u8 = 1; +const HELPER_RESPONSE_OK: u8 = 0; +const HELPER_RESPONSE_ERR: u8 = 1; + #[cfg(feature = "llvm")] -#[derive(Serialize, Deserialize)] struct HelperRequest { code_hash: [u8; 32], spec_id: u8, @@ -501,8 +502,6 @@ struct HelperRequest { } #[cfg(feature = "llvm")] -#[derive(Serialize, Deserialize)] -#[serde(tag = "status", rename_all = "snake_case")] enum HelperResponse { Ok { symbol_name: String, object_bytes: Vec, builtin_symbols: Vec }, Err { error: String }, @@ -510,30 +509,23 @@ enum HelperResponse { #[cfg(feature = "llvm")] fn write_job(mut w: impl Write, job: &CompileJob, config: &RuntimeConfig) -> std::io::Result<()> { - let req = HelperRequest { - code_hash: job.key.code_hash.0, - spec_id: job.key.spec_id as u8, - opt_level: opt_level_to_u8(job.opt_level), - debug_assertions: config.debug_assertions, - no_dedup: config.no_dedup, - no_dse: config.no_dse, - symbol_name: job.symbol_name.clone(), - bytecode: job.bytecode.to_vec(), - }; - serde_json::to_writer(&mut w, &req)?; - w.write_all(b"\n") + let mut frame = Vec::with_capacity(job.bytecode.len() + job.symbol_name.len() + 64); + write_u8(&mut frame, HELPER_PROTOCOL_VERSION)?; + write_fixed_bytes(&mut frame, &job.key.code_hash.0)?; + write_u8(&mut frame, job.key.spec_id as u8)?; + write_u8(&mut frame, opt_level_to_u8(job.opt_level))?; + let flags = u8::from(config.debug_assertions) + | (u8::from(config.no_dedup) << 1) + | (u8::from(config.no_dse) << 2); + write_u8(&mut frame, flags)?; + write_string(&mut frame, &job.symbol_name)?; + write_bytes(&mut frame, &job.bytecode)?; + write_frame(&mut w, &frame) } #[cfg(feature = "llvm")] -fn read_helper_result(r: &mut impl BufRead) -> Result { - let mut line = String::new(); - let n = r.read_line(&mut line).map_err(|e| format!("failed to read helper result: {e}"))?; - if n == 0 { - return Err("JIT helper closed stdout".into()); - } - match serde_json::from_str::(&line) - .map_err(|e| format!("failed to decode helper result: {e}"))? - { +fn read_helper_result(r: &mut impl Read) -> Result { + match read_response(r)? { HelperResponse::Ok { symbol_name, object_bytes, builtin_symbols } => { Ok(WorkerSuccess::JitObject(JitObjectSuccess { symbol_name, @@ -553,7 +545,7 @@ pub(super) fn run_jit_helper_stdio() -> eyre::Result<()> { while let Some((job, config)) = read_helper_job(&mut stdin)? { if compiler.is_none() { - compiler = Some(create_compiler(&config, false).map_err(|e| eyre::eyre!(e))?); + compiler = Some(create_compiler(&config, true).map_err(|e| eyre::eyre!(e))?); } let compiler = compiler.as_mut().unwrap(); compiler.set_opt_level(job.opt_level); @@ -573,12 +565,8 @@ pub(super) fn run_jit_helper_stdio() -> eyre::Result<()> { } #[cfg(feature = "llvm")] -fn read_helper_job(stdin: &mut impl BufRead) -> eyre::Result> { - let mut line = String::new(); - if stdin.read_line(&mut line)? == 0 { - return Ok(None); - } - let req: HelperRequest = serde_json::from_str(&line)?; +fn read_helper_job(stdin: &mut impl Read) -> eyre::Result> { + let Some(req) = read_request(stdin)? else { return Ok(None) }; let spec_id = SpecId::try_from_u8(req.spec_id).ok_or_else(|| eyre::eyre!("invalid spec id"))?; let opt_level = opt_level_from_u8(req.opt_level)?; @@ -605,7 +593,7 @@ fn write_helper_result( mut stdout: impl Write, result: Result, ) -> eyre::Result<()> { - let response = match result { + match result { Ok(WorkerSuccess::JitObject(success)) => HelperResponse::Ok { symbol_name: success.symbol_name, object_bytes: success.object_bytes.to_vec(), @@ -613,12 +601,196 @@ fn write_helper_result( }, Ok(_) => unreachable!(), Err(error) => HelperResponse::Err { error }, + } + .write(&mut stdout)?; + Ok(()) +} + +#[cfg(feature = "llvm")] +impl HelperResponse { + fn write(self, mut w: impl Write) -> std::io::Result<()> { + let mut frame = Vec::new(); + write_u8(&mut frame, HELPER_PROTOCOL_VERSION)?; + match self { + Self::Ok { symbol_name, object_bytes, builtin_symbols } => { + write_u8(&mut frame, HELPER_RESPONSE_OK)?; + write_string(&mut frame, &symbol_name)?; + write_bytes(&mut frame, &object_bytes)?; + write_u32(&mut frame, usize_to_u32(builtin_symbols.len())?)?; + for symbol in builtin_symbols { + write_string(&mut frame, &symbol)?; + } + } + Self::Err { error } => { + write_u8(&mut frame, HELPER_RESPONSE_ERR)?; + write_string(&mut frame, &error)?; + } + } + write_frame(&mut w, &frame) + } +} + +#[cfg(feature = "llvm")] +fn read_request(r: &mut impl Read) -> eyre::Result> { + let Some(frame) = read_frame(r)? else { return Ok(None) }; + let mut r = Cursor::new(frame); + check_protocol_version(read_u8(&mut r)?)?; + let mut code_hash = [0; 32]; + r.read_exact(&mut code_hash)?; + let spec_id = read_u8(&mut r)?; + let opt_level = read_u8(&mut r)?; + let flags = read_u8(&mut r)?; + let symbol_name = read_string(&mut r)?; + let bytecode = read_bytes(&mut r)?; + ensure_frame_consumed(&r)?; + Ok(Some(HelperRequest { + code_hash, + spec_id, + opt_level, + debug_assertions: flags & 1 != 0, + no_dedup: flags & (1 << 1) != 0, + no_dse: flags & (1 << 2) != 0, + symbol_name, + bytecode, + })) +} + +#[cfg(feature = "llvm")] +fn read_response(r: &mut impl Read) -> Result { + let Some(frame) = read_frame(r).map_err(|e| format!("failed to read helper result: {e}"))? + else { + return Err("JIT helper closed stdout".into()); }; - serde_json::to_writer(&mut stdout, &response)?; - stdout.write_all(b"\n")?; + let mut r = Cursor::new(frame); + check_protocol_version( + read_u8(&mut r).map_err(|e| format!("failed to decode helper result: {e}"))?, + ) + .map_err(|e| format!("failed to decode helper result: {e}"))?; + let tag = read_u8(&mut r).map_err(|e| format!("failed to decode helper result: {e}"))?; + let response = match tag { + HELPER_RESPONSE_OK => { + let symbol_name = + read_string(&mut r).map_err(|e| format!("failed to decode helper result: {e}"))?; + let object_bytes = + read_bytes(&mut r).map_err(|e| format!("failed to decode helper result: {e}"))?; + let n_symbols = + read_u32(&mut r).map_err(|e| format!("failed to decode helper result: {e}"))?; + let mut builtin_symbols = Vec::with_capacity(n_symbols as usize); + for _ in 0..n_symbols { + builtin_symbols.push( + read_string(&mut r) + .map_err(|e| format!("failed to decode helper result: {e}"))?, + ); + } + HelperResponse::Ok { symbol_name, object_bytes, builtin_symbols } + } + HELPER_RESPONSE_ERR => { + let error = + read_string(&mut r).map_err(|e| format!("failed to decode helper result: {e}"))?; + HelperResponse::Err { error } + } + _ => return Err(format!("failed to decode helper result: unknown response tag {tag}")), + }; + ensure_frame_consumed(&r).map_err(|e| format!("failed to decode helper result: {e}"))?; + Ok(response) +} + +#[cfg(feature = "llvm")] +fn check_protocol_version(version: u8) -> eyre::Result<()> { + if version != HELPER_PROTOCOL_VERSION { + eyre::bail!("unsupported helper protocol version {version}"); + } Ok(()) } +#[cfg(feature = "llvm")] +fn write_frame(mut w: impl Write, frame: &[u8]) -> std::io::Result<()> { + write_u32(&mut w, usize_to_u32(frame.len())?)?; + w.write_all(frame) +} + +#[cfg(feature = "llvm")] +fn read_frame(r: &mut impl Read) -> std::io::Result>> { + let mut len = [0; 4]; + let n = r.read(&mut len[..1])?; + if n == 0 { + return Ok(None); + } + r.read_exact(&mut len[1..])?; + let len = u32::from_le_bytes(len); + let mut frame = vec![0; len as usize]; + r.read_exact(&mut frame)?; + Ok(Some(frame)) +} + +#[cfg(feature = "llvm")] +fn ensure_frame_consumed(r: &Cursor>) -> eyre::Result<()> { + if r.position() != r.get_ref().len() as u64 { + eyre::bail!("trailing bytes in helper frame"); + } + Ok(()) +} + +#[cfg(feature = "llvm")] +fn write_u8(mut w: impl Write, value: u8) -> std::io::Result<()> { + w.write_all(&[value]) +} + +#[cfg(feature = "llvm")] +fn write_u32(mut w: impl Write, value: u32) -> std::io::Result<()> { + w.write_all(&value.to_le_bytes()) +} + +#[cfg(feature = "llvm")] +fn write_fixed_bytes(mut w: impl Write, bytes: &[u8]) -> std::io::Result<()> { + w.write_all(bytes) +} + +#[cfg(feature = "llvm")] +fn write_bytes(mut w: impl Write, bytes: &[u8]) -> std::io::Result<()> { + write_u32(&mut w, usize_to_u32(bytes.len())?)?; + w.write_all(bytes) +} + +#[cfg(feature = "llvm")] +fn write_string(w: impl Write, value: &str) -> std::io::Result<()> { + write_bytes(w, value.as_bytes()) +} + +#[cfg(feature = "llvm")] +fn read_u8(mut r: impl Read) -> std::io::Result { + let mut buf = [0]; + r.read_exact(&mut buf)?; + Ok(buf[0]) +} + +#[cfg(feature = "llvm")] +fn read_u32(mut r: impl Read) -> std::io::Result { + let mut buf = [0; 4]; + r.read_exact(&mut buf)?; + Ok(u32::from_le_bytes(buf)) +} + +#[cfg(feature = "llvm")] +fn read_bytes(mut r: impl Read) -> std::io::Result> { + let len = read_u32(&mut r)? as usize; + let mut bytes = vec![0; len]; + r.read_exact(&mut bytes)?; + Ok(bytes) +} + +#[cfg(feature = "llvm")] +fn read_string(r: impl Read) -> std::io::Result { + String::from_utf8(read_bytes(r)?) + .map_err(|e| std::io::Error::new(std::io::ErrorKind::InvalidData, e)) +} + +#[cfg(feature = "llvm")] +fn usize_to_u32(value: usize) -> std::io::Result { + u32::try_from(value) + .map_err(|_| std::io::Error::new(std::io::ErrorKind::InvalidInput, "frame too large")) +} + #[cfg(feature = "llvm")] fn opt_level_to_u8(level: OptimizationLevel) -> u8 { match level { @@ -808,12 +980,16 @@ fn compile_jit_object_artifact( compiler .write_object(&mut object_bytes) .map_err(|e| format!("JIT object write failed: {e}"))?; - let builtin_symbols = compiler + let mut builtin_symbols: Vec = compiler .backend() .pending_symbol_names() .into_iter() .map(|name| name.to_string_lossy().into_owned()) .collect(); + if builtin_symbols.is_empty() { + builtin_symbols = + revmc_builtins::Builtin::ALL.iter().map(|builtin| builtin.name().to_string()).collect(); + } debug!( bytecode_len = job.bytecode.len(), diff --git a/docs/out-of-process-jit.md b/docs/out-of-process-jit.md index 5c0929fe5..17606f5e7 100644 --- a/docs/out-of-process-jit.md +++ b/docs/out-of-process-jit.md @@ -27,15 +27,14 @@ Current prototype: - `REVMC_JIT_MODE=out-of-process` switches default runtime configs to out-of-process JIT. `REVMC_JIT_HELPER_PATH` overrides the helper executable path. Test harnesses should point this at a binary that calls `revmc::runtime::maybe_run_jit_helper()` at startup, such as `target/debug/revmc`. - `REVMC_JIT_HELPER_MEMORY_LIMIT_BYTES` and `REVMC_JIT_HELPER_CPU_SECONDS` apply Unix `RLIMIT_AS` and `RLIMIT_CPU` limits to helper processes before `exec`. - Helper binaries must call `revmc::runtime::maybe_run_jit_helper()` at process startup. `revmc-cli` does this already. -- Workers send newline-delimited JSON JIT object requests to the helper over stdin and receive newline-delimited JSON responses from stdout. +- Workers send length-prefixed binary JIT object requests to the helper over stdin and receive length-prefixed binary responses from stdout. - The parent links returned object bytes into its local ORC instance, resolves the symbol, and constructs `JitCodeBacking` with a parent-owned `ResourceTracker`. - `RuntimeTuning::jit_timeout` bounds each helper compilation; timed-out helpers are killed and replaced on the next job. Still needed: - Move the worker pool into a single helper process; the parent should only enqueue IPC requests. -- Replace newline-delimited JSON with a length-prefixed binary protocol once the payload shape settles. The hot payload is bytecode in and object bytes out, so binary framing avoids JSON's array/base64 overhead and gives the parent a single `read_exact` per response. -- Add protocol versioning to the IPC payloads and carry the remaining data: gas params, dump settings, generation, timings, object bytes, and errors. +- Carry the remaining data in the IPC payloads: gas params, dump settings, generation, timings, and richer errors. - Keep AOT jobs either in the helper too or explicitly route them through the existing in-process AOT path; the first option gives consistent isolation. - Define shutdown semantics: close IPC, let the helper drain or cancel queued jobs, then kill on timeout. - Treat helper crash as worker-pool failure: fail pending synchronous jobs, drop pending async jobs, and optionally respawn. From dd4eb94c0dd6cc8c552bde5e2b3a4d954e37b0f8 Mon Sep 17 00:00:00 2001 From: DaniPopes <57450786+DaniPopes@users.noreply.github.com> Date: Sat, 16 May 2026 05:33:58 +0200 Subject: [PATCH 18/43] fix(runtime): cancel out-of-process jit helper --- crates/revmc-runtime/src/runtime/backend.rs | 1 + crates/revmc-runtime/src/runtime/worker.rs | 100 ++++++++++++++------ docs/out-of-process-jit.md | 3 +- 3 files changed, 75 insertions(+), 29 deletions(-) diff --git a/crates/revmc-runtime/src/runtime/backend.rs b/crates/revmc-runtime/src/runtime/backend.rs index 53a503183..23e832945 100644 --- a/crates/revmc-runtime/src/runtime/backend.rs +++ b/crates/revmc-runtime/src/runtime/backend.rs @@ -412,6 +412,7 @@ impl BackendState { } fn handle_clear_resident(&mut self) { + self.workers.cancel_in_flight(); self.inner.resident.clear(); self.resident_meta.clear(); // Notify any pending sync callers before clearing entries. diff --git a/crates/revmc-runtime/src/runtime/worker.rs b/crates/revmc-runtime/src/runtime/worker.rs index 2d3b9f008..25a4bf9de 100644 --- a/crates/revmc-runtime/src/runtime/worker.rs +++ b/crates/revmc-runtime/src/runtime/worker.rs @@ -262,11 +262,19 @@ impl WorkerPool { /// Shuts down all workers after draining queued jobs. pub(crate) fn shutdown(&mut self) { self.shutdown.store(true, Ordering::Release); + self.cancel_in_flight(); if let Some(pool) = &self.pool { pool.broadcast(|_| clear_thread_local_compilers()); } self.pool.take(); } + + /// Cancels any in-flight compilation that can be interrupted externally. + pub(crate) fn cancel_in_flight(&self) { + if self.config.jit_mode == JitMode::OutOfProcess { + reset_jit_helper(); + } + } } impl Drop for WorkerPool { @@ -368,44 +376,72 @@ fn run_helper_job(job: &CompileJob, config: &RuntimeConfig) -> Result = OnceLock::new(); + #[cfg(feature = "llvm")] fn helper_process() -> &'static HelperProcess { - static HELPER: OnceLock = OnceLock::new(); - HELPER.get_or_init(HelperProcess::default) + HELPER_PROCESS.get_or_init(HelperProcess::default) +} + +#[cfg(feature = "llvm")] +fn reset_jit_helper() { + if let Some(helper) = HELPER_PROCESS.get() { + helper.reset(); + } +} + +#[cfg(not(feature = "llvm"))] +fn reset_jit_helper() {} + +#[cfg(feature = "llvm")] +struct HelperIo { + stdin: ChildStdin, + result_rx: chan::Receiver>, } #[cfg(feature = "llvm")] #[derive(Default)] struct HelperProcess { - inner: Mutex>, + inner: Mutex>>, } #[cfg(feature = "llvm")] impl HelperProcess { fn compile(&self, job: &CompileJob, config: &RuntimeConfig) -> Result { - let mut slot = self.inner.lock().unwrap(); - if slot.as_ref().is_none_or(|helper| !helper.matches_config(config)) { - *slot = Some(HelperProcessInner::spawn(config)?); - } + let helper = { + let mut slot = self.inner.lock().unwrap(); + if slot.as_ref().is_none_or(|helper| !helper.matches_config(config)) { + *slot = Some(Arc::new(HelperProcessInner::spawn(config)?)); + } + slot.as_ref().unwrap().clone() + }; - let helper = slot.as_mut().unwrap(); match helper.compile(job, config) { Ok(result) => Ok(result), Err(err) => { - *slot = None; + let mut slot = self.inner.lock().unwrap(); + if slot.as_ref().is_some_and(|current| Arc::ptr_eq(current, &helper)) { + *slot = None; + } Err(err) } } } + + fn reset(&self) { + if let Some(helper) = self.inner.lock().unwrap().take() { + helper.kill(); + } + } } #[cfg(feature = "llvm")] struct HelperProcessInner { path: PathBuf, - child: Child, - stdin: ChildStdin, - result_rx: chan::Receiver>, - reader: Option>, + child: Mutex, + io: Mutex, + reader: Mutex>>, } #[cfg(feature = "llvm")] @@ -433,12 +469,18 @@ impl HelperProcessInner { let mut stdout = BufReader::new(stdout); loop { let result = read_helper_result(&mut stdout); - if result_tx.send(result).is_err() { + let done = result.is_err(); + if result_tx.send(result).is_err() || done { break; } } }); - Ok(Self { path, child, stdin, result_rx, reader: Some(reader) }) + Ok(Self { + path, + child: Mutex::new(child), + io: Mutex::new(HelperIo { stdin, result_rx }), + reader: Mutex::new(Some(reader)), + }) } fn matches_config(&self, config: &RuntimeConfig) -> bool { @@ -448,23 +490,20 @@ impl HelperProcessInner { } } - fn compile( - &mut self, - job: &CompileJob, - config: &RuntimeConfig, - ) -> Result { - write_job(&mut self.stdin, job, config) + fn compile(&self, job: &CompileJob, config: &RuntimeConfig) -> Result { + let mut io = self.io.lock().unwrap(); + write_job(&mut io.stdin, job, config) .map_err(|e| format!("failed to write helper job: {e}"))?; - self.stdin.flush().map_err(|e| format!("failed to flush helper job: {e}"))?; + io.stdin.flush().map_err(|e| format!("failed to flush helper job: {e}"))?; - match self.result_rx.recv_timeout(config.tuning.jit_timeout) { + match io.result_rx.recv_timeout(config.tuning.jit_timeout) { Ok(result) => result, Err(chan::RecvTimeoutError::Timeout) => { - let _ = self.child.kill(); + self.kill(); Err(format!("JIT helper timed out after {:?}", config.tuning.jit_timeout)) } Err(chan::RecvTimeoutError::Disconnected) => { - let status = self.child.try_wait().ok().flatten(); + let status = self.child.lock().unwrap().try_wait().ok().flatten(); Err(match status { Some(status) => format!("JIT helper exited with {status}"), None => "JIT helper disconnected".into(), @@ -472,14 +511,19 @@ impl HelperProcessInner { } } } + + fn kill(&self) { + let mut child = self.child.lock().unwrap(); + let _ = child.kill(); + let _ = child.wait(); + } } #[cfg(feature = "llvm")] impl Drop for HelperProcessInner { fn drop(&mut self) { - let _ = self.child.kill(); - let _ = self.child.wait(); - if let Some(reader) = self.reader.take() { + self.kill(); + if let Some(reader) = self.reader.lock().unwrap().take() { let _ = reader.join(); } } diff --git a/docs/out-of-process-jit.md b/docs/out-of-process-jit.md index 17606f5e7..a36f1b355 100644 --- a/docs/out-of-process-jit.md +++ b/docs/out-of-process-jit.md @@ -30,13 +30,14 @@ Current prototype: - Workers send length-prefixed binary JIT object requests to the helper over stdin and receive length-prefixed binary responses from stdout. - The parent links returned object bytes into its local ORC instance, resolves the symbol, and constructs `JitCodeBacking` with a parent-owned `ResourceTracker`. - `RuntimeTuning::jit_timeout` bounds each helper compilation; timed-out helpers are killed and replaced on the next job. +- Clearing resident code or shutting down the runtime kills the helper process so in-flight out-of-process compiles can be interrupted instead of waiting for LLVM to finish. Still needed: - Move the worker pool into a single helper process; the parent should only enqueue IPC requests. - Carry the remaining data in the IPC payloads: gas params, dump settings, generation, timings, and richer errors. - Keep AOT jobs either in the helper too or explicitly route them through the existing in-process AOT path; the first option gives consistent isolation. -- Define shutdown semantics: close IPC, let the helper drain or cancel queued jobs, then kill on timeout. +- Define graceful shutdown semantics for queued helper jobs; the current implementation kills the helper for cancellation/shutdown. - Treat helper crash as worker-pool failure: fail pending synchronous jobs, drop pending async jobs, and optionally respawn. ## Open questions From 0863c9873430ba9d0720eaeaed971ba81c58d704 Mon Sep 17 00:00:00 2001 From: DaniPopes <57450786+DaniPopes@users.noreply.github.com> Date: Sat, 16 May 2026 20:34:05 +0200 Subject: [PATCH 19/43] fix(runtime): refine out-of-process jit ipc --- Cargo.lock | 32 + Cargo.toml | 3 + crates/revmc-cli/src/main.rs | 2 +- crates/revmc-runtime/Cargo.toml | 3 + crates/revmc-runtime/src/runtime/config.rs | 68 ++- crates/revmc-runtime/src/runtime/mod.rs | 30 +- .../src/runtime/out_of_process.rs | 398 ++++++++++++ crates/revmc-runtime/src/runtime/worker.rs | 575 +----------------- docs/out-of-process-jit.md | 6 +- 9 files changed, 513 insertions(+), 604 deletions(-) create mode 100644 crates/revmc-runtime/src/runtime/out_of_process.rs diff --git a/Cargo.lock b/Cargo.lock index 5cc9fc668..35c5c6067 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -2504,6 +2504,12 @@ version = "1.0.15" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "57c0d7b74b563b49d38dae00a0c37d4d6de9b432382b2892f0574ddcae73fd0a" +[[package]] +name = "pastey" +version = "0.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c5a797f0e07bdf071d15742978fc3128ec6c22891c31a3a931513263904c982a" + [[package]] name = "pathdiff" version = "0.2.3" @@ -3348,10 +3354,13 @@ dependencies = [ "revmc-context", "revmc-llvm", "revmc-statetest", + "serde", + "serde-wincode", "similar-asserts", "tempfile", "tracing", "tracing-subscriber 0.3.23", + "wincode", ] [[package]] @@ -3630,6 +3639,17 @@ dependencies = [ "serde_derive", ] +[[package]] +name = "serde-wincode" +version = "0.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4a62dfa6ae65cb073dfe001fb076eaf827fd4267fcba7eb3be2fee4f1e69ccb5" +dependencies = [ + "serde", + "thiserror 2.0.18", + "wincode", +] + [[package]] name = "serde_core" version = "1.0.228" @@ -4440,6 +4460,18 @@ version = "0.4.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "712e227841d057c1ee1cd2fb22fa7e5a5461ae8e48fa2ca79ec42cfc1931183f" +[[package]] +name = "wincode" +version = "0.4.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "657690780ce23e6f66576a782ffd88eb353512381817029cc1d7a99154bb6d1f" +dependencies = [ + "pastey", + "proc-macro2", + "quote", + "thiserror 2.0.18", +] + [[package]] name = "windows-core" version = "0.62.2" diff --git a/Cargo.toml b/Cargo.toml index 3b035a94b..267abcc4a 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -73,6 +73,9 @@ tracing-subscriber = "0.3" tracing-tracy = "0.11" paste = "1.0" quanta = "0.12" +serde = { version = "1", features = ["derive"] } +serde-wincode = "0.1" +wincode = "0.4.5" [profile.dev] opt-level = 3 diff --git a/crates/revmc-cli/src/main.rs b/crates/revmc-cli/src/main.rs index 23416626c..ee356f419 100644 --- a/crates/revmc-cli/src/main.rs +++ b/crates/revmc-cli/src/main.rs @@ -21,7 +21,7 @@ enum Command { } fn main() -> Result<()> { - if revmc::runtime::maybe_run_jit_helper()? { + if matches!(revmc::runtime::maybe_run_jit_helper()?, std::ops::ControlFlow::Break(())) { return Ok(()); } if std::env::var_os("RUST_BACKTRACE").is_none() { diff --git a/crates/revmc-runtime/Cargo.toml b/crates/revmc-runtime/Cargo.toml index 4b5983af8..7d28408ab 100644 --- a/crates/revmc-runtime/Cargo.toml +++ b/crates/revmc-runtime/Cargo.toml @@ -52,6 +52,9 @@ tempfile = "3.10" quanta.workspace = true rayon.workspace = true tracing.workspace = true +serde.workspace = true +serde-wincode.workspace = true +wincode.workspace = true [dev-dependencies] revmc-statetest = { path = "../revmc-statetest" } diff --git a/crates/revmc-runtime/src/runtime/config.rs b/crates/revmc-runtime/src/runtime/config.rs index cc1e4bc8a..d38c624fd 100644 --- a/crates/revmc-runtime/src/runtime/config.rs +++ b/crates/revmc-runtime/src/runtime/config.rs @@ -1,10 +1,10 @@ //! Runtime configuration. -use crate::{CompileTimings, runtime::storage::ArtifactStore}; +use crate::{CompileTimings, eyre, runtime::storage::ArtifactStore}; use alloy_primitives::B256; use revm_context_interface::cfg::GasParams; use revm_primitives::hardfork::SpecId; -use std::{env, path::PathBuf, str::FromStr, sync::Arc, time::Duration}; +use std::{path::PathBuf, str::FromStr, sync::Arc, time::Duration}; const JIT_MODE_ENV: &str = "REVMC_JIT_MODE"; const JIT_HELPER_PATH_ENV: &str = "REVMC_JIT_HELPER_PATH"; @@ -83,15 +83,16 @@ pub struct RuntimeConfig { /// Where JIT compilation work runs. /// - /// Defaults to [`JitMode::InProcess`], or `REVMC_JIT_MODE` when set. + /// Defaults to [`JitMode::InProcess`]. pub jit_mode: JitMode, /// Helper executable used when [`jit_mode`](Self::jit_mode) /// is [`JitMode::OutOfProcess`]. /// /// When `None`, the runtime spawns `std::env::current_exe()` and expects it - /// to call [`super::maybe_run_jit_helper`] during startup. Defaults to - /// `REVMC_JIT_HELPER_PATH` when set. + /// to call [`super::maybe_run_jit_helper`] during startup. + /// + /// Defaults to `None`. pub jit_helper_path: Option, /// Blocking mode: every lookup synchronously JIT-compiles on miss and never @@ -158,17 +159,36 @@ impl FromStr for JitMode { type Err = String; fn from_str(s: &str) -> Result { - let mode = s.trim().to_ascii_lowercase(); - Ok(match mode.as_str() { - "" | "in-process" | "in_process" | "inprocess" | "in" | "0" => Self::InProcess, - "out-of-process" | "out_of_process" | "outofprocess" | "out" | "oop" | "1" => { - Self::OutOfProcess - } + Ok(match s { + "in-process" => Self::InProcess, + "out-of-process" => Self::OutOfProcess, _ => return Err(format!("unknown JIT mode: {s}")), }) } } +impl RuntimeConfig { + /// Applies runtime environment overrides. + /// + /// Recognized variables are `REVMC_JIT_MODE`, `REVMC_JIT_HELPER_PATH`, + /// `REVMC_JIT_HELPER_MEMORY_LIMIT_BYTES`, and `REVMC_JIT_HELPER_CPU_SECONDS`. + pub fn with_env_overrides(mut self) -> eyre::Result { + if let Some(mode) = env_var(JIT_MODE_ENV) { + self.jit_mode = mode.parse().map_err(|e: String| eyre::eyre!("{JIT_MODE_ENV}: {e}"))?; + } + if let Some(path) = env_path(JIT_HELPER_PATH_ENV) { + self.jit_helper_path = Some(path); + } + if let Some(limit) = parse_env_u64(JIT_HELPER_MEMORY_LIMIT_ENV)? { + self.tuning.jit_helper_memory_limit_bytes = limit; + } + if let Some(secs) = parse_env_u64(JIT_HELPER_CPU_SECONDS_ENV)? { + self.tuning.jit_helper_cpu_time = (secs > 0).then(|| Duration::from_secs(secs)); + } + Ok(self) + } +} + impl Default for RuntimeConfig { fn default() -> Self { Self { @@ -182,8 +202,8 @@ impl Default for RuntimeConfig { no_dse: false, gas_params: None, aot: false, - jit_mode: env::var(JIT_MODE_ENV).ok().and_then(|s| s.parse().ok()).unwrap_or_default(), - jit_helper_path: env_path(JIT_HELPER_PATH_ENV), + jit_mode: JitMode::default(), + jit_helper_path: None, blocking: false, on_compilation: None, } @@ -246,7 +266,7 @@ pub struct RuntimeTuning { /// `0` disables the limit. On Unix this is applied with `RLIMIT_AS` before /// the helper process starts executing. /// - /// Defaults to `0`, or `REVMC_JIT_HELPER_MEMORY_LIMIT_BYTES` when set. + /// Defaults to `0`. pub jit_helper_memory_limit_bytes: u64, /// Maximum CPU time for the out-of-process JIT helper. @@ -254,8 +274,7 @@ pub struct RuntimeTuning { /// `None` disables the limit. On Unix this is applied with `RLIMIT_CPU` /// before the helper process starts executing. /// - /// Defaults to `None`, or `REVMC_JIT_HELPER_CPU_SECONDS` when set to a - /// non-zero integer. + /// Defaults to `None`. pub jit_helper_cpu_time: Option, /// Capacity of the per-worker job queue. @@ -327,10 +346,8 @@ impl Default for RuntimeTuning { jit_max_pending_jobs: 2048, jit_worker_count: worker_count, jit_timeout: Duration::from_secs(5), - jit_helper_memory_limit_bytes: parse_env_u64(JIT_HELPER_MEMORY_LIMIT_ENV).unwrap_or(0), - jit_helper_cpu_time: parse_env_u64(JIT_HELPER_CPU_SECONDS_ENV) - .filter(|secs| *secs > 0) - .map(Duration::from_secs), + jit_helper_memory_limit_bytes: 0, + jit_helper_cpu_time: None, jit_worker_queue_capacity: 64, jit_opt_level: crate::OptimizationLevel::default(), aot_opt_level: crate::OptimizationLevel::default(), @@ -342,12 +359,17 @@ impl Default for RuntimeTuning { } } -fn parse_env_u64(name: &str) -> Option { - env::var(name).ok().and_then(|s| s.parse().ok()) +fn parse_env_u64(name: &str) -> eyre::Result> { + let Some(value) = env_var(name) else { return Ok(None) }; + value.parse().map(Some).map_err(|e| eyre::eyre!("{name}: {e}")) +} + +fn env_var(name: &str) -> Option { + std::env::var(name).ok() } fn env_path(name: &str) -> Option { - env::var_os(name).map(|path| { + std::env::var_os(name).map(|path| { let path = PathBuf::from(path); path.canonicalize().unwrap_or(path) }) diff --git a/crates/revmc-runtime/src/runtime/mod.rs b/crates/revmc-runtime/src/runtime/mod.rs index fdbba0f7a..6a90ec022 100644 --- a/crates/revmc-runtime/src/runtime/mod.rs +++ b/crates/revmc-runtime/src/runtime/mod.rs @@ -16,6 +16,7 @@ use crossbeam_queue::ArrayQueue; use revm_primitives::{B256, hardfork::SpecId, hints_util::cold_path}; use stats::RuntimeStats; use std::{ + ops::ControlFlow, sync::{ Arc, atomic::{AtomicBool, Ordering}, @@ -43,25 +44,27 @@ pub use storage::{ RuntimeCacheKey, StoredArtifact, }; +#[cfg(feature = "llvm")] +mod out_of_process; + mod worker; /// Runs the out-of-process JIT helper if this process was launched as one. /// -/// Returns `Ok(true)` after the helper request has been handled and the caller -/// should exit immediately. Normal application startup should continue on -/// `Ok(false)`. -pub fn maybe_run_jit_helper() -> eyre::Result { - if std::env::var_os("REVMC_JIT_HELPER").is_none() { - return Ok(false); - } +/// Returns [`ControlFlow::Break`] after the helper request has been handled and +/// the caller should exit immediately. Normal application startup should +/// continue on [`ControlFlow::Continue`]. +pub fn maybe_run_jit_helper() -> eyre::Result> { #[cfg(feature = "llvm")] { - worker::run_jit_helper_stdio()?; - Ok(true) + out_of_process::maybe_run_jit_helper() } #[cfg(not(feature = "llvm"))] { - eyre::bail!("LLVM backend not available") + if std::env::var_os("REVMC_JIT_HELPER").is_some() { + eyre::bail!("LLVM backend not available") + } + Ok(ControlFlow::Continue(())) } } @@ -144,7 +147,7 @@ impl JitBackend { /// Call [`set_enabled`](Self::set_enabled) to lazily spawn the backend thread with /// a default [`RuntimeConfig`]. pub fn disabled() -> Self { - Self::new(RuntimeConfig::default()).expect("default config cannot fail") + Self::new_inner(RuntimeConfig::default()).expect("default config cannot fail") } /// Creates a backend from the given config. @@ -153,6 +156,11 @@ impl JitBackend { /// immediately and AOT artifacts are preloaded. Otherwise, both are deferred until the /// first [`set_enabled(true)`](Self::set_enabled) call. pub fn new(mut config: RuntimeConfig) -> eyre::Result { + config = config.with_env_overrides()?; + Self::new_inner(config) + } + + fn new_inner(mut config: RuntimeConfig) -> eyre::Result { if config.blocking { config.enabled = true; config.tuning.jit_hot_threshold = 0; diff --git a/crates/revmc-runtime/src/runtime/out_of_process.rs b/crates/revmc-runtime/src/runtime/out_of_process.rs new file mode 100644 index 000000000..b454a9292 --- /dev/null +++ b/crates/revmc-runtime/src/runtime/out_of_process.rs @@ -0,0 +1,398 @@ +//! Out-of-process JIT helper process and IPC. + +use crate::{ + CompileTimings, EvmCompiler, EvmLlvmBackend, OptimizationLevel, eyre, + runtime::{ + config::{CompilationKind, RuntimeConfig}, + storage::RuntimeCacheKey, + worker::{ + CompileJob, JitObjectSuccess, SyncNotifier, WorkerResult, WorkerSuccess, + compile_jit_object_artifact, create_compiler, + }, + }, +}; +use alloy_primitives::{B256, Bytes}; +use crossbeam_channel as chan; +use revm_primitives::hardfork::SpecId; +use serde::{Deserialize, Serialize}; +use std::{ + io::{BufReader, Read, Write}, + ops::ControlFlow, + path::PathBuf, + process::{Child, ChildStdin, Command, Stdio}, + sync::{Arc, Mutex, OnceLock}, + thread::JoinHandle, + time::Instant, +}; + +const HELPER_ENV: &str = "REVMC_JIT_HELPER"; + +/// Runs the out-of-process JIT helper if this process was launched as one. +pub(super) fn maybe_run_jit_helper() -> eyre::Result> { + if std::env::var_os(HELPER_ENV).is_none() { + return Ok(ControlFlow::Continue(())); + } + run_jit_helper_stdio()?; + Ok(ControlFlow::Break(())) +} + +/// Compiles a job in the out-of-process helper. +pub(super) fn compile_job(job: CompileJob, config: &RuntimeConfig) -> WorkerResult { + let t0 = Instant::now(); + let outcome = run_helper_job(&job, config); + WorkerResult { + key: job.key, + outcome, + kind: job.kind, + sync_notifier: job.sync_notifier, + generation: job.generation, + compile_duration: t0.elapsed(), + timings: CompileTimings::default(), + } +} + +/// Cancels in-flight out-of-process helper work. +pub(super) fn cancel_in_flight() { + if let Some(helper) = HELPER_PROCESS.get() { + helper.reset(); + } +} + +fn run_helper_job(job: &CompileJob, config: &RuntimeConfig) -> Result { + if config.gas_params.is_some() { + return Err("out-of-process JIT does not support custom gas params yet".into()); + } + if config.dump_dir.is_some() { + return Err("out-of-process JIT does not support debug dumps yet".into()); + } + + helper_process().compile(job, config) +} + +static HELPER_PROCESS: OnceLock = OnceLock::new(); + +fn helper_process() -> &'static HelperProcess { + HELPER_PROCESS.get_or_init(HelperProcess::default) +} + +struct HelperIo { + stdin: ChildStdin, + result_rx: chan::Receiver>, +} + +#[derive(Default)] +struct HelperProcess { + inner: Mutex>>, +} + +impl HelperProcess { + fn compile(&self, job: &CompileJob, config: &RuntimeConfig) -> Result { + let helper = { + let mut slot = self.inner.lock().unwrap(); + if slot.as_ref().is_none_or(|helper| !helper.matches_config(config)) { + *slot = Some(Arc::new(HelperProcessInner::spawn(config)?)); + } + slot.as_ref().unwrap().clone() + }; + + match helper.compile(job, config) { + Ok(result) => Ok(result), + Err(err) => { + let mut slot = self.inner.lock().unwrap(); + if slot.as_ref().is_some_and(|current| Arc::ptr_eq(current, &helper)) { + *slot = None; + } + Err(err) + } + } + } + + fn reset(&self) { + if let Some(helper) = self.inner.lock().unwrap().take() { + helper.kill(); + } + } +} + +struct HelperProcessInner { + path: PathBuf, + child: Mutex, + io: Mutex, + reader: Mutex>>, +} + +impl HelperProcessInner { + fn spawn(config: &RuntimeConfig) -> Result { + let path = match &config.jit_helper_path { + Some(path) => path.clone(), + None => { + std::env::current_exe().map_err(|e| format!("failed to locate current exe: {e}"))? + } + }; + let mut command = Command::new(&path); + command + .env(HELPER_ENV, "1") + .stdin(Stdio::piped()) + .stdout(Stdio::piped()) + .stderr(Stdio::inherit()); + apply_helper_limits(&mut command, config); + + let mut child = command.spawn().map_err(|e| format!("failed to spawn JIT helper: {e}"))?; + let stdin = child.stdin.take().ok_or("helper stdin unavailable")?; + let stdout = child.stdout.take().ok_or("helper stdout unavailable")?; + let (result_tx, result_rx) = chan::bounded(1); + let reader = std::thread::spawn(move || { + let mut stdout = BufReader::new(stdout); + loop { + let result = read_helper_result(&mut stdout); + let done = result.is_err(); + if result_tx.send(result).is_err() || done { + break; + } + } + }); + Ok(Self { + path, + child: Mutex::new(child), + io: Mutex::new(HelperIo { stdin, result_rx }), + reader: Mutex::new(Some(reader)), + }) + } + + fn matches_config(&self, config: &RuntimeConfig) -> bool { + match &config.jit_helper_path { + Some(path) => self.path == *path, + None => std::env::current_exe().map(|path| self.path == path).unwrap_or(false), + } + } + + fn compile(&self, job: &CompileJob, config: &RuntimeConfig) -> Result { + let mut io = self.io.lock().unwrap(); + write_job(&mut io.stdin, job, config) + .map_err(|e| format!("failed to write helper job: {e}"))?; + io.stdin.flush().map_err(|e| format!("failed to flush helper job: {e}"))?; + + match io.result_rx.recv_timeout(config.tuning.jit_timeout) { + Ok(result) => result, + Err(chan::RecvTimeoutError::Timeout) => { + self.kill(); + Err(format!("JIT helper timed out after {:?}", config.tuning.jit_timeout)) + } + Err(chan::RecvTimeoutError::Disconnected) => { + let status = self.child.lock().unwrap().try_wait().ok().flatten(); + Err(match status { + Some(status) => format!("JIT helper exited with {status}"), + None => "JIT helper disconnected".into(), + }) + } + } + } + + fn kill(&self) { + let mut child = self.child.lock().unwrap(); + let _ = child.kill(); + let _ = child.wait(); + } +} + +impl Drop for HelperProcessInner { + fn drop(&mut self) { + self.kill(); + if let Some(reader) = self.reader.lock().unwrap().take() { + let _ = reader.join(); + } + } +} + +#[cfg(unix)] +fn apply_helper_limits(command: &mut Command, config: &RuntimeConfig) { + use std::os::unix::process::CommandExt; + + let memory_limit = config.tuning.jit_helper_memory_limit_bytes; + let cpu_time = config.tuning.jit_helper_cpu_time; + if memory_limit == 0 && cpu_time.is_none() { + return; + } + + // SAFETY: `pre_exec` runs in the child after fork and before exec. The closure only calls + // async-signal-safe libc `setrlimit` and constructs an `io::Error` if it fails. + unsafe { + command.pre_exec(move || { + if memory_limit > 0 { + set_rlimit(libc::RLIMIT_AS as _, memory_limit)?; + } + if let Some(cpu_time) = cpu_time { + let seconds = cpu_time.as_secs().max(1); + set_rlimit(libc::RLIMIT_CPU as _, seconds)?; + } + Ok(()) + }); + } +} + +#[cfg(unix)] +fn set_rlimit(resource: libc::c_int, value: u64) -> std::io::Result<()> { + let value = libc::rlim_t::try_from(value).unwrap_or(libc::rlim_t::MAX); + let limit = libc::rlimit { rlim_cur: value, rlim_max: value }; + if unsafe { libc::setrlimit(resource as _, &limit) } != 0 { + return Err(std::io::Error::last_os_error()); + } + Ok(()) +} + +#[cfg(not(unix))] +fn apply_helper_limits(_command: &mut Command, _config: &RuntimeConfig) {} + +#[derive(Serialize, Deserialize)] +struct HelperRequest { + code_hash: [u8; 32], + spec_id: u8, + opt_level: u8, + debug_assertions: bool, + no_dedup: bool, + no_dse: bool, + symbol_name: String, + bytecode: Vec, +} + +#[derive(Serialize, Deserialize)] +enum HelperResponse { + Ok { symbol_name: String, object_bytes: Vec, builtin_symbols: Vec }, + Err { error: String }, +} + +fn write_job(mut w: impl Write, job: &CompileJob, config: &RuntimeConfig) -> std::io::Result<()> { + let req = HelperRequest { + code_hash: job.key.code_hash.0, + spec_id: job.key.spec_id as u8, + opt_level: opt_level_to_u8(job.opt_level), + debug_assertions: config.debug_assertions, + no_dedup: config.no_dedup, + no_dse: config.no_dse, + symbol_name: job.symbol_name.clone(), + bytecode: job.bytecode.to_vec(), + }; + write_message(&mut w, &req) +} + +fn read_helper_result(r: &mut impl Read) -> Result { + match read_message(r).map_err(|e| format!("failed to decode helper result: {e}"))? { + Some(HelperResponse::Ok { symbol_name, object_bytes, builtin_symbols }) => { + Ok(WorkerSuccess::JitObject(JitObjectSuccess { + symbol_name, + object_bytes: Bytes::from(object_bytes), + builtin_symbols, + })) + } + Some(HelperResponse::Err { error }) => Err(error), + None => Err("JIT helper closed stdout".into()), + } +} + +fn run_jit_helper_stdio() -> eyre::Result<()> { + let mut stdin = std::io::stdin().lock(); + let mut stdout = std::io::stdout().lock(); + let mut compiler: Option> = None; + + while let Some((job, config)) = read_helper_job(&mut stdin)? { + if compiler.is_none() { + compiler = Some(create_compiler(&config, true).map_err(|e| eyre::eyre!(e))?); + } + let compiler = compiler.as_mut().unwrap(); + compiler.set_opt_level(job.opt_level); + compiler.debug_assertions(config.debug_assertions); + compiler.set_dedup(!config.no_dedup); + compiler.set_dse(!config.no_dse); + + let result = compile_jit_object_artifact(&job, compiler); + if let Err(err) = compiler.clear_ir() { + warn!(%err, "clear_ir failed"); + } + write_helper_result(&mut stdout, result)?; + stdout.flush()?; + } + + Ok(()) +} + +fn read_helper_job(stdin: &mut impl Read) -> eyre::Result> { + let Some(req) = read_message::(stdin)? else { return Ok(None) }; + let spec_id = SpecId::try_from_u8(req.spec_id).ok_or_else(|| eyre::eyre!("invalid spec id"))?; + let opt_level = opt_level_from_u8(req.opt_level)?; + + let config = RuntimeConfig { + debug_assertions: req.debug_assertions, + no_dedup: req.no_dedup, + no_dse: req.no_dse, + ..Default::default() + }; + let job = CompileJob { + kind: CompilationKind::Jit, + key: RuntimeCacheKey { code_hash: B256::from(req.code_hash), spec_id }, + bytecode: Bytes::from(req.bytecode), + symbol_name: req.symbol_name, + opt_level, + sync_notifier: SyncNotifier::none(), + generation: 0, + }; + Ok(Some((job, config))) +} + +fn write_helper_result( + mut stdout: impl Write, + result: Result, +) -> eyre::Result<()> { + let response = match result { + Ok(WorkerSuccess::JitObject(success)) => HelperResponse::Ok { + symbol_name: success.symbol_name, + object_bytes: success.object_bytes.to_vec(), + builtin_symbols: success.builtin_symbols, + }, + Ok(_) => unreachable!(), + Err(error) => HelperResponse::Err { error }, + }; + write_message(&mut stdout, &response)?; + Ok(()) +} + +fn write_message(mut w: impl Write, message: &T) -> std::io::Result<()> { + let bytes = as wincode::Serialize>::serialize(message) + .map_err(|e| std::io::Error::new(std::io::ErrorKind::InvalidData, e))?; + let len = u32::try_from(bytes.len()) + .map_err(|_| std::io::Error::new(std::io::ErrorKind::InvalidInput, "message too large"))?; + w.write_all(&len.to_le_bytes())?; + w.write_all(&bytes) +} + +fn read_message Deserialize<'de>>(r: &mut impl Read) -> std::io::Result> { + let mut len = [0; 4]; + let n = r.read(&mut len[..1])?; + if n == 0 { + return Ok(None); + } + r.read_exact(&mut len[1..])?; + let mut bytes = vec![0; u32::from_le_bytes(len) as usize]; + r.read_exact(&mut bytes)?; + as wincode::Deserialize>::deserialize(&bytes) + .map(Some) + .map_err(|e| std::io::Error::new(std::io::ErrorKind::InvalidData, e)) +} + +fn opt_level_to_u8(level: OptimizationLevel) -> u8 { + match level { + OptimizationLevel::None => 0, + OptimizationLevel::Less => 1, + OptimizationLevel::Default => 2, + OptimizationLevel::Aggressive => 3, + } +} + +fn opt_level_from_u8(level: u8) -> eyre::Result { + Ok(match level { + 0 => OptimizationLevel::None, + 1 => OptimizationLevel::Less, + 2 => OptimizationLevel::Default, + 3 => OptimizationLevel::Aggressive, + _ => eyre::bail!("invalid optimization level"), + }) +} diff --git a/crates/revmc-runtime/src/runtime/worker.rs b/crates/revmc-runtime/src/runtime/worker.rs index 25a4bf9de..e16449472 100644 --- a/crates/revmc-runtime/src/runtime/worker.rs +++ b/crates/revmc-runtime/src/runtime/worker.rs @@ -10,28 +10,20 @@ //! as the job channel closes. use crate::{ - CompileTimings, EvmCompilerFn, OptimizationLevel, eyre, + CompileTimings, EvmCompilerFn, OptimizationLevel, runtime::{ config::{CompilationKind, JitMode, RuntimeConfig}, storage::RuntimeCacheKey, }, }; -use alloy_primitives::{B256, Bytes}; +use alloy_primitives::Bytes; use crossbeam_channel as chan; use rayon::{ThreadPool, ThreadPoolBuilder}; #[cfg(feature = "llvm")] -use std::{ - cell::RefCell, - fs::File, - io::{BufReader, Cursor, Read, Write}, - path::PathBuf, - process::{Child, ChildStdin, Command, Stdio}, - thread::JoinHandle, - time::Instant, -}; +use std::{cell::RefCell, fs::File, io::Read, time::Instant}; use std::{ sync::{ - Arc, Mutex, OnceLock, + Arc, atomic::{AtomicBool, AtomicUsize, Ordering}, }, time::Duration, @@ -42,8 +34,6 @@ use crate::{ EvmCompiler, EvmLlvmBackend, Linker, llvm::{JitDylibGuard, orc::ResourceTracker}, }; -#[cfg(feature = "llvm")] -use revm_primitives::hardfork::SpecId; /// Notifier for synchronous compilation requests. /// @@ -271,8 +261,9 @@ impl WorkerPool { /// Cancels any in-flight compilation that can be interrupted externally. pub(crate) fn cancel_in_flight(&self) { + #[cfg(feature = "llvm")] if self.config.jit_mode == JitMode::OutOfProcess { - reset_jit_helper(); + super::out_of_process::cancel_in_flight(); } } } @@ -297,7 +288,7 @@ fn compile_job(job: CompileJob, config: &RuntimeConfig) -> WorkerResult { trace!(?job, "received job"); match job.kind { CompilationKind::Jit if config.jit_mode == JitMode::OutOfProcess => { - compile_job_out_of_process(job, config) + super::out_of_process::compile_job(job, config) } CompilationKind::Jit => { JIT_COMPILER.with_borrow_mut(|state| compile_with_state(job, config, state)) @@ -308,554 +299,6 @@ fn compile_job(job: CompileJob, config: &RuntimeConfig) -> WorkerResult { } } -#[cfg(feature = "llvm")] -fn compile_job_out_of_process(job: CompileJob, config: &RuntimeConfig) -> WorkerResult { - let t0 = Instant::now(); - let outcome = run_helper_job(&job, config); - WorkerResult { - key: job.key, - outcome, - kind: job.kind, - sync_notifier: job.sync_notifier, - generation: job.generation, - compile_duration: t0.elapsed(), - timings: CompileTimings::default(), - } -} - -const HELPER_ENV: &str = "REVMC_JIT_HELPER"; - -#[cfg(all(feature = "llvm", unix))] -fn apply_helper_limits(command: &mut Command, config: &RuntimeConfig) { - use std::os::unix::process::CommandExt; - - let memory_limit = config.tuning.jit_helper_memory_limit_bytes; - let cpu_time = config.tuning.jit_helper_cpu_time; - if memory_limit == 0 && cpu_time.is_none() { - return; - } - - // SAFETY: `pre_exec` runs in the child after fork and before exec. The closure only calls - // async-signal-safe libc `setrlimit` and constructs an `io::Error` if it fails. - unsafe { - command.pre_exec(move || { - if memory_limit > 0 { - set_rlimit(libc::RLIMIT_AS as _, memory_limit)?; - } - if let Some(cpu_time) = cpu_time { - let seconds = cpu_time.as_secs().max(1); - set_rlimit(libc::RLIMIT_CPU as _, seconds)?; - } - Ok(()) - }); - } -} - -#[cfg(all(feature = "llvm", unix))] -fn set_rlimit(resource: libc::c_int, value: u64) -> std::io::Result<()> { - let value = libc::rlim_t::try_from(value).unwrap_or(libc::rlim_t::MAX); - let limit = libc::rlimit { rlim_cur: value, rlim_max: value }; - if unsafe { libc::setrlimit(resource as _, &limit) } != 0 { - return Err(std::io::Error::last_os_error()); - } - Ok(()) -} - -#[cfg(all(feature = "llvm", not(unix)))] -fn apply_helper_limits(_command: &mut Command, _config: &RuntimeConfig) {} - -#[cfg(feature = "llvm")] -fn run_helper_job(job: &CompileJob, config: &RuntimeConfig) -> Result { - if config.gas_params.is_some() { - return Err("out-of-process JIT does not support custom gas params yet".into()); - } - if config.dump_dir.is_some() { - return Err("out-of-process JIT does not support debug dumps yet".into()); - } - - helper_process().compile(job, config) -} - -#[cfg(feature = "llvm")] -static HELPER_PROCESS: OnceLock = OnceLock::new(); - -#[cfg(feature = "llvm")] -fn helper_process() -> &'static HelperProcess { - HELPER_PROCESS.get_or_init(HelperProcess::default) -} - -#[cfg(feature = "llvm")] -fn reset_jit_helper() { - if let Some(helper) = HELPER_PROCESS.get() { - helper.reset(); - } -} - -#[cfg(not(feature = "llvm"))] -fn reset_jit_helper() {} - -#[cfg(feature = "llvm")] -struct HelperIo { - stdin: ChildStdin, - result_rx: chan::Receiver>, -} - -#[cfg(feature = "llvm")] -#[derive(Default)] -struct HelperProcess { - inner: Mutex>>, -} - -#[cfg(feature = "llvm")] -impl HelperProcess { - fn compile(&self, job: &CompileJob, config: &RuntimeConfig) -> Result { - let helper = { - let mut slot = self.inner.lock().unwrap(); - if slot.as_ref().is_none_or(|helper| !helper.matches_config(config)) { - *slot = Some(Arc::new(HelperProcessInner::spawn(config)?)); - } - slot.as_ref().unwrap().clone() - }; - - match helper.compile(job, config) { - Ok(result) => Ok(result), - Err(err) => { - let mut slot = self.inner.lock().unwrap(); - if slot.as_ref().is_some_and(|current| Arc::ptr_eq(current, &helper)) { - *slot = None; - } - Err(err) - } - } - } - - fn reset(&self) { - if let Some(helper) = self.inner.lock().unwrap().take() { - helper.kill(); - } - } -} - -#[cfg(feature = "llvm")] -struct HelperProcessInner { - path: PathBuf, - child: Mutex, - io: Mutex, - reader: Mutex>>, -} - -#[cfg(feature = "llvm")] -impl HelperProcessInner { - fn spawn(config: &RuntimeConfig) -> Result { - let path = match &config.jit_helper_path { - Some(path) => path.clone(), - None => { - std::env::current_exe().map_err(|e| format!("failed to locate current exe: {e}"))? - } - }; - let mut command = Command::new(&path); - command - .env(HELPER_ENV, "1") - .stdin(Stdio::piped()) - .stdout(Stdio::piped()) - .stderr(Stdio::inherit()); - apply_helper_limits(&mut command, config); - - let mut child = command.spawn().map_err(|e| format!("failed to spawn JIT helper: {e}"))?; - let stdin = child.stdin.take().ok_or("helper stdin unavailable")?; - let stdout = child.stdout.take().ok_or("helper stdout unavailable")?; - let (result_tx, result_rx) = chan::bounded(1); - let reader = std::thread::spawn(move || { - let mut stdout = BufReader::new(stdout); - loop { - let result = read_helper_result(&mut stdout); - let done = result.is_err(); - if result_tx.send(result).is_err() || done { - break; - } - } - }); - Ok(Self { - path, - child: Mutex::new(child), - io: Mutex::new(HelperIo { stdin, result_rx }), - reader: Mutex::new(Some(reader)), - }) - } - - fn matches_config(&self, config: &RuntimeConfig) -> bool { - match &config.jit_helper_path { - Some(path) => self.path == *path, - None => std::env::current_exe().map(|path| self.path == path).unwrap_or(false), - } - } - - fn compile(&self, job: &CompileJob, config: &RuntimeConfig) -> Result { - let mut io = self.io.lock().unwrap(); - write_job(&mut io.stdin, job, config) - .map_err(|e| format!("failed to write helper job: {e}"))?; - io.stdin.flush().map_err(|e| format!("failed to flush helper job: {e}"))?; - - match io.result_rx.recv_timeout(config.tuning.jit_timeout) { - Ok(result) => result, - Err(chan::RecvTimeoutError::Timeout) => { - self.kill(); - Err(format!("JIT helper timed out after {:?}", config.tuning.jit_timeout)) - } - Err(chan::RecvTimeoutError::Disconnected) => { - let status = self.child.lock().unwrap().try_wait().ok().flatten(); - Err(match status { - Some(status) => format!("JIT helper exited with {status}"), - None => "JIT helper disconnected".into(), - }) - } - } - } - - fn kill(&self) { - let mut child = self.child.lock().unwrap(); - let _ = child.kill(); - let _ = child.wait(); - } -} - -#[cfg(feature = "llvm")] -impl Drop for HelperProcessInner { - fn drop(&mut self) { - self.kill(); - if let Some(reader) = self.reader.lock().unwrap().take() { - let _ = reader.join(); - } - } -} - -const HELPER_PROTOCOL_VERSION: u8 = 1; -const HELPER_RESPONSE_OK: u8 = 0; -const HELPER_RESPONSE_ERR: u8 = 1; - -#[cfg(feature = "llvm")] -struct HelperRequest { - code_hash: [u8; 32], - spec_id: u8, - opt_level: u8, - debug_assertions: bool, - no_dedup: bool, - no_dse: bool, - symbol_name: String, - bytecode: Vec, -} - -#[cfg(feature = "llvm")] -enum HelperResponse { - Ok { symbol_name: String, object_bytes: Vec, builtin_symbols: Vec }, - Err { error: String }, -} - -#[cfg(feature = "llvm")] -fn write_job(mut w: impl Write, job: &CompileJob, config: &RuntimeConfig) -> std::io::Result<()> { - let mut frame = Vec::with_capacity(job.bytecode.len() + job.symbol_name.len() + 64); - write_u8(&mut frame, HELPER_PROTOCOL_VERSION)?; - write_fixed_bytes(&mut frame, &job.key.code_hash.0)?; - write_u8(&mut frame, job.key.spec_id as u8)?; - write_u8(&mut frame, opt_level_to_u8(job.opt_level))?; - let flags = u8::from(config.debug_assertions) - | (u8::from(config.no_dedup) << 1) - | (u8::from(config.no_dse) << 2); - write_u8(&mut frame, flags)?; - write_string(&mut frame, &job.symbol_name)?; - write_bytes(&mut frame, &job.bytecode)?; - write_frame(&mut w, &frame) -} - -#[cfg(feature = "llvm")] -fn read_helper_result(r: &mut impl Read) -> Result { - match read_response(r)? { - HelperResponse::Ok { symbol_name, object_bytes, builtin_symbols } => { - Ok(WorkerSuccess::JitObject(JitObjectSuccess { - symbol_name, - object_bytes: Bytes::from(object_bytes), - builtin_symbols, - })) - } - HelperResponse::Err { error } => Err(error), - } -} - -#[cfg(feature = "llvm")] -pub(super) fn run_jit_helper_stdio() -> eyre::Result<()> { - let mut stdin = std::io::stdin().lock(); - let mut stdout = std::io::stdout().lock(); - let mut compiler: Option> = None; - - while let Some((job, config)) = read_helper_job(&mut stdin)? { - if compiler.is_none() { - compiler = Some(create_compiler(&config, true).map_err(|e| eyre::eyre!(e))?); - } - let compiler = compiler.as_mut().unwrap(); - compiler.set_opt_level(job.opt_level); - compiler.debug_assertions(config.debug_assertions); - compiler.set_dedup(!config.no_dedup); - compiler.set_dse(!config.no_dse); - - let result = compile_jit_object_artifact(&job, compiler); - if let Err(err) = compiler.clear_ir() { - warn!(%err, "clear_ir failed"); - } - write_helper_result(&mut stdout, result)?; - stdout.flush()?; - } - - Ok(()) -} - -#[cfg(feature = "llvm")] -fn read_helper_job(stdin: &mut impl Read) -> eyre::Result> { - let Some(req) = read_request(stdin)? else { return Ok(None) }; - let spec_id = SpecId::try_from_u8(req.spec_id).ok_or_else(|| eyre::eyre!("invalid spec id"))?; - let opt_level = opt_level_from_u8(req.opt_level)?; - - let config = RuntimeConfig { - debug_assertions: req.debug_assertions, - no_dedup: req.no_dedup, - no_dse: req.no_dse, - ..Default::default() - }; - let job = CompileJob { - kind: CompilationKind::Jit, - key: RuntimeCacheKey { code_hash: B256::from(req.code_hash), spec_id }, - bytecode: Bytes::from(req.bytecode), - symbol_name: req.symbol_name, - opt_level, - sync_notifier: SyncNotifier::none(), - generation: 0, - }; - Ok(Some((job, config))) -} - -#[cfg(feature = "llvm")] -fn write_helper_result( - mut stdout: impl Write, - result: Result, -) -> eyre::Result<()> { - match result { - Ok(WorkerSuccess::JitObject(success)) => HelperResponse::Ok { - symbol_name: success.symbol_name, - object_bytes: success.object_bytes.to_vec(), - builtin_symbols: success.builtin_symbols, - }, - Ok(_) => unreachable!(), - Err(error) => HelperResponse::Err { error }, - } - .write(&mut stdout)?; - Ok(()) -} - -#[cfg(feature = "llvm")] -impl HelperResponse { - fn write(self, mut w: impl Write) -> std::io::Result<()> { - let mut frame = Vec::new(); - write_u8(&mut frame, HELPER_PROTOCOL_VERSION)?; - match self { - Self::Ok { symbol_name, object_bytes, builtin_symbols } => { - write_u8(&mut frame, HELPER_RESPONSE_OK)?; - write_string(&mut frame, &symbol_name)?; - write_bytes(&mut frame, &object_bytes)?; - write_u32(&mut frame, usize_to_u32(builtin_symbols.len())?)?; - for symbol in builtin_symbols { - write_string(&mut frame, &symbol)?; - } - } - Self::Err { error } => { - write_u8(&mut frame, HELPER_RESPONSE_ERR)?; - write_string(&mut frame, &error)?; - } - } - write_frame(&mut w, &frame) - } -} - -#[cfg(feature = "llvm")] -fn read_request(r: &mut impl Read) -> eyre::Result> { - let Some(frame) = read_frame(r)? else { return Ok(None) }; - let mut r = Cursor::new(frame); - check_protocol_version(read_u8(&mut r)?)?; - let mut code_hash = [0; 32]; - r.read_exact(&mut code_hash)?; - let spec_id = read_u8(&mut r)?; - let opt_level = read_u8(&mut r)?; - let flags = read_u8(&mut r)?; - let symbol_name = read_string(&mut r)?; - let bytecode = read_bytes(&mut r)?; - ensure_frame_consumed(&r)?; - Ok(Some(HelperRequest { - code_hash, - spec_id, - opt_level, - debug_assertions: flags & 1 != 0, - no_dedup: flags & (1 << 1) != 0, - no_dse: flags & (1 << 2) != 0, - symbol_name, - bytecode, - })) -} - -#[cfg(feature = "llvm")] -fn read_response(r: &mut impl Read) -> Result { - let Some(frame) = read_frame(r).map_err(|e| format!("failed to read helper result: {e}"))? - else { - return Err("JIT helper closed stdout".into()); - }; - let mut r = Cursor::new(frame); - check_protocol_version( - read_u8(&mut r).map_err(|e| format!("failed to decode helper result: {e}"))?, - ) - .map_err(|e| format!("failed to decode helper result: {e}"))?; - let tag = read_u8(&mut r).map_err(|e| format!("failed to decode helper result: {e}"))?; - let response = match tag { - HELPER_RESPONSE_OK => { - let symbol_name = - read_string(&mut r).map_err(|e| format!("failed to decode helper result: {e}"))?; - let object_bytes = - read_bytes(&mut r).map_err(|e| format!("failed to decode helper result: {e}"))?; - let n_symbols = - read_u32(&mut r).map_err(|e| format!("failed to decode helper result: {e}"))?; - let mut builtin_symbols = Vec::with_capacity(n_symbols as usize); - for _ in 0..n_symbols { - builtin_symbols.push( - read_string(&mut r) - .map_err(|e| format!("failed to decode helper result: {e}"))?, - ); - } - HelperResponse::Ok { symbol_name, object_bytes, builtin_symbols } - } - HELPER_RESPONSE_ERR => { - let error = - read_string(&mut r).map_err(|e| format!("failed to decode helper result: {e}"))?; - HelperResponse::Err { error } - } - _ => return Err(format!("failed to decode helper result: unknown response tag {tag}")), - }; - ensure_frame_consumed(&r).map_err(|e| format!("failed to decode helper result: {e}"))?; - Ok(response) -} - -#[cfg(feature = "llvm")] -fn check_protocol_version(version: u8) -> eyre::Result<()> { - if version != HELPER_PROTOCOL_VERSION { - eyre::bail!("unsupported helper protocol version {version}"); - } - Ok(()) -} - -#[cfg(feature = "llvm")] -fn write_frame(mut w: impl Write, frame: &[u8]) -> std::io::Result<()> { - write_u32(&mut w, usize_to_u32(frame.len())?)?; - w.write_all(frame) -} - -#[cfg(feature = "llvm")] -fn read_frame(r: &mut impl Read) -> std::io::Result>> { - let mut len = [0; 4]; - let n = r.read(&mut len[..1])?; - if n == 0 { - return Ok(None); - } - r.read_exact(&mut len[1..])?; - let len = u32::from_le_bytes(len); - let mut frame = vec![0; len as usize]; - r.read_exact(&mut frame)?; - Ok(Some(frame)) -} - -#[cfg(feature = "llvm")] -fn ensure_frame_consumed(r: &Cursor>) -> eyre::Result<()> { - if r.position() != r.get_ref().len() as u64 { - eyre::bail!("trailing bytes in helper frame"); - } - Ok(()) -} - -#[cfg(feature = "llvm")] -fn write_u8(mut w: impl Write, value: u8) -> std::io::Result<()> { - w.write_all(&[value]) -} - -#[cfg(feature = "llvm")] -fn write_u32(mut w: impl Write, value: u32) -> std::io::Result<()> { - w.write_all(&value.to_le_bytes()) -} - -#[cfg(feature = "llvm")] -fn write_fixed_bytes(mut w: impl Write, bytes: &[u8]) -> std::io::Result<()> { - w.write_all(bytes) -} - -#[cfg(feature = "llvm")] -fn write_bytes(mut w: impl Write, bytes: &[u8]) -> std::io::Result<()> { - write_u32(&mut w, usize_to_u32(bytes.len())?)?; - w.write_all(bytes) -} - -#[cfg(feature = "llvm")] -fn write_string(w: impl Write, value: &str) -> std::io::Result<()> { - write_bytes(w, value.as_bytes()) -} - -#[cfg(feature = "llvm")] -fn read_u8(mut r: impl Read) -> std::io::Result { - let mut buf = [0]; - r.read_exact(&mut buf)?; - Ok(buf[0]) -} - -#[cfg(feature = "llvm")] -fn read_u32(mut r: impl Read) -> std::io::Result { - let mut buf = [0; 4]; - r.read_exact(&mut buf)?; - Ok(u32::from_le_bytes(buf)) -} - -#[cfg(feature = "llvm")] -fn read_bytes(mut r: impl Read) -> std::io::Result> { - let len = read_u32(&mut r)? as usize; - let mut bytes = vec![0; len]; - r.read_exact(&mut bytes)?; - Ok(bytes) -} - -#[cfg(feature = "llvm")] -fn read_string(r: impl Read) -> std::io::Result { - String::from_utf8(read_bytes(r)?) - .map_err(|e| std::io::Error::new(std::io::ErrorKind::InvalidData, e)) -} - -#[cfg(feature = "llvm")] -fn usize_to_u32(value: usize) -> std::io::Result { - u32::try_from(value) - .map_err(|_| std::io::Error::new(std::io::ErrorKind::InvalidInput, "frame too large")) -} - -#[cfg(feature = "llvm")] -fn opt_level_to_u8(level: OptimizationLevel) -> u8 { - match level { - OptimizationLevel::None => 0, - OptimizationLevel::Less => 1, - OptimizationLevel::Default => 2, - OptimizationLevel::Aggressive => 3, - } -} - -#[cfg(feature = "llvm")] -fn opt_level_from_u8(level: u8) -> eyre::Result { - Ok(match level { - 0 => OptimizationLevel::None, - 1 => OptimizationLevel::Less, - 2 => OptimizationLevel::Default, - 3 => OptimizationLevel::Aggressive, - _ => eyre::bail!("invalid optimization level"), - }) -} - #[cfg(feature = "llvm")] fn compile_with_state( job: CompileJob, @@ -966,7 +409,7 @@ thread_local! { } #[cfg(feature = "llvm")] -fn create_compiler( +pub(super) fn create_compiler( config: &RuntimeConfig, aot: bool, ) -> Result, String> { @@ -1012,7 +455,7 @@ fn compile_jit_artifact( /// Compiles a single bytecode to a shared library and returns the raw bytes. #[cfg(feature = "llvm")] -fn compile_jit_object_artifact( +pub(super) fn compile_jit_object_artifact( job: &CompileJob, compiler: &mut EvmCompiler, ) -> Result { diff --git a/docs/out-of-process-jit.md b/docs/out-of-process-jit.md index a36f1b355..ec2162f8e 100644 --- a/docs/out-of-process-jit.md +++ b/docs/out-of-process-jit.md @@ -24,10 +24,10 @@ Using LLVM ORC's remote executor APIs (`ExecutorProcessControl`, `SimpleRemoteEP Current prototype: - `RuntimeConfig::jit_mode = JitMode::OutOfProcess` makes the runtime keep a global persistent helper process spawned via `RuntimeConfig::jit_helper_path`, or `std::env::current_exe()` when unset. -- `REVMC_JIT_MODE=out-of-process` switches default runtime configs to out-of-process JIT. `REVMC_JIT_HELPER_PATH` overrides the helper executable path. Test harnesses should point this at a binary that calls `revmc::runtime::maybe_run_jit_helper()` at startup, such as `target/debug/revmc`. +- `RuntimeConfig::default()` uses plain in-process defaults. `JitBackend::new` applies `RuntimeConfig::with_env_overrides()`, which recognizes `REVMC_JIT_MODE=out-of-process` and `REVMC_JIT_MODE=in-process`; other spellings are rejected. `REVMC_JIT_HELPER_PATH` overrides the helper executable path. Test harnesses should point this at a binary that calls `revmc::runtime::maybe_run_jit_helper()` at startup, such as `target/debug/revmc`. - `REVMC_JIT_HELPER_MEMORY_LIMIT_BYTES` and `REVMC_JIT_HELPER_CPU_SECONDS` apply Unix `RLIMIT_AS` and `RLIMIT_CPU` limits to helper processes before `exec`. - Helper binaries must call `revmc::runtime::maybe_run_jit_helper()` at process startup. `revmc-cli` does this already. -- Workers send length-prefixed binary JIT object requests to the helper over stdin and receive length-prefixed binary responses from stdout. +- Workers send length-prefixed wincode-serialized JIT object requests to the helper over stdin and receive length-prefixed wincode-serialized responses from stdout. - The parent links returned object bytes into its local ORC instance, resolves the symbol, and constructs `JitCodeBacking` with a parent-owned `ResourceTracker`. - `RuntimeTuning::jit_timeout` bounds each helper compilation; timed-out helpers are killed and replaced on the next job. - Clearing resident code or shutting down the runtime kills the helper process so in-flight out-of-process compiles can be interrupted instead of waiting for LLVM to finish. @@ -42,7 +42,7 @@ Still needed: ## Open questions -- Serialization crate and stability requirements for the private IPC protocol. +- Stability requirements for the private IPC protocol. - Whether debug dumps should be written by the child, the parent, or both. - Whether compiler recycling is still needed per helper worker once the whole helper can be restarted. - How to expose helper process configuration such as executable path, environment, and restart policy without making the default API noisy. From d409fb0381e5a698806723a9ce436984949afb81 Mon Sep 17 00:00:00 2001 From: DaniPopes <57450786+DaniPopes@users.noreply.github.com> Date: Sat, 16 May 2026 20:39:38 +0200 Subject: [PATCH 20/43] fix(runtime): bound jit helper shutdown --- Cargo.lock | 1 + Cargo.toml | 1 + crates/revmc-runtime/Cargo.toml | 1 + .../src/runtime/out_of_process.rs | 47 +++++++++++++------ docs/out-of-process-jit.md | 8 ++++ 5 files changed, 44 insertions(+), 14 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index 35c5c6067..6ce1466af 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -3360,6 +3360,7 @@ dependencies = [ "tempfile", "tracing", "tracing-subscriber 0.3.23", + "wait-timeout", "wincode", ] diff --git a/Cargo.toml b/Cargo.toml index 267abcc4a..92736f69c 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -75,6 +75,7 @@ paste = "1.0" quanta = "0.12" serde = { version = "1", features = ["derive"] } serde-wincode = "0.1" +wait-timeout = "0.2" wincode = "0.4.5" [profile.dev] diff --git a/crates/revmc-runtime/Cargo.toml b/crates/revmc-runtime/Cargo.toml index 7d28408ab..d8c69141f 100644 --- a/crates/revmc-runtime/Cargo.toml +++ b/crates/revmc-runtime/Cargo.toml @@ -54,6 +54,7 @@ rayon.workspace = true tracing.workspace = true serde.workspace = true serde-wincode.workspace = true +wait-timeout.workspace = true wincode.workspace = true [dev-dependencies] diff --git a/crates/revmc-runtime/src/runtime/out_of_process.rs b/crates/revmc-runtime/src/runtime/out_of_process.rs index b454a9292..60d181ffa 100644 --- a/crates/revmc-runtime/src/runtime/out_of_process.rs +++ b/crates/revmc-runtime/src/runtime/out_of_process.rs @@ -20,10 +20,11 @@ use std::{ ops::ControlFlow, path::PathBuf, process::{Child, ChildStdin, Command, Stdio}, - sync::{Arc, Mutex, OnceLock}, + sync::{Arc, Mutex}, thread::JoinHandle, - time::Instant, + time::{Duration, Instant}, }; +use wait_timeout::ChildExt; const HELPER_ENV: &str = "REVMC_JIT_HELPER"; @@ -53,9 +54,7 @@ pub(super) fn compile_job(job: CompileJob, config: &RuntimeConfig) -> WorkerResu /// Cancels in-flight out-of-process helper work. pub(super) fn cancel_in_flight() { - if let Some(helper) = HELPER_PROCESS.get() { - helper.reset(); - } + HELPER_PROCESS.reset(); } fn run_helper_job(job: &CompileJob, config: &RuntimeConfig) -> Result { @@ -69,10 +68,10 @@ fn run_helper_job(job: &CompileJob, config: &RuntimeConfig) -> Result = OnceLock::new(); +static HELPER_PROCESS: HelperProcess = HelperProcess::new(); fn helper_process() -> &'static HelperProcess { - HELPER_PROCESS.get_or_init(HelperProcess::default) + &HELPER_PROCESS } struct HelperIo { @@ -80,12 +79,15 @@ struct HelperIo { result_rx: chan::Receiver>, } -#[derive(Default)] struct HelperProcess { inner: Mutex>>, } impl HelperProcess { + const fn new() -> Self { + Self { inner: Mutex::new(None) } + } + fn compile(&self, job: &CompileJob, config: &RuntimeConfig) -> Result { let helper = { let mut slot = self.inner.lock().unwrap(); @@ -119,6 +121,7 @@ struct HelperProcessInner { child: Mutex, io: Mutex, reader: Mutex>>, + shutdown_timeout: Duration, } impl HelperProcessInner { @@ -146,7 +149,7 @@ impl HelperProcessInner { loop { let result = read_helper_result(&mut stdout); let done = result.is_err(); - if result_tx.send(result).is_err() || done { + if result_tx.try_send(result).is_err() || done { break; } } @@ -156,6 +159,7 @@ impl HelperProcessInner { child: Mutex::new(child), io: Mutex::new(HelperIo { stdin, result_rx }), reader: Mutex::new(Some(reader)), + shutdown_timeout: config.tuning.shutdown_timeout, }) } @@ -188,17 +192,32 @@ impl HelperProcessInner { } } - fn kill(&self) { + fn kill(&self) -> bool { let mut child = self.child.lock().unwrap(); - let _ = child.kill(); - let _ = child.wait(); + if matches!(child.try_wait(), Ok(Some(_))) { + return true; + } + if let Err(err) = child.kill() { + warn!(%err, "failed to kill JIT helper"); + } + match child.wait_timeout(self.shutdown_timeout) { + Ok(Some(_)) => true, + Ok(None) => { + warn!(timeout = ?self.shutdown_timeout, "timed out waiting for JIT helper exit"); + false + } + Err(err) => { + warn!(%err, "failed to wait for JIT helper exit"); + false + } + } } } impl Drop for HelperProcessInner { fn drop(&mut self) { - self.kill(); - if let Some(reader) = self.reader.lock().unwrap().take() { + let exited = self.kill(); + if exited && let Some(reader) = self.reader.lock().unwrap().take() { let _ = reader.join(); } } diff --git a/docs/out-of-process-jit.md b/docs/out-of-process-jit.md index ec2162f8e..c58fb6968 100644 --- a/docs/out-of-process-jit.md +++ b/docs/out-of-process-jit.md @@ -32,6 +32,14 @@ Current prototype: - `RuntimeTuning::jit_timeout` bounds each helper compilation; timed-out helpers are killed and replaced on the next job. - Clearing resident code or shutting down the runtime kills the helper process so in-flight out-of-process compiles can be interrupted instead of waiting for LLVM to finish. +## Fork-only helper startup + +Using `fork()` without `exec` is not safe with the current lazy helper model. The helper is spawned from a runtime worker after the backend has started threads, and a child forked from a multithreaded process can only safely run async-signal-safe operations until `exec`. The helper would immediately need to run normal Rust code, allocate, use locks, deserialize IPC messages, and initialize/run LLVM, so it does not fit that rule. + +Avoiding LLVM translation in the parent is not sufficient. The parent still uses LLVM ORC to link returned object files, and the Rust runtime, allocator, tracing, channels, and other libraries can have process-global locks or thread-local state before the helper is spawned. + +A fork-only helper may be viable only as an early fork server: fork during single-threaded startup before any LLVM initialization or backend worker creation, then let the child own all helper-side Rust/LLVM state. That would require an explicit startup path instead of the current lazy spawn. + Still needed: - Move the worker pool into a single helper process; the parent should only enqueue IPC requests. From bb46d4c47c536fdb87a29181d1215ea06db68d20 Mon Sep 17 00:00:00 2001 From: DaniPopes <57450786+DaniPopes@users.noreply.github.com> Date: Sat, 16 May 2026 20:45:15 +0200 Subject: [PATCH 21/43] fix(runtime): use wincode schemas for ipc --- Cargo.lock | 71 ++++++++++++++----- Cargo.toml | 4 +- crates/revmc-runtime/Cargo.toml | 2 - .../src/runtime/out_of_process.rs | 20 ++++-- 4 files changed, 67 insertions(+), 30 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index 6ce1466af..192dbb010 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -339,7 +339,7 @@ version = "2.0.4" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "99fce0350197dcd4ba4e9a7dd43915d908c0eb0e7352755791709a705e1c76b6" dependencies = [ - "darling", + "darling 0.23.0", "proc-macro2", "quote", "syn 2.0.117", @@ -1322,14 +1322,38 @@ dependencies = [ "typenum", ] +[[package]] +name = "darling" +version = "0.21.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9cdf337090841a411e2a7f3deb9187445851f91b309c0c0a29e05f74a00a48c0" +dependencies = [ + "darling_core 0.21.3", + "darling_macro 0.21.3", +] + [[package]] name = "darling" version = "0.23.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "25ae13da2f202d56bd7f91c25fba009e7717a1e4a1cc98a76d844b65ae912e9d" dependencies = [ - "darling_core", - "darling_macro", + "darling_core 0.23.0", + "darling_macro 0.23.0", +] + +[[package]] +name = "darling_core" +version = "0.21.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1247195ecd7e3c85f83c8d2a366e4210d588e802133e1e355180a9870b517ea4" +dependencies = [ + "fnv", + "ident_case", + "proc-macro2", + "quote", + "strsim", + "syn 2.0.117", ] [[package]] @@ -1345,13 +1369,24 @@ dependencies = [ "syn 2.0.117", ] +[[package]] +name = "darling_macro" +version = "0.21.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d38308df82d1080de0afee5d069fa14b0326a88c14f15c5ccda35b4a6c414c81" +dependencies = [ + "darling_core 0.21.3", + "quote", + "syn 2.0.117", +] + [[package]] name = "darling_macro" version = "0.23.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "ac3984ec7bd6cfa798e62b4a642426a5be0e68f9401cfc2a01e3fa9ea2fcdb8d" dependencies = [ - "darling_core", + "darling_core 0.23.0", "quote", "syn 2.0.117", ] @@ -3354,8 +3389,6 @@ dependencies = [ "revmc-context", "revmc-llvm", "revmc-statetest", - "serde", - "serde-wincode", "similar-asserts", "tempfile", "tracing", @@ -3640,17 +3673,6 @@ dependencies = [ "serde_derive", ] -[[package]] -name = "serde-wincode" -version = "0.1.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4a62dfa6ae65cb073dfe001fb076eaf827fd4267fcba7eb3be2fee4f1e69ccb5" -dependencies = [ - "serde", - "thiserror 2.0.18", - "wincode", -] - [[package]] name = "serde_core" version = "1.0.228" @@ -3710,7 +3732,7 @@ version = "3.18.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "d3db8978e608f1fe7357e211969fd9abdcae80bac1ba7a3369bb7eb6b404eb65" dependencies = [ - "darling", + "darling 0.23.0", "proc-macro2", "quote", "syn 2.0.117", @@ -4471,6 +4493,19 @@ dependencies = [ "proc-macro2", "quote", "thiserror 2.0.18", + "wincode-derive", +] + +[[package]] +name = "wincode-derive" +version = "0.4.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e262d55d1261f31e2cfe49cc6385a421d14d99faa0526bbe3cc1bda0d3005c62" +dependencies = [ + "darling 0.21.3", + "proc-macro2", + "quote", + "syn 2.0.117", ] [[package]] diff --git a/Cargo.toml b/Cargo.toml index 92736f69c..8094beef6 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -73,10 +73,8 @@ tracing-subscriber = "0.3" tracing-tracy = "0.11" paste = "1.0" quanta = "0.12" -serde = { version = "1", features = ["derive"] } -serde-wincode = "0.1" wait-timeout = "0.2" -wincode = "0.4.5" +wincode = { version = "0.4.5", features = ["derive"] } [profile.dev] opt-level = 3 diff --git a/crates/revmc-runtime/Cargo.toml b/crates/revmc-runtime/Cargo.toml index d8c69141f..5daa6040a 100644 --- a/crates/revmc-runtime/Cargo.toml +++ b/crates/revmc-runtime/Cargo.toml @@ -52,8 +52,6 @@ tempfile = "3.10" quanta.workspace = true rayon.workspace = true tracing.workspace = true -serde.workspace = true -serde-wincode.workspace = true wait-timeout.workspace = true wincode.workspace = true diff --git a/crates/revmc-runtime/src/runtime/out_of_process.rs b/crates/revmc-runtime/src/runtime/out_of_process.rs index 60d181ffa..9723c5451 100644 --- a/crates/revmc-runtime/src/runtime/out_of_process.rs +++ b/crates/revmc-runtime/src/runtime/out_of_process.rs @@ -14,7 +14,6 @@ use crate::{ use alloy_primitives::{B256, Bytes}; use crossbeam_channel as chan; use revm_primitives::hardfork::SpecId; -use serde::{Deserialize, Serialize}; use std::{ io::{BufReader, Read, Write}, ops::ControlFlow, @@ -25,6 +24,7 @@ use std::{ time::{Duration, Instant}, }; use wait_timeout::ChildExt; +use wincode::{SchemaRead, SchemaWrite}; const HELPER_ENV: &str = "REVMC_JIT_HELPER"; @@ -262,7 +262,7 @@ fn set_rlimit(resource: libc::c_int, value: u64) -> std::io::Result<()> { #[cfg(not(unix))] fn apply_helper_limits(_command: &mut Command, _config: &RuntimeConfig) {} -#[derive(Serialize, Deserialize)] +#[derive(SchemaWrite, SchemaRead)] struct HelperRequest { code_hash: [u8; 32], spec_id: u8, @@ -274,7 +274,7 @@ struct HelperRequest { bytecode: Vec, } -#[derive(Serialize, Deserialize)] +#[derive(SchemaWrite, SchemaRead)] enum HelperResponse { Ok { symbol_name: String, object_bytes: Vec, builtin_symbols: Vec }, Err { error: String }, @@ -374,8 +374,11 @@ fn write_helper_result( Ok(()) } -fn write_message(mut w: impl Write, message: &T) -> std::io::Result<()> { - let bytes = as wincode::Serialize>::serialize(message) +fn write_message(mut w: impl Write, message: &T) -> std::io::Result<()> +where + T: wincode::SchemaWrite, +{ + let bytes = wincode::serialize(message) .map_err(|e| std::io::Error::new(std::io::ErrorKind::InvalidData, e))?; let len = u32::try_from(bytes.len()) .map_err(|_| std::io::Error::new(std::io::ErrorKind::InvalidInput, "message too large"))?; @@ -383,7 +386,10 @@ fn write_message(mut w: impl Write, message: &T) -> std::io::Resul w.write_all(&bytes) } -fn read_message Deserialize<'de>>(r: &mut impl Read) -> std::io::Result> { +fn read_message(r: &mut impl Read) -> std::io::Result> +where + for<'de> T: wincode::SchemaRead<'de, wincode::config::DefaultConfig, Dst = T>, +{ let mut len = [0; 4]; let n = r.read(&mut len[..1])?; if n == 0 { @@ -392,7 +398,7 @@ fn read_message Deserialize<'de>>(r: &mut impl Read) -> std::io::Res r.read_exact(&mut len[1..])?; let mut bytes = vec![0; u32::from_le_bytes(len) as usize]; r.read_exact(&mut bytes)?; - as wincode::Deserialize>::deserialize(&bytes) + wincode::deserialize(&bytes) .map(Some) .map_err(|e| std::io::Error::new(std::io::ErrorKind::InvalidData, e)) } From 4c2e625212fd693159b117cf9a5006560361b201 Mon Sep 17 00:00:00 2001 From: DaniPopes <57450786+DaniPopes@users.noreply.github.com> Date: Sat, 16 May 2026 21:16:05 +0200 Subject: [PATCH 22/43] fix(runtime): stream jit helper ipc writes --- .../src/runtime/out_of_process.rs | 83 ++++++++++++++++--- 1 file changed, 72 insertions(+), 11 deletions(-) diff --git a/crates/revmc-runtime/src/runtime/out_of_process.rs b/crates/revmc-runtime/src/runtime/out_of_process.rs index 9723c5451..d491238a7 100644 --- a/crates/revmc-runtime/src/runtime/out_of_process.rs +++ b/crates/revmc-runtime/src/runtime/out_of_process.rs @@ -280,7 +280,7 @@ enum HelperResponse { Err { error: String }, } -fn write_job(mut w: impl Write, job: &CompileJob, config: &RuntimeConfig) -> std::io::Result<()> { +fn write_job(w: &mut dyn Write, job: &CompileJob, config: &RuntimeConfig) -> std::io::Result<()> { let req = HelperRequest { code_hash: job.key.code_hash.0, spec_id: job.key.spec_id as u8, @@ -291,10 +291,10 @@ fn write_job(mut w: impl Write, job: &CompileJob, config: &RuntimeConfig) -> std symbol_name: job.symbol_name.clone(), bytecode: job.bytecode.to_vec(), }; - write_message(&mut w, &req) + write_message(w, &req) } -fn read_helper_result(r: &mut impl Read) -> Result { +fn read_helper_result(r: &mut dyn Read) -> Result { match read_message(r).map_err(|e| format!("failed to decode helper result: {e}"))? { Some(HelperResponse::Ok { symbol_name, object_bytes, builtin_symbols }) => { Ok(WorkerSuccess::JitObject(JitObjectSuccess { @@ -334,7 +334,7 @@ fn run_jit_helper_stdio() -> eyre::Result<()> { Ok(()) } -fn read_helper_job(stdin: &mut impl Read) -> eyre::Result> { +fn read_helper_job(stdin: &mut dyn Read) -> eyre::Result> { let Some(req) = read_message::(stdin)? else { return Ok(None) }; let spec_id = SpecId::try_from_u8(req.spec_id).ok_or_else(|| eyre::eyre!("invalid spec id"))?; let opt_level = opt_level_from_u8(req.opt_level)?; @@ -358,7 +358,7 @@ fn read_helper_job(stdin: &mut impl Read) -> eyre::Result, ) -> eyre::Result<()> { let response = match result { @@ -370,23 +370,24 @@ fn write_helper_result( Ok(_) => unreachable!(), Err(error) => HelperResponse::Err { error }, }; - write_message(&mut stdout, &response)?; + write_message(stdout, &response)?; Ok(()) } -fn write_message(mut w: impl Write, message: &T) -> std::io::Result<()> +fn write_message(w: &mut dyn Write, message: &T) -> std::io::Result<()> where T: wincode::SchemaWrite, { - let bytes = wincode::serialize(message) + let len = wincode::serialized_size(message) .map_err(|e| std::io::Error::new(std::io::ErrorKind::InvalidData, e))?; - let len = u32::try_from(bytes.len()) + let len = u32::try_from(len) .map_err(|_| std::io::Error::new(std::io::ErrorKind::InvalidInput, "message too large"))?; w.write_all(&len.to_le_bytes())?; - w.write_all(&bytes) + wincode::serialize_into(IoWriter(w), message) + .map_err(|e| std::io::Error::new(std::io::ErrorKind::InvalidData, e)) } -fn read_message(r: &mut impl Read) -> std::io::Result> +fn read_message(r: &mut dyn Read) -> std::io::Result> where for<'de> T: wincode::SchemaRead<'de, wincode::config::DefaultConfig, Dst = T>, { @@ -403,6 +404,66 @@ where .map_err(|e| std::io::Error::new(std::io::ErrorKind::InvalidData, e)) } +struct IoWriter<'a>(&'a mut dyn Write); + +impl wincode::io::Writer for IoWriter<'_> { + type Trusted<'a> + = TrustedIoWriter<'a> + where + Self: 'a; + + fn write(&mut self, src: &[u8]) -> wincode::io::WriteResult<()> { + self.0.write_all(src)?; + Ok(()) + } + + unsafe fn as_trusted_for( + &mut self, + n_bytes: usize, + ) -> wincode::io::WriteResult> { + Ok(TrustedIoWriter { inner: self.0, remaining: n_bytes }) + } +} + +struct TrustedIoWriter<'a> { + inner: &'a mut dyn Write, + remaining: usize, +} + +impl wincode::io::Writer for TrustedIoWriter<'_> { + type Trusted<'a> + = TrustedIoWriter<'a> + where + Self: 'a; + + fn finish(&mut self) -> wincode::io::WriteResult<()> { + if self.remaining != 0 { + return Err(wincode::io::WriteError::WriteSizeLimit(self.remaining)); + } + Ok(()) + } + + fn write(&mut self, src: &[u8]) -> wincode::io::WriteResult<()> { + if src.len() > self.remaining { + return Err(wincode::io::WriteError::WriteSizeLimit(src.len())); + } + self.inner.write_all(src)?; + self.remaining -= src.len(); + Ok(()) + } + + unsafe fn as_trusted_for( + &mut self, + n_bytes: usize, + ) -> wincode::io::WriteResult> { + if n_bytes > self.remaining { + return Err(wincode::io::WriteError::WriteSizeLimit(n_bytes)); + } + self.remaining -= n_bytes; + Ok(TrustedIoWriter { inner: self.inner, remaining: n_bytes }) + } +} + fn opt_level_to_u8(level: OptimizationLevel) -> u8 { match level { OptimizationLevel::None => 0, From da3b32ab236638c1e0c47bb93be3c51d7b3ba2ec Mon Sep 17 00:00:00 2001 From: DaniPopes <57450786+DaniPopes@users.noreply.github.com> Date: Sat, 16 May 2026 23:11:26 +0200 Subject: [PATCH 23/43] fix(runtime): stream jit helper ipc reads --- .../src/runtime/out_of_process.rs | 112 +++++++++++++++--- 1 file changed, 96 insertions(+), 16 deletions(-) diff --git a/crates/revmc-runtime/src/runtime/out_of_process.rs b/crates/revmc-runtime/src/runtime/out_of_process.rs index d491238a7..1af564db1 100644 --- a/crates/revmc-runtime/src/runtime/out_of_process.rs +++ b/crates/revmc-runtime/src/runtime/out_of_process.rs @@ -15,7 +15,8 @@ use alloy_primitives::{B256, Bytes}; use crossbeam_channel as chan; use revm_primitives::hardfork::SpecId; use std::{ - io::{BufReader, Read, Write}, + io::{BufRead, BufReader, Read, Write}, + mem::MaybeUninit, ops::ControlFlow, path::PathBuf, process::{Child, ChildStdin, Command, Stdio}, @@ -24,7 +25,7 @@ use std::{ time::{Duration, Instant}, }; use wait_timeout::ChildExt; -use wincode::{SchemaRead, SchemaWrite}; +use wincode::{DeserializeOwned, SchemaRead, SchemaWrite}; const HELPER_ENV: &str = "REVMC_JIT_HELPER"; @@ -145,7 +146,7 @@ impl HelperProcessInner { let stdout = child.stdout.take().ok_or("helper stdout unavailable")?; let (result_tx, result_rx) = chan::bounded(1); let reader = std::thread::spawn(move || { - let mut stdout = BufReader::new(stdout); + let mut stdout = IpcReader::new(stdout); loop { let result = read_helper_result(&mut stdout); let done = result.is_err(); @@ -294,7 +295,7 @@ fn write_job(w: &mut dyn Write, job: &CompileJob, config: &RuntimeConfig) -> std write_message(w, &req) } -fn read_helper_result(r: &mut dyn Read) -> Result { +fn read_helper_result(r: &mut IpcReader) -> Result { match read_message(r).map_err(|e| format!("failed to decode helper result: {e}"))? { Some(HelperResponse::Ok { symbol_name, object_bytes, builtin_symbols }) => { Ok(WorkerSuccess::JitObject(JitObjectSuccess { @@ -309,7 +310,7 @@ fn read_helper_result(r: &mut dyn Read) -> Result { } fn run_jit_helper_stdio() -> eyre::Result<()> { - let mut stdin = std::io::stdin().lock(); + let mut stdin = IpcReader::new(std::io::stdin().lock()); let mut stdout = std::io::stdout().lock(); let mut compiler: Option> = None; @@ -334,8 +335,11 @@ fn run_jit_helper_stdio() -> eyre::Result<()> { Ok(()) } -fn read_helper_job(stdin: &mut dyn Read) -> eyre::Result> { - let Some(req) = read_message::(stdin)? else { return Ok(None) }; +fn read_helper_job( + stdin: &mut IpcReader, +) -> eyre::Result> { + let req: Option = read_message(stdin)?; + let Some(req) = req else { return Ok(None) }; let spec_id = SpecId::try_from_u8(req.spec_id).ok_or_else(|| eyre::eyre!("invalid spec id"))?; let opt_level = opt_level_from_u8(req.opt_level)?; @@ -387,21 +391,97 @@ where .map_err(|e| std::io::Error::new(std::io::ErrorKind::InvalidData, e)) } -fn read_message(r: &mut dyn Read) -> std::io::Result> +fn read_message(r: &mut IpcReader) -> std::io::Result> where - for<'de> T: wincode::SchemaRead<'de, wincode::config::DefaultConfig, Dst = T>, + T: wincode::SchemaReadOwned, { let mut len = [0; 4]; - let n = r.read(&mut len[..1])?; + let n = r.inner.read(&mut len[..1])?; if n == 0 { return Ok(None); } - r.read_exact(&mut len[1..])?; - let mut bytes = vec![0; u32::from_le_bytes(len) as usize]; - r.read_exact(&mut bytes)?; - wincode::deserialize(&bytes) - .map(Some) - .map_err(|e| std::io::Error::new(std::io::ErrorKind::InvalidData, e)) + r.inner.read_exact(&mut len[1..])?; + let mut reader = + LimitedReader { inner: &mut r.inner, remaining: u32::from_le_bytes(len) as usize }; + let value = T::deserialize_from(&mut reader) + .map_err(|e| std::io::Error::new(std::io::ErrorKind::InvalidData, e))?; + if reader.remaining != 0 { + return Err(std::io::Error::new( + std::io::ErrorKind::InvalidData, + "trailing bytes in helper message", + )); + } + Ok(Some(value)) +} + +struct IpcReader { + inner: BufReader, +} + +impl IpcReader { + fn new(inner: R) -> Self { + Self { inner: BufReader::new(inner) } + } +} + +struct LimitedReader<'a, R: Read> { + inner: &'a mut BufReader, + remaining: usize, +} + +impl<'de, R: Read> wincode::io::Reader<'de> for LimitedReader<'_, R> { + type Trusted<'a> + = LimitedReader<'a, R> + where + Self: 'a; + + fn fill_buf(&mut self, n_bytes: usize) -> wincode::io::ReadResult<&[u8]> { + if n_bytes > self.remaining { + return Err(wincode::io::read_size_limit(n_bytes)); + } + let buf = self.inner.fill_buf()?; + Ok(&buf[..buf.len().min(self.remaining)]) + } + + fn copy_into_slice(&mut self, dst: &mut [MaybeUninit]) -> wincode::io::ReadResult<()> { + if dst.len() > self.remaining { + return Err(wincode::io::read_size_limit(dst.len())); + } + let dst = + unsafe { std::slice::from_raw_parts_mut(dst.as_mut_ptr().cast::(), dst.len()) }; + self.inner.read_exact(dst)?; + self.remaining -= dst.len(); + Ok(()) + } + + unsafe fn consume_unchecked(&mut self, amt: usize) { + self.inner.consume(amt); + self.remaining -= amt; + } + + fn consume(&mut self, amt: usize) -> wincode::io::ReadResult<()> { + if amt > self.remaining { + return Err(wincode::io::read_size_limit(amt)); + } + let buffered = self.inner.buffer().len(); + if amt > buffered { + return Err(wincode::io::read_size_limit(amt)); + } + self.inner.consume(amt); + self.remaining -= amt; + Ok(()) + } + + unsafe fn as_trusted_for( + &mut self, + n_bytes: usize, + ) -> wincode::io::ReadResult> { + if n_bytes > self.remaining { + return Err(wincode::io::read_size_limit(n_bytes)); + } + self.remaining -= n_bytes; + Ok(LimitedReader { inner: self.inner, remaining: n_bytes }) + } } struct IoWriter<'a>(&'a mut dyn Write); From eb1a4104a29a10f4bf492f8ad8adac95b9009047 Mon Sep 17 00:00:00 2001 From: DaniPopes <57450786+DaniPopes@users.noreply.github.com> Date: Sat, 16 May 2026 23:56:45 +0200 Subject: [PATCH 24/43] fix(runtime): use wincode std ipc adapters --- Cargo.lock | 4 +- Cargo.toml | 2 +- .../src/runtime/out_of_process.rs | 179 +++--------------- 3 files changed, 29 insertions(+), 156 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index 192dbb010..80314cf1a 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -4485,9 +4485,9 @@ checksum = "712e227841d057c1ee1cd2fb22fa7e5a5461ae8e48fa2ca79ec42cfc1931183f" [[package]] name = "wincode" -version = "0.4.9" +version = "0.5.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "657690780ce23e6f66576a782ffd88eb353512381817029cc1d7a99154bb6d1f" +checksum = "37095eb18dd6254c66217edc61a29d83d51f8818de8a2ffe88e4584ad73fb5f9" dependencies = [ "pastey", "proc-macro2", diff --git a/Cargo.toml b/Cargo.toml index 8094beef6..da3dd5939 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -74,7 +74,7 @@ tracing-tracy = "0.11" paste = "1.0" quanta = "0.12" wait-timeout = "0.2" -wincode = { version = "0.4.5", features = ["derive"] } +wincode = { version = "0.5.4", features = ["derive"] } [profile.dev] opt-level = 3 diff --git a/crates/revmc-runtime/src/runtime/out_of_process.rs b/crates/revmc-runtime/src/runtime/out_of_process.rs index 1af564db1..2b2ff916d 100644 --- a/crates/revmc-runtime/src/runtime/out_of_process.rs +++ b/crates/revmc-runtime/src/runtime/out_of_process.rs @@ -15,8 +15,7 @@ use alloy_primitives::{B256, Bytes}; use crossbeam_channel as chan; use revm_primitives::hardfork::SpecId; use std::{ - io::{BufRead, BufReader, Read, Write}, - mem::MaybeUninit, + io::{BufReader, BufWriter, Read, Write}, ops::ControlFlow, path::PathBuf, process::{Child, ChildStdin, Command, Stdio}, @@ -25,7 +24,7 @@ use std::{ time::{Duration, Instant}, }; use wait_timeout::ChildExt; -use wincode::{DeserializeOwned, SchemaRead, SchemaWrite}; +use wincode::{SchemaRead, SchemaWrite}; const HELPER_ENV: &str = "REVMC_JIT_HELPER"; @@ -76,7 +75,7 @@ fn helper_process() -> &'static HelperProcess { } struct HelperIo { - stdin: ChildStdin, + stdin: BufWriter, result_rx: chan::Receiver>, } @@ -142,11 +141,11 @@ impl HelperProcessInner { apply_helper_limits(&mut command, config); let mut child = command.spawn().map_err(|e| format!("failed to spawn JIT helper: {e}"))?; - let stdin = child.stdin.take().ok_or("helper stdin unavailable")?; + let stdin = BufWriter::new(child.stdin.take().ok_or("helper stdin unavailable")?); let stdout = child.stdout.take().ok_or("helper stdout unavailable")?; let (result_tx, result_rx) = chan::bounded(1); let reader = std::thread::spawn(move || { - let mut stdout = IpcReader::new(stdout); + let mut stdout = BufReader::new(stdout); loop { let result = read_helper_result(&mut stdout); let done = result.is_err(); @@ -281,7 +280,11 @@ enum HelperResponse { Err { error: String }, } -fn write_job(w: &mut dyn Write, job: &CompileJob, config: &RuntimeConfig) -> std::io::Result<()> { +fn write_job( + w: &mut BufWriter, + job: &CompileJob, + config: &RuntimeConfig, +) -> std::io::Result<()> { let req = HelperRequest { code_hash: job.key.code_hash.0, spec_id: job.key.spec_id as u8, @@ -295,7 +298,7 @@ fn write_job(w: &mut dyn Write, job: &CompileJob, config: &RuntimeConfig) -> std write_message(w, &req) } -fn read_helper_result(r: &mut IpcReader) -> Result { +fn read_helper_result(r: &mut BufReader) -> Result { match read_message(r).map_err(|e| format!("failed to decode helper result: {e}"))? { Some(HelperResponse::Ok { symbol_name, object_bytes, builtin_symbols }) => { Ok(WorkerSuccess::JitObject(JitObjectSuccess { @@ -310,8 +313,8 @@ fn read_helper_result(r: &mut IpcReader) -> Result eyre::Result<()> { - let mut stdin = IpcReader::new(std::io::stdin().lock()); - let mut stdout = std::io::stdout().lock(); + let mut stdin = BufReader::new(std::io::stdin().lock()); + let mut stdout = BufWriter::new(std::io::stdout().lock()); let mut compiler: Option> = None; while let Some((job, config)) = read_helper_job(&mut stdin)? { @@ -335,8 +338,8 @@ fn run_jit_helper_stdio() -> eyre::Result<()> { Ok(()) } -fn read_helper_job( - stdin: &mut IpcReader, +fn read_helper_job( + stdin: &mut BufReader, ) -> eyre::Result> { let req: Option = read_message(stdin)?; let Some(req) = req else { return Ok(None) }; @@ -361,8 +364,8 @@ fn read_helper_job( Ok(Some((job, config))) } -fn write_helper_result( - stdout: &mut dyn Write, +fn write_helper_result( + stdout: &mut BufWriter, result: Result, ) -> eyre::Result<()> { let response = match result { @@ -378,7 +381,7 @@ fn write_helper_result( Ok(()) } -fn write_message(w: &mut dyn Write, message: &T) -> std::io::Result<()> +fn write_message(w: &mut BufWriter, message: &T) -> std::io::Result<()> where T: wincode::SchemaWrite, { @@ -387,25 +390,25 @@ where let len = u32::try_from(len) .map_err(|_| std::io::Error::new(std::io::ErrorKind::InvalidInput, "message too large"))?; w.write_all(&len.to_le_bytes())?; - wincode::serialize_into(IoWriter(w), message) + wincode::serialize_into(w, message) .map_err(|e| std::io::Error::new(std::io::ErrorKind::InvalidData, e)) } -fn read_message(r: &mut IpcReader) -> std::io::Result> +fn read_message(r: &mut BufReader) -> std::io::Result> where T: wincode::SchemaReadOwned, { let mut len = [0; 4]; - let n = r.inner.read(&mut len[..1])?; + let n = r.read(&mut len[..1])?; if n == 0 { return Ok(None); } - r.inner.read_exact(&mut len[1..])?; - let mut reader = - LimitedReader { inner: &mut r.inner, remaining: u32::from_le_bytes(len) as usize }; - let value = T::deserialize_from(&mut reader) + r.read_exact(&mut len[1..])?; + let mut reader = BufReader::new(r.take(u64::from(u32::from_le_bytes(len)))); + let value = wincode::deserialize_from(&mut reader) .map_err(|e| std::io::Error::new(std::io::ErrorKind::InvalidData, e))?; - if reader.remaining != 0 { + let remaining = reader.buffer().len() as u64 + reader.get_ref().limit(); + if remaining != 0 { return Err(std::io::Error::new( std::io::ErrorKind::InvalidData, "trailing bytes in helper message", @@ -414,136 +417,6 @@ where Ok(Some(value)) } -struct IpcReader { - inner: BufReader, -} - -impl IpcReader { - fn new(inner: R) -> Self { - Self { inner: BufReader::new(inner) } - } -} - -struct LimitedReader<'a, R: Read> { - inner: &'a mut BufReader, - remaining: usize, -} - -impl<'de, R: Read> wincode::io::Reader<'de> for LimitedReader<'_, R> { - type Trusted<'a> - = LimitedReader<'a, R> - where - Self: 'a; - - fn fill_buf(&mut self, n_bytes: usize) -> wincode::io::ReadResult<&[u8]> { - if n_bytes > self.remaining { - return Err(wincode::io::read_size_limit(n_bytes)); - } - let buf = self.inner.fill_buf()?; - Ok(&buf[..buf.len().min(self.remaining)]) - } - - fn copy_into_slice(&mut self, dst: &mut [MaybeUninit]) -> wincode::io::ReadResult<()> { - if dst.len() > self.remaining { - return Err(wincode::io::read_size_limit(dst.len())); - } - let dst = - unsafe { std::slice::from_raw_parts_mut(dst.as_mut_ptr().cast::(), dst.len()) }; - self.inner.read_exact(dst)?; - self.remaining -= dst.len(); - Ok(()) - } - - unsafe fn consume_unchecked(&mut self, amt: usize) { - self.inner.consume(amt); - self.remaining -= amt; - } - - fn consume(&mut self, amt: usize) -> wincode::io::ReadResult<()> { - if amt > self.remaining { - return Err(wincode::io::read_size_limit(amt)); - } - let buffered = self.inner.buffer().len(); - if amt > buffered { - return Err(wincode::io::read_size_limit(amt)); - } - self.inner.consume(amt); - self.remaining -= amt; - Ok(()) - } - - unsafe fn as_trusted_for( - &mut self, - n_bytes: usize, - ) -> wincode::io::ReadResult> { - if n_bytes > self.remaining { - return Err(wincode::io::read_size_limit(n_bytes)); - } - self.remaining -= n_bytes; - Ok(LimitedReader { inner: self.inner, remaining: n_bytes }) - } -} - -struct IoWriter<'a>(&'a mut dyn Write); - -impl wincode::io::Writer for IoWriter<'_> { - type Trusted<'a> - = TrustedIoWriter<'a> - where - Self: 'a; - - fn write(&mut self, src: &[u8]) -> wincode::io::WriteResult<()> { - self.0.write_all(src)?; - Ok(()) - } - - unsafe fn as_trusted_for( - &mut self, - n_bytes: usize, - ) -> wincode::io::WriteResult> { - Ok(TrustedIoWriter { inner: self.0, remaining: n_bytes }) - } -} - -struct TrustedIoWriter<'a> { - inner: &'a mut dyn Write, - remaining: usize, -} - -impl wincode::io::Writer for TrustedIoWriter<'_> { - type Trusted<'a> - = TrustedIoWriter<'a> - where - Self: 'a; - - fn finish(&mut self) -> wincode::io::WriteResult<()> { - if self.remaining != 0 { - return Err(wincode::io::WriteError::WriteSizeLimit(self.remaining)); - } - Ok(()) - } - - fn write(&mut self, src: &[u8]) -> wincode::io::WriteResult<()> { - if src.len() > self.remaining { - return Err(wincode::io::WriteError::WriteSizeLimit(src.len())); - } - self.inner.write_all(src)?; - self.remaining -= src.len(); - Ok(()) - } - - unsafe fn as_trusted_for( - &mut self, - n_bytes: usize, - ) -> wincode::io::WriteResult> { - if n_bytes > self.remaining { - return Err(wincode::io::WriteError::WriteSizeLimit(n_bytes)); - } - self.remaining -= n_bytes; - Ok(TrustedIoWriter { inner: self.inner, remaining: n_bytes }) - } -} - fn opt_level_to_u8(level: OptimizationLevel) -> u8 { match level { OptimizationLevel::None => 0, From 29f5ce4c1a4434a87f4f306c786c95f1a3fd577b Mon Sep 17 00:00:00 2001 From: DaniPopes <57450786+DaniPopes@users.noreply.github.com> Date: Sun, 17 May 2026 05:23:26 +0200 Subject: [PATCH 25/43] fix(runtime): scope jit helper cancellation --- .../src/runtime/out_of_process.rs | 33 +++++++++---------- crates/revmc-runtime/src/runtime/worker.rs | 30 ++++++++++++++--- 2 files changed, 40 insertions(+), 23 deletions(-) diff --git a/crates/revmc-runtime/src/runtime/out_of_process.rs b/crates/revmc-runtime/src/runtime/out_of_process.rs index 2b2ff916d..52f9bde12 100644 --- a/crates/revmc-runtime/src/runtime/out_of_process.rs +++ b/crates/revmc-runtime/src/runtime/out_of_process.rs @@ -38,9 +38,13 @@ pub(super) fn maybe_run_jit_helper() -> eyre::Result> { } /// Compiles a job in the out-of-process helper. -pub(super) fn compile_job(job: CompileJob, config: &RuntimeConfig) -> WorkerResult { +pub(super) fn compile_job( + job: CompileJob, + config: &RuntimeConfig, + helper: &HelperProcess, +) -> WorkerResult { let t0 = Instant::now(); - let outcome = run_helper_job(&job, config); + let outcome = run_helper_job(&job, config, helper); WorkerResult { key: job.key, outcome, @@ -52,12 +56,11 @@ pub(super) fn compile_job(job: CompileJob, config: &RuntimeConfig) -> WorkerResu } } -/// Cancels in-flight out-of-process helper work. -pub(super) fn cancel_in_flight() { - HELPER_PROCESS.reset(); -} - -fn run_helper_job(job: &CompileJob, config: &RuntimeConfig) -> Result { +fn run_helper_job( + job: &CompileJob, + config: &RuntimeConfig, + helper: &HelperProcess, +) -> Result { if config.gas_params.is_some() { return Err("out-of-process JIT does not support custom gas params yet".into()); } @@ -65,13 +68,7 @@ fn run_helper_job(job: &CompileJob, config: &RuntimeConfig) -> Result &'static HelperProcess { - &HELPER_PROCESS + helper.compile(job, config) } struct HelperIo { @@ -79,12 +76,12 @@ struct HelperIo { result_rx: chan::Receiver>, } -struct HelperProcess { +pub(super) struct HelperProcess { inner: Mutex>>, } impl HelperProcess { - const fn new() -> Self { + pub(super) const fn new() -> Self { Self { inner: Mutex::new(None) } } @@ -109,7 +106,7 @@ impl HelperProcess { } } - fn reset(&self) { + pub(super) fn cancel_in_flight(&self) { if let Some(helper) = self.inner.lock().unwrap().take() { helper.kill(); } diff --git a/crates/revmc-runtime/src/runtime/worker.rs b/crates/revmc-runtime/src/runtime/worker.rs index e16449472..b6d8e51d8 100644 --- a/crates/revmc-runtime/src/runtime/worker.rs +++ b/crates/revmc-runtime/src/runtime/worker.rs @@ -12,7 +12,7 @@ use crate::{ CompileTimings, EvmCompilerFn, OptimizationLevel, runtime::{ - config::{CompilationKind, JitMode, RuntimeConfig}, + config::{CompilationKind, RuntimeConfig}, storage::RuntimeCacheKey, }, }; @@ -33,6 +33,7 @@ use std::{ use crate::{ EvmCompiler, EvmLlvmBackend, Linker, llvm::{JitDylibGuard, orc::ResourceTracker}, + runtime::config::JitMode, }; /// Notifier for synchronous compilation requests. @@ -178,6 +179,9 @@ impl Drop for JitCodeBacking { pub(crate) struct WorkerPool { /// Rayon pool used to execute compilation jobs. pool: Option, + /// Out-of-process helper used by this worker pool. + #[cfg(feature = "llvm")] + out_of_process_helper: Option>, /// Sender for worker results. result_tx: chan::Sender, /// Runtime configuration shared by spawned jobs. @@ -195,6 +199,9 @@ impl WorkerPool { pub(crate) fn new(result_tx: chan::Sender, config: RuntimeConfig) -> Self { let worker_count = config.tuning.jit_worker_count; let queue_capacity = worker_count.saturating_mul(config.tuning.jit_worker_queue_capacity); + #[cfg(feature = "llvm")] + let out_of_process_helper = (config.jit_mode == JitMode::OutOfProcess) + .then(|| Arc::new(super::out_of_process::HelperProcess::new())); let pool = (worker_count > 0).then(|| { ThreadPoolBuilder::new() .num_threads(worker_count) @@ -206,6 +213,8 @@ impl WorkerPool { Self { pool, + #[cfg(feature = "llvm")] + out_of_process_helper, result_tx, config: Arc::new(config), queued: Arc::new(AtomicUsize::new(0)), @@ -241,8 +250,13 @@ impl WorkerPool { let queued = self.queued.clone(); let result_tx = self.result_tx.clone(); let config = self.config.clone(); + #[cfg(feature = "llvm")] + let out_of_process_helper = self.out_of_process_helper.clone(); pool.spawn_fifo(move || { queued.fetch_sub(1, Ordering::AcqRel); + #[cfg(feature = "llvm")] + let result = compile_job(job, &config, out_of_process_helper); + #[cfg(not(feature = "llvm"))] let result = compile_job(job, &config); let _ = result_tx.send(result); }); @@ -262,8 +276,8 @@ impl WorkerPool { /// Cancels any in-flight compilation that can be interrupted externally. pub(crate) fn cancel_in_flight(&self) { #[cfg(feature = "llvm")] - if self.config.jit_mode == JitMode::OutOfProcess { - super::out_of_process::cancel_in_flight(); + if let Some(helper) = &self.out_of_process_helper { + helper.cancel_in_flight(); } } } @@ -284,11 +298,16 @@ fn clear_thread_local_compilers() { fn clear_thread_local_compilers() {} #[cfg(feature = "llvm")] -fn compile_job(job: CompileJob, config: &RuntimeConfig) -> WorkerResult { +fn compile_job( + job: CompileJob, + config: &RuntimeConfig, + out_of_process_helper: Option>, +) -> WorkerResult { trace!(?job, "received job"); match job.kind { CompilationKind::Jit if config.jit_mode == JitMode::OutOfProcess => { - super::out_of_process::compile_job(job, config) + let helper = out_of_process_helper.expect("missing out-of-process JIT helper"); + super::out_of_process::compile_job(job, config, &helper) } CompilationKind::Jit => { JIT_COMPILER.with_borrow_mut(|state| compile_with_state(job, config, state)) @@ -491,6 +510,7 @@ pub(super) fn compile_jit_object_artifact( })) } +#[cfg(feature = "llvm")] fn compile_aot_artifact( job: &CompileJob, compiler: &mut EvmCompiler, From 5b0bb53dd1baa39359bdc7bf2986f12ba9278a1b Mon Sep 17 00:00:00 2001 From: DaniPopes <57450786+DaniPopes@users.noreply.github.com> Date: Mon, 18 May 2026 02:39:15 +0200 Subject: [PATCH 26/43] fix(runtime): carry jit helper config --- crates/revmc-cli/src/main.rs | 2 +- .../src/runtime/out_of_process.rs | 153 +++++++++++++----- docs/out-of-process-jit.md | 2 +- 3 files changed, 115 insertions(+), 42 deletions(-) diff --git a/crates/revmc-cli/src/main.rs b/crates/revmc-cli/src/main.rs index ee356f419..fb2d8c17d 100644 --- a/crates/revmc-cli/src/main.rs +++ b/crates/revmc-cli/src/main.rs @@ -21,7 +21,7 @@ enum Command { } fn main() -> Result<()> { - if matches!(revmc::runtime::maybe_run_jit_helper()?, std::ops::ControlFlow::Break(())) { + if revmc::runtime::maybe_run_jit_helper()?.is_break() { return Ok(()); } if std::env::var_os("RUST_BACKTRACE").is_none() { diff --git a/crates/revmc-runtime/src/runtime/out_of_process.rs b/crates/revmc-runtime/src/runtime/out_of_process.rs index 52f9bde12..c2764fda4 100644 --- a/crates/revmc-runtime/src/runtime/out_of_process.rs +++ b/crates/revmc-runtime/src/runtime/out_of_process.rs @@ -13,6 +13,7 @@ use crate::{ }; use alloy_primitives::{B256, Bytes}; use crossbeam_channel as chan; +use revm_context_interface::cfg::{GasParams, gas_params::GasId}; use revm_primitives::hardfork::SpecId; use std::{ io::{BufReader, BufWriter, Read, Write}, @@ -27,6 +28,9 @@ use wait_timeout::ChildExt; use wincode::{SchemaRead, SchemaWrite}; const HELPER_ENV: &str = "REVMC_JIT_HELPER"; +const GAS_PARAM_COUNT: usize = 256; + +type GasParamPairs = Vec<(u8, u64)>; /// Runs the out-of-process JIT helper if this process was launched as one. pub(super) fn maybe_run_jit_helper() -> eyre::Result> { @@ -61,13 +65,6 @@ fn run_helper_job( config: &RuntimeConfig, helper: &HelperProcess, ) -> Result { - if config.gas_params.is_some() { - return Err("out-of-process JIT does not support custom gas params yet".into()); - } - if config.dump_dir.is_some() { - return Err("out-of-process JIT does not support debug dumps yet".into()); - } - helper.compile(job, config) } @@ -115,6 +112,7 @@ impl HelperProcess { struct HelperProcessInner { path: PathBuf, + init: HelperInit, child: Mutex, io: Mutex, reader: Mutex>>, @@ -129,6 +127,7 @@ impl HelperProcessInner { std::env::current_exe().map_err(|e| format!("failed to locate current exe: {e}"))? } }; + let init = helper_init(config); let mut command = Command::new(&path); command .env(HELPER_ENV, "1") @@ -138,7 +137,9 @@ impl HelperProcessInner { apply_helper_limits(&mut command, config); let mut child = command.spawn().map_err(|e| format!("failed to spawn JIT helper: {e}"))?; - let stdin = BufWriter::new(child.stdin.take().ok_or("helper stdin unavailable")?); + let mut stdin = BufWriter::new(child.stdin.take().ok_or("helper stdin unavailable")?); + write_init(&mut stdin, &init).map_err(|e| format!("failed to write helper init: {e}"))?; + stdin.flush().map_err(|e| format!("failed to flush helper init: {e}"))?; let stdout = child.stdout.take().ok_or("helper stdout unavailable")?; let (result_tx, result_rx) = chan::bounded(1); let reader = std::thread::spawn(move || { @@ -153,6 +154,7 @@ impl HelperProcessInner { }); Ok(Self { path, + init, child: Mutex::new(child), io: Mutex::new(HelperIo { stdin, result_rx }), reader: Mutex::new(Some(reader)), @@ -161,6 +163,9 @@ impl HelperProcessInner { } fn matches_config(&self, config: &RuntimeConfig) -> bool { + if self.init != helper_init(config) { + return false; + } match &config.jit_helper_path { Some(path) => self.path == *path, None => std::env::current_exe().map(|path| self.path == path).unwrap_or(false), @@ -169,8 +174,7 @@ impl HelperProcessInner { fn compile(&self, job: &CompileJob, config: &RuntimeConfig) -> Result { let mut io = self.io.lock().unwrap(); - write_job(&mut io.stdin, job, config) - .map_err(|e| format!("failed to write helper job: {e}"))?; + write_job(&mut io.stdin, job).map_err(|e| format!("failed to write helper job: {e}"))?; io.stdin.flush().map_err(|e| format!("failed to flush helper job: {e}"))?; match io.result_rx.recv_timeout(config.tuning.jit_timeout) { @@ -259,14 +263,26 @@ fn set_rlimit(resource: libc::c_int, value: u64) -> std::io::Result<()> { #[cfg(not(unix))] fn apply_helper_limits(_command: &mut Command, _config: &RuntimeConfig) {} +#[derive(Clone, PartialEq, Eq, SchemaWrite, SchemaRead)] +struct HelperInit { + debug_assertions: bool, + no_dedup: bool, + no_dse: bool, + dump_dir: Option, + gas_params: Option, +} + #[derive(SchemaWrite, SchemaRead)] -struct HelperRequest { +enum HelperRequest { + Init(HelperInit), + Compile(HelperCompile), +} + +#[derive(SchemaWrite, SchemaRead)] +struct HelperCompile { code_hash: [u8; 32], spec_id: u8, opt_level: u8, - debug_assertions: bool, - no_dedup: bool, - no_dse: bool, symbol_name: String, bytecode: Vec, } @@ -277,21 +293,18 @@ enum HelperResponse { Err { error: String }, } -fn write_job( - w: &mut BufWriter, - job: &CompileJob, - config: &RuntimeConfig, -) -> std::io::Result<()> { - let req = HelperRequest { +fn write_init(w: &mut BufWriter, init: &HelperInit) -> std::io::Result<()> { + write_message(w, &HelperRequest::Init(init.clone())) +} + +fn write_job(w: &mut BufWriter, job: &CompileJob) -> std::io::Result<()> { + let req = HelperRequest::Compile(HelperCompile { code_hash: job.key.code_hash.0, spec_id: job.key.spec_id as u8, opt_level: opt_level_to_u8(job.opt_level), - debug_assertions: config.debug_assertions, - no_dedup: config.no_dedup, - no_dse: config.no_dse, symbol_name: job.symbol_name.clone(), bytecode: job.bytecode.to_vec(), - }; + }); write_message(w, &req) } @@ -313,16 +326,15 @@ fn run_jit_helper_stdio() -> eyre::Result<()> { let mut stdin = BufReader::new(std::io::stdin().lock()); let mut stdout = BufWriter::new(std::io::stdout().lock()); let mut compiler: Option> = None; + let config = read_helper_init(&mut stdin)?; - while let Some((job, config)) = read_helper_job(&mut stdin)? { + while let Some(job) = read_helper_job(&mut stdin)? { if compiler.is_none() { compiler = Some(create_compiler(&config, true).map_err(|e| eyre::eyre!(e))?); } let compiler = compiler.as_mut().unwrap(); compiler.set_opt_level(job.opt_level); - compiler.debug_assertions(config.debug_assertions); - compiler.set_dedup(!config.no_dedup); - compiler.set_dse(!config.no_dse); + compiler.set_dump_to(job_dump_dir(&config, &job)); let result = compile_jit_object_artifact(&job, compiler); if let Err(err) = compiler.clear_ir() { @@ -335,20 +347,23 @@ fn run_jit_helper_stdio() -> eyre::Result<()> { Ok(()) } -fn read_helper_job( - stdin: &mut BufReader, -) -> eyre::Result> { - let req: Option = read_message(stdin)?; - let Some(req) = req else { return Ok(None) }; +fn read_helper_init(stdin: &mut BufReader) -> eyre::Result { + match read_message(stdin)? { + Some(HelperRequest::Init(init)) => runtime_config_from_init(init), + Some(HelperRequest::Compile(_)) => eyre::bail!("JIT helper received job before init"), + None => eyre::bail!("JIT helper closed stdin before init"), + } +} + +fn read_helper_job(stdin: &mut BufReader) -> eyre::Result> { + let req = match read_message(stdin)? { + Some(HelperRequest::Compile(req)) => req, + Some(HelperRequest::Init(_)) => eyre::bail!("JIT helper received duplicate init"), + None => return Ok(None), + }; let spec_id = SpecId::try_from_u8(req.spec_id).ok_or_else(|| eyre::eyre!("invalid spec id"))?; let opt_level = opt_level_from_u8(req.opt_level)?; - let config = RuntimeConfig { - debug_assertions: req.debug_assertions, - no_dedup: req.no_dedup, - no_dse: req.no_dse, - ..Default::default() - }; let job = CompileJob { kind: CompilationKind::Jit, key: RuntimeCacheKey { code_hash: B256::from(req.code_hash), spec_id }, @@ -358,7 +373,7 @@ fn read_helper_job( sync_notifier: SyncNotifier::none(), generation: 0, }; - Ok(Some((job, config))) + Ok(Some(job)) } fn write_helper_result( @@ -432,3 +447,61 @@ fn opt_level_from_u8(level: u8) -> eyre::Result { _ => eyre::bail!("invalid optimization level"), }) } + +fn gas_params_to_pairs(gas_params: &GasParams) -> GasParamPairs { + (0..GAS_PARAM_COUNT) + .map(|i| { + let id = i as u8; + (id, gas_params.get(GasId::new(id))) + }) + .collect() +} + +fn gas_params_from_pairs(pairs: GasParamPairs) -> eyre::Result { + if pairs.len() != GAS_PARAM_COUNT { + eyre::bail!("invalid gas params length: {}", pairs.len()); + } + + let mut table = [0; GAS_PARAM_COUNT]; + let mut seen = [false; GAS_PARAM_COUNT]; + for (id, value) in pairs { + let index = usize::from(id); + if seen[index] { + eyre::bail!("duplicate gas param id: {id}"); + } + seen[index] = true; + table[index] = value; + } + if let Some((id, _)) = seen.iter().enumerate().find(|(_, seen)| !**seen) { + eyre::bail!("missing gas param id: {id}"); + } + + Ok(GasParams::new(Arc::new(table))) +} + +fn helper_init(config: &RuntimeConfig) -> HelperInit { + HelperInit { + debug_assertions: config.debug_assertions, + no_dedup: config.no_dedup, + no_dse: config.no_dse, + dump_dir: config.dump_dir.as_ref().map(|path| path.to_string_lossy().into_owned()), + gas_params: config.gas_params.as_ref().map(gas_params_to_pairs), + } +} + +fn runtime_config_from_init(init: HelperInit) -> eyre::Result { + Ok(RuntimeConfig { + dump_dir: init.dump_dir.map(PathBuf::from), + debug_assertions: init.debug_assertions, + no_dedup: init.no_dedup, + no_dse: init.no_dse, + gas_params: init.gas_params.map(gas_params_from_pairs).transpose()?, + ..Default::default() + }) +} + +fn job_dump_dir(config: &RuntimeConfig, job: &CompileJob) -> Option { + config.dump_dir.as_ref().map(|base| { + base.join(format!("{:?}", job.key.spec_id)).join(format!("{}", job.key.code_hash)) + }) +} diff --git a/docs/out-of-process-jit.md b/docs/out-of-process-jit.md index c58fb6968..f29c0a9cf 100644 --- a/docs/out-of-process-jit.md +++ b/docs/out-of-process-jit.md @@ -43,7 +43,7 @@ A fork-only helper may be viable only as an early fork server: fork during singl Still needed: - Move the worker pool into a single helper process; the parent should only enqueue IPC requests. -- Carry the remaining data in the IPC payloads: gas params, dump settings, generation, timings, and richer errors. +- Carry the remaining data in the IPC payloads: generation, timings, and richer errors. - Keep AOT jobs either in the helper too or explicitly route them through the existing in-process AOT path; the first option gives consistent isolation. - Define graceful shutdown semantics for queued helper jobs; the current implementation kills the helper for cancellation/shutdown. - Treat helper crash as worker-pool failure: fail pending synchronous jobs, drop pending async jobs, and optionally respawn. From 37dde3cd34b7fe487bfb77c958560ea1b86989f3 Mon Sep 17 00:00:00 2001 From: DaniPopes <57450786+DaniPopes@users.noreply.github.com> Date: Mon, 18 May 2026 03:06:11 +0200 Subject: [PATCH 27/43] fix(runtime): reuse jit object linker --- crates/revmc-llvm/src/lib.rs | 2 +- crates/revmc-llvm/src/orc.rs | 2 +- crates/revmc-runtime/src/runtime/backend.rs | 74 ++++++++++++------- crates/revmc-runtime/src/runtime/config.rs | 26 ++++--- .../src/runtime/out_of_process.rs | 48 ++++++++++-- docs/out-of-process-jit.md | 3 +- 6 files changed, 111 insertions(+), 44 deletions(-) diff --git a/crates/revmc-llvm/src/lib.rs b/crates/revmc-llvm/src/lib.rs index 451510f94..6e6f8f2d3 100644 --- a/crates/revmc-llvm/src/lib.rs +++ b/crates/revmc-llvm/src/lib.rs @@ -757,7 +757,7 @@ impl EvmLlvmBackend { let orc = self.ensure_orc()?; orc.global.define_builtins(symbols); let tracker = orc.jd().create_resource_tracker(); - orc.global.jit.add_object_with_rt(object, &tracker).map_err(error_msg)?; + orc.global.jit.add_object_with_rt(symbol_name, object, &tracker).map_err(error_msg)?; let addr = orc.global.jit.lookup_in(orc.jd(), symbol_name).map_err(error_msg)?; orc.loaded_trackers.push(tracker); Ok((addr, orc.loaded_trackers.pop().unwrap())) diff --git a/crates/revmc-llvm/src/orc.rs b/crates/revmc-llvm/src/orc.rs index b5dfca8f9..125b123ce 100644 --- a/crates/revmc-llvm/src/orc.rs +++ b/crates/revmc-llvm/src/orc.rs @@ -1404,10 +1404,10 @@ impl LLJIT { /// Add a relocatable object file to the given ResourceTracker's JITDylib. pub fn add_object_with_rt( &self, + name: &CStr, object: &[u8], rt: &ResourceTracker, ) -> Result<(), LLVMString> { - let name = c"revmc-object"; let buf = unsafe { LLVMCreateMemoryBufferWithMemoryRangeCopy( object.as_ptr().cast(), diff --git a/crates/revmc-runtime/src/runtime/backend.rs b/crates/revmc-runtime/src/runtime/backend.rs index 23e832945..4a58ec542 100644 --- a/crates/revmc-runtime/src/runtime/backend.rs +++ b/crates/revmc-runtime/src/runtime/backend.rs @@ -63,33 +63,54 @@ fn jit_total_bytes() -> usize { } #[cfg(feature = "llvm")] -fn link_jit_object( - success: &JitObjectSuccess, -) -> eyre::Result<(EvmCompilerFn, Arc)> { - let mut backend = crate::EvmLlvmBackend::new(false)?; - let symbol_name = CString::new(success.symbol_name.clone())?; - let builtin_symbols = success - .builtin_symbols - .iter() - .map(|name| { - let addr = revmc_builtins::Builtin::parse(name) - .ok_or_else(|| eyre::eyre!("unknown builtin symbol: {name}"))? - .addr(); - Ok((CString::new(name.as_str())?, addr)) - }) - .collect::>>()?; - let (addr, tracker) = - backend.link_jit_object(&symbol_name, &success.object_bytes, &builtin_symbols)?; - let jd_guard = backend.jit_dylib_guard(); - let func = EvmCompilerFn::new(unsafe { std::mem::transmute::(addr) }); - Ok((func, Arc::new(JitCodeBacking::new(tracker, jd_guard)))) +#[derive(Default)] +struct JitObjectLinker { + backend: Option, } +#[cfg(feature = "llvm")] +impl JitObjectLinker { + fn link( + &mut self, + success: &JitObjectSuccess, + ) -> eyre::Result<(EvmCompilerFn, Arc)> { + let backend = match &mut self.backend { + Some(backend) => backend, + None => self.backend.insert(crate::EvmLlvmBackend::new(false)?), + }; + + let symbol_name = CString::new(success.symbol_name.clone())?; + let builtin_symbols = success + .builtin_symbols + .iter() + .map(|name| { + let addr = revmc_builtins::Builtin::parse(name) + .ok_or_else(|| eyre::eyre!("unknown builtin symbol: {name}"))? + .addr(); + Ok((CString::new(name.as_str())?, addr)) + }) + .collect::>>()?; + let (addr, tracker) = + backend.link_jit_object(&symbol_name, &success.object_bytes, &builtin_symbols)?; + let jd_guard = backend.jit_dylib_guard(); + let func = + EvmCompilerFn::new(unsafe { std::mem::transmute::(addr) }); + Ok((func, Arc::new(JitCodeBacking::new(tracker, jd_guard)))) + } +} + +#[cfg(not(feature = "llvm"))] +#[derive(Default)] +struct JitObjectLinker; + #[cfg(not(feature = "llvm"))] -fn link_jit_object( - _success: &JitObjectSuccess, -) -> eyre::Result<(EvmCompilerFn, Arc)> { - eyre::bail!("LLVM backend not available") +impl JitObjectLinker { + fn link( + &mut self, + _success: &JitObjectSuccess, + ) -> eyre::Result<(EvmCompilerFn, Arc)> { + eyre::bail!("LLVM backend not available") + } } /// Commands sent to the backend thread on the bounded command channel. @@ -171,6 +192,8 @@ struct BackendState { entries: HashMap, /// Worker pool for JIT compilation. workers: WorkerPool, + /// Backend-thread-owned linker for out-of-process JIT objects. + jit_object_linker: JitObjectLinker, /// Receiver for worker results. result_rx: chan::Receiver, /// Artifact store for persisted artifacts. @@ -541,7 +564,7 @@ impl BackendState { success: JitObjectSuccess, compile_duration: std::time::Duration, ) { - match link_jit_object(&success) { + match self.jit_object_linker.link(&success) { Ok((func, backing)) => { let program = Arc::new(CompiledProgram::new_jit(key, func, backing)); self.insert_resident(key, program); @@ -792,6 +815,7 @@ pub(crate) fn run( resident_meta: preload_meta, entries: HashMap::default(), workers, + jit_object_linker: JitObjectLinker::default(), result_rx, store: config.store, tuning: config.tuning, diff --git a/crates/revmc-runtime/src/runtime/config.rs b/crates/revmc-runtime/src/runtime/config.rs index d38c624fd..e807c8f6c 100644 --- a/crates/revmc-runtime/src/runtime/config.rs +++ b/crates/revmc-runtime/src/runtime/config.rs @@ -9,7 +9,7 @@ use std::{path::PathBuf, str::FromStr, sync::Arc, time::Duration}; const JIT_MODE_ENV: &str = "REVMC_JIT_MODE"; const JIT_HELPER_PATH_ENV: &str = "REVMC_JIT_HELPER_PATH"; const JIT_HELPER_MEMORY_LIMIT_ENV: &str = "REVMC_JIT_HELPER_MEMORY_LIMIT_BYTES"; -const JIT_HELPER_CPU_SECONDS_ENV: &str = "REVMC_JIT_HELPER_CPU_SECONDS"; +const JIT_HELPER_CPU_COUNT_ENV: &str = "REVMC_JIT_HELPER_CPU_COUNT"; /// Runtime configuration. #[derive(Clone, derive_more::Debug)] @@ -171,7 +171,7 @@ impl RuntimeConfig { /// Applies runtime environment overrides. /// /// Recognized variables are `REVMC_JIT_MODE`, `REVMC_JIT_HELPER_PATH`, - /// `REVMC_JIT_HELPER_MEMORY_LIMIT_BYTES`, and `REVMC_JIT_HELPER_CPU_SECONDS`. + /// `REVMC_JIT_HELPER_MEMORY_LIMIT_BYTES`, and `REVMC_JIT_HELPER_CPU_COUNT`. pub fn with_env_overrides(mut self) -> eyre::Result { if let Some(mode) = env_var(JIT_MODE_ENV) { self.jit_mode = mode.parse().map_err(|e: String| eyre::eyre!("{JIT_MODE_ENV}: {e}"))?; @@ -182,8 +182,8 @@ impl RuntimeConfig { if let Some(limit) = parse_env_u64(JIT_HELPER_MEMORY_LIMIT_ENV)? { self.tuning.jit_helper_memory_limit_bytes = limit; } - if let Some(secs) = parse_env_u64(JIT_HELPER_CPU_SECONDS_ENV)? { - self.tuning.jit_helper_cpu_time = (secs > 0).then(|| Duration::from_secs(secs)); + if let Some(count) = parse_env_usize(JIT_HELPER_CPU_COUNT_ENV)? { + self.tuning.jit_helper_cpu_count = count; } Ok(self) } @@ -269,13 +269,14 @@ pub struct RuntimeTuning { /// Defaults to `0`. pub jit_helper_memory_limit_bytes: u64, - /// Maximum CPU time for the out-of-process JIT helper. + /// Maximum CPU count for the out-of-process JIT helper. /// - /// `None` disables the limit. On Unix this is applied with `RLIMIT_CPU` - /// before the helper process starts executing. + /// `0` disables the limit. On Linux this limits the helper's CPU affinity + /// to the first N CPUs from the helper's current affinity mask before the + /// helper process starts executing. /// - /// Defaults to `None`. - pub jit_helper_cpu_time: Option, + /// Defaults to `0`. + pub jit_helper_cpu_count: usize, /// Capacity of the per-worker job queue. /// @@ -347,7 +348,7 @@ impl Default for RuntimeTuning { jit_worker_count: worker_count, jit_timeout: Duration::from_secs(5), jit_helper_memory_limit_bytes: 0, - jit_helper_cpu_time: None, + jit_helper_cpu_count: 0, jit_worker_queue_capacity: 64, jit_opt_level: crate::OptimizationLevel::default(), aot_opt_level: crate::OptimizationLevel::default(), @@ -364,6 +365,11 @@ fn parse_env_u64(name: &str) -> eyre::Result> { value.parse().map(Some).map_err(|e| eyre::eyre!("{name}: {e}")) } +fn parse_env_usize(name: &str) -> eyre::Result> { + let Some(value) = env_var(name) else { return Ok(None) }; + value.parse().map(Some).map_err(|e| eyre::eyre!("{name}: {e}")) +} + fn env_var(name: &str) -> Option { std::env::var(name).ok() } diff --git a/crates/revmc-runtime/src/runtime/out_of_process.rs b/crates/revmc-runtime/src/runtime/out_of_process.rs index c2764fda4..509b0bf33 100644 --- a/crates/revmc-runtime/src/runtime/out_of_process.rs +++ b/crates/revmc-runtime/src/runtime/out_of_process.rs @@ -229,21 +229,20 @@ fn apply_helper_limits(command: &mut Command, config: &RuntimeConfig) { use std::os::unix::process::CommandExt; let memory_limit = config.tuning.jit_helper_memory_limit_bytes; - let cpu_time = config.tuning.jit_helper_cpu_time; - if memory_limit == 0 && cpu_time.is_none() { + let cpu_count = config.tuning.jit_helper_cpu_count; + if memory_limit == 0 && cpu_count == 0 { return; } // SAFETY: `pre_exec` runs in the child after fork and before exec. The closure only calls - // async-signal-safe libc `setrlimit` and constructs an `io::Error` if it fails. + // libc resource/affinity syscalls and constructs an `io::Error` if they fail. unsafe { command.pre_exec(move || { if memory_limit > 0 { set_rlimit(libc::RLIMIT_AS as _, memory_limit)?; } - if let Some(cpu_time) = cpu_time { - let seconds = cpu_time.as_secs().max(1); - set_rlimit(libc::RLIMIT_CPU as _, seconds)?; + if cpu_count > 0 { + limit_cpu_affinity(cpu_count)?; } Ok(()) }); @@ -260,6 +259,43 @@ fn set_rlimit(resource: libc::c_int, value: u64) -> std::io::Result<()> { Ok(()) } +#[cfg(any(target_os = "linux", target_os = "android"))] +fn limit_cpu_affinity(cpu_count: usize) -> std::io::Result<()> { + let mut current = unsafe { std::mem::zeroed::() }; + let size = std::mem::size_of::(); + if unsafe { libc::sched_getaffinity(0, size, &mut current) } != 0 { + return Err(std::io::Error::last_os_error()); + } + + let mut limited = unsafe { std::mem::zeroed::() }; + let mut remaining = cpu_count; + for cpu in 0..(8 * size) { + if unsafe { libc::CPU_ISSET(cpu, ¤t) } { + unsafe { libc::CPU_SET(cpu, &mut limited) }; + remaining -= 1; + if remaining == 0 { + break; + } + } + } + if remaining == cpu_count { + return Err(std::io::Error::new( + std::io::ErrorKind::InvalidInput, + "current CPU affinity mask is empty", + )); + } + + if unsafe { libc::sched_setaffinity(0, size, &limited) } != 0 { + return Err(std::io::Error::last_os_error()); + } + Ok(()) +} + +#[cfg(not(any(target_os = "linux", target_os = "android")))] +fn limit_cpu_affinity(_cpu_count: usize) -> std::io::Result<()> { + Ok(()) +} + #[cfg(not(unix))] fn apply_helper_limits(_command: &mut Command, _config: &RuntimeConfig) {} diff --git a/docs/out-of-process-jit.md b/docs/out-of-process-jit.md index f29c0a9cf..d9a88e912 100644 --- a/docs/out-of-process-jit.md +++ b/docs/out-of-process-jit.md @@ -25,7 +25,8 @@ Current prototype: - `RuntimeConfig::jit_mode = JitMode::OutOfProcess` makes the runtime keep a global persistent helper process spawned via `RuntimeConfig::jit_helper_path`, or `std::env::current_exe()` when unset. - `RuntimeConfig::default()` uses plain in-process defaults. `JitBackend::new` applies `RuntimeConfig::with_env_overrides()`, which recognizes `REVMC_JIT_MODE=out-of-process` and `REVMC_JIT_MODE=in-process`; other spellings are rejected. `REVMC_JIT_HELPER_PATH` overrides the helper executable path. Test harnesses should point this at a binary that calls `revmc::runtime::maybe_run_jit_helper()` at startup, such as `target/debug/revmc`. -- `REVMC_JIT_HELPER_MEMORY_LIMIT_BYTES` and `REVMC_JIT_HELPER_CPU_SECONDS` apply Unix `RLIMIT_AS` and `RLIMIT_CPU` limits to helper processes before `exec`. +- `REVMC_JIT_HELPER_MEMORY_LIMIT_BYTES` applies a Unix `RLIMIT_AS` limit to helper processes before `exec`. +- `REVMC_JIT_HELPER_CPU_COUNT` limits helper CPU affinity on Linux before `exec`. - Helper binaries must call `revmc::runtime::maybe_run_jit_helper()` at process startup. `revmc-cli` does this already. - Workers send length-prefixed wincode-serialized JIT object requests to the helper over stdin and receive length-prefixed wincode-serialized responses from stdout. - The parent links returned object bytes into its local ORC instance, resolves the symbol, and constructs `JitCodeBacking` with a parent-owned `ResourceTracker`. From 3fb4d2c350981e8f317de7cccc40f330151d2064 Mon Sep 17 00:00:00 2001 From: DaniPopes <57450786+DaniPopes@users.noreply.github.com> Date: Mon, 18 May 2026 03:07:16 +0200 Subject: [PATCH 28/43] fix(runtime): log jit helper restarts --- crates/revmc-runtime/src/runtime/out_of_process.rs | 14 +++++++++++--- 1 file changed, 11 insertions(+), 3 deletions(-) diff --git a/crates/revmc-runtime/src/runtime/out_of_process.rs b/crates/revmc-runtime/src/runtime/out_of_process.rs index 509b0bf33..838bce7f6 100644 --- a/crates/revmc-runtime/src/runtime/out_of_process.rs +++ b/crates/revmc-runtime/src/runtime/out_of_process.rs @@ -86,6 +86,7 @@ impl HelperProcess { let helper = { let mut slot = self.inner.lock().unwrap(); if slot.as_ref().is_none_or(|helper| !helper.matches_config(config)) { + debug!("spawning JIT helper"); *slot = Some(Arc::new(HelperProcessInner::spawn(config)?)); } slot.as_ref().unwrap().clone() @@ -96,6 +97,7 @@ impl HelperProcess { Err(err) => { let mut slot = self.inner.lock().unwrap(); if slot.as_ref().is_some_and(|current| Arc::ptr_eq(current, &helper)) { + warn!(error = %err, "discarding JIT helper after failed job"); *slot = None; } Err(err) @@ -180,15 +182,21 @@ impl HelperProcessInner { match io.result_rx.recv_timeout(config.tuning.jit_timeout) { Ok(result) => result, Err(chan::RecvTimeoutError::Timeout) => { + warn!(timeout = ?config.tuning.jit_timeout, "JIT helper timed out"); self.kill(); - Err(format!("JIT helper timed out after {:?}", config.tuning.jit_timeout)) + Err(format!( + "JIT helper timed out after {:?}; helper will be restarted", + config.tuning.jit_timeout + )) } Err(chan::RecvTimeoutError::Disconnected) => { let status = self.child.lock().unwrap().try_wait().ok().flatten(); - Err(match status { + let message = match status { Some(status) => format!("JIT helper exited with {status}"), None => "JIT helper disconnected".into(), - }) + }; + warn!(message, "JIT helper disconnected"); + Err(format!("{message}; helper will be restarted")) } } } From a1eb76b8e7e1cb27d69f3f2a66ac576794ba61a9 Mon Sep 17 00:00:00 2001 From: DaniPopes <57450786+DaniPopes@users.noreply.github.com> Date: Mon, 18 May 2026 03:13:14 +0200 Subject: [PATCH 29/43] static --- crates/revmc-builtins/src/ir.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/crates/revmc-builtins/src/ir.rs b/crates/revmc-builtins/src/ir.rs index a88bef999..da35636c7 100644 --- a/crates/revmc-builtins/src/ir.rs +++ b/crates/revmc-builtins/src/ir.rs @@ -126,7 +126,7 @@ macro_rules! builtins { #[allow(unused_variables)] impl Builtin { pub const COUNT: usize = builtins!(@count $($ident),*); - pub const ALL: [Self; Self::COUNT] = [$(Self::$ident),*]; + pub const ALL: &[Self; Self::COUNT] = &[$(Self::$ident),*]; pub const fn name(self) -> &'static str { match self { From 4878a1793d301fc36521b771e9124dd8d1f349c2 Mon Sep 17 00:00:00 2001 From: DaniPopes <57450786+DaniPopes@users.noreply.github.com> Date: Mon, 18 May 2026 03:19:24 +0200 Subject: [PATCH 30/43] fix(runtime): kill stalled jit helper groups --- .../src/runtime/out_of_process.rs | 41 +++++++++++++++---- 1 file changed, 34 insertions(+), 7 deletions(-) diff --git a/crates/revmc-runtime/src/runtime/out_of_process.rs b/crates/revmc-runtime/src/runtime/out_of_process.rs index 838bce7f6..1e598e20d 100644 --- a/crates/revmc-runtime/src/runtime/out_of_process.rs +++ b/crates/revmc-runtime/src/runtime/out_of_process.rs @@ -206,9 +206,7 @@ impl HelperProcessInner { if matches!(child.try_wait(), Ok(Some(_))) { return true; } - if let Err(err) = child.kill() { - warn!(%err, "failed to kill JIT helper"); - } + kill_helper(&mut child); match child.wait_timeout(self.shutdown_timeout) { Ok(Some(_)) => true, Ok(None) => { @@ -238,14 +236,12 @@ fn apply_helper_limits(command: &mut Command, config: &RuntimeConfig) { let memory_limit = config.tuning.jit_helper_memory_limit_bytes; let cpu_count = config.tuning.jit_helper_cpu_count; - if memory_limit == 0 && cpu_count == 0 { - return; - } // SAFETY: `pre_exec` runs in the child after fork and before exec. The closure only calls - // libc resource/affinity syscalls and constructs an `io::Error` if they fail. + // libc process/resource/affinity syscalls and constructs an `io::Error` if they fail. unsafe { command.pre_exec(move || { + set_process_group()?; if memory_limit > 0 { set_rlimit(libc::RLIMIT_AS as _, memory_limit)?; } @@ -257,6 +253,30 @@ fn apply_helper_limits(command: &mut Command, config: &RuntimeConfig) { } } +#[cfg(unix)] +fn set_process_group() -> std::io::Result<()> { + if unsafe { libc::setpgid(0, 0) } != 0 { + return Err(std::io::Error::last_os_error()); + } + Ok(()) +} + +#[cfg(unix)] +fn kill_helper(child: &mut Child) { + let pid = child.id() as libc::pid_t; + if unsafe { libc::kill(-pid, libc::SIGKILL) } == 0 { + return; + } + + let err = std::io::Error::last_os_error(); + if err.raw_os_error() != Some(libc::ESRCH) { + warn!(%err, "failed to kill JIT helper process group"); + } + if let Err(err) = child.kill() { + warn!(%err, "failed to kill JIT helper"); + } +} + #[cfg(unix)] fn set_rlimit(resource: libc::c_int, value: u64) -> std::io::Result<()> { let value = libc::rlim_t::try_from(value).unwrap_or(libc::rlim_t::MAX); @@ -307,6 +327,13 @@ fn limit_cpu_affinity(_cpu_count: usize) -> std::io::Result<()> { #[cfg(not(unix))] fn apply_helper_limits(_command: &mut Command, _config: &RuntimeConfig) {} +#[cfg(not(unix))] +fn kill_helper(child: &mut Child) { + if let Err(err) = child.kill() { + warn!(%err, "failed to kill JIT helper"); + } +} + #[derive(Clone, PartialEq, Eq, SchemaWrite, SchemaRead)] struct HelperInit { debug_assertions: bool, From eb3d846071ac7681ba347a0e4016f0128c93c64e Mon Sep 17 00:00:00 2001 From: DaniPopes <57450786+DaniPopes@users.noreply.github.com> Date: Mon, 18 May 2026 03:32:35 +0200 Subject: [PATCH 31/43] refactor(runtime): gate jit helper on unix --- crates/revmc-runtime/src/runtime/mod.rs | 12 ++- .../src/runtime/out_of_process.rs | 17 +--- crates/revmc-runtime/src/runtime/worker.rs | 79 ++++++++++++++----- 3 files changed, 70 insertions(+), 38 deletions(-) diff --git a/crates/revmc-runtime/src/runtime/mod.rs b/crates/revmc-runtime/src/runtime/mod.rs index 6a90ec022..d4c4aecb8 100644 --- a/crates/revmc-runtime/src/runtime/mod.rs +++ b/crates/revmc-runtime/src/runtime/mod.rs @@ -44,7 +44,7 @@ pub use storage::{ RuntimeCacheKey, StoredArtifact, }; -#[cfg(feature = "llvm")] +#[cfg(all(feature = "llvm", unix))] mod out_of_process; mod worker; @@ -55,14 +55,14 @@ mod worker; /// the caller should exit immediately. Normal application startup should /// continue on [`ControlFlow::Continue`]. pub fn maybe_run_jit_helper() -> eyre::Result> { - #[cfg(feature = "llvm")] + #[cfg(all(feature = "llvm", unix))] { out_of_process::maybe_run_jit_helper() } - #[cfg(not(feature = "llvm"))] + #[cfg(not(all(feature = "llvm", unix)))] { if std::env::var_os("REVMC_JIT_HELPER").is_some() { - eyre::bail!("LLVM backend not available") + eyre::bail!("out-of-process JIT helper is only available on Unix with LLVM") } Ok(ControlFlow::Continue(())) } @@ -165,6 +165,10 @@ impl JitBackend { config.enabled = true; config.tuning.jit_hot_threshold = 0; } + #[cfg(not(unix))] + if config.jit_mode == JitMode::OutOfProcess { + eyre::bail!("out-of-process JIT is only available on Unix"); + } let enabled = config.enabled; let (tx, rx) = chan::bounded::(config.tuning.channel_capacity); diff --git a/crates/revmc-runtime/src/runtime/out_of_process.rs b/crates/revmc-runtime/src/runtime/out_of_process.rs index 1e598e20d..f30554996 100644 --- a/crates/revmc-runtime/src/runtime/out_of_process.rs +++ b/crates/revmc-runtime/src/runtime/out_of_process.rs @@ -18,6 +18,7 @@ use revm_primitives::hardfork::SpecId; use std::{ io::{BufReader, BufWriter, Read, Write}, ops::ControlFlow, + os::unix::process::CommandExt, path::PathBuf, process::{Child, ChildStdin, Command, Stdio}, sync::{Arc, Mutex}, @@ -230,10 +231,7 @@ impl Drop for HelperProcessInner { } } -#[cfg(unix)] fn apply_helper_limits(command: &mut Command, config: &RuntimeConfig) { - use std::os::unix::process::CommandExt; - let memory_limit = config.tuning.jit_helper_memory_limit_bytes; let cpu_count = config.tuning.jit_helper_cpu_count; @@ -253,7 +251,6 @@ fn apply_helper_limits(command: &mut Command, config: &RuntimeConfig) { } } -#[cfg(unix)] fn set_process_group() -> std::io::Result<()> { if unsafe { libc::setpgid(0, 0) } != 0 { return Err(std::io::Error::last_os_error()); @@ -261,7 +258,6 @@ fn set_process_group() -> std::io::Result<()> { Ok(()) } -#[cfg(unix)] fn kill_helper(child: &mut Child) { let pid = child.id() as libc::pid_t; if unsafe { libc::kill(-pid, libc::SIGKILL) } == 0 { @@ -277,7 +273,6 @@ fn kill_helper(child: &mut Child) { } } -#[cfg(unix)] fn set_rlimit(resource: libc::c_int, value: u64) -> std::io::Result<()> { let value = libc::rlim_t::try_from(value).unwrap_or(libc::rlim_t::MAX); let limit = libc::rlimit { rlim_cur: value, rlim_max: value }; @@ -324,16 +319,6 @@ fn limit_cpu_affinity(_cpu_count: usize) -> std::io::Result<()> { Ok(()) } -#[cfg(not(unix))] -fn apply_helper_limits(_command: &mut Command, _config: &RuntimeConfig) {} - -#[cfg(not(unix))] -fn kill_helper(child: &mut Child) { - if let Err(err) = child.kill() { - warn!(%err, "failed to kill JIT helper"); - } -} - #[derive(Clone, PartialEq, Eq, SchemaWrite, SchemaRead)] struct HelperInit { debug_assertions: bool, diff --git a/crates/revmc-runtime/src/runtime/worker.rs b/crates/revmc-runtime/src/runtime/worker.rs index b6d8e51d8..c8cc68ae3 100644 --- a/crates/revmc-runtime/src/runtime/worker.rs +++ b/crates/revmc-runtime/src/runtime/worker.rs @@ -36,6 +36,11 @@ use crate::{ runtime::config::JitMode, }; +#[cfg(all(feature = "llvm", unix))] +type OutOfProcessHelper = Option>; +#[cfg(not(all(feature = "llvm", unix)))] +type OutOfProcessHelper = (); + /// Notifier for synchronous compilation requests. /// /// Wraps an optional sender that is notified when the compilation @@ -180,8 +185,7 @@ pub(crate) struct WorkerPool { /// Rayon pool used to execute compilation jobs. pool: Option, /// Out-of-process helper used by this worker pool. - #[cfg(feature = "llvm")] - out_of_process_helper: Option>, + out_of_process_helper: OutOfProcessHelper, /// Sender for worker results. result_tx: chan::Sender, /// Runtime configuration shared by spawned jobs. @@ -199,9 +203,7 @@ impl WorkerPool { pub(crate) fn new(result_tx: chan::Sender, config: RuntimeConfig) -> Self { let worker_count = config.tuning.jit_worker_count; let queue_capacity = worker_count.saturating_mul(config.tuning.jit_worker_queue_capacity); - #[cfg(feature = "llvm")] - let out_of_process_helper = (config.jit_mode == JitMode::OutOfProcess) - .then(|| Arc::new(super::out_of_process::HelperProcess::new())); + let out_of_process_helper = create_out_of_process_helper(&config); let pool = (worker_count > 0).then(|| { ThreadPoolBuilder::new() .num_threads(worker_count) @@ -213,7 +215,6 @@ impl WorkerPool { Self { pool, - #[cfg(feature = "llvm")] out_of_process_helper, result_tx, config: Arc::new(config), @@ -250,14 +251,10 @@ impl WorkerPool { let queued = self.queued.clone(); let result_tx = self.result_tx.clone(); let config = self.config.clone(); - #[cfg(feature = "llvm")] let out_of_process_helper = self.out_of_process_helper.clone(); pool.spawn_fifo(move || { queued.fetch_sub(1, Ordering::AcqRel); - #[cfg(feature = "llvm")] let result = compile_job(job, &config, out_of_process_helper); - #[cfg(not(feature = "llvm"))] - let result = compile_job(job, &config); let _ = result_tx.send(result); }); Ok(()) @@ -275,10 +272,7 @@ impl WorkerPool { /// Cancels any in-flight compilation that can be interrupted externally. pub(crate) fn cancel_in_flight(&self) { - #[cfg(feature = "llvm")] - if let Some(helper) = &self.out_of_process_helper { - helper.cancel_in_flight(); - } + cancel_out_of_process_helper(&self.out_of_process_helper); } } @@ -288,6 +282,25 @@ impl Drop for WorkerPool { } } +#[cfg(all(feature = "llvm", unix))] +fn create_out_of_process_helper(config: &RuntimeConfig) -> OutOfProcessHelper { + (config.jit_mode == JitMode::OutOfProcess) + .then(|| Arc::new(super::out_of_process::HelperProcess::new())) +} + +#[cfg(not(all(feature = "llvm", unix)))] +fn create_out_of_process_helper(_config: &RuntimeConfig) -> OutOfProcessHelper {} + +#[cfg(all(feature = "llvm", unix))] +fn cancel_out_of_process_helper(helper: &OutOfProcessHelper) { + if let Some(helper) = helper { + helper.cancel_in_flight(); + } +} + +#[cfg(not(all(feature = "llvm", unix)))] +fn cancel_out_of_process_helper(_helper: &OutOfProcessHelper) {} + #[cfg(feature = "llvm")] fn clear_thread_local_compilers() { JIT_COMPILER.with_borrow_mut(Option::take); @@ -301,13 +314,12 @@ fn clear_thread_local_compilers() {} fn compile_job( job: CompileJob, config: &RuntimeConfig, - out_of_process_helper: Option>, + out_of_process_helper: OutOfProcessHelper, ) -> WorkerResult { trace!(?job, "received job"); match job.kind { CompilationKind::Jit if config.jit_mode == JitMode::OutOfProcess => { - let helper = out_of_process_helper.expect("missing out-of-process JIT helper"); - super::out_of_process::compile_job(job, config, &helper) + compile_out_of_process_job(job, config, out_of_process_helper) } CompilationKind::Jit => { JIT_COMPILER.with_borrow_mut(|state| compile_with_state(job, config, state)) @@ -318,6 +330,33 @@ fn compile_job( } } +#[cfg(all(feature = "llvm", unix))] +fn compile_out_of_process_job( + job: CompileJob, + config: &RuntimeConfig, + helper: OutOfProcessHelper, +) -> WorkerResult { + let helper = helper.expect("missing out-of-process JIT helper"); + super::out_of_process::compile_job(job, config, &helper) +} + +#[cfg(all(feature = "llvm", not(unix)))] +fn compile_out_of_process_job( + job: CompileJob, + _config: &RuntimeConfig, + _helper: OutOfProcessHelper, +) -> WorkerResult { + WorkerResult { + key: job.key, + outcome: Err("out-of-process JIT is only available on Unix".into()), + kind: job.kind, + sync_notifier: job.sync_notifier, + generation: job.generation, + compile_duration: Duration::ZERO, + timings: CompileTimings::default(), + } +} + #[cfg(feature = "llvm")] fn compile_with_state( job: CompileJob, @@ -552,7 +591,11 @@ fn compile_aot_artifact( } #[cfg(not(feature = "llvm"))] -fn compile_job(job: CompileJob, _config: &RuntimeConfig) -> WorkerResult { +fn compile_job( + job: CompileJob, + _config: &RuntimeConfig, + _helper: OutOfProcessHelper, +) -> WorkerResult { WorkerResult { key: job.key, outcome: Err("LLVM backend not available".into()), From e91d7ad471aa3ec69d7cb4dd0bf147b88286c132 Mon Sep 17 00:00:00 2001 From: DaniPopes <57450786+DaniPopes@users.noreply.github.com> Date: Mon, 18 May 2026 03:38:03 +0200 Subject: [PATCH 32/43] refactor(runtime): use wincode stream framing --- .../src/runtime/out_of_process.rs | 26 ++++--------------- 1 file changed, 5 insertions(+), 21 deletions(-) diff --git a/crates/revmc-runtime/src/runtime/out_of_process.rs b/crates/revmc-runtime/src/runtime/out_of_process.rs index f30554996..39c7edb1d 100644 --- a/crates/revmc-runtime/src/runtime/out_of_process.rs +++ b/crates/revmc-runtime/src/runtime/out_of_process.rs @@ -16,7 +16,7 @@ use crossbeam_channel as chan; use revm_context_interface::cfg::{GasParams, gas_params::GasId}; use revm_primitives::hardfork::SpecId; use std::{ - io::{BufReader, BufWriter, Read, Write}, + io::{BufRead, BufReader, BufWriter, Read, Write}, ops::ControlFlow, os::unix::process::CommandExt, path::PathBuf, @@ -453,11 +453,6 @@ fn write_message(w: &mut BufWriter, message: &T) -> std where T: wincode::SchemaWrite, { - let len = wincode::serialized_size(message) - .map_err(|e| std::io::Error::new(std::io::ErrorKind::InvalidData, e))?; - let len = u32::try_from(len) - .map_err(|_| std::io::Error::new(std::io::ErrorKind::InvalidInput, "message too large"))?; - w.write_all(&len.to_le_bytes())?; wincode::serialize_into(w, message) .map_err(|e| std::io::Error::new(std::io::ErrorKind::InvalidData, e)) } @@ -466,23 +461,12 @@ fn read_message(r: &mut BufReader) -> std::io::Result, { - let mut len = [0; 4]; - let n = r.read(&mut len[..1])?; - if n == 0 { + if r.fill_buf()?.is_empty() { return Ok(None); } - r.read_exact(&mut len[1..])?; - let mut reader = BufReader::new(r.take(u64::from(u32::from_le_bytes(len)))); - let value = wincode::deserialize_from(&mut reader) - .map_err(|e| std::io::Error::new(std::io::ErrorKind::InvalidData, e))?; - let remaining = reader.buffer().len() as u64 + reader.get_ref().limit(); - if remaining != 0 { - return Err(std::io::Error::new( - std::io::ErrorKind::InvalidData, - "trailing bytes in helper message", - )); - } - Ok(Some(value)) + wincode::deserialize_from(r) + .map(Some) + .map_err(|e| std::io::Error::new(std::io::ErrorKind::InvalidData, e)) } fn opt_level_to_u8(level: OptimizationLevel) -> u8 { From 6273c0b5c16379450599d2894719a489b6024a2c Mon Sep 17 00:00:00 2001 From: DaniPopes <57450786+DaniPopes@users.noreply.github.com> Date: Mon, 18 May 2026 03:53:13 +0200 Subject: [PATCH 33/43] refactor(runtime): remove helper stream peek --- .../src/runtime/out_of_process.rs | 49 ++++++++++++------- 1 file changed, 30 insertions(+), 19 deletions(-) diff --git a/crates/revmc-runtime/src/runtime/out_of_process.rs b/crates/revmc-runtime/src/runtime/out_of_process.rs index 39c7edb1d..56be1b52d 100644 --- a/crates/revmc-runtime/src/runtime/out_of_process.rs +++ b/crates/revmc-runtime/src/runtime/out_of_process.rs @@ -16,7 +16,7 @@ use crossbeam_channel as chan; use revm_context_interface::cfg::{GasParams, gas_params::GasId}; use revm_primitives::hardfork::SpecId; use std::{ - io::{BufRead, BufReader, BufWriter, Read, Write}, + io::{BufReader, BufWriter, Read, Write}, ops::ControlFlow, os::unix::process::CommandExt, path::PathBuf, @@ -366,15 +366,14 @@ fn write_job(w: &mut BufWriter, job: &CompileJob) -> std:: fn read_helper_result(r: &mut BufReader) -> Result { match read_message(r).map_err(|e| format!("failed to decode helper result: {e}"))? { - Some(HelperResponse::Ok { symbol_name, object_bytes, builtin_symbols }) => { + HelperResponse::Ok { symbol_name, object_bytes, builtin_symbols } => { Ok(WorkerSuccess::JitObject(JitObjectSuccess { symbol_name, object_bytes: Bytes::from(object_bytes), builtin_symbols, })) } - Some(HelperResponse::Err { error }) => Err(error), - None => Err("JIT helper closed stdout".into()), + HelperResponse::Err { error } => Err(error), } } @@ -384,7 +383,12 @@ fn run_jit_helper_stdio() -> eyre::Result<()> { let mut compiler: Option> = None; let config = read_helper_init(&mut stdin)?; - while let Some(job) = read_helper_job(&mut stdin)? { + loop { + let job = match read_helper_job(&mut stdin) { + Ok(job) => job, + Err(err) if is_unexpected_eof(&err) => break, + Err(err) => return Err(err), + }; if compiler.is_none() { compiler = Some(create_compiler(&config, true).map_err(|e| eyre::eyre!(e))?); } @@ -405,17 +409,15 @@ fn run_jit_helper_stdio() -> eyre::Result<()> { fn read_helper_init(stdin: &mut BufReader) -> eyre::Result { match read_message(stdin)? { - Some(HelperRequest::Init(init)) => runtime_config_from_init(init), - Some(HelperRequest::Compile(_)) => eyre::bail!("JIT helper received job before init"), - None => eyre::bail!("JIT helper closed stdin before init"), + HelperRequest::Init(init) => runtime_config_from_init(init), + HelperRequest::Compile(_) => eyre::bail!("JIT helper received job before init"), } } -fn read_helper_job(stdin: &mut BufReader) -> eyre::Result> { +fn read_helper_job(stdin: &mut BufReader) -> eyre::Result { let req = match read_message(stdin)? { - Some(HelperRequest::Compile(req)) => req, - Some(HelperRequest::Init(_)) => eyre::bail!("JIT helper received duplicate init"), - None => return Ok(None), + HelperRequest::Compile(req) => req, + HelperRequest::Init(_) => eyre::bail!("JIT helper received duplicate init"), }; let spec_id = SpecId::try_from_u8(req.spec_id).ok_or_else(|| eyre::eyre!("invalid spec id"))?; let opt_level = opt_level_from_u8(req.opt_level)?; @@ -429,7 +431,7 @@ fn read_helper_job(stdin: &mut BufReader) -> eyre::Result( @@ -457,16 +459,25 @@ where .map_err(|e| std::io::Error::new(std::io::ErrorKind::InvalidData, e)) } -fn read_message(r: &mut BufReader) -> std::io::Result> +fn read_message(r: &mut BufReader) -> std::io::Result where T: wincode::SchemaReadOwned, { - if r.fill_buf()?.is_empty() { - return Ok(None); + wincode::deserialize_from(r).map_err(|e| std::io::Error::new(read_error_kind(&e), e)) +} + +fn read_error_kind(err: &wincode::ReadError) -> std::io::ErrorKind { + match err { + wincode::ReadError::Io(wincode::io::ReadError::ReadSizeLimit(_)) => { + std::io::ErrorKind::UnexpectedEof + } + _ => std::io::ErrorKind::InvalidData, } - wincode::deserialize_from(r) - .map(Some) - .map_err(|e| std::io::Error::new(std::io::ErrorKind::InvalidData, e)) +} + +fn is_unexpected_eof(err: &eyre::Report) -> bool { + err.downcast_ref::() + .is_some_and(|err| err.kind() == std::io::ErrorKind::UnexpectedEof) } fn opt_level_to_u8(level: OptimizationLevel) -> u8 { From 7838d64577441922ce5a081a7ae47695bd6b4895 Mon Sep 17 00:00:00 2001 From: DaniPopes <57450786+DaniPopes@users.noreply.github.com> Date: Mon, 18 May 2026 22:30:56 +0200 Subject: [PATCH 34/43] refactor(runtime): share jit compile state --- .../src/runtime/out_of_process.rs | 129 ++++++++++++------ crates/revmc-runtime/src/runtime/worker.rs | 48 ++++--- 2 files changed, 117 insertions(+), 60 deletions(-) diff --git a/crates/revmc-runtime/src/runtime/out_of_process.rs b/crates/revmc-runtime/src/runtime/out_of_process.rs index 56be1b52d..7fa8122c9 100644 --- a/crates/revmc-runtime/src/runtime/out_of_process.rs +++ b/crates/revmc-runtime/src/runtime/out_of_process.rs @@ -1,13 +1,13 @@ //! Out-of-process JIT helper process and IPC. use crate::{ - CompileTimings, EvmCompiler, EvmLlvmBackend, OptimizationLevel, eyre, + CompileTimings, OptimizationLevel, eyre, runtime::{ config::{CompilationKind, RuntimeConfig}, storage::RuntimeCacheKey, worker::{ - CompileJob, JitObjectSuccess, SyncNotifier, WorkerResult, WorkerSuccess, - compile_jit_object_artifact, create_compiler, + CompileJob, CompilerState, CompilerTarget, JitObjectSuccess, SyncNotifier, + WorkerResult, WorkerSuccess, compile_with_state, }, }, }; @@ -49,7 +49,10 @@ pub(super) fn compile_job( helper: &HelperProcess, ) -> WorkerResult { let t0 = Instant::now(); - let outcome = run_helper_job(&job, config, helper); + let (outcome, timings) = match run_helper_job(&job, config, helper) { + Ok(result) => (result.outcome, result.timings), + Err(error) => (Err(error), CompileTimings::default()), + }; WorkerResult { key: job.key, outcome, @@ -57,7 +60,7 @@ pub(super) fn compile_job( sync_notifier: job.sync_notifier, generation: job.generation, compile_duration: t0.elapsed(), - timings: CompileTimings::default(), + timings, } } @@ -65,13 +68,18 @@ fn run_helper_job( job: &CompileJob, config: &RuntimeConfig, helper: &HelperProcess, -) -> Result { +) -> Result { helper.compile(job, config) } +struct HelperJobResult { + outcome: Result, + timings: CompileTimings, +} + struct HelperIo { stdin: BufWriter, - result_rx: chan::Receiver>, + result_rx: chan::Receiver>, } pub(super) struct HelperProcess { @@ -83,7 +91,7 @@ impl HelperProcess { Self { inner: Mutex::new(None) } } - fn compile(&self, job: &CompileJob, config: &RuntimeConfig) -> Result { + fn compile(&self, job: &CompileJob, config: &RuntimeConfig) -> Result { let helper = { let mut slot = self.inner.lock().unwrap(); if slot.as_ref().is_none_or(|helper| !helper.matches_config(config)) { @@ -175,7 +183,7 @@ impl HelperProcessInner { } } - fn compile(&self, job: &CompileJob, config: &RuntimeConfig) -> Result { + fn compile(&self, job: &CompileJob, config: &RuntimeConfig) -> Result { let mut io = self.io.lock().unwrap(); write_job(&mut io.stdin, job).map_err(|e| format!("failed to write helper job: {e}"))?; io.stdin.flush().map_err(|e| format!("failed to flush helper job: {e}"))?; @@ -326,6 +334,7 @@ struct HelperInit { no_dse: bool, dump_dir: Option, gas_params: Option, + compiler_recycle_threshold: usize, } #[derive(SchemaWrite, SchemaRead)] @@ -345,8 +354,24 @@ struct HelperCompile { #[derive(SchemaWrite, SchemaRead)] enum HelperResponse { - Ok { symbol_name: String, object_bytes: Vec, builtin_symbols: Vec }, - Err { error: String }, + Ok { + symbol_name: String, + object_bytes: Vec, + builtin_symbols: Vec, + timings: HelperTimings, + }, + Err { + error: String, + timings: HelperTimings, + }, +} + +#[derive(Clone, Copy, Default, SchemaWrite, SchemaRead)] +struct HelperTimings { + parse: u64, + translate: u64, + optimize: u64, + codegen: u64, } fn write_init(w: &mut BufWriter, init: &HelperInit) -> std::io::Result<()> { @@ -364,23 +389,54 @@ fn write_job(w: &mut BufWriter, job: &CompileJob) -> std:: write_message(w, &req) } -fn read_helper_result(r: &mut BufReader) -> Result { +fn read_helper_result(r: &mut BufReader) -> Result { match read_message(r).map_err(|e| format!("failed to decode helper result: {e}"))? { - HelperResponse::Ok { symbol_name, object_bytes, builtin_symbols } => { - Ok(WorkerSuccess::JitObject(JitObjectSuccess { - symbol_name, - object_bytes: Bytes::from(object_bytes), - builtin_symbols, - })) + HelperResponse::Ok { symbol_name, object_bytes, builtin_symbols, timings } => { + Ok(HelperJobResult { + outcome: Ok(WorkerSuccess::JitObject(JitObjectSuccess { + symbol_name, + object_bytes: Bytes::from(object_bytes), + builtin_symbols, + })), + timings: timings.into(), + }) + } + HelperResponse::Err { error, timings } => { + Ok(HelperJobResult { outcome: Err(error), timings: timings.into() }) + } + } +} + +impl From for HelperTimings { + fn from(timings: CompileTimings) -> Self { + Self { + parse: duration_to_nanos(timings.parse), + translate: duration_to_nanos(timings.translate), + optimize: duration_to_nanos(timings.optimize), + codegen: duration_to_nanos(timings.codegen), + } + } +} + +impl From for CompileTimings { + fn from(timings: HelperTimings) -> Self { + Self { + parse: Duration::from_nanos(timings.parse), + translate: Duration::from_nanos(timings.translate), + optimize: Duration::from_nanos(timings.optimize), + codegen: Duration::from_nanos(timings.codegen), } - HelperResponse::Err { error } => Err(error), } } +fn duration_to_nanos(duration: Duration) -> u64 { + u64::try_from(duration.as_nanos()).unwrap_or(u64::MAX) +} + fn run_jit_helper_stdio() -> eyre::Result<()> { let mut stdin = BufReader::new(std::io::stdin().lock()); let mut stdout = BufWriter::new(std::io::stdout().lock()); - let mut compiler: Option> = None; + let mut compiler = None::; let config = read_helper_init(&mut stdin)?; loop { @@ -389,17 +445,7 @@ fn run_jit_helper_stdio() -> eyre::Result<()> { Err(err) if is_unexpected_eof(&err) => break, Err(err) => return Err(err), }; - if compiler.is_none() { - compiler = Some(create_compiler(&config, true).map_err(|e| eyre::eyre!(e))?); - } - let compiler = compiler.as_mut().unwrap(); - compiler.set_opt_level(job.opt_level); - compiler.set_dump_to(job_dump_dir(&config, &job)); - - let result = compile_jit_object_artifact(&job, compiler); - if let Err(err) = compiler.clear_ir() { - warn!(%err, "clear_ir failed"); - } + let result = compile_with_state(job, &config, CompilerTarget::JitObject, &mut compiler); write_helper_result(&mut stdout, result)?; stdout.flush()?; } @@ -436,16 +482,18 @@ fn read_helper_job(stdin: &mut BufReader) -> eyre::Result( stdout: &mut BufWriter, - result: Result, + result: WorkerResult, ) -> eyre::Result<()> { - let response = match result { + let timings = result.timings.into(); + let response = match result.outcome { Ok(WorkerSuccess::JitObject(success)) => HelperResponse::Ok { symbol_name: success.symbol_name, object_bytes: success.object_bytes.to_vec(), builtin_symbols: success.builtin_symbols, + timings, }, Ok(_) => unreachable!(), - Err(error) => HelperResponse::Err { error }, + Err(error) => HelperResponse::Err { error, timings }, }; write_message(stdout, &response)?; Ok(()) @@ -537,22 +585,19 @@ fn helper_init(config: &RuntimeConfig) -> HelperInit { no_dse: config.no_dse, dump_dir: config.dump_dir.as_ref().map(|path| path.to_string_lossy().into_owned()), gas_params: config.gas_params.as_ref().map(gas_params_to_pairs), + compiler_recycle_threshold: config.tuning.compiler_recycle_threshold, } } fn runtime_config_from_init(init: HelperInit) -> eyre::Result { - Ok(RuntimeConfig { + let mut config = RuntimeConfig { dump_dir: init.dump_dir.map(PathBuf::from), debug_assertions: init.debug_assertions, no_dedup: init.no_dedup, no_dse: init.no_dse, gas_params: init.gas_params.map(gas_params_from_pairs).transpose()?, ..Default::default() - }) -} - -fn job_dump_dir(config: &RuntimeConfig, job: &CompileJob) -> Option { - config.dump_dir.as_ref().map(|base| { - base.join(format!("{:?}", job.key.spec_id)).join(format!("{}", job.key.code_hash)) - }) + }; + config.tuning.compiler_recycle_threshold = init.compiler_recycle_threshold; + Ok(config) } diff --git a/crates/revmc-runtime/src/runtime/worker.rs b/crates/revmc-runtime/src/runtime/worker.rs index c8cc68ae3..370b47c92 100644 --- a/crates/revmc-runtime/src/runtime/worker.rs +++ b/crates/revmc-runtime/src/runtime/worker.rs @@ -321,12 +321,10 @@ fn compile_job( CompilationKind::Jit if config.jit_mode == JitMode::OutOfProcess => { compile_out_of_process_job(job, config, out_of_process_helper) } - CompilationKind::Jit => { - JIT_COMPILER.with_borrow_mut(|state| compile_with_state(job, config, state)) - } - CompilationKind::Aot => { - AOT_COMPILER.with_borrow_mut(|state| compile_with_state(job, config, state)) - } + CompilationKind::Jit => JIT_COMPILER + .with_borrow_mut(|state| compile_with_state(job, config, CompilerTarget::Jit, state)), + CompilationKind::Aot => AOT_COMPILER + .with_borrow_mut(|state| compile_with_state(job, config, CompilerTarget::Aot, state)), } } @@ -358,9 +356,25 @@ fn compile_out_of_process_job( } #[cfg(feature = "llvm")] -fn compile_with_state( +#[derive(Clone, Copy, Debug, PartialEq, Eq)] +pub(super) enum CompilerTarget { + Jit, + JitObject, + Aot, +} + +#[cfg(feature = "llvm")] +impl CompilerTarget { + const fn is_aot_backend(self) -> bool { + matches!(self, Self::JitObject | Self::Aot) + } +} + +#[cfg(feature = "llvm")] +pub(super) fn compile_with_state( job: CompileJob, config: &RuntimeConfig, + target: CompilerTarget, state_slot: &mut Option, ) -> WorkerResult { let _span = match job.kind { @@ -374,7 +388,7 @@ fn compile_with_state( let t0 = Instant::now(); if state_slot.is_none() { - match CompilerState::new(config, job.kind) { + match CompilerState::new(config, target) { Ok(s) => *state_slot = Some(s), Err(e) => { error!(error = %e, "failed to create LLVM backend"); @@ -403,12 +417,10 @@ fn compile_with_state( } compiler.set_opt_level(job.opt_level); - let outcome = match job.kind { - CompilationKind::Jit if config.jit_mode == JitMode::OutOfProcess => { - compile_jit_object_artifact(&job, compiler) - } - CompilationKind::Jit => compile_jit_artifact(&job, compiler), - CompilationKind::Aot => compile_aot_artifact(&job, compiler), + let outcome = match target { + CompilerTarget::Jit => compile_jit_artifact(&job, compiler), + CompilerTarget::JitObject => compile_jit_object_artifact(&job, compiler), + CompilerTarget::Aot => compile_aot_artifact(&job, compiler), }; let timings = compiler.take_timings(); @@ -421,7 +433,7 @@ fn compile_with_state( && state.compilations_since_recycle >= config.tuning.compiler_recycle_threshold { debug!(compilations_since_recycle = state.compilations_since_recycle, "recycling compiler"); - match CompilerState::new(config, job.kind) { + match CompilerState::new(config, target) { Ok(new_state) => { *state_slot = Some(new_state); revmc_llvm::global_gc(); @@ -445,16 +457,16 @@ fn compile_with_state( } #[cfg(feature = "llvm")] -struct CompilerState { +pub(super) struct CompilerState { compiler: EvmCompiler, compilations_since_recycle: usize, } #[cfg(feature = "llvm")] impl CompilerState { - fn new(config: &RuntimeConfig, kind: CompilationKind) -> Result { + fn new(config: &RuntimeConfig, target: CompilerTarget) -> Result { Ok(Self { - compiler: create_compiler(config, kind == CompilationKind::Aot)?, + compiler: create_compiler(config, target.is_aot_backend())?, compilations_since_recycle: 0, }) } From 9e615f3edc1c1f1d192990f32580ed5c5c35cc69 Mon Sep 17 00:00:00 2001 From: DaniPopes <57450786+DaniPopes@users.noreply.github.com> Date: Tue, 19 May 2026 19:54:49 +0200 Subject: [PATCH 35/43] fix(runtime): isolate linked jit objects --- crates/revmc-llvm/src/lib.rs | 35 +++++++++-- crates/revmc-runtime/src/runtime/backend.rs | 67 +++++++++++++++++++-- 2 files changed, 92 insertions(+), 10 deletions(-) diff --git a/crates/revmc-llvm/src/lib.rs b/crates/revmc-llvm/src/lib.rs index 6e6f8f2d3..1257eb028 100644 --- a/crates/revmc-llvm/src/lib.rs +++ b/crates/revmc-llvm/src/lib.rs @@ -453,6 +453,20 @@ impl OrcJitState { } } +fn link_jit_object_in_dylib( + orc: &OrcJitState, + jd: orc::JITDylibRef, + symbol_name: &CStr, + object: &[u8], + symbols: &[(CString, usize)], +) -> Result<(usize, orc::ResourceTracker)> { + orc.global.define_builtins(symbols); + let tracker = jd.create_resource_tracker(); + orc.global.jit.add_object_with_rt(symbol_name, object, &tracker).map_err(error_msg)?; + let addr = orc.global.jit.lookup_in(jd, symbol_name).map_err(error_msg)?; + Ok((addr, tracker)) +} + /// Wraps a module in a [`orc::ThreadSafeModule`] for transfer to LLJIT. /// /// Uses a raw pointer cast to work around `Module<'ctx>` invariance — the module's @@ -755,14 +769,27 @@ impl EvmLlvmBackend { symbols: &[(CString, usize)], ) -> Result<(usize, orc::ResourceTracker)> { let orc = self.ensure_orc()?; - orc.global.define_builtins(symbols); - let tracker = orc.jd().create_resource_tracker(); - orc.global.jit.add_object_with_rt(symbol_name, object, &tracker).map_err(error_msg)?; - let addr = orc.global.jit.lookup_in(orc.jd(), symbol_name).map_err(error_msg)?; + let (addr, tracker) = + link_jit_object_in_dylib(orc, orc.jd(), symbol_name, object, symbols)?; orc.loaded_trackers.push(tracker); Ok((addr, orc.loaded_trackers.pop().unwrap())) } + /// Links a relocatable object into a fresh JITDylib and returns the function address, + /// resource tracker, and guard for the JITDylib that owns the linked code. + pub fn link_jit_object_in_fresh_dylib( + &mut self, + symbol_name: &CStr, + object: &[u8], + symbols: &[(CString, usize)], + ) -> Result<(usize, orc::ResourceTracker, Arc)> { + let orc = self.ensure_orc()?; + let jd = orc.global.create_jit_dylib(); + let jd_guard = Arc::new(JitDylibGuard { global: orc.global, jd }); + let (addr, tracker) = link_jit_object_in_dylib(orc, jd, symbol_name, object, symbols)?; + Ok((addr, tracker, jd_guard)) + } + /// Pops and returns the [`ResourceTracker`](orc::ResourceTracker) for the last committed /// JIT module. /// diff --git a/crates/revmc-runtime/src/runtime/backend.rs b/crates/revmc-runtime/src/runtime/backend.rs index 4a58ec542..7550d302a 100644 --- a/crates/revmc-runtime/src/runtime/backend.rs +++ b/crates/revmc-runtime/src/runtime/backend.rs @@ -63,13 +63,16 @@ fn jit_total_bytes() -> usize { } #[cfg(feature = "llvm")] -#[derive(Default)] struct JitObjectLinker { backend: Option, } #[cfg(feature = "llvm")] impl JitObjectLinker { + const fn new() -> Self { + Self { backend: None } + } + fn link( &mut self, success: &JitObjectSuccess, @@ -90,9 +93,11 @@ impl JitObjectLinker { Ok((CString::new(name.as_str())?, addr)) }) .collect::>>()?; - let (addr, tracker) = - backend.link_jit_object(&symbol_name, &success.object_bytes, &builtin_symbols)?; - let jd_guard = backend.jit_dylib_guard(); + let (addr, tracker, jd_guard) = backend.link_jit_object_in_fresh_dylib( + &symbol_name, + &success.object_bytes, + &builtin_symbols, + )?; let func = EvmCompilerFn::new(unsafe { std::mem::transmute::(addr) }); Ok((func, Arc::new(JitCodeBacking::new(tracker, jd_guard)))) @@ -100,11 +105,14 @@ impl JitObjectLinker { } #[cfg(not(feature = "llvm"))] -#[derive(Default)] struct JitObjectLinker; #[cfg(not(feature = "llvm"))] impl JitObjectLinker { + const fn new() -> Self { + Self + } + fn link( &mut self, _success: &JitObjectSuccess, @@ -815,7 +823,7 @@ pub(crate) fn run( resident_meta: preload_meta, entries: HashMap::default(), workers, - jit_object_linker: JitObjectLinker::default(), + jit_object_linker: JitObjectLinker::new(), result_rx, store: config.store, tuning: config.tuning, @@ -859,3 +867,50 @@ pub(crate) fn run( state.workers.shutdown(); while state.result_rx.try_recv().is_ok() {} } + +#[cfg(all(test, feature = "llvm"))] +mod tests { + use super::*; + use crate::runtime::worker::{ + CompileJob, WorkerSuccess, compile_jit_object_artifact, create_compiler, + }; + use revm_primitives::hardfork::SpecId; + + /// PUSH1 0x42 PUSH0 MSTORE PUSH1 0x20 PUSH0 RETURN. + const BYTECODE_RET42: &[u8] = &[0x60, 0x42, 0x5f, 0x52, 0x60, 0x20, 0x5f, 0xf3]; + + fn compile_jit_object(symbol_name: &str) -> JitObjectSuccess { + let config = RuntimeConfig::default(); + let mut compiler = create_compiler(&config, true).unwrap(); + let job = CompileJob { + kind: CompilationKind::Jit, + key: RuntimeCacheKey { code_hash: keccak256(BYTECODE_RET42), spec_id: SpecId::CANCUN }, + bytecode: Bytes::copy_from_slice(BYTECODE_RET42), + symbol_name: symbol_name.to_owned(), + opt_level: config.tuning.jit_opt_level, + sync_notifier: SyncNotifier::none(), + generation: 0, + }; + + match compile_jit_object_artifact(&job, &mut compiler).unwrap() { + WorkerSuccess::JitObject(success) => success, + _ => unreachable!(), + } + } + + #[test] + fn jit_object_linker_relinks_live_symbol_name() { + let success = compile_jit_object("jit_duplicate_symbol"); + let success2 = JitObjectSuccess { + symbol_name: success.symbol_name.clone(), + object_bytes: success.object_bytes.clone(), + builtin_symbols: success.builtin_symbols.clone(), + }; + + let mut linker = JitObjectLinker::new(); + let (_first_func, _first_backing) = linker.link(&success).unwrap(); + let (_second_func, _second_backing) = linker + .link(&success2) + .expect("same symbol should link while previous backing remains alive"); + } +} From 62c9a93cac46986c7762ba70130959460fbc373e Mon Sep 17 00:00:00 2001 From: DaniPopes <57450786+DaniPopes@users.noreply.github.com> Date: Tue, 19 May 2026 21:12:07 +0200 Subject: [PATCH 36/43] feat(runtime): track jit helper stats --- crates/revmc-runtime/src/runtime/backend.rs | 2 +- crates/revmc-runtime/src/runtime/mod.rs | 4 +-- .../src/runtime/out_of_process.rs | 30 +++++++++++++++---- crates/revmc-runtime/src/runtime/stats.rs | 25 ++++++++++++++++ crates/revmc-runtime/src/runtime/worker.rs | 22 ++++++++++---- 5 files changed, 70 insertions(+), 13 deletions(-) diff --git a/crates/revmc-runtime/src/runtime/backend.rs b/crates/revmc-runtime/src/runtime/backend.rs index 7550d302a..c3285ce16 100644 --- a/crates/revmc-runtime/src/runtime/backend.rs +++ b/crates/revmc-runtime/src/runtime/backend.rs @@ -806,7 +806,7 @@ pub(crate) fn run( let (result_tx, result_rx) = chan::unbounded::(); - let workers = WorkerPool::new(result_tx, config.clone()); + let workers = WorkerPool::new(result_tx, config.clone(), Arc::clone(&inner.stats)); let sweep_interval = config.tuning.eviction_sweep_interval; let event_drain_interval = config.tuning.event_drain_interval; diff --git a/crates/revmc-runtime/src/runtime/mod.rs b/crates/revmc-runtime/src/runtime/mod.rs index d4c4aecb8..83322285a 100644 --- a/crates/revmc-runtime/src/runtime/mod.rs +++ b/crates/revmc-runtime/src/runtime/mod.rs @@ -89,7 +89,7 @@ pub(crate) struct BackendShared { events: EventQueue, /// Shared stats counters. #[debug(skip)] - stats: RuntimeStats, + stats: Arc, } /// Inner state for [`JitBackend`]. Owns the backend thread lifecycle. @@ -177,7 +177,7 @@ impl JitBackend { let shared = Arc::new(BackendShared { resident: ResidentMap::default(), events, - stats: RuntimeStats::default(), + stats: Arc::new(RuntimeStats::default()), }); let this = Self { inner: Arc::new(BackendInner { diff --git a/crates/revmc-runtime/src/runtime/out_of_process.rs b/crates/revmc-runtime/src/runtime/out_of_process.rs index 7fa8122c9..9d97ccaba 100644 --- a/crates/revmc-runtime/src/runtime/out_of_process.rs +++ b/crates/revmc-runtime/src/runtime/out_of_process.rs @@ -4,6 +4,7 @@ use crate::{ CompileTimings, OptimizationLevel, eyre, runtime::{ config::{CompilationKind, RuntimeConfig}, + stats::RuntimeStats, storage::RuntimeCacheKey, worker::{ CompileJob, CompilerState, CompilerTarget, JitObjectSuccess, SyncNotifier, @@ -21,7 +22,7 @@ use std::{ os::unix::process::CommandExt, path::PathBuf, process::{Child, ChildStdin, Command, Stdio}, - sync::{Arc, Mutex}, + sync::{Arc, Mutex, atomic::Ordering}, thread::JoinHandle, time::{Duration, Instant}, }; @@ -84,19 +85,33 @@ struct HelperIo { pub(super) struct HelperProcess { inner: Mutex>>, + stats: Arc, } impl HelperProcess { - pub(super) const fn new() -> Self { - Self { inner: Mutex::new(None) } + pub(super) fn new(stats: Arc) -> Self { + Self { inner: Mutex::new(None), stats } } fn compile(&self, job: &CompileJob, config: &RuntimeConfig) -> Result { let helper = { let mut slot = self.inner.lock().unwrap(); if slot.as_ref().is_none_or(|helper| !helper.matches_config(config)) { + let restarting = slot.is_some(); debug!("spawning JIT helper"); - *slot = Some(Arc::new(HelperProcessInner::spawn(config)?)); + match HelperProcessInner::spawn(config, self.stats.clone()) { + Ok(helper) => { + self.stats.jit_helper_spawns.fetch_add(1, Ordering::Relaxed); + if restarting { + self.stats.jit_helper_restarts.fetch_add(1, Ordering::Relaxed); + } + *slot = Some(Arc::new(helper)); + } + Err(err) => { + self.stats.jit_helper_spawn_failures.fetch_add(1, Ordering::Relaxed); + return Err(err); + } + } } slot.as_ref().unwrap().clone() }; @@ -107,6 +122,7 @@ impl HelperProcess { let mut slot = self.inner.lock().unwrap(); if slot.as_ref().is_some_and(|current| Arc::ptr_eq(current, &helper)) { warn!(error = %err, "discarding JIT helper after failed job"); + self.stats.jit_helper_restarts.fetch_add(1, Ordering::Relaxed); *slot = None; } Err(err) @@ -128,10 +144,11 @@ struct HelperProcessInner { io: Mutex, reader: Mutex>>, shutdown_timeout: Duration, + stats: Arc, } impl HelperProcessInner { - fn spawn(config: &RuntimeConfig) -> Result { + fn spawn(config: &RuntimeConfig, stats: Arc) -> Result { let path = match &config.jit_helper_path { Some(path) => path.clone(), None => { @@ -170,6 +187,7 @@ impl HelperProcessInner { io: Mutex::new(HelperIo { stdin, result_rx }), reader: Mutex::new(Some(reader)), shutdown_timeout: config.tuning.shutdown_timeout, + stats, }) } @@ -192,6 +210,7 @@ impl HelperProcessInner { Ok(result) => result, Err(chan::RecvTimeoutError::Timeout) => { warn!(timeout = ?config.tuning.jit_timeout, "JIT helper timed out"); + self.stats.jit_helper_timeouts.fetch_add(1, Ordering::Relaxed); self.kill(); Err(format!( "JIT helper timed out after {:?}; helper will be restarted", @@ -205,6 +224,7 @@ impl HelperProcessInner { None => "JIT helper disconnected".into(), }; warn!(message, "JIT helper disconnected"); + self.stats.jit_helper_disconnects.fetch_add(1, Ordering::Relaxed); Err(format!("{message}; helper will be restarted")) } } diff --git a/crates/revmc-runtime/src/runtime/stats.rs b/crates/revmc-runtime/src/runtime/stats.rs index c02a32c71..49a79a56c 100644 --- a/crates/revmc-runtime/src/runtime/stats.rs +++ b/crates/revmc-runtime/src/runtime/stats.rs @@ -19,6 +19,16 @@ pub(crate) struct RuntimeStats { pub(crate) compilations_succeeded: AtomicU64, /// Total number of failed compilations (JIT + AOT). pub(crate) compilations_failed: AtomicU64, + /// Total number of out-of-process JIT helper processes spawned successfully. + pub(crate) jit_helper_spawns: AtomicU64, + /// Total number of failed out-of-process JIT helper spawn attempts. + pub(crate) jit_helper_spawn_failures: AtomicU64, + /// Total number of out-of-process JIT helpers discarded for restart. + pub(crate) jit_helper_restarts: AtomicU64, + /// Total number of out-of-process JIT helper job timeouts. + pub(crate) jit_helper_timeouts: AtomicU64, + /// Total number of out-of-process JIT helper disconnects. + pub(crate) jit_helper_disconnects: AtomicU64, } /// Gauge values sampled at snapshot time. @@ -64,6 +74,16 @@ pub struct RuntimeStatsSnapshot { pub compilations_succeeded: u64, /// Total number of failed compilations (JIT + AOT). pub compilations_failed: u64, + /// Total number of out-of-process JIT helper processes spawned successfully. + pub jit_helper_spawns: u64, + /// Total number of failed out-of-process JIT helper spawn attempts. + pub jit_helper_spawn_failures: u64, + /// Total number of out-of-process JIT helpers discarded for restart. + pub jit_helper_restarts: u64, + /// Total number of out-of-process JIT helper job timeouts. + pub jit_helper_timeouts: u64, + /// Total number of out-of-process JIT helper disconnects. + pub jit_helper_disconnects: u64, } impl RuntimeStatsSnapshot { @@ -101,6 +121,11 @@ impl RuntimeStats { compilations_dispatched: dispatched, compilations_succeeded: succeeded, compilations_failed: failed, + jit_helper_spawns: self.jit_helper_spawns.load(Ordering::Relaxed), + jit_helper_spawn_failures: self.jit_helper_spawn_failures.load(Ordering::Relaxed), + jit_helper_restarts: self.jit_helper_restarts.load(Ordering::Relaxed), + jit_helper_timeouts: self.jit_helper_timeouts.load(Ordering::Relaxed), + jit_helper_disconnects: self.jit_helper_disconnects.load(Ordering::Relaxed), } } } diff --git a/crates/revmc-runtime/src/runtime/worker.rs b/crates/revmc-runtime/src/runtime/worker.rs index 370b47c92..fc8e59cf5 100644 --- a/crates/revmc-runtime/src/runtime/worker.rs +++ b/crates/revmc-runtime/src/runtime/worker.rs @@ -13,6 +13,7 @@ use crate::{ CompileTimings, EvmCompilerFn, OptimizationLevel, runtime::{ config::{CompilationKind, RuntimeConfig}, + stats::RuntimeStats, storage::RuntimeCacheKey, }, }; @@ -200,10 +201,14 @@ pub(crate) struct WorkerPool { impl WorkerPool { /// Creates and starts the worker pool. - pub(crate) fn new(result_tx: chan::Sender, config: RuntimeConfig) -> Self { + pub(crate) fn new( + result_tx: chan::Sender, + config: RuntimeConfig, + stats: Arc, + ) -> Self { let worker_count = config.tuning.jit_worker_count; let queue_capacity = worker_count.saturating_mul(config.tuning.jit_worker_queue_capacity); - let out_of_process_helper = create_out_of_process_helper(&config); + let out_of_process_helper = create_out_of_process_helper(&config, stats); let pool = (worker_count > 0).then(|| { ThreadPoolBuilder::new() .num_threads(worker_count) @@ -283,13 +288,20 @@ impl Drop for WorkerPool { } #[cfg(all(feature = "llvm", unix))] -fn create_out_of_process_helper(config: &RuntimeConfig) -> OutOfProcessHelper { +fn create_out_of_process_helper( + config: &RuntimeConfig, + stats: Arc, +) -> OutOfProcessHelper { (config.jit_mode == JitMode::OutOfProcess) - .then(|| Arc::new(super::out_of_process::HelperProcess::new())) + .then(|| Arc::new(super::out_of_process::HelperProcess::new(stats))) } #[cfg(not(all(feature = "llvm", unix)))] -fn create_out_of_process_helper(_config: &RuntimeConfig) -> OutOfProcessHelper {} +fn create_out_of_process_helper( + _config: &RuntimeConfig, + _stats: Arc, +) -> OutOfProcessHelper { +} #[cfg(all(feature = "llvm", unix))] fn cancel_out_of_process_helper(helper: &OutOfProcessHelper) { From 1e249bef34ad6f8052df4c25d5f03f846055d0c0 Mon Sep 17 00:00:00 2001 From: DaniPopes <57450786+DaniPopes@users.noreply.github.com> Date: Tue, 19 May 2026 21:15:41 +0200 Subject: [PATCH 37/43] feat(runtime): parallelize jit helper jobs --- .../src/runtime/out_of_process.rs | 131 ++++++++++++++---- 1 file changed, 102 insertions(+), 29 deletions(-) diff --git a/crates/revmc-runtime/src/runtime/out_of_process.rs b/crates/revmc-runtime/src/runtime/out_of_process.rs index 9d97ccaba..6bf2d1680 100644 --- a/crates/revmc-runtime/src/runtime/out_of_process.rs +++ b/crates/revmc-runtime/src/runtime/out_of_process.rs @@ -14,15 +14,21 @@ use crate::{ }; use alloy_primitives::{B256, Bytes}; use crossbeam_channel as chan; +use rayon::ThreadPoolBuilder; use revm_context_interface::cfg::{GasParams, gas_params::GasId}; use revm_primitives::hardfork::SpecId; use std::{ + cell::RefCell, + collections::HashMap, io::{BufReader, BufWriter, Read, Write}, ops::ControlFlow, os::unix::process::CommandExt, path::PathBuf, process::{Child, ChildStdin, Command, Stdio}, - sync::{Arc, Mutex, atomic::Ordering}, + sync::{ + Arc, Mutex, + atomic::{AtomicU64, Ordering}, + }, thread::JoinHandle, time::{Duration, Instant}, }; @@ -33,6 +39,7 @@ const HELPER_ENV: &str = "REVMC_JIT_HELPER"; const GAS_PARAM_COUNT: usize = 256; type GasParamPairs = Vec<(u8, u64)>; +type PendingResponses = Arc>>>>; /// Runs the out-of-process JIT helper if this process was launched as one. pub(super) fn maybe_run_jit_helper() -> eyre::Result> { @@ -80,7 +87,6 @@ struct HelperJobResult { struct HelperIo { stdin: BufWriter, - result_rx: chan::Receiver>, } pub(super) struct HelperProcess { @@ -142,7 +148,9 @@ struct HelperProcessInner { init: HelperInit, child: Mutex, io: Mutex, + pending: PendingResponses, reader: Mutex>>, + next_job_id: AtomicU64, shutdown_timeout: Duration, stats: Arc, } @@ -169,14 +177,26 @@ impl HelperProcessInner { write_init(&mut stdin, &init).map_err(|e| format!("failed to write helper init: {e}"))?; stdin.flush().map_err(|e| format!("failed to flush helper init: {e}"))?; let stdout = child.stdout.take().ok_or("helper stdout unavailable")?; - let (result_tx, result_rx) = chan::bounded(1); + let pending = PendingResponses::default(); + let reader_pending = pending.clone(); + let reader_stats = stats.clone(); let reader = std::thread::spawn(move || { let mut stdout = BufReader::new(stdout); loop { let result = read_helper_result(&mut stdout); - let done = result.is_err(); - if result_tx.try_send(result).is_err() || done { - break; + match result { + Ok((id, result)) => { + if let Some(tx) = reader_pending.lock().unwrap().remove(&id) { + let _ = tx.send(Ok(result)); + } + } + Err(error) => { + reader_stats.jit_helper_disconnects.fetch_add(1, Ordering::Relaxed); + for (_, tx) in reader_pending.lock().unwrap().drain() { + let _ = tx.send(Err(error.clone())); + } + break; + } } } }); @@ -184,8 +204,10 @@ impl HelperProcessInner { path, init, child: Mutex::new(child), - io: Mutex::new(HelperIo { stdin, result_rx }), + io: Mutex::new(HelperIo { stdin }), + pending, reader: Mutex::new(Some(reader)), + next_job_id: AtomicU64::new(0), shutdown_timeout: config.tuning.shutdown_timeout, stats, }) @@ -202,14 +224,27 @@ impl HelperProcessInner { } fn compile(&self, job: &CompileJob, config: &RuntimeConfig) -> Result { - let mut io = self.io.lock().unwrap(); - write_job(&mut io.stdin, job).map_err(|e| format!("failed to write helper job: {e}"))?; - io.stdin.flush().map_err(|e| format!("failed to flush helper job: {e}"))?; + let id = self.next_job_id.fetch_add(1, Ordering::Relaxed); + let (tx, rx) = chan::bounded(1); + self.pending.lock().unwrap().insert(id, tx); + + { + let mut io = self.io.lock().unwrap(); + if let Err(e) = write_job(&mut io.stdin, id, job) { + self.pending.lock().unwrap().remove(&id); + return Err(format!("failed to write helper job: {e}")); + } + if let Err(e) = io.stdin.flush() { + self.pending.lock().unwrap().remove(&id); + return Err(format!("failed to flush helper job: {e}")); + } + } - match io.result_rx.recv_timeout(config.tuning.jit_timeout) { + match rx.recv_timeout(config.tuning.jit_timeout) { Ok(result) => result, Err(chan::RecvTimeoutError::Timeout) => { warn!(timeout = ?config.tuning.jit_timeout, "JIT helper timed out"); + self.pending.lock().unwrap().remove(&id); self.stats.jit_helper_timeouts.fetch_add(1, Ordering::Relaxed); self.kill(); Err(format!( @@ -354,6 +389,7 @@ struct HelperInit { no_dse: bool, dump_dir: Option, gas_params: Option, + jit_worker_count: usize, compiler_recycle_threshold: usize, } @@ -365,6 +401,7 @@ enum HelperRequest { #[derive(SchemaWrite, SchemaRead)] struct HelperCompile { + id: u64, code_hash: [u8; 32], spec_id: u8, opt_level: u8, @@ -375,12 +412,14 @@ struct HelperCompile { #[derive(SchemaWrite, SchemaRead)] enum HelperResponse { Ok { + id: u64, symbol_name: String, object_bytes: Vec, builtin_symbols: Vec, timings: HelperTimings, }, Err { + id: u64, error: String, timings: HelperTimings, }, @@ -398,8 +437,13 @@ fn write_init(w: &mut BufWriter, init: &HelperInit) -> std write_message(w, &HelperRequest::Init(init.clone())) } -fn write_job(w: &mut BufWriter, job: &CompileJob) -> std::io::Result<()> { +fn write_job( + w: &mut BufWriter, + id: u64, + job: &CompileJob, +) -> std::io::Result<()> { let req = HelperRequest::Compile(HelperCompile { + id, code_hash: job.key.code_hash.0, spec_id: job.key.spec_id as u8, opt_level: opt_level_to_u8(job.opt_level), @@ -409,20 +453,23 @@ fn write_job(w: &mut BufWriter, job: &CompileJob) -> std:: write_message(w, &req) } -fn read_helper_result(r: &mut BufReader) -> Result { +fn read_helper_result( + r: &mut BufReader, +) -> Result<(u64, HelperJobResult), String> { match read_message(r).map_err(|e| format!("failed to decode helper result: {e}"))? { - HelperResponse::Ok { symbol_name, object_bytes, builtin_symbols, timings } => { - Ok(HelperJobResult { + HelperResponse::Ok { id, symbol_name, object_bytes, builtin_symbols, timings } => Ok(( + id, + HelperJobResult { outcome: Ok(WorkerSuccess::JitObject(JitObjectSuccess { symbol_name, object_bytes: Bytes::from(object_bytes), builtin_symbols, })), timings: timings.into(), - }) - } - HelperResponse::Err { error, timings } => { - Ok(HelperJobResult { outcome: Err(error), timings: timings.into() }) + }, + )), + HelperResponse::Err { id, error, timings } => { + Ok((id, HelperJobResult { outcome: Err(error), timings: timings.into() })) } } } @@ -453,21 +500,43 @@ fn duration_to_nanos(duration: Duration) -> u64 { u64::try_from(duration.as_nanos()).unwrap_or(u64::MAX) } +thread_local! { + static HELPER_COMPILER: RefCell> = const { RefCell::new(None) }; +} + fn run_jit_helper_stdio() -> eyre::Result<()> { let mut stdin = BufReader::new(std::io::stdin().lock()); - let mut stdout = BufWriter::new(std::io::stdout().lock()); - let mut compiler = None::; - let config = read_helper_init(&mut stdin)?; + let config = Arc::new(read_helper_init(&mut stdin)?); + let worker_count = config.tuning.jit_worker_count.max(1); + let pool = ThreadPoolBuilder::new() + .num_threads(worker_count) + .thread_name(|i| format!("revmc-helper-{i:02}")) + .exit_handler(|_| { + HELPER_COMPILER.with_borrow_mut(Option::take); + }) + .build()?; + let stdout = Arc::new(Mutex::new(BufWriter::new(std::io::stdout()))); loop { - let job = match read_helper_job(&mut stdin) { + let (id, job) = match read_helper_job(&mut stdin) { Ok(job) => job, Err(err) if is_unexpected_eof(&err) => break, Err(err) => return Err(err), }; - let result = compile_with_state(job, &config, CompilerTarget::JitObject, &mut compiler); - write_helper_result(&mut stdout, result)?; - stdout.flush()?; + let config = Arc::clone(&config); + let stdout = Arc::clone(&stdout); + pool.spawn_fifo(move || { + let result = HELPER_COMPILER.with_borrow_mut(|compiler| { + compile_with_state(job, &config, CompilerTarget::JitObject, compiler) + }); + let mut stdout = stdout.lock().unwrap(); + if let Err(err) = write_helper_result(&mut stdout, id, result) + .and_then(|()| stdout.flush().map_err(Into::into)) + { + error!(%err, "failed to write helper result"); + std::process::exit(1); + } + }); } Ok(()) @@ -480,7 +549,7 @@ fn read_helper_init(stdin: &mut BufReader) -> eyre::Result< } } -fn read_helper_job(stdin: &mut BufReader) -> eyre::Result { +fn read_helper_job(stdin: &mut BufReader) -> eyre::Result<(u64, CompileJob)> { let req = match read_message(stdin)? { HelperRequest::Compile(req) => req, HelperRequest::Init(_) => eyre::bail!("JIT helper received duplicate init"), @@ -497,23 +566,25 @@ fn read_helper_job(stdin: &mut BufReader) -> eyre::Result( stdout: &mut BufWriter, + id: u64, result: WorkerResult, ) -> eyre::Result<()> { let timings = result.timings.into(); let response = match result.outcome { Ok(WorkerSuccess::JitObject(success)) => HelperResponse::Ok { + id, symbol_name: success.symbol_name, object_bytes: success.object_bytes.to_vec(), builtin_symbols: success.builtin_symbols, timings, }, Ok(_) => unreachable!(), - Err(error) => HelperResponse::Err { error, timings }, + Err(error) => HelperResponse::Err { id, error, timings }, }; write_message(stdout, &response)?; Ok(()) @@ -605,6 +676,7 @@ fn helper_init(config: &RuntimeConfig) -> HelperInit { no_dse: config.no_dse, dump_dir: config.dump_dir.as_ref().map(|path| path.to_string_lossy().into_owned()), gas_params: config.gas_params.as_ref().map(gas_params_to_pairs), + jit_worker_count: config.tuning.jit_worker_count, compiler_recycle_threshold: config.tuning.compiler_recycle_threshold, } } @@ -618,6 +690,7 @@ fn runtime_config_from_init(init: HelperInit) -> eyre::Result { gas_params: init.gas_params.map(gas_params_from_pairs).transpose()?, ..Default::default() }; + config.tuning.jit_worker_count = init.jit_worker_count; config.tuning.compiler_recycle_threshold = init.compiler_recycle_threshold; Ok(config) } From 83190a1571f114d54bb459efc32425d4f4c092f8 Mon Sep 17 00:00:00 2001 From: DaniPopes <57450786+DaniPopes@users.noreply.github.com> Date: Tue, 19 May 2026 21:16:52 +0200 Subject: [PATCH 38/43] feat(runtime): pause background jit --- crates/revmc-runtime/src/runtime/backend.rs | 4 ++- crates/revmc-runtime/src/runtime/mod.rs | 32 ++++++++++++++++++++- crates/revmc-runtime/src/runtime/tests.rs | 27 +++++++++++++++++ 3 files changed, 61 insertions(+), 2 deletions(-) diff --git a/crates/revmc-runtime/src/runtime/backend.rs b/crates/revmc-runtime/src/runtime/backend.rs index c3285ce16..58fea4348 100644 --- a/crates/revmc-runtime/src/runtime/backend.rs +++ b/crates/revmc-runtime/src/runtime/backend.rs @@ -234,7 +234,9 @@ impl BackendState { } fn tick(&mut self) { - self.drain_events(); + if self.inner.pause_depth.load(Ordering::Relaxed) == 0 { + self.drain_events(); + } self.run_eviction_sweep(); } diff --git a/crates/revmc-runtime/src/runtime/mod.rs b/crates/revmc-runtime/src/runtime/mod.rs index 83322285a..7ceaa7a9d 100644 --- a/crates/revmc-runtime/src/runtime/mod.rs +++ b/crates/revmc-runtime/src/runtime/mod.rs @@ -19,7 +19,7 @@ use std::{ ops::ControlFlow, sync::{ Arc, - atomic::{AtomicBool, Ordering}, + atomic::{AtomicBool, AtomicUsize, Ordering}, }, time::Duration, }; @@ -87,6 +87,8 @@ pub(crate) struct BackendShared { /// Lock-free queue of events. #[debug(skip)] events: EventQueue, + /// Number of active background lookup-processing pauses. + pause_depth: AtomicUsize, /// Shared stats counters. #[debug(skip)] stats: Arc, @@ -177,6 +179,7 @@ impl JitBackend { let shared = Arc::new(BackendShared { resident: ResidentMap::default(), events, + pause_depth: AtomicUsize::new(0), stats: Arc::new(RuntimeStats::default()), }); let this = Self { @@ -225,6 +228,11 @@ impl JitBackend { LookupDecision::Interpret(InterpretReason::NotReady) }; + if shared.pause_depth.load(Ordering::Relaxed) != 0 { + cold_path(); + return decision; + } + if let Err(_v) = shared.events.push(req) { cold_path(); shared.stats.events_dropped.fetch_add(1, Ordering::Relaxed); @@ -356,6 +364,28 @@ impl JitBackend { self.inner.enabled.load(Ordering::Relaxed) } + /// Pauses background JIT promotion from lookup observations. + /// + /// Resident compiled functions are still returned by [`lookup`](Self::lookup), but + /// lookup events are not enqueued or drained while paused. + pub fn pause(&self) { + self.inner.shared.pause_depth.fetch_add(1, Ordering::Relaxed); + } + + /// Resumes background JIT promotion from lookup observations. + pub fn resume(&self) { + let _ = self.inner.shared.pause_depth.fetch_update( + Ordering::Relaxed, + Ordering::Relaxed, + |depth| Some(depth.saturating_sub(1)), + ); + } + + /// Returns whether background JIT promotion is paused. + pub fn is_paused(&self) -> bool { + self.inner.shared.pause_depth.load(Ordering::Relaxed) != 0 + } + /// Sets whether the runtime is enabled, spawning the backend thread on first enable. /// /// When `enabled` is `true` and the backend thread has not been spawned yet, this diff --git a/crates/revmc-runtime/src/runtime/tests.rs b/crates/revmc-runtime/src/runtime/tests.rs index fe36af84e..b41ed037a 100644 --- a/crates/revmc-runtime/src/runtime/tests.rs +++ b/crates/revmc-runtime/src/runtime/tests.rs @@ -283,6 +283,33 @@ fn set_enabled_toggle() { assert!(matches!(tb.lookup(req), LookupDecision::Interpret(InterpretReason::Disabled))); } +#[test] +fn pause_skips_background_lookup_events() { + let tb = TestBackend::with_tuning(RuntimeTuning { jit_worker_count: 0, ..Default::default() }); + let req = TestBackend::req_cancun(&[0x00]); + + assert!(!tb.is_paused()); + tb.pause(); + tb.pause(); + assert!(tb.is_paused()); + assert!(matches!(tb.lookup(req.clone()), LookupDecision::Interpret(InterpretReason::NotReady))); + + std::thread::sleep(std::time::Duration::from_millis(50)); + let stats = tb.stats(); + assert_eq!(stats.lookup_misses, 0); + assert_eq!(stats.lookup_hits, 0); + + tb.resume(); + assert!(tb.is_paused()); + tb.resume(); + assert!(!tb.is_paused()); + assert!(matches!(tb.lookup(req), LookupDecision::Interpret(InterpretReason::NotReady))); + + let stats = tb.wait_stats(|s| s.lookup_misses == 1); + assert_eq!(stats.lookup_misses, 1); + assert_eq!(stats.lookup_hits, 0); +} + #[test] fn lookup_increments_miss_counter() { let tb = TestBackend::new(RuntimeConfig { enabled: true, ..Default::default() }); From 42b387962fe474af0ee5a92c96b3e9c3886b76c8 Mon Sep 17 00:00:00 2001 From: DaniPopes <57450786+DaniPopes@users.noreply.github.com> Date: Tue, 19 May 2026 21:41:29 +0200 Subject: [PATCH 39/43] fix(runtime): signal jit helper on pause --- crates/revmc-runtime/src/runtime/backend.rs | 6 + crates/revmc-runtime/src/runtime/mod.rs | 20 +++- .../src/runtime/out_of_process.rs | 107 ++++++++++++++---- crates/revmc-runtime/src/runtime/worker.rs | 30 +++++ 4 files changed, 132 insertions(+), 31 deletions(-) diff --git a/crates/revmc-runtime/src/runtime/backend.rs b/crates/revmc-runtime/src/runtime/backend.rs index 58fea4348..5b3e0b2d9 100644 --- a/crates/revmc-runtime/src/runtime/backend.rs +++ b/crates/revmc-runtime/src/runtime/backend.rs @@ -136,6 +136,10 @@ pub(crate) enum Command { ClearPersisted, /// Clear both resident and persisted. ClearAll, + /// Pause out-of-process helper execution. + Pause, + /// Resume out-of-process helper execution. + Resume, /// Shut down the backend. Shutdown, } @@ -228,6 +232,8 @@ impl BackendState { Command::ClearResident => self.handle_clear_resident(), Command::ClearPersisted => self.handle_clear_persisted(), Command::ClearAll => self.handle_clear_all(), + Command::Pause => self.workers.pause(), + Command::Resume => self.workers.resume(), Command::Shutdown => return ControlFlow::Break(()), } ControlFlow::Continue(()) diff --git a/crates/revmc-runtime/src/runtime/mod.rs b/crates/revmc-runtime/src/runtime/mod.rs index 7ceaa7a9d..d5d79f7e9 100644 --- a/crates/revmc-runtime/src/runtime/mod.rs +++ b/crates/revmc-runtime/src/runtime/mod.rs @@ -369,16 +369,24 @@ impl JitBackend { /// Resident compiled functions are still returned by [`lookup`](Self::lookup), but /// lookup events are not enqueued or drained while paused. pub fn pause(&self) { - self.inner.shared.pause_depth.fetch_add(1, Ordering::Relaxed); + if self.inner.shared.pause_depth.fetch_add(1, Ordering::Relaxed) == 0 { + let _ = self.inner.tx.send(Command::Pause); + } } /// Resumes background JIT promotion from lookup observations. pub fn resume(&self) { - let _ = self.inner.shared.pause_depth.fetch_update( - Ordering::Relaxed, - Ordering::Relaxed, - |depth| Some(depth.saturating_sub(1)), - ); + if self + .inner + .shared + .pause_depth + .fetch_update(Ordering::Relaxed, Ordering::Relaxed, |depth| { + Some(depth.saturating_sub(1)) + }) + .is_ok_and(|depth| depth == 1) + { + let _ = self.inner.tx.send(Command::Resume); + } } /// Returns whether background JIT promotion is paused. diff --git a/crates/revmc-runtime/src/runtime/out_of_process.rs b/crates/revmc-runtime/src/runtime/out_of_process.rs index 6bf2d1680..f81ddac94 100644 --- a/crates/revmc-runtime/src/runtime/out_of_process.rs +++ b/crates/revmc-runtime/src/runtime/out_of_process.rs @@ -27,7 +27,7 @@ use std::{ process::{Child, ChildStdin, Command, Stdio}, sync::{ Arc, Mutex, - atomic::{AtomicU64, Ordering}, + atomic::{AtomicBool, AtomicU64, Ordering}, }, thread::JoinHandle, time::{Duration, Instant}, @@ -91,12 +91,13 @@ struct HelperIo { pub(super) struct HelperProcess { inner: Mutex>>, + paused: Arc, stats: Arc, } impl HelperProcess { pub(super) fn new(stats: Arc) -> Self { - Self { inner: Mutex::new(None), stats } + Self { inner: Mutex::new(None), paused: Arc::new(AtomicBool::new(false)), stats } } fn compile(&self, job: &CompileJob, config: &RuntimeConfig) -> Result { @@ -105,8 +106,11 @@ impl HelperProcess { if slot.as_ref().is_none_or(|helper| !helper.matches_config(config)) { let restarting = slot.is_some(); debug!("spawning JIT helper"); - match HelperProcessInner::spawn(config, self.stats.clone()) { + match HelperProcessInner::spawn(config, self.stats.clone(), self.paused.clone()) { Ok(helper) => { + if self.paused.load(Ordering::Relaxed) { + helper.pause(); + } self.stats.jit_helper_spawns.fetch_add(1, Ordering::Relaxed); if restarting { self.stats.jit_helper_restarts.fetch_add(1, Ordering::Relaxed); @@ -141,6 +145,20 @@ impl HelperProcess { helper.kill(); } } + + pub(super) fn pause(&self) { + self.paused.store(true, Ordering::Relaxed); + if let Some(helper) = self.inner.lock().unwrap().as_ref() { + helper.pause(); + } + } + + pub(super) fn resume(&self) { + self.paused.store(false, Ordering::Relaxed); + if let Some(helper) = self.inner.lock().unwrap().as_ref() { + helper.resume(); + } + } } struct HelperProcessInner { @@ -153,10 +171,15 @@ struct HelperProcessInner { next_job_id: AtomicU64, shutdown_timeout: Duration, stats: Arc, + paused: Arc, } impl HelperProcessInner { - fn spawn(config: &RuntimeConfig, stats: Arc) -> Result { + fn spawn( + config: &RuntimeConfig, + stats: Arc, + paused: Arc, + ) -> Result { let path = match &config.jit_helper_path { Some(path) => path.clone(), None => { @@ -210,6 +233,7 @@ impl HelperProcessInner { next_job_id: AtomicU64::new(0), shutdown_timeout: config.tuning.shutdown_timeout, stats, + paused, }) } @@ -240,27 +264,32 @@ impl HelperProcessInner { } } - match rx.recv_timeout(config.tuning.jit_timeout) { - Ok(result) => result, - Err(chan::RecvTimeoutError::Timeout) => { - warn!(timeout = ?config.tuning.jit_timeout, "JIT helper timed out"); - self.pending.lock().unwrap().remove(&id); - self.stats.jit_helper_timeouts.fetch_add(1, Ordering::Relaxed); - self.kill(); - Err(format!( - "JIT helper timed out after {:?}; helper will be restarted", - config.tuning.jit_timeout - )) - } - Err(chan::RecvTimeoutError::Disconnected) => { - let status = self.child.lock().unwrap().try_wait().ok().flatten(); - let message = match status { - Some(status) => format!("JIT helper exited with {status}"), - None => "JIT helper disconnected".into(), - }; - warn!(message, "JIT helper disconnected"); - self.stats.jit_helper_disconnects.fetch_add(1, Ordering::Relaxed); - Err(format!("{message}; helper will be restarted")) + loop { + match rx.recv_timeout(config.tuning.jit_timeout) { + Ok(result) => return result, + Err(chan::RecvTimeoutError::Timeout) if self.paused.load(Ordering::Relaxed) => { + continue; + } + Err(chan::RecvTimeoutError::Timeout) => { + warn!(timeout = ?config.tuning.jit_timeout, "JIT helper timed out"); + self.pending.lock().unwrap().remove(&id); + self.stats.jit_helper_timeouts.fetch_add(1, Ordering::Relaxed); + self.kill(); + return Err(format!( + "JIT helper timed out after {:?}; helper will be restarted", + config.tuning.jit_timeout + )); + } + Err(chan::RecvTimeoutError::Disconnected) => { + let status = self.child.lock().unwrap().try_wait().ok().flatten(); + let message = match status { + Some(status) => format!("JIT helper exited with {status}"), + None => "JIT helper disconnected".into(), + }; + warn!(message, "JIT helper disconnected"); + self.stats.jit_helper_disconnects.fetch_add(1, Ordering::Relaxed); + return Err(format!("{message}; helper will be restarted")); + } } } } @@ -283,6 +312,22 @@ impl HelperProcessInner { } } } + + fn pause(&self) { + self.signal(libc::SIGSTOP, "pause"); + } + + fn resume(&self) { + self.signal(libc::SIGCONT, "resume"); + } + + fn signal(&self, signal: libc::c_int, action: &str) { + let mut child = self.child.lock().unwrap(); + if matches!(child.try_wait(), Ok(Some(_))) { + return; + } + signal_helper(&child, signal, action); + } } impl Drop for HelperProcessInner { @@ -336,6 +381,18 @@ fn kill_helper(child: &mut Child) { } } +fn signal_helper(child: &Child, signal: libc::c_int, action: &str) { + let pid = child.id() as libc::pid_t; + if unsafe { libc::kill(-pid, signal) } == 0 { + return; + } + + let err = std::io::Error::last_os_error(); + if err.raw_os_error() != Some(libc::ESRCH) { + warn!(%err, signal, action, "failed to signal JIT helper process group"); + } +} + fn set_rlimit(resource: libc::c_int, value: u64) -> std::io::Result<()> { let value = libc::rlim_t::try_from(value).unwrap_or(libc::rlim_t::MAX); let limit = libc::rlimit { rlim_cur: value, rlim_max: value }; diff --git a/crates/revmc-runtime/src/runtime/worker.rs b/crates/revmc-runtime/src/runtime/worker.rs index fc8e59cf5..6fe661874 100644 --- a/crates/revmc-runtime/src/runtime/worker.rs +++ b/crates/revmc-runtime/src/runtime/worker.rs @@ -279,6 +279,16 @@ impl WorkerPool { pub(crate) fn cancel_in_flight(&self) { cancel_out_of_process_helper(&self.out_of_process_helper); } + + /// Pauses out-of-process helper execution. + pub(crate) fn pause(&self) { + pause_out_of_process_helper(&self.out_of_process_helper); + } + + /// Resumes out-of-process helper execution. + pub(crate) fn resume(&self) { + resume_out_of_process_helper(&self.out_of_process_helper); + } } impl Drop for WorkerPool { @@ -313,6 +323,26 @@ fn cancel_out_of_process_helper(helper: &OutOfProcessHelper) { #[cfg(not(all(feature = "llvm", unix)))] fn cancel_out_of_process_helper(_helper: &OutOfProcessHelper) {} +#[cfg(all(feature = "llvm", unix))] +fn pause_out_of_process_helper(helper: &OutOfProcessHelper) { + if let Some(helper) = helper { + helper.pause(); + } +} + +#[cfg(not(all(feature = "llvm", unix)))] +fn pause_out_of_process_helper(_helper: &OutOfProcessHelper) {} + +#[cfg(all(feature = "llvm", unix))] +fn resume_out_of_process_helper(helper: &OutOfProcessHelper) { + if let Some(helper) = helper { + helper.resume(); + } +} + +#[cfg(not(all(feature = "llvm", unix)))] +fn resume_out_of_process_helper(_helper: &OutOfProcessHelper) {} + #[cfg(feature = "llvm")] fn clear_thread_local_compilers() { JIT_COMPILER.with_borrow_mut(Option::take); From fcf9273a77b09d0d11a73bc48e6f0a654db167ac Mon Sep 17 00:00:00 2001 From: DaniPopes <57450786+DaniPopes@users.noreply.github.com> Date: Tue, 19 May 2026 21:44:53 +0200 Subject: [PATCH 40/43] fix(runtime): keep events flowing while paused --- crates/revmc-runtime/src/runtime/backend.rs | 4 +--- crates/revmc-runtime/src/runtime/mod.rs | 20 +++++++++----------- crates/revmc-runtime/src/runtime/tests.rs | 11 +++++------ 3 files changed, 15 insertions(+), 20 deletions(-) diff --git a/crates/revmc-runtime/src/runtime/backend.rs b/crates/revmc-runtime/src/runtime/backend.rs index 5b3e0b2d9..b827df4a3 100644 --- a/crates/revmc-runtime/src/runtime/backend.rs +++ b/crates/revmc-runtime/src/runtime/backend.rs @@ -240,9 +240,7 @@ impl BackendState { } fn tick(&mut self) { - if self.inner.pause_depth.load(Ordering::Relaxed) == 0 { - self.drain_events(); - } + self.drain_events(); self.run_eviction_sweep(); } diff --git a/crates/revmc-runtime/src/runtime/mod.rs b/crates/revmc-runtime/src/runtime/mod.rs index d5d79f7e9..bc5919775 100644 --- a/crates/revmc-runtime/src/runtime/mod.rs +++ b/crates/revmc-runtime/src/runtime/mod.rs @@ -87,7 +87,7 @@ pub(crate) struct BackendShared { /// Lock-free queue of events. #[debug(skip)] events: EventQueue, - /// Number of active background lookup-processing pauses. + /// Number of active out-of-process helper pauses. pause_depth: AtomicUsize, /// Shared stats counters. #[debug(skip)] @@ -228,11 +228,6 @@ impl JitBackend { LookupDecision::Interpret(InterpretReason::NotReady) }; - if shared.pause_depth.load(Ordering::Relaxed) != 0 { - cold_path(); - return decision; - } - if let Err(_v) = shared.events.push(req) { cold_path(); shared.stats.events_dropped.fetch_add(1, Ordering::Relaxed); @@ -364,17 +359,20 @@ impl JitBackend { self.inner.enabled.load(Ordering::Relaxed) } - /// Pauses background JIT promotion from lookup observations. + /// Pauses out-of-process helper execution. /// - /// Resident compiled functions are still returned by [`lookup`](Self::lookup), but - /// lookup events are not enqueued or drained while paused. + /// Resident compiled functions are still returned by [`lookup`](Self::lookup), and lookup + /// events are still processed for stats, hotness tracking, and compilation dispatch. In + /// out-of-process mode, the helper process group is stopped until the pause depth returns to + /// zero, so dispatched helper requests remain buffered and resume once the helper continues. + /// In in-process mode, pause only tracks pause depth. pub fn pause(&self) { if self.inner.shared.pause_depth.fetch_add(1, Ordering::Relaxed) == 0 { let _ = self.inner.tx.send(Command::Pause); } } - /// Resumes background JIT promotion from lookup observations. + /// Resumes out-of-process helper execution once all active pauses have been released. pub fn resume(&self) { if self .inner @@ -389,7 +387,7 @@ impl JitBackend { } } - /// Returns whether background JIT promotion is paused. + /// Returns whether out-of-process helper execution is paused. pub fn is_paused(&self) -> bool { self.inner.shared.pause_depth.load(Ordering::Relaxed) != 0 } diff --git a/crates/revmc-runtime/src/runtime/tests.rs b/crates/revmc-runtime/src/runtime/tests.rs index b41ed037a..be20e47ae 100644 --- a/crates/revmc-runtime/src/runtime/tests.rs +++ b/crates/revmc-runtime/src/runtime/tests.rs @@ -284,7 +284,7 @@ fn set_enabled_toggle() { } #[test] -fn pause_skips_background_lookup_events() { +fn pause_processes_lookup_events() { let tb = TestBackend::with_tuning(RuntimeTuning { jit_worker_count: 0, ..Default::default() }); let req = TestBackend::req_cancun(&[0x00]); @@ -294,9 +294,8 @@ fn pause_skips_background_lookup_events() { assert!(tb.is_paused()); assert!(matches!(tb.lookup(req.clone()), LookupDecision::Interpret(InterpretReason::NotReady))); - std::thread::sleep(std::time::Duration::from_millis(50)); - let stats = tb.stats(); - assert_eq!(stats.lookup_misses, 0); + let stats = tb.wait_stats(|s| s.lookup_misses == 1); + assert_eq!(stats.lookup_misses, 1); assert_eq!(stats.lookup_hits, 0); tb.resume(); @@ -305,8 +304,8 @@ fn pause_skips_background_lookup_events() { assert!(!tb.is_paused()); assert!(matches!(tb.lookup(req), LookupDecision::Interpret(InterpretReason::NotReady))); - let stats = tb.wait_stats(|s| s.lookup_misses == 1); - assert_eq!(stats.lookup_misses, 1); + let stats = tb.wait_stats(|s| s.lookup_misses == 2); + assert_eq!(stats.lookup_misses, 2); assert_eq!(stats.lookup_hits, 0); } From f5c352d5f4e30163811200d7bd5025900ad8e308 Mon Sep 17 00:00:00 2001 From: DaniPopes <57450786+DaniPopes@users.noreply.github.com> Date: Tue, 19 May 2026 21:55:39 +0200 Subject: [PATCH 41/43] fix(runtime): gracefully pause jit helper --- .../src/runtime/out_of_process.rs | 194 ++++++++++++++---- 1 file changed, 159 insertions(+), 35 deletions(-) diff --git a/crates/revmc-runtime/src/runtime/out_of_process.rs b/crates/revmc-runtime/src/runtime/out_of_process.rs index f81ddac94..d0f4bab49 100644 --- a/crates/revmc-runtime/src/runtime/out_of_process.rs +++ b/crates/revmc-runtime/src/runtime/out_of_process.rs @@ -26,7 +26,7 @@ use std::{ path::PathBuf, process::{Child, ChildStdin, Command, Stdio}, sync::{ - Arc, Mutex, + Arc, Condvar, Mutex, atomic::{AtomicBool, AtomicU64, Ordering}, }, thread::JoinHandle, @@ -37,9 +37,11 @@ use wincode::{SchemaRead, SchemaWrite}; const HELPER_ENV: &str = "REVMC_JIT_HELPER"; const GAS_PARAM_COUNT: usize = 256; +const HELPER_PAUSE_TIMEOUT: Duration = Duration::from_millis(100); type GasParamPairs = Vec<(u8, u64)>; type PendingResponses = Arc>>>>; +type PendingPauses = Arc>>>>; /// Runs the out-of-process JIT helper if this process was launched as one. pub(super) fn maybe_run_jit_helper() -> eyre::Result> { @@ -167,6 +169,7 @@ struct HelperProcessInner { child: Mutex, io: Mutex, pending: PendingResponses, + pending_pauses: PendingPauses, reader: Mutex>>, next_job_id: AtomicU64, shutdown_timeout: Duration, @@ -201,23 +204,33 @@ impl HelperProcessInner { stdin.flush().map_err(|e| format!("failed to flush helper init: {e}"))?; let stdout = child.stdout.take().ok_or("helper stdout unavailable")?; let pending = PendingResponses::default(); + let pending_pauses = PendingPauses::default(); let reader_pending = pending.clone(); + let reader_pending_pauses = pending_pauses.clone(); let reader_stats = stats.clone(); let reader = std::thread::spawn(move || { let mut stdout = BufReader::new(stdout); loop { - let result = read_helper_result(&mut stdout); + let result = read_helper_response(&mut stdout); match result { - Ok((id, result)) => { + Ok(HelperResponseMessage::Job(id, result)) => { if let Some(tx) = reader_pending.lock().unwrap().remove(&id) { let _ = tx.send(Ok(result)); } } + Ok(HelperResponseMessage::Paused(id)) => { + if let Some(tx) = reader_pending_pauses.lock().unwrap().remove(&id) { + let _ = tx.send(Ok(())); + } + } Err(error) => { reader_stats.jit_helper_disconnects.fetch_add(1, Ordering::Relaxed); for (_, tx) in reader_pending.lock().unwrap().drain() { let _ = tx.send(Err(error.clone())); } + for (_, tx) in reader_pending_pauses.lock().unwrap().drain() { + let _ = tx.send(Err(error.clone())); + } break; } } @@ -229,6 +242,7 @@ impl HelperProcessInner { child: Mutex::new(child), io: Mutex::new(HelperIo { stdin }), pending, + pending_pauses, reader: Mutex::new(Some(reader)), next_job_id: AtomicU64::new(0), shutdown_timeout: config.tuning.shutdown_timeout, @@ -314,11 +328,44 @@ impl HelperProcessInner { } fn pause(&self) { + let id = self.next_job_id.fetch_add(1, Ordering::Relaxed); + let (tx, rx) = chan::bounded(1); + self.pending_pauses.lock().unwrap().insert(id, tx); + + let send_result = { + let mut io = self.io.lock().unwrap(); + write_pause(&mut io.stdin, id).and_then(|()| io.stdin.flush()) + }; + + if let Err(err) = send_result { + self.pending_pauses.lock().unwrap().remove(&id); + warn!(%err, "failed to request graceful JIT helper pause"); + } else { + match rx.recv_timeout(HELPER_PAUSE_TIMEOUT) { + Ok(Ok(())) => {} + Ok(Err(err)) => warn!(%err, "JIT helper graceful pause failed"), + Err(chan::RecvTimeoutError::Timeout) => { + self.pending_pauses.lock().unwrap().remove(&id); + warn!(timeout = ?HELPER_PAUSE_TIMEOUT, "timed out waiting for JIT helper pause"); + } + Err(chan::RecvTimeoutError::Disconnected) => { + warn!("JIT helper disconnected before pause acknowledgement"); + } + } + } + self.signal(libc::SIGSTOP, "pause"); } fn resume(&self) { self.signal(libc::SIGCONT, "resume"); + let send_result = { + let mut io = self.io.lock().unwrap(); + write_resume(&mut io.stdin).and_then(|()| io.stdin.flush()) + }; + if let Err(err) = send_result { + warn!(%err, "failed to request JIT helper resume"); + } } fn signal(&self, signal: libc::c_int, action: &str) { @@ -454,6 +501,8 @@ struct HelperInit { enum HelperRequest { Init(HelperInit), Compile(HelperCompile), + Pause { id: u64 }, + Resume, } #[derive(SchemaWrite, SchemaRead)] @@ -480,6 +529,9 @@ enum HelperResponse { error: String, timings: HelperTimings, }, + Paused { + id: u64, + }, } #[derive(Clone, Copy, Default, SchemaWrite, SchemaRead)] @@ -510,24 +562,41 @@ fn write_job( write_message(w, &req) } -fn read_helper_result( +fn write_pause(w: &mut BufWriter, id: u64) -> std::io::Result<()> { + write_message(w, &HelperRequest::Pause { id }) +} + +fn write_resume(w: &mut BufWriter) -> std::io::Result<()> { + write_message(w, &HelperRequest::Resume) +} + +enum HelperResponseMessage { + Job(u64, HelperJobResult), + Paused(u64), +} + +fn read_helper_response( r: &mut BufReader, -) -> Result<(u64, HelperJobResult), String> { +) -> Result { match read_message(r).map_err(|e| format!("failed to decode helper result: {e}"))? { - HelperResponse::Ok { id, symbol_name, object_bytes, builtin_symbols, timings } => Ok(( + HelperResponse::Ok { id, symbol_name, object_bytes, builtin_symbols, timings } => { + Ok(HelperResponseMessage::Job( + id, + HelperJobResult { + outcome: Ok(WorkerSuccess::JitObject(JitObjectSuccess { + symbol_name, + object_bytes: Bytes::from(object_bytes), + builtin_symbols, + })), + timings: timings.into(), + }, + )) + } + HelperResponse::Err { id, error, timings } => Ok(HelperResponseMessage::Job( id, - HelperJobResult { - outcome: Ok(WorkerSuccess::JitObject(JitObjectSuccess { - symbol_name, - object_bytes: Bytes::from(object_bytes), - builtin_symbols, - })), - timings: timings.into(), - }, + HelperJobResult { outcome: Err(error), timings: timings.into() }, )), - HelperResponse::Err { id, error, timings } => { - Ok((id, HelperJobResult { outcome: Err(error), timings: timings.into() })) - } + HelperResponse::Paused { id } => Ok(HelperResponseMessage::Paused(id)), } } @@ -561,6 +630,30 @@ thread_local! { static HELPER_COMPILER: RefCell> = const { RefCell::new(None) }; } +#[derive(Default)] +struct HelperPauseState { + paused: Mutex, + resumed: Condvar, +} + +impl HelperPauseState { + fn pause(&self) { + *self.paused.lock().unwrap() = true; + } + + fn resume(&self) { + *self.paused.lock().unwrap() = false; + self.resumed.notify_all(); + } + + fn wait_resumed(&self) { + let mut paused = self.paused.lock().unwrap(); + while *paused { + paused = self.resumed.wait(paused).unwrap(); + } + } +} + fn run_jit_helper_stdio() -> eyre::Result<()> { let mut stdin = BufReader::new(std::io::stdin().lock()); let config = Arc::new(read_helper_init(&mut stdin)?); @@ -573,27 +666,43 @@ fn run_jit_helper_stdio() -> eyre::Result<()> { }) .build()?; let stdout = Arc::new(Mutex::new(BufWriter::new(std::io::stdout()))); + let pause_state = Arc::new(HelperPauseState::default()); loop { - let (id, job) = match read_helper_job(&mut stdin) { - Ok(job) => job, + let request = match read_helper_request(&mut stdin) { + Ok(request) => request, Err(err) if is_unexpected_eof(&err) => break, Err(err) => return Err(err), }; - let config = Arc::clone(&config); - let stdout = Arc::clone(&stdout); - pool.spawn_fifo(move || { - let result = HELPER_COMPILER.with_borrow_mut(|compiler| { - compile_with_state(job, &config, CompilerTarget::JitObject, compiler) - }); - let mut stdout = stdout.lock().unwrap(); - if let Err(err) = write_helper_result(&mut stdout, id, result) - .and_then(|()| stdout.flush().map_err(Into::into)) - { - error!(%err, "failed to write helper result"); - std::process::exit(1); + match request { + HelperWork::Compile { id, job } => { + let config = Arc::clone(&config); + let stdout = Arc::clone(&stdout); + let pause_state = Arc::clone(&pause_state); + pool.spawn_fifo(move || { + let result = HELPER_COMPILER.with_borrow_mut(|compiler| { + compile_with_state(job, &config, CompilerTarget::JitObject, compiler) + }); + pause_state.wait_resumed(); + let mut stdout = stdout.lock().unwrap(); + if let Err(err) = write_helper_result(&mut stdout, id, result) + .and_then(|()| stdout.flush().map_err(Into::into)) + { + error!(%err, "failed to write helper result"); + std::process::exit(1); + } + }); } - }); + HelperWork::Pause { id } => { + pause_state.pause(); + let mut stdout = stdout.lock().unwrap(); + write_pause_ack(&mut stdout, id)?; + stdout.flush()?; + } + HelperWork::Resume => { + pause_state.resume(); + } + } } Ok(()) @@ -602,13 +711,23 @@ fn run_jit_helper_stdio() -> eyre::Result<()> { fn read_helper_init(stdin: &mut BufReader) -> eyre::Result { match read_message(stdin)? { HelperRequest::Init(init) => runtime_config_from_init(init), - HelperRequest::Compile(_) => eyre::bail!("JIT helper received job before init"), + HelperRequest::Compile(_) | HelperRequest::Pause { .. } | HelperRequest::Resume => { + eyre::bail!("JIT helper received request before init") + } } } -fn read_helper_job(stdin: &mut BufReader) -> eyre::Result<(u64, CompileJob)> { +enum HelperWork { + Compile { id: u64, job: CompileJob }, + Pause { id: u64 }, + Resume, +} + +fn read_helper_request(stdin: &mut BufReader) -> eyre::Result { let req = match read_message(stdin)? { HelperRequest::Compile(req) => req, + HelperRequest::Pause { id } => return Ok(HelperWork::Pause { id }), + HelperRequest::Resume => return Ok(HelperWork::Resume), HelperRequest::Init(_) => eyre::bail!("JIT helper received duplicate init"), }; let spec_id = SpecId::try_from_u8(req.spec_id).ok_or_else(|| eyre::eyre!("invalid spec id"))?; @@ -623,7 +742,7 @@ fn read_helper_job(stdin: &mut BufReader) -> eyre::Result<( sync_notifier: SyncNotifier::none(), generation: 0, }; - Ok((req.id, job)) + Ok(HelperWork::Compile { id: req.id, job }) } fn write_helper_result( @@ -647,6 +766,11 @@ fn write_helper_result( Ok(()) } +fn write_pause_ack(stdout: &mut BufWriter, id: u64) -> eyre::Result<()> { + write_message(stdout, &HelperResponse::Paused { id })?; + Ok(()) +} + fn write_message(w: &mut BufWriter, message: &T) -> std::io::Result<()> where T: wincode::SchemaWrite, From 668dbb9398d64ed206226782c723348ad119e768 Mon Sep 17 00:00:00 2001 From: DaniPopes <57450786+DaniPopes@users.noreply.github.com> Date: Tue, 19 May 2026 22:05:21 +0200 Subject: [PATCH 42/43] test(runtime): cover jit helper pause --- .../src/runtime/out_of_process.rs | 50 +++++++++++++++++++ 1 file changed, 50 insertions(+) diff --git a/crates/revmc-runtime/src/runtime/out_of_process.rs b/crates/revmc-runtime/src/runtime/out_of_process.rs index d0f4bab49..b3f24d350 100644 --- a/crates/revmc-runtime/src/runtime/out_of_process.rs +++ b/crates/revmc-runtime/src/runtime/out_of_process.rs @@ -875,3 +875,53 @@ fn runtime_config_from_init(init: HelperInit) -> eyre::Result { config.tuning.compiler_recycle_threshold = init.compiler_recycle_threshold; Ok(config) } + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn helper_pause_state_blocks_until_resume() { + let pause_state = Arc::new(HelperPauseState::default()); + pause_state.pause(); + + let (tx, rx) = chan::bounded(1); + let thread_pause_state = Arc::clone(&pause_state); + let thread = std::thread::spawn(move || { + thread_pause_state.wait_resumed(); + tx.send(()).unwrap(); + }); + + assert!(rx.recv_timeout(Duration::from_millis(50)).is_err()); + pause_state.resume(); + rx.recv_timeout(Duration::from_secs(1)).unwrap(); + thread.join().unwrap(); + } + + #[test] + fn helper_pause_protocol_roundtrips() { + let mut request = Vec::new(); + { + let mut writer = BufWriter::new(&mut request); + write_pause(&mut writer, 42).unwrap(); + writer.flush().unwrap(); + } + let mut reader = BufReader::new(request.as_slice()); + match read_helper_request(&mut reader).unwrap() { + HelperWork::Pause { id } => assert_eq!(id, 42), + HelperWork::Compile { .. } | HelperWork::Resume => panic!("unexpected helper request"), + } + + let mut response = Vec::new(); + { + let mut writer = BufWriter::new(&mut response); + write_pause_ack(&mut writer, 42).unwrap(); + writer.flush().unwrap(); + } + let mut reader = BufReader::new(response.as_slice()); + match read_helper_response(&mut reader).unwrap() { + HelperResponseMessage::Paused(id) => assert_eq!(id, 42), + HelperResponseMessage::Job(..) => panic!("unexpected helper response"), + } + } +} From 4e6efca65a340ff2a5463196176b93c39f67b035 Mon Sep 17 00:00:00 2001 From: DaniPopes <57450786+DaniPopes@users.noreply.github.com> Date: Wed, 20 May 2026 00:54:13 +0200 Subject: [PATCH 43/43] feat(runtime): track jit helper pause metrics --- .../src/runtime/out_of_process.rs | 33 +++++++++++++++++-- crates/revmc-runtime/src/runtime/stats.rs | 32 ++++++++++++++++++ 2 files changed, 63 insertions(+), 2 deletions(-) diff --git a/crates/revmc-runtime/src/runtime/out_of_process.rs b/crates/revmc-runtime/src/runtime/out_of_process.rs index b3f24d350..556b1125f 100644 --- a/crates/revmc-runtime/src/runtime/out_of_process.rs +++ b/crates/revmc-runtime/src/runtime/out_of_process.rs @@ -149,6 +149,7 @@ impl HelperProcess { } pub(super) fn pause(&self) { + self.stats.jit_helper_pause_requests.fetch_add(1, Ordering::Relaxed); self.paused.store(true, Ordering::Relaxed); if let Some(helper) = self.inner.lock().unwrap().as_ref() { helper.pause(); @@ -156,6 +157,7 @@ impl HelperProcess { } pub(super) fn resume(&self) { + self.stats.jit_helper_resume_requests.fetch_add(1, Ordering::Relaxed); self.paused.store(false, Ordering::Relaxed); if let Some(helper) = self.inner.lock().unwrap().as_ref() { helper.resume(); @@ -339,16 +341,24 @@ impl HelperProcessInner { if let Err(err) = send_result { self.pending_pauses.lock().unwrap().remove(&id); + self.stats.jit_helper_pause_failures.fetch_add(1, Ordering::Relaxed); warn!(%err, "failed to request graceful JIT helper pause"); } else { match rx.recv_timeout(HELPER_PAUSE_TIMEOUT) { - Ok(Ok(())) => {} - Ok(Err(err)) => warn!(%err, "JIT helper graceful pause failed"), + Ok(Ok(())) => { + self.stats.jit_helper_pause_acknowledgements.fetch_add(1, Ordering::Relaxed); + } + Ok(Err(err)) => { + self.stats.jit_helper_pause_failures.fetch_add(1, Ordering::Relaxed); + warn!(%err, "JIT helper graceful pause failed"); + } Err(chan::RecvTimeoutError::Timeout) => { self.pending_pauses.lock().unwrap().remove(&id); + self.stats.jit_helper_pause_timeouts.fetch_add(1, Ordering::Relaxed); warn!(timeout = ?HELPER_PAUSE_TIMEOUT, "timed out waiting for JIT helper pause"); } Err(chan::RecvTimeoutError::Disconnected) => { + self.stats.jit_helper_pause_failures.fetch_add(1, Ordering::Relaxed); warn!("JIT helper disconnected before pause acknowledgement"); } } @@ -364,6 +374,7 @@ impl HelperProcessInner { write_resume(&mut io.stdin).and_then(|()| io.stdin.flush()) }; if let Err(err) = send_result { + self.stats.jit_helper_resume_failures.fetch_add(1, Ordering::Relaxed); warn!(%err, "failed to request JIT helper resume"); } } @@ -924,4 +935,22 @@ mod tests { HelperResponseMessage::Job(..) => panic!("unexpected helper response"), } } + + #[test] + fn helper_pause_resume_requests_update_stats() { + let stats = Arc::new(RuntimeStats::default()); + let helper = HelperProcess::new(Arc::clone(&stats)); + + helper.pause(); + helper.pause(); + helper.resume(); + + let snapshot = stats.snapshot(crate::runtime::stats::RuntimeStatsGauges::default()); + assert_eq!(snapshot.jit_helper_pause_requests, 2); + assert_eq!(snapshot.jit_helper_pause_acknowledgements, 0); + assert_eq!(snapshot.jit_helper_pause_failures, 0); + assert_eq!(snapshot.jit_helper_pause_timeouts, 0); + assert_eq!(snapshot.jit_helper_resume_requests, 1); + assert_eq!(snapshot.jit_helper_resume_failures, 0); + } } diff --git a/crates/revmc-runtime/src/runtime/stats.rs b/crates/revmc-runtime/src/runtime/stats.rs index 49a79a56c..b8d1a9700 100644 --- a/crates/revmc-runtime/src/runtime/stats.rs +++ b/crates/revmc-runtime/src/runtime/stats.rs @@ -29,6 +29,18 @@ pub(crate) struct RuntimeStats { pub(crate) jit_helper_timeouts: AtomicU64, /// Total number of out-of-process JIT helper disconnects. pub(crate) jit_helper_disconnects: AtomicU64, + /// Total number of out-of-process JIT helper pause requests. + pub(crate) jit_helper_pause_requests: AtomicU64, + /// Total number of graceful out-of-process JIT helper pause acknowledgements. + pub(crate) jit_helper_pause_acknowledgements: AtomicU64, + /// Total number of graceful out-of-process JIT helper pause failures. + pub(crate) jit_helper_pause_failures: AtomicU64, + /// Total number of graceful out-of-process JIT helper pause acknowledgement timeouts. + pub(crate) jit_helper_pause_timeouts: AtomicU64, + /// Total number of out-of-process JIT helper resume requests. + pub(crate) jit_helper_resume_requests: AtomicU64, + /// Total number of out-of-process JIT helper resume request failures. + pub(crate) jit_helper_resume_failures: AtomicU64, } /// Gauge values sampled at snapshot time. @@ -84,6 +96,18 @@ pub struct RuntimeStatsSnapshot { pub jit_helper_timeouts: u64, /// Total number of out-of-process JIT helper disconnects. pub jit_helper_disconnects: u64, + /// Total number of out-of-process JIT helper pause requests. + pub jit_helper_pause_requests: u64, + /// Total number of graceful out-of-process JIT helper pause acknowledgements. + pub jit_helper_pause_acknowledgements: u64, + /// Total number of graceful out-of-process JIT helper pause failures. + pub jit_helper_pause_failures: u64, + /// Total number of graceful out-of-process JIT helper pause acknowledgement timeouts. + pub jit_helper_pause_timeouts: u64, + /// Total number of out-of-process JIT helper resume requests. + pub jit_helper_resume_requests: u64, + /// Total number of out-of-process JIT helper resume request failures. + pub jit_helper_resume_failures: u64, } impl RuntimeStatsSnapshot { @@ -126,6 +150,14 @@ impl RuntimeStats { jit_helper_restarts: self.jit_helper_restarts.load(Ordering::Relaxed), jit_helper_timeouts: self.jit_helper_timeouts.load(Ordering::Relaxed), jit_helper_disconnects: self.jit_helper_disconnects.load(Ordering::Relaxed), + jit_helper_pause_requests: self.jit_helper_pause_requests.load(Ordering::Relaxed), + jit_helper_pause_acknowledgements: self + .jit_helper_pause_acknowledgements + .load(Ordering::Relaxed), + jit_helper_pause_failures: self.jit_helper_pause_failures.load(Ordering::Relaxed), + jit_helper_pause_timeouts: self.jit_helper_pause_timeouts.load(Ordering::Relaxed), + jit_helper_resume_requests: self.jit_helper_resume_requests.load(Ordering::Relaxed), + jit_helper_resume_failures: self.jit_helper_resume_failures.load(Ordering::Relaxed), } } }