-
Notifications
You must be signed in to change notification settings - Fork 5.5k
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
[Feature/Question] Reuse JsRuntime
and MainWorker
, or create a new v8::Isolate
after running execute_script
#17861
Comments
v8::Isolate
without initializing a new JsRuntime
or MainWorker
JsRuntime
or MainWorker
or create a new v8::Isolate
after running execute_script
JsRuntime
or MainWorker
or create a new v8::Isolate
after running execute_script
JsRuntime
and MainWorker
, or create a new v8::Isolate
after running execute_script
I'm not sure I fully understand the request, but... Every instance of There's no way to discard |
@bartlomieju, apologies for initial description not being super clear. Given the performance difference between the Maybe the better question to ask would've been: is there a way to use the pattern in the above code snippet, without creating a memory leak? And am hoping to better understand why calling repeatedly (Just for additional context, I'm including the "purely // v8
use actix_web::{get, App, HttpServer, Responder};
use deno_core::DetachedBuffer;
use deno_core::serde_v8;
use deno_core::v8;
fn run_js(src: &str) -> DetachedBuffer {
let isolate = &mut v8::Isolate::new(v8::CreateParams::default());
let handle_scope = &mut v8::HandleScope::new(isolate);
let context = v8::Context::new(handle_scope);
let scope = &mut v8::ContextScope::new(handle_scope, context);
let code = v8::String::new(scope, src).unwrap();
let script = v8::Script::compile(scope, code, None).unwrap();
let result = script.run(scope).unwrap();
serde_v8::from_v8::<DetachedBuffer>(scope, result).unwrap()
}
#[get("/js")]
async fn greet() -> impl Responder {
format!("Result: {:?}", run_js("new Uint8Array([0,1,2,3])").as_ref())
}
#[actix_web::main]
async fn main() -> std::io::Result<()> {
let platform = v8::new_default_platform(0, false).make_shared();
v8::V8::initialize_platform(platform);
v8::V8::initialize();
HttpServer::new(|| {
App::new().service(greet)
})
.bind(("127.0.0.1", 8080))?
.run()
.await
} // shared JsRuntime
use actix_web::{get, App, HttpServer, Responder};
use std::cell::RefCell;
use deno_core::DetachedBuffer;
use deno_core::JsRuntime;
use deno_core::RuntimeOptions;
use deno_core::serde_v8;
use deno_core::v8;
thread_local! {
static JS_RUNTIME: RefCell<JsRuntime> = {
RefCell::new(JsRuntime::new(RuntimeOptions::default()))
}
}
fn run_js(src: &str) -> DetachedBuffer {
JS_RUNTIME.with(|js_runtime| {
let mut js_runtime = js_runtime.borrow_mut();
let result = js_runtime.execute_script("<usage>", src).unwrap();
let scope = &mut js_runtime.handle_scope();
let local = v8::Local::new(scope, result);
serde_v8::from_v8::<DetachedBuffer>(scope, local).unwrap()
})
}
#[get("/js")]
async fn greet() -> impl Responder {
format!("Result: {:?}", run_js("new Uint8Array([0,1,2,3])").as_ref())
}
#[actix_web::main]
async fn main() -> std::io::Result<()> {
HttpServer::new(|| {
App::new().service(greet)
})
.bind(("127.0.0.1", 8080))?
.run()
.await
} |
I don't think that's a memory leak - when you are executing this code, V8 treats every execution of the provided script as separate and thus each execution eats up more memory. Once you evaluate the code, V8 stores its source which IIRC will not be freed until the context it belongs to is destroyed. Currently there's no way to destroy a context from |
Thank you so much for pointing me towards fn run_js(src: &str) -> DetachedBuffer {
JS_RUNTIME.with(|js_runtime| {
let mut js_runtime = js_runtime.borrow_mut();
let isolate = js_runtime.v8_isolate();
let js_realm: JsRealm;
{
let mut handle_scope = v8::HandleScope::new(isolate);
let context = v8::Context::new(&mut handle_scope);
let mut context_scope = v8::ContextScope::new(&mut handle_scope, context);
let global = v8::Global::new(&mut context_scope, context);
js_realm = JsRealm::new(global);
};
let result = js_realm.execute_script(isolate, "<usage>", src).unwrap();
let scope = &mut js_realm.handle_scope(isolate);
let local = v8::Local::new(scope, result);
serde_v8::from_v8::<DetachedBuffer>(scope, local).unwrap()
})
} I think you're right that it's not actually a leak per se, and in fact the large amount of source code being stored is the cause for the OOM error. For reference (in case someone else comes across the issue) this is the error I was getting when running without a Finished dev [unoptimized + debuginfo] target(s) in 3.41s
Running `target/debug/js_runtime_shared`
<--- Last few GCs --->
[46642:0x130018000] 7020 ms: Scavenge 359.0 (395.2) -> 350.5 (398.9) MB, 7.7 / 0.0 ms (average mu = 0.962, current mu = 0.967) allocation failure;
[46642:0x130018000] 7246 ms: Scavenge 366.2 (402.4) -> 357.8 (406.7) MB, 6.3 / 0.0 ms (average mu = 0.962, current mu = 0.967) allocation failure;
[46642:0x130018000] 7475 ms: Scavenge 373.5 (409.7) -> 365.0 (413.9) MB, 5.1 / 0.0 ms (average mu = 0.962, current mu = 0.967) allocation failure;
<--- JS stacktrace --->
#
# Fatal javascript OOM in MarkCompactCollector: young object promotion failed
#
[1] 46641 trace trap npm run js_runtime_shared Closing issue out! |
Kudos to @andreubotella for implementing |
Note that both |
@andreubotella, great to know! One quick thing that I did notice: it looks like the documentation / comments in the Is it possible that the global context (which I believe is the difference maker) is still getting linked to the created // works great but doesn't come with `init_cb` or `init_extension_js`
fn run_js(src: &str) -> DetachedBuffer {
JS_RUNTIME.with(|js_runtime| {
let mut js_runtime = js_runtime.borrow_mut();
let isolate = js_runtime.v8_isolate();
let js_realm: JsRealm;
{
let mut handle_scope = v8::HandleScope::new(isolate);
let context = v8::Context::new(&mut handle_scope);
let mut context_scope = v8::ContextScope::new(&mut handle_scope, context);
let global = v8::Global::new(&mut context_scope, context);
js_realm = JsRealm::new(global);
};
let result = js_realm.execute_script(isolate, "<usage>", src).unwrap();
let scope = &mut js_realm.handle_scope(isolate);
let local = v8::Local::new(scope, result);
serde_v8::from_v8::<DetachedBuffer>(scope, local).unwrap()
})
}
// has more Deno functionality, but OOM when called too many times
fn run_js(src: &str) -> DetachedBuffer {
JS_RUNTIME.with(|js_runtime| {
let mut js_runtime = js_runtime.borrow_mut();
let js_realm = js_runtime.create_realm().unwrap();
let isolate = js_runtime.v8_isolate();
let result = js_realm.execute_script(isolate, "<usage>", src).unwrap();
let scope = &mut js_realm.handle_scope(isolate);
let local = v8::Local::new(scope, result);
serde_v8::from_v8::<DetachedBuffer>(scope, local).unwrap()
})
} The other interesting thing, is that when I use the |
I think you might be running into #17199.
|
I ran into this the other day and did switch to using But I guess it does make sense that creating a realm from the |
I've been working on a project where I would like to be able to embed Deno in a web server written in Rust, so that I can post a string of JavaScript code and have it executed in the Deno/v8 sandbox.
The challenge I'm facing right now is that I can't seem to find a good way to reuse a
JsRuntime
orMainWorker
instance for each request without creating a memory leak (I believe thev8::Isolate
is filling up and getting sad when I runexecute_script
on the sameJsRuntime
/MainWorker
instance repeatedly).I've done some tests and have a repository here to demonstrate the situation.
The results were:
v8
(newIsolate
created for every request)JsRuntime
(created on every request)MainWorker
(created on every request)JsRuntime
(shared)MainWorker
(shared)Based on that information, the slowest is initializing
MainWorker
on every request (which makes sense),JsRuntime
a bit better, andv8
with a newIsolate
is the fastest of the "re-initialize stuff on every request." But, when theJsRuntime
andMainWorker
can be reused for multiple requests we get sub-millisecond latency. Based on that, I would love to have an option where av8::Isolate
could be reused and maybe cleared manually? or at the end of.execute_script()
?I'm very new to
deno_core
anddeno_runtime
so there may already be a way for me to do what I want to do without creating a memory leak, but I'm also not av8
expert, so I don't know if there's even such a thing as clearing the currentv8::Isolate
or if creating a new one on each request is always necessary.Thanks!
The text was updated successfully, but these errors were encountered: