Skip to content

Commit 64366d7

Browse files
cleanup auto created realms
1 parent dc5d366 commit 64366d7

File tree

7 files changed

+152
-104
lines changed

7 files changed

+152
-104
lines changed

CHANGELOG.md

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,7 @@
1+
# 0.15.2
2+
3+
* put a max on number of realms auto created by calling runtimefacade::eval() with a realm id
4+
15
# 0.15.1
26

37
* support string rope in bellard version

Cargo.toml

Lines changed: 5 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
[package]
22
name = "quickjs_runtime"
3-
version = "0.15.1"
3+
version = "0.15.2"
44
authors = ["Andries Hiemstra <[email protected]>"]
55
edition = "2021"
66
description = "Wrapper API and utils for the QuickJS JavaScript engine with support for Promise, Modules, Async/await"
@@ -43,7 +43,9 @@ serde_json = "1.0"
4343
serde = { version = "1.0", features = ["derive"] }
4444
string_cache = "0.8"
4545
flume = { version = "0.11", features = ["async"] }
46-
46+
either = "1"
47+
lru = "0.14.0"
48+
anyhow = "1"
4749
#swc
4850
# like the good people at denoland said:
4951
# "swc's version bumping is very buggy and there will often be patch versions
@@ -102,7 +104,7 @@ tracing-log = "0.1"
102104
tracing-gelf = "0.7"
103105
simple-logging = "2.0.2"
104106
tokio = { version = "1", features = ["macros"] }
105-
anyhow = "1"
107+
106108

107109
[dev-dependencies.cargo-husky]
108110
version = "1.5.0"

src/facades.rs

Lines changed: 118 additions & 56 deletions
Original file line numberDiff line numberDiff line change
@@ -11,10 +11,14 @@ use crate::quickjsruntimeadapter::{
1111
use crate::quickjsvalueadapter::QuickJsValueAdapter;
1212
use crate::reflection;
1313
use crate::values::JsValueFacade;
14+
use either::{Either, Left, Right};
1415
use hirofa_utils::eventloop::EventLoop;
1516
use hirofa_utils::task_manager::TaskManager;
1617
use libquickjs_sys as q;
18+
use lru::LruCache;
19+
use std::cell::RefCell;
1720
use std::future::Future;
21+
use std::num::NonZeroUsize;
1822
use std::pin::Pin;
1923
use std::rc::Rc;
2024
use std::sync::{Arc, Weak};
@@ -76,7 +80,7 @@ impl QuickjsRuntimeFacadeInner {
7680
pub fn add_rt_task_to_event_loop<C, R: Send + 'static>(
7781
&self,
7882
consumer: C,
79-
) -> impl Future<Output = R>
83+
) -> impl Future<Output=R>
8084
where
8185
C: FnOnce(&QuickJsRuntimeAdapter) -> R + Send + 'static,
8286
{
@@ -121,7 +125,7 @@ impl QuickjsRuntimeFacadeInner {
121125
})
122126
}
123127

124-
pub fn add_task_to_event_loop<C, R: Send + 'static>(&self, task: C) -> impl Future<Output = R>
128+
pub fn add_task_to_event_loop<C, R: Send + 'static>(&self, task: C) -> impl Future<Output=R>
125129
where
126130
C: FnOnce() -> R + Send + 'static,
127131
{
@@ -291,7 +295,7 @@ impl QuickJsRuntimeFacade {
291295
self.exe_task_in_event_loop(|| {
292296
let context_ids = QuickJsRuntimeAdapter::get_context_ids();
293297
for id in context_ids {
294-
QuickJsRuntimeAdapter::remove_context(id.as_str());
298+
let _ = QuickJsRuntimeAdapter::remove_context(id.as_str());
295299
}
296300
});
297301
}
@@ -312,7 +316,7 @@ impl QuickJsRuntimeFacade {
312316
self.inner.exe_task_in_event_loop(task)
313317
}
314318

315-
pub fn add_task_to_event_loop<C, R: Send + 'static>(&self, task: C) -> impl Future<Output = R>
319+
pub fn add_task_to_event_loop<C, R: Send + 'static>(&self, task: C) -> impl Future<Output=R>
316320
where
317321
C: FnOnce() -> R + Send + 'static,
318322
{
@@ -333,7 +337,7 @@ impl QuickJsRuntimeFacade {
333337
pub fn add_rt_task_to_event_loop<C, R: Send + 'static>(
334338
&self,
335339
task: C,
336-
) -> impl Future<Output = R>
340+
) -> impl Future<Output=R>
337341
where
338342
C: FnOnce(&QuickJsRuntimeAdapter) -> R + Send + 'static,
339343
{
@@ -422,8 +426,8 @@ impl QuickJsRuntimeFacade {
422426
) -> Result<(), JsError>
423427
where
424428
F: Fn(&QuickJsRealmAdapter, Vec<JsValueFacade>) -> Result<JsValueFacade, JsError>
425-
+ Send
426-
+ 'static,
429+
+ Send
430+
+ 'static,
427431
{
428432
let name = name.to_string();
429433

@@ -479,9 +483,9 @@ impl QuickJsRuntimeFacade {
479483
}
480484

481485
/// add an async task the the "helper" thread pool
482-
pub fn add_helper_task_async<R: Send + 'static, T: Future<Output = R> + Send + 'static>(
486+
pub fn add_helper_task_async<R: Send + 'static, T: Future<Output=R> + Send + 'static>(
483487
task: T,
484-
) -> impl Future<Output = Result<R, JoinError>> {
488+
) -> impl Future<Output=Result<R, JoinError>> {
485489
log::trace!("adding an async helper task");
486490
HELPER_TASKS.add_task_async(task)
487491
}
@@ -506,59 +510,96 @@ impl QuickJsRuntimeFacade {
506510
}
507511

508512
/// drop a context which was created earlier with a call to [create_context()](struct.EsRuntime.html#method.create_context)
509-
pub fn drop_context(&self, id: &str) {
513+
pub fn drop_context(&self, id: &str) -> anyhow::Result<()> {
510514
let id = id.to_string();
511515
self.inner
512516
.event_loop
513517
.exe(move || QuickJsRuntimeAdapter::remove_context(id.as_str()))
514518
}
515519
}
516520

521+
thread_local! {
522+
// Each thread has its own LRU cache with capacity 128 to limit the number of auto-created contexts
523+
// todo make this configurable via env..
524+
static REALM_ID_LRU_CACHE: RefCell<LruCache<String, ()>> = RefCell::new(LruCache::new(NonZeroUsize::new(8).unwrap()));
525+
}
526+
517527
fn loop_realm_func<
518528
R: Send + 'static,
519529
C: FnOnce(&QuickJsRuntimeAdapter, &QuickJsRealmAdapter) -> R + Send + 'static,
520530
>(
521531
realm_name: Option<String>,
522532
consumer: C,
523533
) -> R {
524-
let res = QuickJsRuntimeAdapter::do_with(|q_js_rt| {
534+
// housekeeping, lru map for realms
535+
// the problem with doing those in drop in a realm is that finalizers cant find the realm anymore
536+
// so we need to actively delete realms here instead of just making runtimeadapter::context a lru cache
537+
538+
if let Some(realm_str) = realm_name.as_ref() {
539+
REALM_ID_LRU_CACHE.with(|cache_cell| {
540+
let mut cache = cache_cell.borrow_mut();
541+
// it's ok if this str does not yet exist
542+
cache.promote(realm_str);
543+
});
544+
}
545+
546+
// run in existing realm
547+
548+
let res: Either<R, C> = QuickJsRuntimeAdapter::do_with(|q_js_rt| {
525549
if let Some(realm_str) = realm_name.as_ref() {
526550
if let Some(realm) = q_js_rt.get_realm(realm_str) {
527-
(Some(consumer(q_js_rt, realm)), None)
551+
Left(consumer(q_js_rt, realm))
528552
} else {
529-
(None, Some(consumer))
553+
Right(consumer)
530554
}
531555
} else {
532-
(Some(consumer(q_js_rt, q_js_rt.get_main_realm())), None)
556+
Left(consumer(q_js_rt, q_js_rt.get_main_realm()))
533557
}
534558
});
535559

536-
if let Some(res) = res.0 {
537-
res
538-
} else {
539-
// create realm first
540-
let consumer = res.1.unwrap();
541-
let realm_str = realm_name.expect("invalid state");
560+
match res {
561+
Left(r) => r,
562+
Right(consumer) => {
563+
// create realm first
564+
// if more than max present, drop the least used realm
565+
566+
let realm_str = realm_name.expect("invalid state");
567+
568+
REALM_ID_LRU_CACHE.with(|cache_cell| {
569+
let mut cache = cache_cell.borrow_mut();
570+
// it's ok if this str does not yet exist
571+
if cache.len() == cache.cap().get() {
572+
if let Some((evicted_key, _evicted_value)) = cache.pop_lru() {
573+
// cleanup evicted key
574+
QuickJsRuntimeAdapter::remove_context(evicted_key.as_str()).expect("could not destroy realm");
575+
}
576+
}
577+
cache.put(realm_str.to_string(), ());
578+
});
579+
580+
// create realm
542581

543-
QuickJsRuntimeAdapter::do_with_mut(|m_rt| {
544-
let ctx = QuickJsRealmAdapter::new(realm_str.to_string(), m_rt);
545-
m_rt.contexts.insert(realm_str.to_string(), ctx);
546-
});
547582

548-
QuickJsRuntimeAdapter::do_with(|q_js_rt| {
549-
let realm = q_js_rt
550-
.get_realm(realm_str.as_str())
551-
.expect("invalid state");
552-
let hooks = &*q_js_rt.context_init_hooks.borrow();
553-
for hook in hooks {
554-
let res = hook(q_js_rt, realm);
555-
if res.is_err() {
556-
panic!("realm init hook failed: {}", res.err().unwrap());
583+
QuickJsRuntimeAdapter::do_with_mut(|m_rt| {
584+
let ctx = QuickJsRealmAdapter::new(realm_str.to_string(), m_rt);
585+
m_rt.contexts.insert(realm_str.to_string(), ctx);
586+
});
587+
588+
QuickJsRuntimeAdapter::do_with(|q_js_rt| {
589+
let realm = q_js_rt
590+
.get_realm(realm_str.as_str())
591+
.expect("invalid state");
592+
let hooks = &*q_js_rt.context_init_hooks.borrow();
593+
for hook in hooks {
594+
let res = hook(q_js_rt, realm);
595+
if res.is_err() {
596+
panic!("realm init hook failed: {}", res.err().unwrap());
597+
}
557598
}
558-
}
559599

560-
consumer(q_js_rt, realm)
561-
})
600+
consumer(q_js_rt, realm)
601+
})
602+
}
562603
}
563604
}
564605

@@ -570,16 +611,9 @@ impl QuickJsRuntimeFacade {
570611
.exe(move || QuickJsRuntimeAdapter::create_context(name.as_str()))
571612
}
572613

573-
pub fn destroy_realm(&self, name: &str) -> Result<(), JsError> {
614+
pub fn destroy_realm(&self, name: &str) -> anyhow::Result<()> {
574615
let name = name.to_string();
575-
self.exe_task_in_event_loop(move || {
576-
QuickJsRuntimeAdapter::do_with_mut(|rt| {
577-
if rt.get_realm(name.as_str()).is_some() {
578-
rt.remove_realm(name.as_str());
579-
}
580-
Ok(())
581-
})
582-
})
616+
self.exe_task_in_event_loop(move || QuickJsRuntimeAdapter::remove_context(name.as_str()))
583617
}
584618

585619
pub fn has_realm(&self, name: &str) -> Result<bool, JsError> {
@@ -613,7 +647,7 @@ impl QuickJsRuntimeFacade {
613647
>(
614648
&self,
615649
consumer: C,
616-
) -> Pin<Box<dyn Future<Output = R> + Send>> {
650+
) -> Pin<Box<dyn Future<Output=R> + Send>> {
617651
Box::pin(self.add_rt_task_to_event_loop(consumer))
618652
}
619653

@@ -644,7 +678,7 @@ impl QuickJsRuntimeFacade {
644678
&self,
645679
realm_name: Option<&str>,
646680
consumer: C,
647-
) -> Pin<Box<dyn Future<Output = R>>> {
681+
) -> Pin<Box<dyn Future<Output=R>>> {
648682
let realm_name = realm_name.map(|s| s.to_string());
649683
Box::pin(self.add_task_to_event_loop(|| loop_realm_func(realm_name, consumer)))
650684
}
@@ -679,7 +713,7 @@ impl QuickJsRuntimeFacade {
679713
&self,
680714
realm_name: Option<&str>,
681715
script: Script,
682-
) -> Pin<Box<dyn Future<Output = Result<JsValueFacade, JsError>>>> {
716+
) -> Pin<Box<dyn Future<Output=Result<JsValueFacade, JsError>>>> {
683717
self.loop_realm(realm_name, |_rt, realm| {
684718
let res = realm.eval(script);
685719
match res {
@@ -752,7 +786,7 @@ impl QuickJsRuntimeFacade {
752786
&self,
753787
realm_name: Option<&str>,
754788
script: Script,
755-
) -> Pin<Box<dyn Future<Output = Result<JsValueFacade, JsError>>>> {
789+
) -> Pin<Box<dyn Future<Output=Result<JsValueFacade, JsError>>>> {
756790
self.loop_realm(realm_name, |_rt, realm| {
757791
let res = realm.eval_module(script)?;
758792
realm.to_js_value_facade(&res)
@@ -868,7 +902,7 @@ impl QuickJsRuntimeFacade {
868902
namespace: &[&str],
869903
method_name: &str,
870904
args: Vec<JsValueFacade>,
871-
) -> Pin<Box<dyn Future<Output = Result<JsValueFacade, JsError>>>> {
905+
) -> Pin<Box<dyn Future<Output=Result<JsValueFacade, JsError>>>> {
872906
let movable_namespace: Vec<String> = namespace.iter().map(|s| s.to_string()).collect();
873907
let movable_method_name = method_name.to_string();
874908

@@ -951,7 +985,6 @@ lazy_static! {
951985

952986
#[cfg(test)]
953987
pub mod tests {
954-
955988
use crate::facades::QuickJsRuntimeFacade;
956989
use crate::jsutils::modules::{NativeModuleLoader, ScriptModuleLoader};
957990
use crate::jsutils::JsError;
@@ -1319,7 +1352,7 @@ pub mod abstraction_tests {
13191352
.to_js_value_facade(&value_adapter)
13201353
.expect("conversion failed")
13211354
})
1322-
.await
1355+
.await
13231356
}
13241357

13251358
#[test]
@@ -1393,8 +1426,8 @@ pub mod abstraction_tests {
13931426
"#,
13941427
),
13951428
)
1396-
.await
1397-
.expect("script failed");
1429+
.await
1430+
.expect("script failed");
13981431

13991432
// create a user obj
14001433
let test_user_input = User {
@@ -1446,8 +1479,8 @@ pub mod abstraction_tests {
14461479
"#,
14471480
),
14481481
)
1449-
.await
1450-
.expect("script failed");
1482+
.await
1483+
.expect("script failed");
14511484

14521485
// create a user obj
14531486
let test_user_input = User {
@@ -1477,4 +1510,33 @@ pub mod abstraction_tests {
14771510
assert_eq!(user_output.name.as_str(), "proc_Mister");
14781511
assert_eq!(user_output.last_name.as_str(), "proc_Anderson");
14791512
}
1513+
1514+
#[tokio::test]
1515+
async fn test_realm_lifetime() -> anyhow::Result<()> {
1516+
let rt = QuickJsRuntimeBuilder::new().build();
1517+
1518+
rt.add_rt_task_to_event_loop(|rt| {
1519+
println!("ctx list: [{}]", rt.list_contexts().join(",").as_str());
1520+
}).await;
1521+
1522+
for x in 0..10240 {
1523+
let rid = format!("x_{x}");
1524+
let _ = rt.eval(Some(rid.as_str()), Script::new("x.js", "const a = 1;")).await;
1525+
}
1526+
1527+
rt.add_rt_task_to_event_loop(|rt| {
1528+
println!("ctx list: [{}]", rt.list_contexts().join(",").as_str());
1529+
}).await;
1530+
1531+
for x in 0..8 {
1532+
let rid = format!("x_{x}");
1533+
let _ = rt.eval(Some(rid.as_str()), Script::new("x.js", "const a = 1;")).await;
1534+
}
1535+
1536+
rt.add_rt_task_to_event_loop(|rt| {
1537+
println!("ctx list: [{}]", rt.list_contexts().join(",").as_str());
1538+
}).await;
1539+
1540+
Ok(())
1541+
}
14801542
}

src/jsutils/mod.rs

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -96,6 +96,11 @@ impl std::error::Error for JsError {
9696
}
9797
}
9898

99+
impl From<anyhow::Error> for JsError {
100+
fn from(err: anyhow::Error) -> Self {
101+
JsError::new_string(err.to_string())
102+
}
103+
}
99104
impl std::fmt::Display for JsError {
100105
fn fmt(&self, f: &mut Formatter<'_>) -> Result<(), Error> {
101106
let e = format!("{}: {}\n{}", self.name, self.message, self.stack);

0 commit comments

Comments
 (0)