use std::cell::RefCell;
use std::ffi::CStr;
use std::ptr;
use mozjs::jsapi;
use mozjs::panic::maybe_resume_unwind;
use mozjs::realm::AutoRealm;
use mozjs::rooted;
use mozjs::rust::{
CompileOptionsWrapper, HandleObject, MutableHandleValue, transform_str_to_source_text,
wrappers2,
};
const MAX_ENTRIES: usize = 16;
const MIN_CACHED_SOURCE_BYTES: usize = 1024;
struct CacheEntry {
key: u64,
source: Box<str>,
filename: Box<[u8]>,
line: u32,
stencil: *mut jsapi::Stencil,
}
impl Drop for CacheEntry {
fn drop(&mut self) {
if !self.stencil.is_null() {
unsafe { jsapi::StencilRelease(self.stencil) };
}
}
}
struct StencilCache {
owner_cx: *mut jsapi::JSContext,
entries: Vec<CacheEntry>,
hits: u64,
misses: u64,
bypasses: u64,
}
impl StencilCache {
const fn new() -> Self {
StencilCache {
owner_cx: ptr::null_mut(),
entries: Vec::new(),
hits: 0,
misses: 0,
bypasses: 0,
}
}
fn reset(&mut self, owner_cx: *mut jsapi::JSContext) {
self.entries.clear(); self.owner_cx = owner_cx;
}
fn lookup(&mut self, key: u64, source: &str, filename: &CStr, line: u32) -> Option<*mut jsapi::Stencil> {
let idx = self.entries.iter().position(|e| {
e.key == key
&& e.line == line
&& e.filename.as_ref() == filename.to_bytes()
&& e.source.as_bytes() == source.as_bytes()
})?;
let entry = self.entries.remove(idx);
let stencil = entry.stencil;
self.entries.insert(0, entry);
Some(stencil)
}
fn insert(&mut self, key: u64, source: &str, filename: &CStr, line: u32, stencil: *mut jsapi::Stencil) {
if let Some(idx) = self.entries.iter().position(|e| e.key == key) {
self.entries.remove(idx); }
while self.entries.len() >= MAX_ENTRIES {
self.entries.pop(); }
self.entries.insert(
0,
CacheEntry {
key,
source: source.into(),
filename: filename.to_bytes().to_vec().into_boxed_slice(),
line,
stencil,
},
);
}
}
thread_local! {
static CACHE: RefCell<StencilCache> = RefCell::new(StencilCache::new());
}
fn hash_key(source: &str, filename: &CStr, line: u32) -> u64 {
let mut h = bun_wyhash::Wyhash::init(0);
h.update(source.as_bytes());
h.update(filename.to_bytes());
h.update(&line.to_ne_bytes());
h.final_()
}
pub fn evaluate_script_cached(
cx: &mut mozjs::context::JSContext,
glob: HandleObject,
script: &str,
filename: &CStr,
line: u32,
rval: MutableHandleValue,
) -> Result<(), ()> {
if script.len() < MIN_CACHED_SOURCE_BYTES {
CACHE.with(|c| c.borrow_mut().bypasses += 1);
let options = CompileOptionsWrapper::new(cx, filename.to_owned(), line);
return mozjs::rust::evaluate_script(cx, glob, script, rval, options);
}
let key = hash_key(script, filename, line);
let raw_cx = unsafe { cx.raw_cx() };
let hit = CACHE.with(|c| {
let mut cache = c.borrow_mut();
if cache.owner_cx != raw_cx {
cache.reset(raw_cx);
}
match cache.lookup(key, script, filename, line) {
Some(stencil) => {
cache.hits += 1;
Some(stencil)
}
None => {
cache.misses += 1;
None
}
}
});
let stencil: *mut jsapi::Stencil = match hit {
Some(s) => {
unsafe { jsapi::StencilAddRef(s) };
s
}
None => {
let mut realm = AutoRealm::new_from_handle(cx, glob);
let realm_cx: &mut mozjs::context::JSContext = &mut realm;
let options = CompileOptionsWrapper::new(realm_cx, filename.to_owned(), line);
let mut source = transform_str_to_source_text(script);
let addrefed = unsafe {
wrappers2::CompileGlobalScriptToStencil(realm_cx, options.ptr, &mut source)
};
let raw = addrefed.mRawPtr;
if raw.is_null() {
maybe_resume_unwind();
return Err(());
}
unsafe { jsapi::StencilAddRef(raw) }; CACHE.with(|c| {
let mut cache = c.borrow_mut();
if cache.owner_cx != raw_cx {
cache.reset(raw_cx);
}
cache.insert(key, script, filename, line, raw);
});
raw
}
};
let result = unsafe { instantiate_and_execute(cx, glob, stencil, rval) };
unsafe { jsapi::StencilRelease(stencil) };
result
}
unsafe fn instantiate_and_execute(
cx: &mut mozjs::context::JSContext,
glob: HandleObject,
stencil: *mut jsapi::Stencil,
rval: MutableHandleValue,
) -> Result<(), ()> {
let inst_opts = jsapi::InstantiateOptions {
skipFilenameValidation: false,
hideScriptFromDebugger: false,
deferDebugMetadata: false,
eagerDelazificationStrategy_: jsapi::DelazificationOption::OnDemandOnly,
};
let mut realm = AutoRealm::new_from_handle(cx, glob);
let realm_cx: &mut mozjs::context::JSContext = &mut realm;
rooted!(&in(realm_cx) let script = unsafe {
wrappers2::InstantiateGlobalStencil(
realm_cx,
&inst_opts as *const jsapi::InstantiateOptions,
stencil,
ptr::null_mut(), )
});
if script.get().is_null() {
maybe_resume_unwind();
return Err(());
}
if !unsafe { wrappers2::JS_ExecuteScript(realm_cx, script.handle(), rval) } {
maybe_resume_unwind();
return Err(());
}
Ok(())
}
#[doc(hidden)]
pub fn clear_thread_cache() -> usize {
CACHE.with(|c| {
let mut cache = c.borrow_mut();
let n = cache.entries.len();
cache.reset(ptr::null_mut());
n
})
}
#[doc(hidden)]
pub fn thread_cache_len() -> usize {
CACHE.with(|c| c.borrow().entries.len())
}
#[doc(hidden)]
pub fn thread_cache_counters() -> (u64, u64, u64) {
CACHE.with(|c| {
let cache = c.borrow();
(cache.hits, cache.misses, cache.bypasses)
})
}
#[cfg(test)]
mod tests {
use super::*;
use crate::context::JsContext;
use crate::value::JsValue;
use mozjs::jsval::UndefinedValue;
use mozjs::rooted;
use std::ffi::CString;
const PAYLOAD: &str = r#"
(function() {
function roundToPrecision(t) { return Math.round(t / 5) * 5; }
var origDateNow = Date.now;
Date.now = function() { return roundToPrecision(origDateNow()); };
globalThis.__probe = {
dateNowSrc: Date.now.toString(),
hasClosure: (function() { var c = 41; return function() { return c + 1; }; })()(),
names: Object.keys(globalThis).filter(function(n) { return n.indexOf('__probe') === 0 || n.indexOf('__extra') === 0; }).sort()
};
})();
globalThis.__extra = 7;
__probe.names = Object.keys(globalThis).filter(function(n) { return n.indexOf('__probe') === 0 || n.indexOf('__extra') === 0; }).sort();
JSON.stringify(__probe)
"#;
fn pad_to_floor(base: &str) -> String {
let mut pad = String::new();
while pad.len() + base.len() < MIN_CACHED_SOURCE_BYTES {
pad.push_str(&format!(
"globalThis.__pad_{} = {};\n",
pad.len(),
pad.len() % 97
));
}
format!("{pad}{base}")
}
fn eval_cached_fresh_realm(src: &str, filename: &str) -> Result<JsValue, String> {
let mut ctx = JsContext::for_test().map_err(|e| e.message)?;
let mut cx = ctx.cx();
let global_ptr = ctx
.ensure_realm_global(&mut cx, None)
.map_err(|e| e.message)?;
rooted!(&in(cx) let global = global_ptr);
let c_filename = CString::new(filename).unwrap();
rooted!(&in(cx) let mut rval = UndefinedValue());
evaluate_script_cached(&mut cx, global.handle(), src, &c_filename, 1, rval.handle_mut())
.map_err(|_| "cached eval failed".to_string())?;
unsafe { Ok(crate::value::jsval_to_jsvalue(cx.raw_cx_no_gc(), rval.get())) }
}
fn eval_plain_fresh_realm(src: &str, filename: &str) -> Result<JsValue, String> {
let mut ctx = JsContext::for_test().map_err(|e| e.message)?;
ctx.eval(src, filename).map_err(|e| e.message)
}
#[test]
fn cached_hit_state_is_byte_equal_to_plain_eval() -> Result<(), String> {
clear_thread_cache();
let payload = pad_to_floor(PAYLOAD);
let plain = match eval_plain_fresh_realm(&payload, "<stencil-eq>")? {
JsValue::String(s) => s,
other => panic!("plain path returned {other:?}, expected JSON string"),
};
let miss = match eval_cached_fresh_realm(&payload, "<stencil-eq>")? {
JsValue::String(s) => s,
other => panic!("cached miss path returned {other:?}"),
};
let hit = match eval_cached_fresh_realm(&payload, "<stencil-eq>")? {
JsValue::String(s) => s,
other => panic!("cached hit path returned {other:?}"),
};
assert_eq!(plain, miss, "miss path must be byte-equal to plain eval");
assert_eq!(plain, hit, "hit path must be byte-equal to plain eval");
let (hits, misses, _) = thread_cache_counters();
assert!(hits >= 1, "second cached call must be a hit");
assert!(misses >= 1, "first cached call must be a miss");
assert_eq!(thread_cache_len(), 1);
Ok(())
}
#[test]
fn cache_admission_floor_bypasses_tiny_sources() {
clear_thread_cache();
let v = eval_cached_fresh_realm("1+1", "<tiny>").unwrap();
assert!(matches!(v, JsValue::Number(n) if n == 2.0));
assert_eq!(thread_cache_len(), 0, "tiny source must not be cached");
let (_, _, bypasses) = thread_cache_counters();
assert!(bypasses >= 1);
}
#[test]
fn capacity_lru_evicts_oldest() {
clear_thread_cache();
for i in 0..(MAX_ENTRIES + 2) {
let src = pad_to_floor(&format!(
"globalThis.__lru_marker = {i};\n{PAYLOAD}"
));
let v = eval_cached_fresh_realm(&src, "<lru>").unwrap();
assert!(matches!(v, JsValue::String(_)), "iter {i} must evaluate");
}
assert_eq!(
thread_cache_len(),
MAX_ENTRIES,
"capacity bound must hold with LRU eviction"
);
let (hits_before, _, _) = thread_cache_counters();
let src = pad_to_floor(&format!(
"globalThis.__lru_marker = {};\n{PAYLOAD}",
MAX_ENTRIES + 1
));
eval_cached_fresh_realm(&src, "<lru>").unwrap();
let (hits_after, _, _) = thread_cache_counters();
assert!(hits_after > hits_before, "newest entry must hit");
}
#[test]
fn distinct_sources_never_cross_contaminate() {
clear_thread_cache();
let a = pad_to_floor("globalThis.__x = 'A';\nglobalThis.__ret = globalThis.__x;");
let b = pad_to_floor("globalThis.__x = 'B';\nglobalThis.__ret = globalThis.__x;");
for src in [&a, &b, &a, &b] {
let _ = eval_cached_fresh_realm(src, "<same-file>").unwrap();
}
let _ = eval_cached_fresh_realm(&a, "<same-file>").unwrap();
let a_ret = pad_to_floor("globalThis.__x = 'A';\nglobalThis.__ret = globalThis.__x; globalThis.__ret");
match eval_cached_fresh_realm(&a_ret, "<same-file>").unwrap() {
JsValue::String(s) => assert_eq!(s, "A"),
other => panic!("expected 'A', got {other:?}"),
}
assert_eq!(thread_cache_len(), 3, "a, b, a_ret are 3 distinct sources");
}
#[test]
fn runtime_error_and_syntax_error_paths_match_plain_contract() {
clear_thread_cache();
fn clear_pending() {
let ctx = JsContext::for_test().unwrap();
unsafe { mozjs::jsapi::JS_ClearPendingException(ctx.cx().raw_cx()) };
}
let bad = pad_to_floor("this is not valid javascript !!!");
assert!(eval_cached_fresh_realm(&bad, "<syntax-err>").is_err());
clear_pending();
assert_eq!(thread_cache_len(), 0, "failed compile must not be cached");
let throwing = pad_to_floor("throw new Error('boom');");
assert!(eval_cached_fresh_realm(&throwing, "<throw>").is_err());
clear_pending();
assert_eq!(thread_cache_len(), 1, "throwing source is compiled fine (cached)");
assert!(eval_cached_fresh_realm(&throwing, "<throw>").is_err());
clear_pending();
let (hits, _, _) = thread_cache_counters();
assert!(hits >= 1, "throwing source second eval must be a hit");
let v = eval_plain_fresh_realm("2+3", "<health>").unwrap();
assert!(matches!(v, JsValue::Number(n) if n == 5.0));
}
#[test]
fn filename_and_line_are_part_of_the_key() {
clear_thread_cache();
let src = pad_to_floor(PAYLOAD);
let _ = eval_cached_fresh_realm(&src, "<file-a>").unwrap();
let _ = eval_cached_fresh_realm(&src, "<file-b>").unwrap();
assert_eq!(thread_cache_len(), 2, "same source, different filename → 2 entries");
}
#[test]
fn clear_thread_cache_releases_everything() {
clear_thread_cache();
let src = pad_to_floor(PAYLOAD);
let _ = eval_cached_fresh_realm(&src, "<clear>").unwrap();
assert_eq!(thread_cache_len(), 1);
assert_eq!(clear_thread_cache(), 1);
assert_eq!(thread_cache_len(), 0);
let v = eval_cached_fresh_realm(&src, "<clear>").unwrap();
assert!(matches!(v, JsValue::String(_)));
}
}