diff options
Diffstat (limited to 'core/runtime.rs')
-rw-r--r-- | core/runtime.rs | 414 |
1 files changed, 201 insertions, 213 deletions
diff --git a/core/runtime.rs b/core/runtime.rs index fd95bb0b3..dcadd6639 100644 --- a/core/runtime.rs +++ b/core/runtime.rs @@ -19,7 +19,7 @@ use crate::modules::ModuleMap; use crate::ops::*; use crate::realm::ContextState; use crate::realm::JsRealm; -use crate::realm::JsRealmLocal; +use crate::realm::JsRealmInner; use crate::snapshot_util; use crate::source_map::SourceMapCache; use crate::source_map::SourceMapGetter; @@ -36,7 +36,6 @@ use futures::future::poll_fn; use futures::future::Future; use futures::future::FutureExt; use futures::future::MaybeDone; -use futures::stream::FuturesUnordered; use futures::stream::StreamExt; use futures::task::noop_waker; use futures::task::AtomicWaker; @@ -156,7 +155,7 @@ pub type CompiledWasmModuleStore = CrossIsolateStore<v8::CompiledWasmModule>; /// embedder slots. pub struct JsRuntimeState { global_realm: Option<JsRealm>, - known_realms: Vec<v8::Weak<v8::Context>>, + known_realms: Vec<JsRealmInner>, pub(crate) has_tick_scheduled: bool, pub(crate) pending_dyn_mod_evaluate: Vec<DynImportModEvaluate>, pub(crate) pending_mod_evaluate: Option<ModEvaluate>, @@ -165,7 +164,6 @@ pub struct JsRuntimeState { dyn_module_evaluate_idle_counter: u32, pub(crate) source_map_getter: Option<Rc<Box<dyn SourceMapGetter>>>, pub(crate) source_map_cache: Rc<RefCell<SourceMapCache>>, - pub(crate) pending_ops: FuturesUnordered<OpCall>, pub(crate) have_unpolled_ops: bool, pub(crate) op_state: Rc<RefCell<OpState>>, pub(crate) shared_array_buffer_store: Option<SharedArrayBufferStore>, @@ -180,6 +178,24 @@ pub struct JsRuntimeState { waker: AtomicWaker, } +impl JsRuntimeState { + pub(crate) fn destroy_all_realms(&mut self) { + self.global_realm.take(); + for realm in self.known_realms.drain(..) { + realm.destroy() + } + } + + pub(crate) fn remove_realm( + &mut self, + realm_context: &Rc<v8::Global<v8::Context>>, + ) { + self + .known_realms + .retain(|realm| !realm.is_same(realm_context)); + } +} + fn v8_init( v8_platform: Option<v8::SharedRef<v8::Platform>>, predictable: bool, @@ -285,9 +301,13 @@ pub struct RuntimeOptions { impl Drop for JsRuntime { fn drop(&mut self) { + // Forcibly destroy all outstanding realms + self.state.borrow_mut().destroy_all_realms(); if let Some(v8_isolate) = self.v8_isolate.as_mut() { Self::drop_state_and_module_map(v8_isolate); } + // Ensure that we've correctly dropped all references + debug_assert_eq!(Rc::strong_count(&self.state), 1); } } @@ -341,7 +361,6 @@ impl JsRuntime { has_tick_scheduled: false, source_map_getter: options.source_map_getter.map(Rc::new), source_map_cache: Default::default(), - pending_ops: FuturesUnordered::new(), shared_array_buffer_store: options.shared_array_buffer_store, compiled_wasm_module_store: options.compiled_wasm_module_store, op_state: op_state.clone(), @@ -355,20 +374,29 @@ impl JsRuntime { })); let weak = Rc::downgrade(&state_rc); + let context_state = Rc::new(RefCell::new(ContextState::default())); let op_ctxs = ops .into_iter() .enumerate() .map(|(id, decl)| { - OpCtx::new(id as u16, 0, Rc::new(decl), op_state.clone(), weak.clone()) + OpCtx::new( + id as u16, + context_state.clone(), + Rc::new(decl), + op_state.clone(), + weak.clone(), + ) }) .collect::<Vec<_>>() .into_boxed_slice(); + context_state.borrow_mut().op_ctxs = op_ctxs; + context_state.borrow_mut().isolate = Some(isolate_ptr); let snapshot_options = snapshot_util::SnapshotOptions::from_bools( options.startup_snapshot.is_some(), options.will_snapshot, ); - let refs = bindings::external_references(&op_ctxs); + let refs = bindings::external_references(&context_state.borrow().op_ctxs); // V8 takes ownership of external_references. let refs: &'static v8::ExternalReferences = Box::leak(Box::new(refs)); let global_context; @@ -380,8 +408,11 @@ impl JsRuntime { let mut isolate = JsRuntime::setup_isolate(snapshot_creator); { let scope = &mut v8::HandleScope::new(&mut isolate); - let context = - bindings::initialize_context(scope, &op_ctxs, snapshot_options); + let context = bindings::initialize_context( + scope, + &context_state.borrow().op_ctxs, + snapshot_options, + ); // Get module map data from the snapshot if has_startup_snapshot { @@ -415,8 +446,11 @@ impl JsRuntime { let mut isolate = JsRuntime::setup_isolate(isolate); { let scope = &mut v8::HandleScope::new(&mut isolate); - let context = - bindings::initialize_context(scope, &op_ctxs, snapshot_options); + let context = bindings::initialize_context( + scope, + &context_state.borrow().op_ctxs, + snapshot_options, + ); // Get module map data from the snapshot if has_startup_snapshot { @@ -437,13 +471,9 @@ impl JsRuntime { isolate_ptr.read() }; - global_context.open(&mut isolate).set_slot( - &mut isolate, - Rc::new(RefCell::new(ContextState { - op_ctxs, - ..Default::default() - })), - ); + global_context + .open(&mut isolate) + .set_slot(&mut isolate, context_state.clone()); op_state.borrow_mut().put(isolate_ptr); let inspector = if options.inspector { @@ -485,12 +515,16 @@ impl JsRuntime { )); { + let global_realm = JsRealmInner::new( + context_state, + global_context.clone(), + state_rc.clone(), + true, + ); let mut state = state_rc.borrow_mut(); - state.global_realm = Some(JsRealm::new(global_context.clone())); + state.global_realm = Some(JsRealm::new(global_realm.clone())); state.inspector = inspector; - state - .known_realms - .push(v8::Weak::new(&mut isolate, &global_context)); + state.known_realms.push(global_realm); } isolate.set_data( Self::STATE_DATA_OFFSET, @@ -554,9 +588,14 @@ impl JsRuntime { #[inline] pub fn global_context(&mut self) -> v8::Global<v8::Context> { - let state = self.state.borrow(); - let global_realm = state.global_realm.as_ref().unwrap(); - global_realm.context().clone() + self + .state + .borrow() + .known_realms + .get(0) + .unwrap() + .context() + .clone() } #[inline] @@ -581,50 +620,50 @@ impl JsRuntime { /// constructed. pub fn create_realm(&mut self) -> Result<JsRealm, Error> { let realm = { - let realm_idx = self.state.borrow().known_realms.len() as u16; - + let context_state = Rc::new(RefCell::new(ContextState::default())); let op_ctxs: Box<[OpCtx]> = self .global_realm() - .state(self.v8_isolate()) + .0 + .state() .borrow() .op_ctxs .iter() .map(|op_ctx| { OpCtx::new( op_ctx.id, - realm_idx, + context_state.clone(), op_ctx.decl.clone(), op_ctx.state.clone(), op_ctx.runtime_state.clone(), ) }) .collect(); + context_state.borrow_mut().op_ctxs = op_ctxs; + context_state.borrow_mut().isolate = Some(self.v8_isolate() as _); + let raw_ptr = self.v8_isolate() as *mut v8::OwnedIsolate; // SAFETY: Having the scope tied to self's lifetime makes it impossible to // reference JsRuntimeState::op_ctxs while the scope is alive. Here we // turn it into an unbound lifetime, which is sound because 1. it only // lives until the end of this block, and 2. the HandleScope only has // access to the isolate, and nothing else we're accessing from self does. - let scope = &mut v8::HandleScope::new(unsafe { - &mut *(self.v8_isolate() as *mut v8::OwnedIsolate) - }); - let context = - bindings::initialize_context(scope, &op_ctxs, self.snapshot_options); - context.set_slot( + let isolate = unsafe { raw_ptr.as_mut() }.unwrap(); + let scope = &mut v8::HandleScope::new(isolate); + let context = bindings::initialize_context( scope, - Rc::new(RefCell::new(ContextState { - op_ctxs, - ..Default::default() - })), + &context_state.borrow().op_ctxs, + self.snapshot_options, ); - - self - .state - .borrow_mut() - .known_realms - .push(v8::Weak::new(scope, context)); - - JsRealm::new(v8::Global::new(scope, context)) + context.set_slot(scope, context_state.clone()); + let realm = JsRealmInner::new( + context_state, + v8::Global::new(scope, context), + self.state.clone(), + false, + ); + let mut state = self.state.borrow_mut(); + state.known_realms.push(realm.clone()); + JsRealm::new(realm) }; self @@ -915,7 +954,7 @@ impl JsRuntime { }; // Put global handles in the realm's ContextState - let state_rc = realm.state(self.v8_isolate()); + let state_rc = realm.0.state(); let mut state = state_rc.borrow_mut(); state .js_event_loop_tick_cb @@ -1043,24 +1082,8 @@ impl JsRuntime { // Drop other v8::Global handles before snapshotting { - for weak_context in &self.state.clone().borrow().known_realms { - let scope = &mut self.handle_scope(); - if let Some(context) = weak_context.to_local(scope) { - let realm = JsRealmLocal::new(context); - let realm_state_rc = realm.state(scope); - let mut realm_state = realm_state_rc.borrow_mut(); - std::mem::take(&mut realm_state.js_event_loop_tick_cb); - std::mem::take(&mut realm_state.js_build_custom_error_cb); - std::mem::take(&mut realm_state.js_promise_reject_cb); - std::mem::take(&mut realm_state.js_format_exception_cb); - std::mem::take(&mut realm_state.js_wasm_streaming_cb); - context.clear_all_slots(scope); - } - } - - let mut state = self.state.borrow_mut(); - state.known_realms.clear(); - state.global_realm.take(); + let state = self.state.clone(); + state.borrow_mut().destroy_all_realms(); } let snapshot_creator = self.v8_isolate.take().unwrap(); @@ -1494,21 +1517,14 @@ impl EventLoopPendingState { module_map: &ModuleMap, ) -> EventLoopPendingState { let mut num_unrefed_ops = 0; - - if state.known_realms.len() == 1 { - let realm = state.global_realm.as_ref().unwrap(); - num_unrefed_ops += realm.state(scope).borrow().unrefed_ops.len(); - } else { - for weak_context in &state.known_realms { - if let Some(context) = weak_context.to_local(scope) { - let realm = JsRealmLocal::new(context); - num_unrefed_ops += realm.state(scope).borrow().unrefed_ops.len(); - } - } + let mut num_pending_ops = 0; + for realm in &state.known_realms { + num_unrefed_ops += realm.num_unrefed_ops(); + num_pending_ops += realm.num_pending_ops(); } EventLoopPendingState { - has_pending_refed_ops: state.pending_ops.len() > num_unrefed_ops, + has_pending_refed_ops: num_pending_ops > num_unrefed_ops, has_pending_dyn_imports: module_map.has_pending_dynamic_imports(), has_pending_dyn_module_evaluation: !state .pending_dyn_mod_evaluate @@ -1823,7 +1839,8 @@ impl JsRuntime { .contains(&promise_global); if !pending_rejection_was_already_handled { global_realm - .state(tc_scope) + .0 + .state() .borrow_mut() .pending_promise_rejections .retain(|(key, _)| key != &promise_global); @@ -2258,52 +2275,33 @@ impl JsRuntime { let state = self.state.clone(); let scope = &mut self.handle_scope(); let state = state.borrow(); - for weak_context in &state.known_realms { - if let Some(context) = weak_context.to_local(scope) { - JsRealmLocal::new(context).check_promise_rejections(scope)?; - } + for realm in &state.known_realms { + realm.check_promise_rejections(scope)?; } Ok(()) } // Polls pending ops and then runs `Deno.core.eventLoopTick` callback. fn do_js_event_loop_tick(&mut self, cx: &mut Context) -> Result<(), Error> { - // We have a specialized implementation of this method for the common case - // where there is only one realm. - let num_realms = self.state.borrow().known_realms.len(); - if num_realms == 1 { - return self.do_single_realm_js_event_loop_tick(cx); - } - - // `responses_per_realm[idx]` is a vector containing the promise ID and - // response for all promises in realm `self.state.known_realms[idx]`. - let mut responses_per_realm: Vec<Vec<(PromiseId, OpResult)>> = - (0..num_realms).map(|_| vec![]).collect(); - // Now handle actual ops. { let mut state = self.state.borrow_mut(); state.have_unpolled_ops = false; - - while let Poll::Ready(Some(item)) = state.pending_ops.poll_next_unpin(cx) - { - let (realm_idx, promise_id, op_id, resp) = item; - state.op_state.borrow().tracker.track_async_completed(op_id); - responses_per_realm[realm_idx as usize].push((promise_id, resp)); - } } // Handle responses for each realm. let isolate = self.v8_isolate.as_mut().unwrap(); - for (realm_idx, responses) in responses_per_realm.into_iter().enumerate() { - let realm = { - let context = self.state.borrow().known_realms[realm_idx] - .to_global(isolate) - .unwrap(); - JsRealm::new(context) - }; - let context_state_rc = realm.state(isolate); - let mut context_state = context_state_rc.borrow_mut(); + let realm_count = self.state.clone().borrow().known_realms.len(); + for realm_idx in 0..realm_count { + let realm = self + .state + .borrow() + .known_realms + .get(realm_idx) + .unwrap() + .clone(); + let context_state = realm.state(); + let mut context_state = context_state.borrow_mut(); let scope = &mut realm.handle_scope(isolate); // We return async responses to JS in unbounded batches (may change), @@ -2317,9 +2315,19 @@ impl JsRuntime { // This can handle 15 promises futures in a single batch without heap // allocations. let mut args: SmallVec<[v8::Local<v8::Value>; 32]> = - SmallVec::with_capacity(responses.len() * 2 + 2); + SmallVec::with_capacity(32); - for (promise_id, mut resp) in responses { + while let Poll::Ready(Some(item)) = + context_state.pending_ops.poll_next_unpin(cx) + { + let (promise_id, op_id, mut resp) = item; + self + .state + .borrow() + .op_state + .borrow() + .tracker + .track_async_completed(op_id); context_state.unrefed_ops.remove(&promise_id); args.push(v8::Integer::new(scope, promise_id).into()); args.push(match resp.to_v8(scope) { @@ -2355,88 +2363,6 @@ impl JsRuntime { Ok(()) } - - fn do_single_realm_js_event_loop_tick( - &mut self, - cx: &mut Context, - ) -> Result<(), Error> { - let isolate = self.v8_isolate.as_mut().unwrap(); - let scope = &mut self - .state - .borrow() - .global_realm - .as_ref() - .unwrap() - .handle_scope(isolate); - - // We return async responses to JS in unbounded batches (may change), - // each batch is a flat vector of tuples: - // `[promise_id1, op_result1, promise_id2, op_result2, ...]` - // promise_id is a simple integer, op_result is an ops::OpResult - // which contains a value OR an error, encoded as a tuple. - // This batch is received in JS via the special `arguments` variable - // and then each tuple is used to resolve or reject promises - // - // This can handle 15 promises futures in a single batch without heap - // allocations. - let mut args: SmallVec<[v8::Local<v8::Value>; 32]> = SmallVec::new(); - - // Now handle actual ops. - { - let mut state = self.state.borrow_mut(); - state.have_unpolled_ops = false; - - let realm_state_rc = state.global_realm.as_ref().unwrap().state(scope); - let mut realm_state = realm_state_rc.borrow_mut(); - - while let Poll::Ready(Some(item)) = state.pending_ops.poll_next_unpin(cx) - { - let (realm_idx, promise_id, op_id, mut resp) = item; - debug_assert_eq!( - state.known_realms[realm_idx as usize], - state.global_realm.as_ref().unwrap().context() - ); - realm_state.unrefed_ops.remove(&promise_id); - state.op_state.borrow().tracker.track_async_completed(op_id); - args.push(v8::Integer::new(scope, promise_id).into()); - args.push(match resp.to_v8(scope) { - Ok(v) => v, - Err(e) => OpResult::Err(OpError::new(&|_| "TypeError", e.into())) - .to_v8(scope) - .unwrap(), - }); - } - } - - let has_tick_scheduled = - v8::Boolean::new(scope, self.state.borrow().has_tick_scheduled); - args.push(has_tick_scheduled.into()); - - let js_event_loop_tick_cb_handle = { - let state = self.state.borrow_mut(); - let realm_state_rc = state.global_realm.as_ref().unwrap().state(scope); - let handle = realm_state_rc - .borrow() - .js_event_loop_tick_cb - .clone() - .unwrap(); - handle - }; - let tc_scope = &mut v8::TryCatch::new(scope); - let js_event_loop_tick_cb = js_event_loop_tick_cb_handle.open(tc_scope); - let this = v8::undefined(tc_scope).into(); - js_event_loop_tick_cb.call(tc_scope, this, args.as_slice()); - - if let Some(exception) = tc_scope.exception() { - return exception_to_err_result(tc_scope, exception, false); - } - - if tc_scope.has_terminated() || tc_scope.is_execution_terminating() { - return Ok(()); - } - - Ok(()) - } } #[inline] @@ -2459,7 +2385,9 @@ pub fn queue_fast_async_op<R: serde::Serialize + 'static>( .map(|result| crate::_ops::to_op_result(get_class, result)) .boxed_local(); let mut state = runtime_state.borrow_mut(); - state + ctx + .context_state + .borrow_mut() .pending_ops .push(OpCall::pending(ctx, promise_id, fut)); state.have_unpolled_ops = true; @@ -2551,10 +2479,11 @@ pub fn queue_async_op<'s>( // which it is invoked. Otherwise, we might have cross-realm object exposure. // deno_core doesn't currently support such exposure, even though embedders // can cause them, so we panic in debug mode (since the check is expensive). - debug_assert_eq!( - runtime_state.borrow().known_realms[ctx.realm_idx as usize].to_local(scope), - Some(scope.get_current_context()) - ); + // TODO(mmastrac): Restore this + // debug_assert_eq!( + // runtime_state.borrow().context(ctx.realm_idx as usize, scope), + // Some(scope.get_current_context()) + // ); // All ops are polled immediately let waker = noop_waker(); @@ -2584,7 +2513,7 @@ pub fn queue_async_op<'s>( // Otherwise we will push it to the `pending_ops` and let it be polled again // or resolved on the next tick of the event loop. let mut state = runtime_state.borrow_mut(); - state.pending_ops.push(op_call); + ctx.context_state.borrow_mut().pending_ops.push(op_call); state.have_unpolled_ops = true; None } @@ -2716,10 +2645,8 @@ pub mod tests { .unwrap(); { let realm = runtime.global_realm(); - let isolate = runtime.v8_isolate(); - let state_rc = JsRuntime::state(isolate); - assert_eq!(state_rc.borrow().pending_ops.len(), 2); - assert_eq!(realm.state(isolate).borrow().unrefed_ops.len(), 0); + assert_eq!(realm.num_pending_ops(), 2); + assert_eq!(realm.num_unrefed_ops(), 0); } runtime .execute_script_static( @@ -2732,10 +2659,8 @@ pub mod tests { .unwrap(); { let realm = runtime.global_realm(); - let isolate = runtime.v8_isolate(); - let state_rc = JsRuntime::state(isolate); - assert_eq!(state_rc.borrow().pending_ops.len(), 2); - assert_eq!(realm.state(isolate).borrow().unrefed_ops.len(), 2); + assert_eq!(realm.num_pending_ops(), 2); + assert_eq!(realm.num_unrefed_ops(), 2); } runtime .execute_script_static( @@ -2748,10 +2673,8 @@ pub mod tests { .unwrap(); { let realm = runtime.global_realm(); - let isolate = runtime.v8_isolate(); - let state_rc = JsRuntime::state(isolate); - assert_eq!(state_rc.borrow().pending_ops.len(), 2); - assert_eq!(realm.state(isolate).borrow().unrefed_ops.len(), 0); + assert_eq!(realm.num_pending_ops(), 2); + assert_eq!(realm.num_unrefed_ops(), 0); } } @@ -4697,6 +4620,71 @@ Deno.core.opAsync("op_async_serialize_object_with_numbers_as_keys", { } #[tokio::test] + async fn js_realm_gc() { + static INVOKE_COUNT: AtomicUsize = AtomicUsize::new(0); + struct PendingFuture {} + + impl Future for PendingFuture { + type Output = (); + fn poll(self: Pin<&mut Self>, _cx: &mut Context<'_>) -> Poll<()> { + Poll::Pending + } + } + + impl Drop for PendingFuture { + fn drop(&mut self) { + assert_eq!(INVOKE_COUNT.fetch_sub(1, Ordering::SeqCst), 1); + } + } + + // Never resolves. + #[op] + async fn op_pending() { + assert_eq!(INVOKE_COUNT.fetch_add(1, Ordering::SeqCst), 0); + PendingFuture {}.await + } + + deno_core::extension!(test_ext, ops = [op_pending]); + let mut runtime = JsRuntime::new(RuntimeOptions { + extensions: vec![test_ext::init_ops()], + ..Default::default() + }); + + // Detect a drop in OpState + let opstate_drop_detect = Rc::new(()); + runtime + .op_state() + .borrow_mut() + .put(opstate_drop_detect.clone()); + assert_eq!(Rc::strong_count(&opstate_drop_detect), 2); + + let other_realm = runtime.create_realm().unwrap(); + other_realm + .execute_script( + runtime.v8_isolate(), + "future", + ModuleCode::from_static("Deno.core.opAsync('op_pending')"), + ) + .unwrap(); + while INVOKE_COUNT.load(Ordering::SeqCst) == 0 { + poll_fn(|cx: &mut Context| runtime.poll_event_loop(cx, false)) + .await + .unwrap(); + } + drop(other_realm); + while INVOKE_COUNT.load(Ordering::SeqCst) == 1 { + poll_fn(|cx| runtime.poll_event_loop(cx, false)) + .await + .unwrap(); + } + + drop(runtime); + + // Make sure the OpState was dropped properly when the runtime dropped + assert_eq!(Rc::strong_count(&opstate_drop_detect), 1); + } + + #[tokio::test] async fn js_realm_ref_unref_ops() { // Never resolves. #[op] |