#[cfg(not(all(target_arch = "wasm32", target_os = "unknown")))]
use std::time::Instant;
use std::{
cell::Cell,
error::Error,
fmt,
sync::atomic::{AtomicUsize, Ordering},
time::Duration,
};
#[cfg(all(target_arch = "wasm32", target_os = "unknown"))]
use web_time::Instant;
pub const OOM_EXIT_CODE: i32 = 65;
pub static LIVE_MEMORY: AtomicUsize = AtomicUsize::new(0);
pub static BASELINE_MEMORY: AtomicUsize = AtomicUsize::new(usize::MAX);
pub const LARGE_RESULT_THRESHOLD: usize = 100_000;
#[derive(Debug, Clone)]
pub enum ResourceError {
Time { limit: Duration, elapsed: Duration },
Memory { limit: usize, used: usize },
Recursion { limit: usize, depth: usize },
}
impl fmt::Display for ResourceError {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
match self {
Self::Time { limit, elapsed } => {
write!(f, "time limit exceeded: {elapsed:?} > {limit:?}")
}
Self::Memory { limit, used } => {
write!(f, "memory limit exceeded: {used} bytes > {limit} bytes")
}
Self::Recursion { .. } => {
write!(f, "maximum recursion depth exceeded")
}
}
}
}
impl Error for ResourceError {}
#[derive(Debug, Clone, serde::Serialize, serde::Deserialize)]
pub struct ResourceLimits {
pub max_duration: Option<Duration>,
pub max_memory: Option<usize>,
pub gc_interval: Option<usize>,
pub max_recursion_depth: usize,
}
pub const DEFAULT_MAX_RECURSION_DEPTH: usize = 1000;
impl Default for ResourceLimits {
fn default() -> Self {
Self {
max_duration: None,
max_memory: None,
gc_interval: None,
max_recursion_depth: DEFAULT_MAX_RECURSION_DEPTH,
}
}
}
impl ResourceLimits {
#[must_use]
pub fn max_duration(mut self, limit: Duration) -> Self {
self.max_duration = Some(limit);
self
}
#[must_use]
pub fn max_memory(mut self, limit: usize) -> Self {
self.max_memory = Some(limit);
self
}
#[must_use]
pub fn gc_interval(mut self, interval: usize) -> Self {
self.gc_interval = Some(interval);
self
}
#[must_use]
pub fn max_recursion_depth(mut self, limit: usize) -> Self {
self.max_recursion_depth = limit;
self
}
}
const TIME_CHECK_INTERVAL: u16 = 10;
#[derive(Debug, serde::Serialize, serde::Deserialize)]
pub struct ResourceTracker {
limits: ResourceLimits,
#[serde(default)]
total_execution_time: Cell<Duration>,
#[serde(skip)]
running_since: Cell<Option<Instant>>,
check_counter: Cell<u16>,
#[serde(default)]
recursion_limit_override: Cell<Option<usize>>,
}
impl Default for ResourceTracker {
fn default() -> Self {
Self::new(ResourceLimits::default())
}
}
impl ResourceTracker {
#[must_use]
pub fn new(limits: ResourceLimits) -> Self {
Self {
limits,
total_execution_time: Cell::new(Duration::ZERO),
running_since: Cell::new(None),
check_counter: Cell::new(0),
recursion_limit_override: Cell::new(None),
}
}
#[inline]
fn active_recursion_limit(&self) -> usize {
self.recursion_limit_override
.get()
.unwrap_or(self.limits.max_recursion_depth)
}
#[must_use]
pub fn elapsed(&self) -> Duration {
let running = self.running_since.get().map_or(Duration::ZERO, |t| t.elapsed());
self.total_execution_time.get() + running
}
#[must_use]
pub fn max_duration(&self) -> Option<Duration> {
self.limits.max_duration
}
#[must_use]
pub fn max_memory(&self) -> Option<usize> {
self.limits.max_memory
}
pub fn set_max_duration(&mut self, duration: Duration) {
self.limits.max_duration = Some(duration);
self.total_execution_time.set(Duration::ZERO);
}
#[inline]
pub fn check_allocation(&self, additional: usize) -> Result<(), ResourceError> {
if let Some(limit) = self.limits.max_memory {
let used = probe_memory().saturating_add(additional);
if used > limit {
return Err(ResourceError::Memory { limit, used });
}
}
Ok(())
}
#[inline]
pub fn check_time(&self) -> Result<(), ResourceError> {
if let Some(limit) = self.limits.max_memory {
let used = probe_memory();
if used > limit {
return Err(ResourceError::Memory { limit, used });
}
}
if let Some(max) = self.limits.max_duration {
self.check_counter.update(|c| c.wrapping_add(1));
if self.check_counter.get().is_multiple_of(TIME_CHECK_INTERVAL) {
let elapsed = self.elapsed();
if elapsed > max {
self.check_counter.set(TIME_CHECK_INTERVAL.wrapping_sub(1));
return Err(ResourceError::Time { limit: max, elapsed });
}
}
}
Ok(())
}
#[inline]
pub fn check_recursion_depth(&self, current_depth: usize) -> Result<(), ResourceError> {
let limit = self.active_recursion_limit();
if current_depth >= limit {
return Err(ResourceError::Recursion {
limit,
depth: current_depth + 1,
});
}
Ok(())
}
#[inline]
pub fn check_large_result(&self, estimated_bytes: usize) -> Result<(), ResourceError> {
self.check_allocation(estimated_bytes)
}
#[must_use]
#[inline]
pub fn gc_interval(&self) -> Option<usize> {
self.limits.gc_interval
}
pub fn on_execution_start(&self) {
debug_assert!(
self.running_since.get().is_none(),
"nested on_execution_start: VM-internal re-entry must use the raw run loop, not run_external"
);
self.running_since.set(Some(Instant::now()));
}
pub fn on_execution_stop(&self) {
if let Some(started) = self.running_since.take() {
self.total_execution_time
.set(self.total_execution_time.get() + started.elapsed());
}
}
#[cfg(feature = "test-hooks")]
pub fn lower_recursion_limit(&self, new_limit: usize) -> Result<(), usize> {
let limit = self.active_recursion_limit();
if new_limit > limit {
return Err(limit);
}
self.recursion_limit_override.set(Some(new_limit));
Ok(())
}
}
fn probe_memory() -> usize {
LIVE_MEMORY
.load(Ordering::Relaxed)
.saturating_sub(BASELINE_MEMORY.load(Ordering::Relaxed))
}