1use crate::profile::Profile;
2use std::sync::atomic::{AtomicBool, Ordering};
3use std::sync::Arc;
4use std::time::Duration;
5use wasmtime::{Config, Engine, Module, Store};
6
7const GC_HEAP_INITIAL_BYTES: u64 = 1 << 30;
10const HOST_STACK_BYTES: usize = 16 << 20;
13
14static WASM_STACK_BYTES: std::sync::atomic::AtomicUsize = std::sync::atomic::AtomicUsize::new(0);
16
17pub fn deterministic_config() -> Config {
22 let mut config = Config::new();
23 config.wasm_gc(true);
24 config.wasm_function_references(true);
25 config.gc_heap_initial_size(GC_HEAP_INITIAL_BYTES);
26 #[cfg(feature = "compiler")]
29 config.wasm_component_model(false);
30 config.concurrency_support(false);
31 let wasm_stack = wasm_stack_bytes();
32 if wasm_stack > 0 {
33 config.async_stack_size(wasm_stack).max_wasm_stack(wasm_stack);
35 }
36 config
37}
38
39pub fn wasm_stack_bytes() -> usize {
42 WASM_STACK_BYTES.load(std::sync::atomic::Ordering::Relaxed)
43}
44
45pub fn give_main_stack_to_wasm() {
49 let wasm_stack = main_stack_bytes().saturating_sub(HOST_STACK_BYTES);
50 if wasm_stack > 512 << 10 {
51 WASM_STACK_BYTES.store(wasm_stack, std::sync::atomic::Ordering::Relaxed);
52 }
53}
54
55#[cfg(target_os = "macos")]
56fn main_stack_bytes() -> usize {
57 unsafe { libc::pthread_get_stacksize_np(libc::pthread_self()) }
59}
60
61#[cfg(all(unix, not(target_os = "macos")))]
62fn main_stack_bytes() -> usize {
63 let mut limit = libc::rlimit { rlim_cur: 0, rlim_max: 0 };
64 let known = unsafe { libc::getrlimit(libc::RLIMIT_STACK, &mut limit) } == 0;
66 match known {
67 true if limit.rlim_cur != libc::RLIM_INFINITY => limit.rlim_cur as usize,
68 _ => 0,
69 }
70}
71
72#[cfg(not(unix))]
73fn main_stack_bytes() -> usize {
74 0
75}
76
77pub fn metered_config(fuel: bool) -> Config {
81 let mut config = deterministic_config();
82 config.epoch_interruption(true);
83 config.consume_fuel(fuel);
84 config
85}
86
87pub const EPOCH_TICK: Duration = Duration::from_millis(10);
89
90pub fn ticking(engine: Engine) -> Engine {
93 let ticked = engine.clone();
94 std::thread::spawn(move || loop {
95 std::thread::sleep(EPOCH_TICK);
96 ticked.increment_epoch();
97 });
98 engine
99}
100
101pub fn limited_store<T: 'static>(engine: &Engine, data: T, fuel: u64, time: Duration) -> Store<T> {
105 checked_store(engine, data, fuel, time, || Ok(()))
106}
107
108pub fn checked_store<T: 'static>(
111 engine: &Engine,
112 data: T,
113 fuel: u64,
114 time: Duration,
115 checkpoint: impl FnMut() -> wasmtime::Result<()> + Send + Sync + 'static,
116) -> Store<T> {
117 store_with(engine, data, fuel, time, checkpoint, None)
118}
119
120pub fn profiled_store<T: 'static>(engine: &Engine, data: T, fuel: u64, time: Duration, module: &Module) -> Store<T> {
122 store_with(engine, data, fuel, time, || Ok(()), Profile::of(engine, module))
123}
124
125fn store_with<T: 'static>(
126 engine: &Engine,
127 data: T,
128 fuel: u64,
129 time: Duration,
130 mut checkpoint: impl FnMut() -> wasmtime::Result<()> + Send + Sync + 'static,
131 profile: Option<Profile>,
132) -> Store<T> {
133 let mut store = Store::new(engine, data);
134 let mut clock = store.set_fuel(fuel).is_err().then(|| ComputingClock::new(time));
135 let new_turn = clock.as_ref().map(|clock| clock.new_turn.clone());
136 if new_turn.is_some() || profile.is_some() {
137 let hook_profile = profile.clone();
138 store.call_hook(move |store, hook| {
139 if let (Some(new_turn), wasmtime::CallHook::CallingWasm) = (&new_turn, hook) {
140 new_turn.store(true, Ordering::Relaxed);
141 }
142 if let Some(profile) = &hook_profile {
143 profile.call_hook(&store, hook);
144 }
145 Ok(())
146 });
147 }
148 store.epoch_deadline_callback(move |store| {
149 if let Some(profile) = &profile {
150 profile.sample(&store);
151 }
152 checkpoint()?;
153 if let Some(clock) = clock.as_mut() {
154 clock.check()?;
155 }
156 Ok(wasmtime::UpdateDeadline::Continue(1))
157 });
158 store.set_epoch_deadline(1);
159 cap_memory(&mut store);
160 store
161}
162
163#[derive(Debug)]
165pub struct OutOfTime(pub Duration);
166
167impl std::fmt::Display for OutOfTime {
168 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
169 let variable = crate::fuel::TIME_LIMIT_VARIABLE;
170 write!(f, "out of time after {:?} of computing in one turn (give it more with {variable}=<seconds>)", self.0)
171 }
172}
173
174impl std::error::Error for OutOfTime {}
175
176struct ComputingClock {
178 limit: Duration,
179 started: Option<Duration>,
180 new_turn: Arc<AtomicBool>,
182}
183
184impl ComputingClock {
185 fn new(limit: Duration) -> Self {
186 ComputingClock { limit, started: None, new_turn: Arc::default() }
187 }
188
189 fn check(&mut self) -> wasmtime::Result<()> {
190 let now = thread_cpu_time();
191 if self.new_turn.swap(false, Ordering::Relaxed) {
192 self.started = None;
193 }
194 match now.saturating_sub(*self.started.get_or_insert(now)) > self.limit {
195 true => Err(OutOfTime(self.limit).into()),
196 false => Ok(()),
197 }
198 }
199}
200
201#[cfg(unix)]
202pub(crate) fn thread_cpu_time() -> Duration {
203 let mut time = libc::timespec { tv_sec: 0, tv_nsec: 0 };
204 unsafe { libc::clock_gettime(libc::CLOCK_THREAD_CPUTIME_ID, &mut time) };
206 Duration::new(time.tv_sec as u64, time.tv_nsec as u32)
207}
208
209#[cfg(not(unix))]
211pub(crate) fn thread_cpu_time() -> Duration {
212 static START: std::sync::OnceLock<std::time::Instant> = std::sync::OnceLock::new();
213 START.get_or_init(std::time::Instant::now).elapsed()
214}
215
216pub const DEFAULT_MEMORY_CAP_MB: usize = 2048;
219pub const MEMORY_CAP_VARIABLE: &str = "WARP_MEMORY_CAP_MB";
221const BYTES_PER_MB: usize = 1 << 20;
222
223pub fn memory_cap_bytes() -> usize {
225 static CAP: std::sync::OnceLock<usize> = std::sync::OnceLock::new();
226 *CAP.get_or_init(|| {
227 let megabytes = std::env::var(MEMORY_CAP_VARIABLE).ok().and_then(|value| value.trim().parse().ok());
228 megabytes.unwrap_or(DEFAULT_MEMORY_CAP_MB).saturating_mul(BYTES_PER_MB)
229 })
230}
231
232pub fn cap_memory<T>(store: &mut Store<T>) {
235 store.limiter(|_| Box::leak(Box::new(MemoryCap)));
237}
238
239struct MemoryCap;
240
241impl wasmtime::ResourceLimiter for MemoryCap {
242 fn memory_growing(&mut self, _current: usize, desired: usize, maximum: Option<usize>) -> wasmtime::Result<bool> {
243 let cap = memory_cap_bytes();
244 if desired > cap {
245 let refusal = format!(
246 "memory cap: growing a memory to {} MB passes the cap of {} MB ({MEMORY_CAP_VARIABLE})",
247 desired / BYTES_PER_MB,
248 cap / BYTES_PER_MB
249 );
250 eprintln!("{refusal}");
252 wasmtime::bail!(refusal);
253 }
254 Ok(maximum.is_none_or(|maximum| desired <= maximum))
255 }
256
257 fn table_growing(&mut self, _current: usize, desired: usize, maximum: Option<usize>) -> wasmtime::Result<bool> {
258 Ok(maximum.is_none_or(|maximum| desired <= maximum))
259 }
260}