use crate::profile::Profile;
use std::sync::atomic::{AtomicBool, Ordering};
use std::sync::Arc;
use std::time::Duration;
use wasmtime::{Config, Engine, Module, Store};
const GC_HEAP_INITIAL_BYTES: u64 = 1 << 30;
const HOST_STACK_BYTES: usize = 16 << 20;
static WASM_STACK_BYTES: std::sync::atomic::AtomicUsize = std::sync::atomic::AtomicUsize::new(0);
pub fn deterministic_config() -> Config {
let mut config = Config::new();
config.wasm_gc(true);
config.wasm_function_references(true);
config.gc_heap_initial_size(GC_HEAP_INITIAL_BYTES);
#[cfg(feature = "compiler")]
config.wasm_component_model(false);
config.concurrency_support(false);
let wasm_stack = wasm_stack_bytes();
if wasm_stack > 0 {
config.async_stack_size(wasm_stack).max_wasm_stack(wasm_stack);
}
config
}
pub fn wasm_stack_bytes() -> usize {
WASM_STACK_BYTES.load(std::sync::atomic::Ordering::Relaxed)
}
pub fn give_main_stack_to_wasm() {
let wasm_stack = main_stack_bytes().saturating_sub(HOST_STACK_BYTES);
if wasm_stack > 512 << 10 {
WASM_STACK_BYTES.store(wasm_stack, std::sync::atomic::Ordering::Relaxed);
}
}
#[cfg(target_os = "macos")]
fn main_stack_bytes() -> usize {
unsafe { libc::pthread_get_stacksize_np(libc::pthread_self()) }
}
#[cfg(all(unix, not(target_os = "macos")))]
fn main_stack_bytes() -> usize {
let mut limit = libc::rlimit { rlim_cur: 0, rlim_max: 0 };
let known = unsafe { libc::getrlimit(libc::RLIMIT_STACK, &mut limit) } == 0;
match known {
true if limit.rlim_cur != libc::RLIM_INFINITY => limit.rlim_cur as usize,
_ => 0,
}
}
#[cfg(not(unix))]
fn main_stack_bytes() -> usize {
0
}
pub fn metered_config(fuel: bool) -> Config {
let mut config = deterministic_config();
config.epoch_interruption(true);
config.consume_fuel(fuel);
config
}
pub const EPOCH_TICK: Duration = Duration::from_millis(10);
pub fn ticking(engine: Engine) -> Engine {
let ticked = engine.clone();
std::thread::spawn(move || loop {
std::thread::sleep(EPOCH_TICK);
ticked.increment_epoch();
});
engine
}
pub fn limited_store<T: 'static>(engine: &Engine, data: T, fuel: u64, time: Duration) -> Store<T> {
checked_store(engine, data, fuel, time, || Ok(()))
}
pub fn checked_store<T: 'static>(
engine: &Engine,
data: T,
fuel: u64,
time: Duration,
checkpoint: impl FnMut() -> wasmtime::Result<()> + Send + Sync + 'static,
) -> Store<T> {
store_with(engine, data, fuel, time, checkpoint, None)
}
pub fn profiled_store<T: 'static>(engine: &Engine, data: T, fuel: u64, time: Duration, module: &Module) -> Store<T> {
store_with(engine, data, fuel, time, || Ok(()), Profile::of(engine, module))
}
fn store_with<T: 'static>(
engine: &Engine,
data: T,
fuel: u64,
time: Duration,
mut checkpoint: impl FnMut() -> wasmtime::Result<()> + Send + Sync + 'static,
profile: Option<Profile>,
) -> Store<T> {
let mut store = Store::new(engine, data);
let mut clock = store.set_fuel(fuel).is_err().then(|| ComputingClock::new(time));
let new_turn = clock.as_ref().map(|clock| clock.new_turn.clone());
if new_turn.is_some() || profile.is_some() {
let hook_profile = profile.clone();
store.call_hook(move |store, hook| {
if let (Some(new_turn), wasmtime::CallHook::CallingWasm) = (&new_turn, hook) {
new_turn.store(true, Ordering::Relaxed);
}
if let Some(profile) = &hook_profile {
profile.call_hook(&store, hook);
}
Ok(())
});
}
store.epoch_deadline_callback(move |store| {
if let Some(profile) = &profile {
profile.sample(&store);
}
checkpoint()?;
if let Some(clock) = clock.as_mut() {
clock.check()?;
}
Ok(wasmtime::UpdateDeadline::Continue(1))
});
store.set_epoch_deadline(1);
cap_memory(&mut store);
store
}
#[derive(Debug)]
pub struct OutOfTime(pub Duration);
impl std::fmt::Display for OutOfTime {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
let variable = crate::fuel::TIME_LIMIT_VARIABLE;
write!(f, "out of time after {:?} of computing in one turn (give it more with {variable}=<seconds>)", self.0)
}
}
impl std::error::Error for OutOfTime {}
struct ComputingClock {
limit: Duration,
started: Option<Duration>,
new_turn: Arc<AtomicBool>,
}
impl ComputingClock {
fn new(limit: Duration) -> Self {
ComputingClock { limit, started: None, new_turn: Arc::default() }
}
fn check(&mut self) -> wasmtime::Result<()> {
let now = thread_cpu_time();
if self.new_turn.swap(false, Ordering::Relaxed) {
self.started = None;
}
match now.saturating_sub(*self.started.get_or_insert(now)) > self.limit {
true => Err(OutOfTime(self.limit).into()),
false => Ok(()),
}
}
}
#[cfg(unix)]
pub(crate) fn thread_cpu_time() -> Duration {
let mut time = libc::timespec { tv_sec: 0, tv_nsec: 0 };
unsafe { libc::clock_gettime(libc::CLOCK_THREAD_CPUTIME_ID, &mut time) };
Duration::new(time.tv_sec as u64, time.tv_nsec as u32)
}
#[cfg(not(unix))]
pub(crate) fn thread_cpu_time() -> Duration {
static START: std::sync::OnceLock<std::time::Instant> = std::sync::OnceLock::new();
START.get_or_init(std::time::Instant::now).elapsed()
}
pub const DEFAULT_MEMORY_CAP_MB: usize = 2048;
pub const MEMORY_CAP_VARIABLE: &str = "WARP_MEMORY_CAP_MB";
const BYTES_PER_MB: usize = 1 << 20;
pub fn memory_cap_bytes() -> usize {
static CAP: std::sync::OnceLock<usize> = std::sync::OnceLock::new();
*CAP.get_or_init(|| {
let megabytes = std::env::var(MEMORY_CAP_VARIABLE).ok().and_then(|value| value.trim().parse().ok());
megabytes.unwrap_or(DEFAULT_MEMORY_CAP_MB).saturating_mul(BYTES_PER_MB)
})
}
pub fn cap_memory<T>(store: &mut Store<T>) {
store.limiter(|_| Box::leak(Box::new(MemoryCap)));
}
struct MemoryCap;
impl wasmtime::ResourceLimiter for MemoryCap {
fn memory_growing(&mut self, _current: usize, desired: usize, maximum: Option<usize>) -> wasmtime::Result<bool> {
let cap = memory_cap_bytes();
if desired > cap {
let refusal = format!(
"memory cap: growing a memory to {} MB passes the cap of {} MB ({MEMORY_CAP_VARIABLE})",
desired / BYTES_PER_MB,
cap / BYTES_PER_MB
);
eprintln!("{refusal}");
wasmtime::bail!(refusal);
}
Ok(maximum.is_none_or(|maximum| desired <= maximum))
}
fn table_growing(&mut self, _current: usize, desired: usize, maximum: Option<usize>) -> wasmtime::Result<bool> {
Ok(maximum.is_none_or(|maximum| desired <= maximum))
}
}