#![allow(non_snake_case)]
use crate::RealIsolate;
use crate::isolate::NearHeapLimitCallback;
use crate::jsc::jsc_sys::*;
use std::collections::HashMap;
use std::os::raw::c_void;
use std::sync::atomic::{AtomicBool, AtomicUsize, Ordering};
use std::sync::{Arc, Mutex, OnceLock};
const WATCHDOG_INTERVAL_SECS: f64 = 0.25;
struct Watch {
group: JSContextGroupRef,
terminate: AtomicBool,
heap_limit: AtomicUsize,
initial_heap_limit: AtomicUsize,
heap_cb: AtomicUsize,
heap_cb_data: AtomicUsize,
in_heap_cb: AtomicBool,
}
unsafe impl Send for Watch {}
unsafe impl Sync for Watch {}
fn registry() -> &'static Mutex<HashMap<usize, Arc<Watch>>> {
static REG: OnceLock<Mutex<HashMap<usize, Arc<Watch>>>> = OnceLock::new();
REG.get_or_init(|| Mutex::new(HashMap::new()))
}
fn lookup(iso: *mut RealIsolate) -> Option<Arc<Watch>> {
if iso.is_null() {
return None;
}
registry().lock().ok()?.get(&(iso as usize)).cloned()
}
type ShouldTerminateCb =
unsafe extern "C" fn(ctx: JSContextRef, context: *mut c_void) -> bool;
unsafe extern "C" {
fn JSContextGroupSetExecutionTimeLimit(
group: JSContextGroupRef,
limit: f64,
callback: ShouldTerminateCb,
context: *mut c_void,
);
fn JSContextGroupClearExecutionTimeLimit(group: JSContextGroupRef);
}
unsafe extern "C" fn watchdog(ctx: JSContextRef, context: *mut c_void) -> bool {
if context.is_null() {
return false;
}
let watch = unsafe { &*(context as *const Watch) };
std::panic::catch_unwind(std::panic::AssertUnwindSafe(|| {
maybe_drive_heap_callback(watch, ctx);
watch.terminate.load(Ordering::SeqCst)
}))
.unwrap_or(false)
}
fn maybe_drive_heap_callback(watch: &Watch, _ctx: JSContextRef) {
let cb_addr = watch.heap_cb.load(Ordering::Acquire);
if cb_addr == 0 || watch.in_heap_cb.load(Ordering::Relaxed) {
return;
}
let limit = watch.heap_limit.load(Ordering::Relaxed);
if limit == 0 {
return;
}
watch.in_heap_cb.store(true, Ordering::Relaxed);
let cb: NearHeapLimitCallback = unsafe { std::mem::transmute(cb_addr) };
let data = watch.heap_cb_data.load(Ordering::Relaxed) as *mut c_void;
let initial = watch.initial_heap_limit.load(Ordering::Relaxed);
let new_limit = unsafe { cb(data, limit, initial) };
if new_limit > limit {
watch.heap_limit.store(new_limit, Ordering::Relaxed);
}
watch.in_heap_cb.store(false, Ordering::Relaxed);
}
pub(crate) fn install(
iso: *mut RealIsolate,
group: JSContextGroupRef,
heap_limit: usize,
) {
let watch = Arc::new(Watch {
group,
terminate: AtomicBool::new(false),
heap_limit: AtomicUsize::new(heap_limit),
initial_heap_limit: AtomicUsize::new(heap_limit),
heap_cb: AtomicUsize::new(0),
heap_cb_data: AtomicUsize::new(0),
in_heap_cb: AtomicBool::new(false),
});
let context = Arc::as_ptr(&watch) as *mut c_void;
unsafe {
JSContextGroupSetExecutionTimeLimit(
group,
WATCHDOG_INTERVAL_SECS,
watchdog,
context,
)
};
if let Ok(mut reg) = registry().lock() {
reg.insert(iso as usize, watch);
}
}
pub(crate) fn uninstall(iso: *mut RealIsolate) {
let Ok(mut reg) = registry().lock() else {
return;
};
if let Some(watch) = reg.remove(&(iso as usize)) {
unsafe { JSContextGroupClearExecutionTimeLimit(watch.group) };
}
}
pub(crate) fn request_terminate(iso: *mut RealIsolate) {
if let Some(w) = lookup(iso) {
w.terminate.store(true, Ordering::SeqCst);
}
}
pub(crate) fn cancel_terminate(iso: *mut RealIsolate) {
if let Some(w) = lookup(iso) {
w.terminate.store(false, Ordering::SeqCst);
}
}
pub(crate) fn is_terminating(iso: *mut RealIsolate) -> bool {
lookup(iso).is_some_and(|w| w.terminate.load(Ordering::SeqCst))
}
pub(crate) fn set_heap_callback(
iso: *mut RealIsolate,
callback: NearHeapLimitCallback,
data: *mut c_void,
) {
if let Some(w) = lookup(iso) {
w.heap_cb_data.store(data as usize, Ordering::Relaxed);
w.heap_cb.store(callback as usize, Ordering::Release);
if w.heap_limit.load(Ordering::Relaxed) == 0 {
const DEFAULT_LIMIT: usize = 16 * 1024 * 1024;
w.heap_limit.store(DEFAULT_LIMIT, Ordering::Relaxed);
w.initial_heap_limit.store(DEFAULT_LIMIT, Ordering::Relaxed);
}
}
}
pub(crate) fn clear_heap_callback(iso: *mut RealIsolate) {
if let Some(w) = lookup(iso) {
w.heap_cb.store(0, Ordering::Release);
w.heap_cb_data.store(0, Ordering::Relaxed);
}
}