mirror of
https://github.com/denoland/deno.git
synced 2025-03-03 09:31:22 -05:00
core: memory limits & callbacks (#6914)
This commit is contained in:
parent
eed77aa020
commit
f425b51419
1 changed files with 172 additions and 10 deletions
|
@ -20,9 +20,11 @@ use futures::stream::FuturesUnordered;
|
|||
use futures::stream::StreamExt;
|
||||
use futures::task::AtomicWaker;
|
||||
use futures::Future;
|
||||
use std::any::Any;
|
||||
use std::cell::RefCell;
|
||||
use std::collections::HashMap;
|
||||
use std::convert::From;
|
||||
use std::ffi::c_void;
|
||||
use std::mem::forget;
|
||||
use std::ops::Deref;
|
||||
use std::ops::DerefMut;
|
||||
|
@ -71,8 +73,25 @@ pub enum StartupData<'a> {
|
|||
None,
|
||||
}
|
||||
|
||||
impl StartupData<'_> {
|
||||
fn into_options(self) -> (Option<OwnedScript>, Option<Snapshot>) {
|
||||
match self {
|
||||
Self::Script(script) => (Some(script.into()), None),
|
||||
Self::Snapshot(snapshot) => (None, Some(snapshot)),
|
||||
Self::None => (None, None),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
type JSErrorCreateFn = dyn Fn(JSError) -> ErrBox;
|
||||
|
||||
/// Objects that need to live as long as the isolate
|
||||
#[derive(Default)]
|
||||
struct IsolateAllocations {
|
||||
near_heap_limit_callback_data:
|
||||
Option<(Box<RefCell<dyn Any>>, v8::NearHeapLimitCallback)>,
|
||||
}
|
||||
|
||||
/// A single execution context of JavaScript. Corresponds roughly to the "Web
|
||||
/// Worker" concept in the DOM. An CoreIsolate is a Future that can be used with
|
||||
/// Tokio. The CoreIsolate future completes when there is an error or when all
|
||||
|
@ -89,6 +108,7 @@ pub struct CoreIsolate {
|
|||
has_snapshotted: bool,
|
||||
needs_init: bool,
|
||||
startup_script: Option<OwnedScript>,
|
||||
allocations: IsolateAllocations,
|
||||
}
|
||||
|
||||
/// Internal state for CoreIsolate which is stored in one of v8::Isolate's
|
||||
|
@ -162,25 +182,75 @@ pub unsafe fn v8_init() {
|
|||
v8::V8::set_flags_from_command_line(argv);
|
||||
}
|
||||
|
||||
/// Minimum and maximum bytes of heap used in an isolate
|
||||
pub struct HeapLimits {
|
||||
/// By default V8 starts with a small heap and dynamically grows it to match
|
||||
/// the set of live objects. This may lead to ineffective garbage collections
|
||||
/// at startup if the live set is large. Setting the initial heap size avoids
|
||||
/// such garbage collections. Note that this does not affect young generation
|
||||
/// garbage collections.
|
||||
pub initial: usize,
|
||||
/// When the heap size approaches `max`, V8 will perform series of
|
||||
/// garbage collections and invoke the
|
||||
/// [NearHeapLimitCallback](TODO).
|
||||
/// If the garbage collections do not help and the callback does not
|
||||
/// increase the limit, then V8 will crash with V8::FatalProcessOutOfMemory.
|
||||
pub max: usize,
|
||||
}
|
||||
|
||||
pub(crate) struct IsolateOptions {
|
||||
will_snapshot: bool,
|
||||
startup_script: Option<OwnedScript>,
|
||||
startup_snapshot: Option<Snapshot>,
|
||||
heap_limits: Option<HeapLimits>,
|
||||
}
|
||||
|
||||
impl CoreIsolate {
|
||||
/// startup_data defines the snapshot or script used at startup to initialize
|
||||
/// the isolate.
|
||||
pub fn new(startup_data: StartupData, will_snapshot: bool) -> Self {
|
||||
let (startup_script, startup_snapshot) = startup_data.into_options();
|
||||
let options = IsolateOptions {
|
||||
will_snapshot,
|
||||
startup_script,
|
||||
startup_snapshot,
|
||||
heap_limits: None,
|
||||
};
|
||||
|
||||
Self::from_options(options)
|
||||
}
|
||||
|
||||
/// This is useful for controlling memory usage of scripts.
|
||||
///
|
||||
/// See [`HeapLimits`](struct.HeapLimits.html) for more details.
|
||||
///
|
||||
/// Make sure to use [`add_near_heap_limit_callback`](#method.add_near_heap_limit_callback)
|
||||
/// to prevent v8 from crashing when reaching the upper limit.
|
||||
pub fn with_heap_limits(
|
||||
startup_data: StartupData,
|
||||
heap_limits: HeapLimits,
|
||||
) -> Self {
|
||||
let (startup_script, startup_snapshot) = startup_data.into_options();
|
||||
let options = IsolateOptions {
|
||||
will_snapshot: false,
|
||||
startup_script,
|
||||
startup_snapshot,
|
||||
heap_limits: Some(heap_limits),
|
||||
};
|
||||
|
||||
Self::from_options(options)
|
||||
}
|
||||
|
||||
fn from_options(options: IsolateOptions) -> Self {
|
||||
static DENO_INIT: Once = Once::new();
|
||||
DENO_INIT.call_once(|| {
|
||||
unsafe { v8_init() };
|
||||
});
|
||||
|
||||
let (startup_script, startup_snapshot) = match startup_data {
|
||||
StartupData::Script(script) => (Some(script.into()), None),
|
||||
StartupData::Snapshot(snapshot) => (None, Some(snapshot)),
|
||||
StartupData::None => (None, None),
|
||||
};
|
||||
|
||||
let global_context;
|
||||
let (mut isolate, maybe_snapshot_creator) = if will_snapshot {
|
||||
let (mut isolate, maybe_snapshot_creator) = if options.will_snapshot {
|
||||
// TODO(ry) Support loading snapshots before snapshotting.
|
||||
assert!(startup_snapshot.is_none());
|
||||
assert!(options.startup_snapshot.is_none());
|
||||
let mut creator =
|
||||
v8::SnapshotCreator::new(Some(&bindings::EXTERNAL_REFERENCES));
|
||||
let isolate = unsafe { creator.get_owned_isolate() };
|
||||
|
@ -195,7 +265,7 @@ impl CoreIsolate {
|
|||
} else {
|
||||
let mut params = v8::Isolate::create_params()
|
||||
.external_references(&**bindings::EXTERNAL_REFERENCES);
|
||||
let snapshot_loaded = if let Some(snapshot) = startup_snapshot {
|
||||
let snapshot_loaded = if let Some(snapshot) = options.startup_snapshot {
|
||||
params = match snapshot {
|
||||
Snapshot::Static(data) => params.snapshot_blob(data),
|
||||
Snapshot::JustCreated(data) => params.snapshot_blob(data),
|
||||
|
@ -206,6 +276,10 @@ impl CoreIsolate {
|
|||
false
|
||||
};
|
||||
|
||||
if let Some(heap_limits) = options.heap_limits {
|
||||
params = params.heap_limits(heap_limits.initial, heap_limits.max)
|
||||
}
|
||||
|
||||
let isolate = v8::Isolate::new(params);
|
||||
let mut isolate = CoreIsolate::setup_isolate(isolate);
|
||||
{
|
||||
|
@ -243,7 +317,8 @@ impl CoreIsolate {
|
|||
snapshot_creator: maybe_snapshot_creator,
|
||||
has_snapshotted: false,
|
||||
needs_init: true,
|
||||
startup_script,
|
||||
startup_script: options.startup_script,
|
||||
allocations: IsolateAllocations::default(),
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -353,6 +428,49 @@ impl CoreIsolate {
|
|||
let mut state = state_rc.borrow_mut();
|
||||
state.op_registry.register(name, op)
|
||||
}
|
||||
|
||||
/// Registers a callback on the isolate when the memory limits are approached.
|
||||
/// Use this to prevent V8 from crashing the process when reaching the limit.
|
||||
///
|
||||
/// Calls the closure with the current heap limit and the initial heap limit.
|
||||
/// The return value of the closure is set as the new limit.
|
||||
pub fn add_near_heap_limit_callback<C>(&mut self, cb: C)
|
||||
where
|
||||
C: FnMut(usize, usize) -> usize + 'static,
|
||||
{
|
||||
let boxed_cb = Box::new(RefCell::new(cb));
|
||||
let data = boxed_cb.as_ptr() as *mut c_void;
|
||||
self.allocations.near_heap_limit_callback_data =
|
||||
Some((boxed_cb, near_heap_limit_callback::<C>));
|
||||
self
|
||||
.v8_isolate
|
||||
.as_mut()
|
||||
.unwrap()
|
||||
.add_near_heap_limit_callback(near_heap_limit_callback::<C>, data);
|
||||
}
|
||||
|
||||
pub fn remove_near_heap_limit_callback(&mut self, heap_limit: usize) {
|
||||
if let Some((_, cb)) = self.allocations.near_heap_limit_callback_data.take()
|
||||
{
|
||||
self
|
||||
.v8_isolate
|
||||
.as_mut()
|
||||
.unwrap()
|
||||
.remove_near_heap_limit_callback(cb, heap_limit);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
extern "C" fn near_heap_limit_callback<F>(
|
||||
data: *mut c_void,
|
||||
current_heap_limit: usize,
|
||||
initial_heap_limit: usize,
|
||||
) -> usize
|
||||
where
|
||||
F: FnMut(usize, usize) -> usize,
|
||||
{
|
||||
let callback = unsafe { &mut *(data as *mut F) };
|
||||
callback(current_heap_limit, initial_heap_limit)
|
||||
}
|
||||
|
||||
impl Future for CoreIsolate {
|
||||
|
@ -1188,4 +1306,48 @@ pub mod tests {
|
|||
let mut isolate2 = CoreIsolate::new(startup_data, false);
|
||||
js_check(isolate2.execute("check.js", "if (a != 3) throw Error('x')"));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_heap_limits() {
|
||||
let heap_limits = HeapLimits {
|
||||
initial: 0,
|
||||
max: 20 * 1024, // 20 kB
|
||||
};
|
||||
let mut isolate =
|
||||
CoreIsolate::with_heap_limits(StartupData::None, heap_limits);
|
||||
let cb_handle = isolate.thread_safe_handle();
|
||||
|
||||
let callback_invoke_count = Rc::new(AtomicUsize::default());
|
||||
let inner_invoke_count = Rc::clone(&callback_invoke_count);
|
||||
|
||||
isolate.add_near_heap_limit_callback(
|
||||
move |current_limit, _initial_limit| {
|
||||
inner_invoke_count.fetch_add(1, Ordering::SeqCst);
|
||||
cb_handle.terminate_execution();
|
||||
current_limit * 2
|
||||
},
|
||||
);
|
||||
let err = isolate
|
||||
.execute(
|
||||
"script name",
|
||||
r#"let s = ""; while(true) { s += "Hello"; }"#,
|
||||
)
|
||||
.expect_err("script should fail");
|
||||
assert_eq!(
|
||||
"Uncaught Error: execution terminated",
|
||||
err.downcast::<JSError>().unwrap().message
|
||||
);
|
||||
assert!(callback_invoke_count.load(Ordering::SeqCst) > 0)
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_heap_limit_cb_remove() {
|
||||
let mut isolate = CoreIsolate::new(StartupData::None, false);
|
||||
|
||||
isolate.add_near_heap_limit_callback(|current_limit, _initial_limit| {
|
||||
current_limit * 2
|
||||
});
|
||||
isolate.remove_near_heap_limit_callback(20 * 1024);
|
||||
assert!(isolate.allocations.near_heap_limit_callback_data.is_none());
|
||||
}
|
||||
}
|
||||
|
|
Loading…
Add table
Reference in a new issue