From 782db80629847957e09c10cd5a2dd4de12108b2c Mon Sep 17 00:00:00 2001 From: Matt Mastracci Date: Wed, 31 May 2023 08:19:06 -0600 Subject: [PATCH] chore(core): Split JsRuntimeForSnapshot from JsRuntime (#19308) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit This cleans up `JsRuntime` a bit more: * We no longer print cargo's rerun-if-changed messages in `JsRuntime` -- those are printed elsewhere * We no longer special case the OwnedIsolate for snapshots. Instead we make use of an inner object that has the `Drop` impl and allows us to `std::mem::forget` it if we need to extract the isolate for a snapshot * The `snapshot` method is only available on `JsRuntimeForSnapshot`, not `JsRuntime`. * `OpState` construction is slightly cleaner, though I'd still like to extract more --------- Co-authored-by: Bartek IwaƄczuk --- cli/build.rs | 13 +- core/bindings.rs | 23 +- core/error.rs | 6 +- core/examples/http_bench_json_ops/main.rs | 16 +- core/extensions.rs | 2 +- core/lib.rs | 1 + core/modules.rs | 25 +- core/ops.rs | 8 +- core/ops_builtin_v8.rs | 24 +- core/realm.rs | 2 +- core/runtime.rs | 921 +++++++++++++--------- core/snapshot_util.rs | 83 +- runtime/build.rs | 5 +- 13 files changed, 697 insertions(+), 432 deletions(-) diff --git a/cli/build.rs b/cli/build.rs index 72b8e78183..5ff86fa20c 100644 --- a/cli/build.rs +++ b/cli/build.rs @@ -262,7 +262,7 @@ mod ts { ) .unwrap(); - create_snapshot(CreateSnapshotOptions { + let output = create_snapshot(CreateSnapshotOptions { cargo_manifest_dir: env!("CARGO_MANIFEST_DIR"), snapshot_path, startup_snapshot: None, @@ -289,6 +289,9 @@ mod ts { })), snapshot_module_load_cb: None, }); + for path in output.files_loaded_during_snapshot { + println!("cargo:rerun-if-changed={}", path.display()); + } } pub(crate) fn version() -> String { @@ -326,7 +329,8 @@ deno_core::extension!( } ); -fn create_cli_snapshot(snapshot_path: PathBuf) { +#[must_use = "The files listed by create_cli_snapshot should be printed as 'cargo:rerun-if-changed' lines"] +fn create_cli_snapshot(snapshot_path: PathBuf) -> CreateSnapshotOutput { // NOTE(bartlomieju): ordering is important here, keep it in sync with // `runtime/worker.rs`, `runtime/web_worker.rs` and `runtime/build.rs`! let fs = Arc::new(deno_fs::RealFs); @@ -481,7 +485,10 @@ fn main() { ts::create_compiler_snapshot(compiler_snapshot_path, &c); let cli_snapshot_path = o.join("CLI_SNAPSHOT.bin"); - create_cli_snapshot(cli_snapshot_path); + let output = create_cli_snapshot(cli_snapshot_path); + for path in output.files_loaded_during_snapshot { + println!("cargo:rerun-if-changed={}", path.display()) + } #[cfg(target_os = "windows")] { diff --git a/core/bindings.rs b/core/bindings.rs index 1f35d12460..d91e4c309d 100644 --- a/core/bindings.rs +++ b/core/bindings.rs @@ -15,10 +15,19 @@ use crate::modules::ImportAssertionsKind; use crate::modules::ModuleMap; use crate::modules::ResolutionKind; use crate::ops::OpCtx; -use crate::snapshot_util::SnapshotOptions; use crate::JsRealm; use crate::JsRuntime; +#[derive(Copy, Clone, Eq, PartialEq)] +pub(crate) enum BindingsMode { + /// We have no snapshot -- this is a pristine context. + New, + /// We have initialized before, are reloading a snapshot, and will snapshot. + Loaded, + /// We have initialized before, are reloading a snapshot, and will not snapshot again. + LoadedFinal, +} + pub(crate) fn external_references(ops: &[OpCtx]) -> v8::ExternalReferences { // Overallocate a bit, it's better than having to resize the vector. let mut references = Vec::with_capacity(4 + ops.len() * 4); @@ -118,7 +127,7 @@ pub(crate) fn initialize_context<'s>( scope: &mut v8::HandleScope<'s>, context: v8::Local<'s, v8::Context>, op_ctxs: &[OpCtx], - snapshot_options: SnapshotOptions, + bindings_mode: BindingsMode, ) -> v8::Local<'s, v8::Context> { let global = context.global(scope); @@ -128,13 +137,13 @@ pub(crate) fn initialize_context<'s>( codegen, "Deno.__op__ = function(opFns, callConsole, console) {{" ); - if !snapshot_options.loaded() { + if bindings_mode == BindingsMode::New { _ = writeln!(codegen, "Deno.__op__console(callConsole, console);"); } for op_ctx in op_ctxs { if op_ctx.decl.enabled { // If we're loading from a snapshot, we can skip registration for most ops - if matches!(snapshot_options, SnapshotOptions::Load) + if bindings_mode == BindingsMode::LoadedFinal && !op_ctx.decl.force_registration { continue; @@ -173,7 +182,7 @@ pub(crate) fn initialize_context<'s>( let op_fn = op_ctx_function(scope, op_ctx); op_fns.set_index(scope, op_ctx.id as u32, op_fn.into()); } - if snapshot_options.loaded() { + if bindings_mode != BindingsMode::New { op_fn.call(scope, recv.into(), &[op_fns.into()]); } else { // Bind functions to Deno.core.* @@ -284,7 +293,7 @@ pub fn host_import_module_dynamically_callback<'s>( let resolver_handle = v8::Global::new(scope, resolver); { - let state_rc = JsRuntime::state(scope); + let state_rc = JsRuntime::state_from(scope); let module_map_rc = JsRuntime::module_map_from(scope); debug!( @@ -484,7 +493,7 @@ pub extern "C" fn promise_reject_callback(message: v8::PromiseRejectMessage) { }; if has_unhandled_rejection_handler { - let state_rc = JsRuntime::state(tc_scope); + let state_rc = JsRuntime::state_from(tc_scope); let mut state = state_rc.borrow_mut(); if let Some(pending_mod_evaluate) = state.pending_mod_evaluate.as_mut() { if !pending_mod_evaluate.has_evaluated { diff --git a/core/error.rs b/core/error.rs index 3d0b20b0a8..16f813b896 100644 --- a/core/error.rs +++ b/core/error.rs @@ -209,7 +209,7 @@ impl JsStackFrame { let l = message.get_line_number(scope)? as i64; // V8's column numbers are 0-based, we want 1-based. let c = message.get_start_column() as i64 + 1; - let state_rc = JsRuntime::state(scope); + let state_rc = JsRuntime::state_from(scope); let (getter, cache) = { let state = state_rc.borrow(); ( @@ -282,7 +282,7 @@ impl JsError { frames = vec![stack_frame]; } { - let state_rc = JsRuntime::state(scope); + let state_rc = JsRuntime::state_from(scope); let (getter, cache) = { let state = state_rc.borrow(); ( @@ -414,7 +414,7 @@ impl JsError { } } { - let state_rc = JsRuntime::state(scope); + let state_rc = JsRuntime::state_from(scope); let (getter, cache) = { let state = state_rc.borrow(); ( diff --git a/core/examples/http_bench_json_ops/main.rs b/core/examples/http_bench_json_ops/main.rs index 7c15f7bf24..36c0996c3a 100644 --- a/core/examples/http_bench_json_ops/main.rs +++ b/core/examples/http_bench_json_ops/main.rs @@ -3,7 +3,7 @@ use deno_core::anyhow::Error; use deno_core::op; use deno_core::AsyncRefCell; use deno_core::AsyncResult; -use deno_core::JsRuntime; +use deno_core::JsRuntimeForSnapshot; use deno_core::OpState; use deno_core::Resource; use deno_core::ResourceId; @@ -93,7 +93,7 @@ impl From for TcpStream { } } -fn create_js_runtime() -> JsRuntime { +fn create_js_runtime() -> JsRuntimeForSnapshot { let ext = deno_core::Extension::builder("my_ext") .ops(vec![ op_listen::decl(), @@ -103,11 +103,13 @@ fn create_js_runtime() -> JsRuntime { ]) .build(); - JsRuntime::new(deno_core::RuntimeOptions { - extensions: vec![ext], - will_snapshot: false, - ..Default::default() - }) + JsRuntimeForSnapshot::new( + deno_core::RuntimeOptions { + extensions: vec![ext], + ..Default::default() + }, + Default::default(), + ) } #[op] diff --git a/core/extensions.rs b/core/extensions.rs index a8b52eb3b6..ff86fec648 100644 --- a/core/extensions.rs +++ b/core/extensions.rs @@ -349,6 +349,7 @@ macro_rules! extension { #[derive(Default)] pub struct Extension { + pub(crate) name: &'static str, js_files: Option>, esm_files: Option>, esm_entry_point: Option<&'static str>, @@ -358,7 +359,6 @@ pub struct Extension { event_loop_middleware: Option>, initialized: bool, enabled: bool, - name: &'static str, deps: Option<&'static [&'static str]>, force_op_registration: bool, pub(crate) is_core: bool, diff --git a/core/lib.rs b/core/lib.rs index 8edc8be18b..336d9c2b98 100644 --- a/core/lib.rs +++ b/core/lib.rs @@ -113,6 +113,7 @@ pub use crate::runtime::CrossIsolateStore; pub use crate::runtime::GetErrorClassFn; pub use crate::runtime::JsErrorCreateFn; pub use crate::runtime::JsRuntime; +pub use crate::runtime::JsRuntimeForSnapshot; pub use crate::runtime::RuntimeOptions; pub use crate::runtime::SharedArrayBufferStore; pub use crate::runtime::Snapshot; diff --git a/core/modules.rs b/core/modules.rs index 174dadd2d6..353119660c 100644 --- a/core/modules.rs +++ b/core/modules.rs @@ -1719,6 +1719,7 @@ mod tests { use super::*; use crate::ascii_str; use crate::JsRuntime; + use crate::JsRuntimeForSnapshot; use crate::RuntimeOptions; use crate::Snapshot; use deno_ops::op; @@ -2889,11 +2890,13 @@ if (import.meta.url != 'file:///main_with_code.js') throw Error(); ); let loader = MockLoader::new(); - let mut runtime = JsRuntime::new(RuntimeOptions { - module_loader: Some(loader), - will_snapshot: true, - ..Default::default() - }); + let mut runtime = JsRuntimeForSnapshot::new( + RuntimeOptions { + module_loader: Some(loader), + ..Default::default() + }, + Default::default(), + ); // In default resolution code should be empty. // Instead we explicitly pass in our own code. // The behavior should be very similar to /a.js. @@ -2931,11 +2934,13 @@ if (import.meta.url != 'file:///main_with_code.js') throw Error(); ); let loader = MockLoader::new(); - let mut runtime = JsRuntime::new(RuntimeOptions { - module_loader: Some(loader), - will_snapshot: true, - ..Default::default() - }); + let mut runtime = JsRuntimeForSnapshot::new( + RuntimeOptions { + module_loader: Some(loader), + ..Default::default() + }, + Default::default(), + ); // In default resolution code should be empty. // Instead we explicitly pass in our own code. // The behavior should be very similar to /a.js. diff --git a/core/ops.rs b/core/ops.rs index 2614c3a87e..5f1bf67ef6 100644 --- a/core/ops.rs +++ b/core/ops.rs @@ -183,7 +183,7 @@ pub struct OpState { pub get_error_class_fn: GetErrorClassFn, pub tracker: OpsTracker, pub last_fast_op_error: Option, - gotham_state: GothamState, + pub(crate) gotham_state: GothamState, } impl OpState { @@ -196,6 +196,12 @@ impl OpState { tracker: OpsTracker::new(ops_count), } } + + /// Clear all user-provided resources and state. + pub(crate) fn clear(&mut self) { + std::mem::take(&mut self.gotham_state); + std::mem::take(&mut self.resource_table); + } } impl Deref for OpState { diff --git a/core/ops_builtin_v8.rs b/core/ops_builtin_v8.rs index 8987a56b6a..8416546cbc 100644 --- a/core/ops_builtin_v8.rs +++ b/core/ops_builtin_v8.rs @@ -72,14 +72,14 @@ fn op_run_microtasks(scope: &mut v8::HandleScope) { #[op(v8)] fn op_has_tick_scheduled(scope: &mut v8::HandleScope) -> bool { - let state_rc = JsRuntime::state(scope); + let state_rc = JsRuntime::state_from(scope); let state = state_rc.borrow(); state.has_tick_scheduled } #[op(v8)] fn op_set_has_tick_scheduled(scope: &mut v8::HandleScope, v: bool) { - let state_rc = JsRuntime::state(scope); + let state_rc = JsRuntime::state_from(scope); state_rc.borrow_mut().has_tick_scheduled = v; } @@ -242,7 +242,7 @@ impl<'a> v8::ValueSerializerImpl for SerializeDeserialize<'a> { if self.for_storage { return None; } - let state_rc = JsRuntime::state(scope); + let state_rc = JsRuntime::state_from(scope); let state = state_rc.borrow_mut(); if let Some(shared_array_buffer_store) = &state.shared_array_buffer_store { let backing_store = shared_array_buffer.get_backing_store(); @@ -263,7 +263,7 @@ impl<'a> v8::ValueSerializerImpl for SerializeDeserialize<'a> { self.throw_data_clone_error(scope, message); return None; } - let state_rc = JsRuntime::state(scope); + let state_rc = JsRuntime::state_from(scope); let state = state_rc.borrow_mut(); if let Some(compiled_wasm_module_store) = &state.compiled_wasm_module_store { @@ -305,7 +305,7 @@ impl<'a> v8::ValueDeserializerImpl for SerializeDeserialize<'a> { if self.for_storage { return None; } - let state_rc = JsRuntime::state(scope); + let state_rc = JsRuntime::state_from(scope); let state = state_rc.borrow_mut(); if let Some(shared_array_buffer_store) = &state.shared_array_buffer_store { let backing_store = shared_array_buffer_store.take(transfer_id)?; @@ -325,7 +325,7 @@ impl<'a> v8::ValueDeserializerImpl for SerializeDeserialize<'a> { if self.for_storage { return None; } - let state_rc = JsRuntime::state(scope); + let state_rc = JsRuntime::state_from(scope); let state = state_rc.borrow_mut(); if let Some(compiled_wasm_module_store) = &state.compiled_wasm_module_store { @@ -409,7 +409,7 @@ fn op_serialize( value_serializer.write_header(); if let Some(transferred_array_buffers) = transferred_array_buffers { - let state_rc = JsRuntime::state(scope); + let state_rc = JsRuntime::state_from(scope); let state = state_rc.borrow_mut(); for index in 0..transferred_array_buffers.length() { let i = v8::Number::new(scope, index as f64).into(); @@ -494,7 +494,7 @@ fn op_deserialize<'a>( } if let Some(transferred_array_buffers) = transferred_array_buffers { - let state_rc = JsRuntime::state(scope); + let state_rc = JsRuntime::state_from(scope); let state = state_rc.borrow_mut(); if let Some(shared_array_buffer_store) = &state.shared_array_buffer_store { for i in 0..transferred_array_buffers.length() { @@ -724,7 +724,7 @@ fn op_set_wasm_streaming_callback( .as_ref() .unwrap() .clone(); - let state_rc = JsRuntime::state(scope); + let state_rc = JsRuntime::state_from(scope); let streaming_rid = state_rc .borrow() .op_state @@ -751,7 +751,7 @@ fn op_abort_wasm_streaming( error: serde_v8::Value, ) -> Result<(), Error> { let wasm_streaming = { - let state_rc = JsRuntime::state(scope); + let state_rc = JsRuntime::state_from(scope); let state = state_rc.borrow(); let wsr = state .op_state @@ -790,7 +790,7 @@ fn op_dispatch_exception( scope: &mut v8::HandleScope, exception: serde_v8::Value, ) { - let state_rc = JsRuntime::state(scope); + let state_rc = JsRuntime::state_from(scope); let mut state = state_rc.borrow_mut(); if let Some(inspector) = &state.inspector { let inspector = inspector.borrow(); @@ -828,7 +828,7 @@ fn op_apply_source_map( scope: &mut v8::HandleScope, location: Location, ) -> Result { - let state_rc = JsRuntime::state(scope); + let state_rc = JsRuntime::state_from(scope); let (getter, cache) = { let state = state_rc.borrow(); ( diff --git a/core/realm.rs b/core/realm.rs index fc0ff4b4b9..94ce77464d 100644 --- a/core/realm.rs +++ b/core/realm.rs @@ -162,7 +162,7 @@ impl JsRealmInner { }; let exception = v8::Local::new(scope, handle); - let state_rc = JsRuntime::state(scope); + let state_rc = JsRuntime::state_from(scope); let state = state_rc.borrow(); if let Some(inspector) = &state.inspector { let inspector = inspector.borrow(); diff --git a/core/runtime.rs b/core/runtime.rs index b4d271f661..b4876f0afb 100644 --- a/core/runtime.rs +++ b/core/runtime.rs @@ -1,6 +1,7 @@ // Copyright 2018-2023 the Deno authors. All rights reserved. MIT license. use crate::bindings; +use crate::bindings::BindingsMode; use crate::error::generic_error; use crate::error::to_v8_type_error; use crate::error::JsError; @@ -23,6 +24,8 @@ use crate::realm::ContextState; use crate::realm::JsRealm; use crate::realm::JsRealmInner; use crate::snapshot_util; +use crate::snapshot_util::SnapshotOptions; +use crate::snapshot_util::SnapshottedData; use crate::source_map::SourceMapCache; use crate::source_map::SourceMapGetter; use crate::Extension; @@ -47,16 +50,23 @@ use std::any::Any; use std::cell::RefCell; use std::collections::HashMap; use std::ffi::c_void; +use std::mem::ManuallyDrop; +use std::ops::Deref; +use std::ops::DerefMut; use std::option::Option; use std::pin::Pin; use std::rc::Rc; +use std::sync::atomic::AtomicBool; +use std::sync::atomic::Ordering; use std::sync::Arc; use std::sync::Mutex; use std::sync::Once; use std::task::Context; use std::task::Poll; use v8::CreateParams; -use v8::OwnedIsolate; + +const STATE_DATA_OFFSET: u32 = 0; +const MODULE_MAP_DATA_OFFSET: u32 = 1; pub enum Snapshot { Static(&'static [u8]), @@ -75,31 +85,166 @@ struct IsolateAllocations { Option<(Box>, v8::NearHeapLimitCallback)>, } +/// ManuallyDrop> is clone, but it returns a ManuallyDrop> which is a massive +/// memory-leak footgun. +struct ManuallyDropRc(ManuallyDrop>); + +impl ManuallyDropRc { + pub fn clone(&self) -> Rc { + self.0.deref().clone() + } +} + +impl Deref for ManuallyDropRc { + type Target = Rc; + fn deref(&self) -> &Self::Target { + self.0.deref() + } +} + +impl DerefMut for ManuallyDropRc { + fn deref_mut(&mut self) -> &mut Self::Target { + self.0.deref_mut() + } +} + +/// This struct contains the [`JsRuntimeState`] and [`v8::OwnedIsolate`] that are required +/// to do an orderly shutdown of V8. We keep these in a separate struct to allow us to control +/// the destruction more closely, as snapshots require the isolate to be destroyed by the +/// snapshot process, not the destructor. +/// +/// The way rusty_v8 works w/snapshots is that the [`v8::OwnedIsolate`] gets consumed by a +/// [`v8::snapshot::SnapshotCreator`] that is stored in its annex. It's a bit awkward, because this +/// means we cannot let it drop (because we don't have it after a snapshot). On top of that, we have +/// to consume it in the snapshot creator because otherwise it panics. +/// +/// This inner struct allows us to let the outer JsRuntime drop normally without a Drop impl, while we +/// control dropping more closely here using ManuallyDrop. +struct InnerIsolateState { + snapshotting: bool, + state: ManuallyDropRc>, + v8_isolate: ManuallyDrop, +} + +impl InnerIsolateState { + /// Clean out the opstate and take the inspector to prevent the inspector from getting destroyed + /// after we've torn down the contexts. If the inspector is not correctly torn down, random crashes + /// happen in tests (and possibly for users using the inspector). + pub fn prepare_for_cleanup(&mut self) { + let mut state = self.state.borrow_mut(); + let inspector = state.inspector.take(); + state.op_state.borrow_mut().clear(); + if let Some(inspector) = inspector { + assert_eq!( + Rc::strong_count(&inspector), + 1, + "The inspector must be dropped before the runtime" + ); + } + } + + pub fn cleanup(&mut self) { + self.prepare_for_cleanup(); + + let state_ptr = self.v8_isolate.get_data(STATE_DATA_OFFSET); + // SAFETY: We are sure that it's a valid pointer for whole lifetime of + // the runtime. + _ = unsafe { Rc::from_raw(state_ptr as *const RefCell) }; + + let module_map_ptr = self.v8_isolate.get_data(MODULE_MAP_DATA_OFFSET); + // SAFETY: We are sure that it's a valid pointer for whole lifetime of + // the runtime. + _ = unsafe { Rc::from_raw(module_map_ptr as *const RefCell) }; + + self.state.borrow_mut().destroy_all_realms(); + + debug_assert_eq!(Rc::strong_count(&self.state), 1); + } + + pub fn prepare_for_snapshot(mut self) -> v8::OwnedIsolate { + self.cleanup(); + // SAFETY: We're copying out of self and then immediately forgetting self + let (state, isolate) = unsafe { + ( + ManuallyDrop::take(&mut self.state.0), + ManuallyDrop::take(&mut self.v8_isolate), + ) + }; + std::mem::forget(self); + drop(state); + isolate + } +} + +impl Drop for InnerIsolateState { + fn drop(&mut self) { + self.cleanup(); + // SAFETY: We gotta drop these + unsafe { + ManuallyDrop::drop(&mut self.state.0); + if self.snapshotting { + // Create the snapshot and just drop it. + eprintln!("WARNING: v8::OwnedIsolate for snapshot was leaked"); + } else { + ManuallyDrop::drop(&mut self.v8_isolate); + } + } + } +} + /// A single execution context of JavaScript. Corresponds roughly to the "Web -/// Worker" concept in the DOM. A JsRuntime is a Future that can be used with -/// an event loop (Tokio, async_std). +/// Worker" concept in the DOM. //// -/// The JsRuntime future completes when there is an error or when all +/// The JsRuntimeImpl future completes when there is an error or when all /// pending ops have completed. /// -/// Pending ops are created in JavaScript by calling Deno.core.opAsync(), and in Rust -/// by implementing an async function that takes a serde::Deserialize "control argument" -/// and an optional zero copy buffer, each async Op is tied to a Promise in JavaScript. -pub struct JsRuntime { - state: Rc>, +/// API consumers will want to use either the [`JsRuntime`] or [`JsRuntimeForSnapshot`] +/// type aliases. +pub struct JsRuntimeImpl { + inner: InnerIsolateState, module_map: Option>>, - // This is an Option instead of just OwnedIsolate to workaround - // a safety issue with SnapshotCreator. See JsRuntime::drop. - v8_isolate: Option, - snapshot_options: snapshot_util::SnapshotOptions, - snapshot_module_load_cb: Option>, allocations: IsolateAllocations, - extensions: Rc>>, + extensions: Vec, event_loop_middlewares: Vec>, + bindings_mode: BindingsMode, // Marks if this is considered the top-level runtime. Used only be inspector. is_main: bool, } +/// The runtime type that most users will use when not creating a snapshot. +pub struct JsRuntime(JsRuntimeImpl); + +impl Deref for JsRuntime { + type Target = JsRuntimeImpl; + + fn deref(&self) -> &Self::Target { + &self.0 + } +} + +impl DerefMut for JsRuntime { + fn deref_mut(&mut self) -> &mut Self::Target { + &mut self.0 + } +} + +/// The runtime type used for snapshot creation. +pub struct JsRuntimeForSnapshot(JsRuntimeImpl); + +impl Deref for JsRuntimeForSnapshot { + type Target = JsRuntimeImpl; + + fn deref(&self) -> &Self::Target { + &self.0 + } +} + +impl DerefMut for JsRuntimeForSnapshot { + fn deref_mut(&mut self) -> &mut Self::Target { + &mut self.0 + } +} + pub(crate) struct DynImportModEvaluate { load_id: ModuleLoadId, module_id: ModuleId, @@ -156,7 +301,7 @@ pub type SharedArrayBufferStore = pub type CompiledWasmModuleStore = CrossIsolateStore; -/// Internal state for JsRuntime which is stored in one of v8::Isolate's +/// Internal state for JsRuntimeImpl which is stored in one of v8::Isolate's /// embedder slots. pub struct JsRuntimeState { global_realm: Option, @@ -253,7 +398,7 @@ pub struct RuntimeOptions { /// executed tries to load modules. pub module_loader: Option>, - /// JsRuntime extensions, not to be confused with ES modules. + /// JsRuntimeImpl extensions, not to be confused with ES modules. /// Only ops registered by extensions will be initialized. If you need /// to execute JS code from extensions, pass source files in `js` or `esm` /// option on `ExtensionBuilder`. @@ -265,15 +410,6 @@ pub struct RuntimeOptions { /// V8 snapshot that should be loaded on startup. pub startup_snapshot: Option, - /// Prepare runtime to take snapshot of loaded code. - /// The snapshot is deterministic and uses predictable random numbers. - pub will_snapshot: bool, - - /// An optional callback that will be called for each module that is loaded - /// during snapshotting. This callback can be used to transpile source on the - /// fly, during snapshotting, eg. to transpile TypeScript to JavaScript. - pub snapshot_module_load_cb: Option, - /// Isolate creation parameters. pub create_params: Option, @@ -304,50 +440,59 @@ pub struct RuntimeOptions { pub is_main: bool, } -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); - } +#[derive(Default)] +pub struct RuntimeSnapshotOptions { + /// An optional callback that will be called for each module that is loaded + /// during snapshotting. This callback can be used to transpile source on the + /// fly, during snapshotting, eg. to transpile TypeScript to JavaScript. + pub snapshot_module_load_cb: Option, } -impl JsRuntime { - const STATE_DATA_OFFSET: u32 = 0; - const MODULE_MAP_DATA_OFFSET: u32 = 1; - - /// Only constructor, configuration is done through `options`. - pub fn new(mut options: RuntimeOptions) -> Self { - let v8_platform = options.v8_platform.take(); +trait JsRuntimeInternalTrait { + fn create_raw_isolate( + refs: &'static v8::ExternalReferences, + params: Option, + snapshot: SnapshotOptions, + ) -> v8::OwnedIsolate; +} +impl JsRuntimeImpl { + fn init_v8(v8_platform: Option>) { static DENO_INIT: Once = Once::new(); - DENO_INIT.call_once(move || v8_init(v8_platform, options.will_snapshot)); + static DENO_PREDICTABLE: AtomicBool = AtomicBool::new(false); + static DENO_PREDICTABLE_SET: AtomicBool = AtomicBool::new(false); - // Add builtins extension - // TODO(bartlomieju): remove this in favor of `SnapshotOptions`. - let has_startup_snapshot = options.startup_snapshot.is_some(); - if !has_startup_snapshot { - options - .extensions - .insert(0, crate::ops_builtin::core::init_ops_and_esm()); - } else { - options - .extensions - .insert(0, crate::ops_builtin::core::init_ops()); + let predictable = FOR_SNAPSHOT || cfg!(test); + if DENO_PREDICTABLE_SET.load(Ordering::SeqCst) { + let current = DENO_PREDICTABLE.load(Ordering::SeqCst); + assert_eq!(current, predictable, "V8 may only be initialized once in either snapshotting or non-snapshotting mode. Either snapshotting or non-snapshotting mode may be used in a single process, not both."); + DENO_PREDICTABLE_SET.store(true, Ordering::SeqCst); + DENO_PREDICTABLE.store(predictable, Ordering::SeqCst); } - let ops = Self::collect_ops(&mut options.extensions); - let mut op_state = OpState::new(ops.len()); + DENO_INIT.call_once(move || v8_init(v8_platform, predictable)); + } - if let Some(get_error_class_fn) = options.get_error_class_fn { - op_state.get_error_class_fn = get_error_class_fn; - } + fn new_runtime( + mut options: RuntimeOptions, + snapshot_options: SnapshotOptions, + maybe_load_callback: Option, + ) -> JsRuntimeImpl + where + JsRuntimeImpl: JsRuntimeInternalTrait, + { + let (op_state, ops) = Self::create_opstate(&mut options, &snapshot_options); let op_state = Rc::new(RefCell::new(op_state)); + // Collect event-loop middleware + let mut event_loop_middlewares = + Vec::with_capacity(options.extensions.len()); + for extension in &mut options.extensions { + if let Some(middleware) = extension.init_event_loop_middleware() { + event_loop_middlewares.push(middleware); + } + } + let align = std::mem::align_of::(); let layout = std::alloc::Layout::from_size_align( std::mem::size_of::<*mut v8::OwnedIsolate>(), @@ -397,20 +542,22 @@ impl JsRuntime { 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(&context_state.borrow().op_ctxs); // V8 takes ownership of external_references. let refs: &'static v8::ExternalReferences = Box::leak(Box::new(refs)); - let mut maybe_snapshotted_data = None; - let (mut isolate, global_context) = Self::create_isolate( - snapshot_options.will_snapshot(), + let bindings_mode = match snapshot_options { + SnapshotOptions::None => bindings::BindingsMode::New, + SnapshotOptions::Create => bindings::BindingsMode::New, + SnapshotOptions::Load(_) => bindings::BindingsMode::LoadedFinal, + SnapshotOptions::CreateFromExisting(_) => bindings::BindingsMode::Loaded, + }; + let snapshotting = snapshot_options.will_snapshot(); + + let (mut isolate, global_context, snapshotted_data) = Self::create_isolate( refs, options.create_params.take(), - options.startup_snapshot.take(), + snapshot_options, ); // SAFETY: this is first use of `isolate_ptr` so we are sure we're // not overwriting an existing pointer. @@ -419,7 +566,7 @@ impl JsRuntime { isolate_ptr.read() }; - let mut context_scope = + let mut context_scope: v8::HandleScope = v8::HandleScope::with_context(&mut isolate, global_context.clone()); let scope = &mut context_scope; let context = v8::Local::new(scope, global_context.clone()); @@ -428,15 +575,9 @@ impl JsRuntime { scope, context, &context_state.borrow().op_ctxs, - snapshot_options, + bindings_mode, ); - // Get module map data from the snapshot - if has_startup_snapshot { - maybe_snapshotted_data = - Some(snapshot_util::get_snapshotted_data(scope, context)); - } - context.set_slot(scope, context_state.clone()); op_state.borrow_mut().put(isolate_ptr); @@ -449,23 +590,6 @@ impl JsRuntime { let loader = options .module_loader .unwrap_or_else(|| Rc::new(NoopModuleLoader)); - #[cfg(feature = "include_js_files_for_snapshotting")] - if snapshot_options.will_snapshot() { - for source in options - .extensions - .iter() - .flat_map(|e| vec![e.get_esm_sources(), e.get_js_sources()]) - .flatten() - .flatten() - { - use crate::ExtensionFileSourceCode; - if let ExtensionFileSourceCode::LoadedFromFsDuringSnapshot(path) = - &source.code - { - println!("cargo:rerun-if-changed={}", path.display()) - } - } - } { let global_realm = JsRealmInner::new( @@ -480,70 +604,58 @@ impl JsRuntime { state.known_realms.push(global_realm); } scope.set_data( - Self::STATE_DATA_OFFSET, + STATE_DATA_OFFSET, Rc::into_raw(state_rc.clone()) as *mut c_void, ); let module_map_rc = Rc::new(RefCell::new(ModuleMap::new(loader, op_state))); - if let Some(snapshotted_data) = maybe_snapshotted_data { + if let Some(snapshotted_data) = snapshotted_data { let mut module_map = module_map_rc.borrow_mut(); module_map.update_with_snapshotted_data(scope, snapshotted_data); } scope.set_data( - Self::MODULE_MAP_DATA_OFFSET, + MODULE_MAP_DATA_OFFSET, Rc::into_raw(module_map_rc.clone()) as *mut c_void, ); drop(context_scope); - let mut js_runtime = Self { - v8_isolate: Some(isolate), - snapshot_options, - snapshot_module_load_cb: options.snapshot_module_load_cb.map(Rc::new), + let mut js_runtime = JsRuntimeImpl { + inner: InnerIsolateState { + snapshotting, + state: ManuallyDropRc(ManuallyDrop::new(state_rc)), + v8_isolate: ManuallyDrop::new(isolate), + }, + bindings_mode, allocations: IsolateAllocations::default(), - event_loop_middlewares: Vec::with_capacity(options.extensions.len()), - extensions: Rc::new(RefCell::new(options.extensions)), - state: state_rc, + event_loop_middlewares, + extensions: options.extensions, module_map: Some(module_map_rc), is_main: options.is_main, }; - // Init resources and ops before extensions to make sure they are - // available during the initialization process. - js_runtime.init_extension_ops().unwrap(); let realm = js_runtime.global_realm(); - js_runtime.init_extension_js(&realm).unwrap(); - + // TODO(mmastrac): We should thread errors back out of the runtime + js_runtime + .init_extension_js(&realm, maybe_load_callback) + .unwrap(); js_runtime } /// Create a new [`v8::OwnedIsolate`] and its global [`v8::Context`] from optional parameters and snapshot. fn create_isolate( - will_snapshot: bool, refs: &'static v8::ExternalReferences, params: Option, - snapshot: Option, - ) -> (v8::OwnedIsolate, v8::Global) { - let mut isolate = if will_snapshot { - snapshot_util::create_snapshot_creator(refs, snapshot) - } else { - let mut params = params - .unwrap_or_default() - .embedder_wrapper_type_info_offsets( - V8_WRAPPER_TYPE_INDEX, - V8_WRAPPER_OBJECT_INDEX, - ) - .external_references(&**refs); - - if let Some(snapshot) = snapshot { - params = match snapshot { - Snapshot::Static(data) => params.snapshot_blob(data), - Snapshot::JustCreated(data) => params.snapshot_blob(data), - Snapshot::Boxed(data) => params.snapshot_blob(data), - }; - } - - v8::Isolate::new(params) - }; + snapshot: SnapshotOptions, + ) -> ( + v8::OwnedIsolate, + v8::Global, + Option, + ) + where + JsRuntimeImpl: JsRuntimeInternalTrait, + { + let has_snapshot = snapshot.loaded(); + let mut isolate = Self::create_raw_isolate(refs, params, snapshot); isolate.set_capture_stack_trace_for_uncaught_exceptions(true, 10); isolate.set_promise_reject_callback(bindings::promise_reject_callback); @@ -557,38 +669,32 @@ impl JsRuntime { bindings::wasm_async_resolve_promise_callback, ); - let context = { + let (context, snapshotted_data) = { let scope = &mut v8::HandleScope::new(&mut isolate); let context = v8::Context::new(scope); - v8::Global::new(scope, context) + + // Get module map data from the snapshot + let snapshotted_data = if has_snapshot { + Some(snapshot_util::get_snapshotted_data(scope, context)) + } else { + None + }; + + (v8::Global::new(scope, context), snapshotted_data) }; - (isolate, context) - } - fn drop_state_and_module_map(v8_isolate: &mut OwnedIsolate) { - let state_ptr = v8_isolate.get_data(Self::STATE_DATA_OFFSET); - let state_rc = - // SAFETY: We are sure that it's a valid pointer for whole lifetime of - // the runtime. - unsafe { Rc::from_raw(state_ptr as *const RefCell) }; - drop(state_rc); - - let module_map_ptr = v8_isolate.get_data(Self::MODULE_MAP_DATA_OFFSET); - let module_map_rc = - // SAFETY: We are sure that it's a valid pointer for whole lifetime of - // the runtime. - unsafe { Rc::from_raw(module_map_ptr as *const RefCell) }; - drop(module_map_rc); + (isolate, context, snapshotted_data) } #[inline] - pub(crate) fn module_map(&mut self) -> &Rc> { + pub(crate) fn module_map(&self) -> &Rc> { self.module_map.as_ref().unwrap() } #[inline] pub fn global_context(&self) -> v8::Global { self + .inner .state .borrow() .known_realms @@ -600,23 +706,28 @@ impl JsRuntime { #[inline] pub fn v8_isolate(&mut self) -> &mut v8::OwnedIsolate { - self.v8_isolate.as_mut().unwrap() + &mut self.inner.v8_isolate } #[inline] pub fn inspector(&mut self) -> Rc> { - self.state.borrow().inspector() + self.inner.state.borrow().inspector() } #[inline] pub fn global_realm(&mut self) -> JsRealm { - let state = self.state.borrow(); + let state = self.inner.state.borrow(); state.global_realm.clone().unwrap() } + /// Returns the extensions that this runtime is using (including internal ones). + pub fn extensions(&self) -> &Vec { + &self.extensions + } + /// Creates a new realm (V8 context) in this JS execution context, /// pre-initialized with all of the extensions that were passed in - /// [`RuntimeOptions::extensions`] when the [`JsRuntime`] was + /// [`RuntimeOptions::extensions`] when the [`JsRuntimeImpl`] was /// constructed. pub fn create_realm(&mut self) -> Result { let realm = { @@ -656,21 +767,21 @@ impl JsRuntime { scope, context, &context_state.borrow().op_ctxs, - self.snapshot_options, + self.bindings_mode, ); context.set_slot(scope, context_state.clone()); let realm = JsRealmInner::new( context_state, v8::Global::new(scope, context), - self.state.clone(), + self.inner.state.clone(), false, ); - let mut state = self.state.borrow_mut(); + let mut state = self.inner.state.borrow_mut(); state.known_realms.push(realm.clone()); JsRealm::new(realm) }; - self.init_extension_js(&realm)?; + self.init_extension_js(&realm, None)?; Ok(realm) } @@ -679,65 +790,38 @@ impl JsRuntime { self.global_realm().handle_scope(self.v8_isolate()) } - pub(crate) fn state(isolate: &v8::Isolate) -> Rc> { - let state_ptr = isolate.get_data(Self::STATE_DATA_OFFSET); - let state_rc = - // SAFETY: We are sure that it's a valid pointer for whole lifetime of - // the runtime. - unsafe { Rc::from_raw(state_ptr as *const RefCell) }; - let state = state_rc.clone(); - Rc::into_raw(state_rc); - state - } - - pub(crate) fn module_map_from( - isolate: &v8::Isolate, - ) -> Rc> { - let module_map_ptr = isolate.get_data(Self::MODULE_MAP_DATA_OFFSET); - let module_map_rc = - // SAFETY: We are sure that it's a valid pointer for whole lifetime of - // the runtime. - unsafe { Rc::from_raw(module_map_ptr as *const RefCell) }; - let module_map = module_map_rc.clone(); - Rc::into_raw(module_map_rc); - module_map - } - /// Initializes JS of provided Extensions in the given realm. - fn init_extension_js(&mut self, realm: &JsRealm) -> Result<(), Error> { - // Initalization of JS happens in phases: + fn init_extension_js( + &mut self, + realm: &JsRealm, + maybe_load_callback: Option, + ) -> Result<(), Error> { + // Initialization of JS happens in phases: // 1. Iterate through all extensions: // a. Execute all extension "script" JS files // b. Load all extension "module" JS files (but do not execute them yet) // 2. Iterate through all extensions: // a. If an extension has a `esm_entry_point`, execute it. + // Take extensions temporarily so we can avoid have a mutable reference to self + let extensions = std::mem::take(&mut self.extensions); + // TODO(nayeemrmn): Module maps should be per-realm. let module_map = self.module_map.as_ref().unwrap(); let loader = module_map.borrow().loader.clone(); let ext_loader = Rc::new(ExtModuleLoader::new( - &self.extensions.borrow(), - self.snapshot_module_load_cb.clone(), + &extensions, + maybe_load_callback.map(Rc::new), )); module_map.borrow_mut().loader = ext_loader; let mut esm_entrypoints = vec![]; - // Take extensions to avoid double-borrow - let extensions = std::mem::take(&mut self.extensions); - futures::executor::block_on(async { - let num_of_extensions = extensions.borrow().len(); - for i in 0..num_of_extensions { - let (maybe_esm_files, maybe_esm_entry_point) = { - let exts = extensions.borrow(); - ( - exts[i].get_esm_sources().map(|e| e.to_owned()), - exts[i].get_esm_entry_point(), - ) - }; + for extension in &extensions { + let maybe_esm_entry_point = extension.get_esm_entry_point(); - if let Some(esm_files) = maybe_esm_files { + if let Some(esm_files) = extension.get_esm_sources() { for file_source in esm_files { self .load_side_module( @@ -752,10 +836,7 @@ impl JsRuntime { esm_entrypoints.push(entry_point); } - let exts = extensions.borrow(); - let ext = &exts[i]; - - if let Some(js_files) = ext.get_js_sources() { + if let Some(js_files) = extension.get_js_sources() { for file_source in js_files { realm.execute_script( self.v8_isolate(), @@ -765,7 +846,7 @@ impl JsRuntime { } } - if ext.is_core { + if extension.is_core { self.init_cbs(realm); } } @@ -799,9 +880,7 @@ impl JsRuntime { Ok::<_, anyhow::Error>(()) })?; - // Restore extensions self.extensions = extensions; - self.module_map.as_ref().unwrap().borrow_mut().loader = loader; Ok(()) } @@ -866,19 +945,36 @@ impl JsRuntime { } /// Initializes ops of provided Extensions - fn init_extension_ops(&mut self) -> Result<(), Error> { - let op_state = self.op_state(); - // Setup state - for e in self.extensions.borrow_mut().iter_mut() { - // ops are already registered during in bindings::initialize_context(); - e.init_state(&mut op_state.borrow_mut()); - - // Setup event-loop middleware - if let Some(middleware) = e.init_event_loop_middleware() { - self.event_loop_middlewares.push(middleware); - } + fn create_opstate( + options: &mut RuntimeOptions, + snapshot_options: &SnapshotOptions, + ) -> (OpState, Vec) { + // Add built-in extension + if snapshot_options.loaded() { + options + .extensions + .insert(0, crate::ops_builtin::core::init_ops()); + } else { + options + .extensions + .insert(0, crate::ops_builtin::core::init_ops_and_esm()); } - Ok(()) + + let ops = Self::collect_ops(&mut options.extensions); + + let mut op_state = OpState::new(ops.len()); + + if let Some(get_error_class_fn) = options.get_error_class_fn { + op_state.get_error_class_fn = get_error_class_fn; + } + + // Setup state + for e in &mut options.extensions { + // ops are already registered during in bindings::initialize_context(); + e.init_state(&mut op_state); + } + + (op_state, ops) } pub fn eval<'s, T>( @@ -954,7 +1050,7 @@ impl JsRuntime { /// Returns the runtime's op state, which can be used to maintain ops /// and access resources between op calls. pub fn op_state(&mut self) -> Rc> { - let state = self.state.borrow(); + let state = self.inner.state.borrow(); state.op_state.clone() } @@ -1029,56 +1125,6 @@ impl JsRuntime { self.resolve_value(promise).await } - /// Takes a snapshot. The isolate should have been created with will_snapshot - /// set to true. - /// - /// `Error` can usually be downcast to `JsError`. - pub fn snapshot(mut self) -> v8::StartupData { - self.state.borrow_mut().inspector.take(); - - // Set the context to be snapshot's default context - { - let context = self.global_context(); - let mut scope = self.handle_scope(); - let local_context = v8::Local::new(&mut scope, context); - scope.set_default_context(local_context); - } - - // Serialize the module map and store its data in the snapshot. - { - let snapshotted_data = { - let module_map_rc = self.module_map.take().unwrap(); - let module_map = module_map_rc.borrow(); - module_map.serialize_for_snapshotting(&mut self.handle_scope()) - }; - - let context = self.global_context(); - let mut scope = self.handle_scope(); - snapshot_util::set_snapshotted_data( - &mut scope, - context, - snapshotted_data, - ); - } - - // Drop existing ModuleMap to drop v8::Global handles - { - let v8_isolate = self.v8_isolate(); - Self::drop_state_and_module_map(v8_isolate); - } - - // Drop other v8::Global handles before snapshotting - { - let state = self.state.clone(); - state.borrow_mut().destroy_all_realms(); - } - - let snapshot_creator = self.v8_isolate.take().unwrap(); - snapshot_creator - .create_blob(v8::FunctionCodeHandling::Keep) - .unwrap() - } - /// Returns the namespace object of a module. /// /// This is only available after module evaluation has completed. @@ -1170,18 +1216,18 @@ impl JsRuntime { } pub fn maybe_init_inspector(&mut self) { - if self.state.borrow().inspector.is_some() { + if self.inner.state.borrow().inspector.is_some() { return; } let context = self.global_context(); let scope = &mut v8::HandleScope::with_context( - self.v8_isolate.as_mut().unwrap(), + self.inner.v8_isolate.as_mut(), context.clone(), ); let context = v8::Local::new(scope, context); - let mut state = self.state.borrow_mut(); + let mut state = self.inner.state.borrow_mut(); state.inspector = Some(JsRuntimeInspector::new(scope, context, self.is_main)); } @@ -1258,7 +1304,7 @@ impl JsRuntime { let has_inspector: bool; { - let state = self.state.borrow(); + let state = self.inner.state.borrow(); has_inspector = state.inspector.is_some(); state.waker.register(cx.waker()); } @@ -1306,7 +1352,7 @@ impl JsRuntime { // Event loop middlewares let mut maybe_scheduling = false; { - let op_state = self.state.borrow().op_state.clone(); + let op_state = self.inner.state.borrow().op_state.clone(); for f in &self.event_loop_middlewares { if f(op_state.clone(), cx) { maybe_scheduling = true; @@ -1342,7 +1388,7 @@ impl JsRuntime { return Poll::Ready(Ok(())); } - let state = self.state.borrow(); + let state = self.inner.state.borrow(); // Check if more async ops have been dispatched // during this turn of event loop. @@ -1391,7 +1437,8 @@ impl JsRuntime { || pending_state.has_tick_scheduled { // pass, will be polled again - } else if self.state.borrow().dyn_module_evaluate_idle_counter >= 1 { + } else if self.inner.state.borrow().dyn_module_evaluate_idle_counter >= 1 + { let scope = &mut self.handle_scope(); let messages = find_stalled_top_level_await(scope); // We are gonna print only a single message to provide a nice formatting @@ -1403,7 +1450,7 @@ impl JsRuntime { let js_error = JsError::from_v8_message(scope, msg); return Poll::Ready(Err(js_error.into())); } else { - let mut state = self.state.borrow_mut(); + let mut state = self.inner.state.borrow_mut(); // Delay the above error by one spin of the event loop. A dynamic import // evaluation may complete during this, in which case the counter will // reset. @@ -1416,20 +1463,63 @@ impl JsRuntime { } fn event_loop_pending_state(&mut self) -> EventLoopPendingState { - let isolate = self.v8_isolate.as_mut().unwrap(); - let mut scope = v8::HandleScope::new(isolate); + let mut scope = v8::HandleScope::new(self.inner.v8_isolate.as_mut()); EventLoopPendingState::new( &mut scope, - &mut self.state.borrow_mut(), + &mut self.inner.state.borrow_mut(), &self.module_map.as_ref().unwrap().borrow(), ) } +} + +impl JsRuntime { + /// Only constructor, configuration is done through `options`. + pub fn new(mut options: RuntimeOptions) -> JsRuntime { + JsRuntimeImpl::::init_v8(options.v8_platform.take()); + + let snapshot_options = snapshot_util::SnapshotOptions::new_from( + options.startup_snapshot.take(), + false, + ); + + JsRuntime(JsRuntimeImpl::::new_runtime( + options, + snapshot_options, + None, + )) + } + + pub(crate) fn state_from( + isolate: &v8::Isolate, + ) -> Rc> { + let state_ptr = isolate.get_data(STATE_DATA_OFFSET); + let state_rc = + // SAFETY: We are sure that it's a valid pointer for whole lifetime of + // the runtime. + unsafe { Rc::from_raw(state_ptr as *const RefCell) }; + let state = state_rc.clone(); + std::mem::forget(state_rc); + state + } + + pub(crate) fn module_map_from( + isolate: &v8::Isolate, + ) -> Rc> { + let module_map_ptr = isolate.get_data(MODULE_MAP_DATA_OFFSET); + let module_map_rc = + // SAFETY: We are sure that it's a valid pointer for whole lifetime of + // the runtime. + unsafe { Rc::from_raw(module_map_ptr as *const RefCell) }; + let module_map = module_map_rc.clone(); + std::mem::forget(module_map_rc); + module_map + } pub(crate) fn event_loop_pending_state_from_scope( scope: &mut v8::HandleScope, ) -> EventLoopPendingState { - let state = Self::state(scope); - let module_map = Self::module_map_from(scope); + let state = JsRuntime::state_from(scope); + let module_map = JsRuntime::module_map_from(scope); let state = EventLoopPendingState::new( scope, &mut state.borrow_mut(), @@ -1439,6 +1529,102 @@ impl JsRuntime { } } +impl JsRuntimeForSnapshot { + pub fn new( + mut options: RuntimeOptions, + runtime_snapshot_options: RuntimeSnapshotOptions, + ) -> JsRuntimeForSnapshot { + JsRuntimeImpl::::init_v8(options.v8_platform.take()); + + let snapshot_options = snapshot_util::SnapshotOptions::new_from( + options.startup_snapshot.take(), + true, + ); + + JsRuntimeForSnapshot(JsRuntimeImpl::::new_runtime( + options, + snapshot_options, + runtime_snapshot_options.snapshot_module_load_cb, + )) + } + + /// Takes a snapshot and consumes the runtime. + /// + /// `Error` can usually be downcast to `JsError`. + pub fn snapshot(mut self) -> v8::StartupData { + // Ensure there are no live inspectors to prevent crashes. + self.inner.prepare_for_cleanup(); + + // Set the context to be snapshot's default context + { + let context = self.global_context(); + let mut scope = self.handle_scope(); + let local_context = v8::Local::new(&mut scope, context); + scope.set_default_context(local_context); + } + + // Serialize the module map and store its data in the snapshot. + { + let snapshotted_data = { + let module_map_rc = self.module_map.take().unwrap(); + let module_map = module_map_rc.borrow(); + module_map.serialize_for_snapshotting(&mut self.handle_scope()) + }; + + let context = self.global_context(); + let mut scope = self.handle_scope(); + snapshot_util::set_snapshotted_data( + &mut scope, + context, + snapshotted_data, + ); + } + + self + .0 + .inner + .prepare_for_snapshot() + .create_blob(v8::FunctionCodeHandling::Keep) + .unwrap() + } +} + +impl JsRuntimeInternalTrait for JsRuntimeImpl { + fn create_raw_isolate( + refs: &'static v8::ExternalReferences, + _params: Option, + snapshot: SnapshotOptions, + ) -> v8::OwnedIsolate { + snapshot_util::create_snapshot_creator(refs, snapshot) + } +} + +impl JsRuntimeInternalTrait for JsRuntimeImpl { + fn create_raw_isolate( + refs: &'static v8::ExternalReferences, + params: Option, + snapshot: SnapshotOptions, + ) -> v8::OwnedIsolate { + let mut params = params + .unwrap_or_default() + .embedder_wrapper_type_info_offsets( + V8_WRAPPER_TYPE_INDEX, + V8_WRAPPER_OBJECT_INDEX, + ) + .external_references(&**refs); + + if let Some(snapshot) = snapshot.snapshot() { + params = match snapshot { + Snapshot::Static(data) => params.snapshot_blob(data), + Snapshot::JustCreated(data) => params.snapshot_blob(data), + Snapshot::Boxed(data) => params.snapshot_blob(data), + }; + } + + v8::Isolate::new(params) + } +} + fn get_stalled_top_level_await_message_for_module( scope: &mut v8::HandleScope, module_id: ModuleId, @@ -1544,7 +1730,7 @@ where F: FnMut(usize, usize) -> usize, { // SAFETY: The data is a pointer to the Rust callback function. It is stored - // in `JsRuntime::allocations` and thus is guaranteed to outlive the isolate. + // in `JsRuntimeImpl::allocations` and thus is guaranteed to outlive the isolate. let callback = unsafe { &mut *(data as *mut F) }; callback(current_heap_limit, initial_heap_limit) } @@ -1567,7 +1753,7 @@ pub(crate) fn exception_to_err_result( exception: v8::Local, in_promise: bool, ) -> Result { - let state_rc = JsRuntime::state(scope); + let state_rc = JsRuntime::state_from(scope); let was_terminating_execution = scope.is_execution_terminating(); // Disable running microtasks for a moment. When upgrading to V8 v11.4 @@ -1613,7 +1799,7 @@ pub(crate) fn exception_to_err_result( } // Related to module loading -impl JsRuntime { +impl JsRuntimeImpl { pub(crate) fn instantiate_module( &mut self, id: ModuleId, @@ -1680,9 +1866,9 @@ impl JsRuntime { // For more details see: // https://github.com/denoland/deno/issues/4908 // https://v8.dev/features/top-level-await#module-execution-order - let global_realm = self.state.borrow_mut().global_realm.clone().unwrap(); - let scope = - &mut global_realm.handle_scope(self.v8_isolate.as_mut().unwrap()); + let global_realm = + self.inner.state.borrow_mut().global_realm.clone().unwrap(); + let scope = &mut global_realm.handle_scope(&mut self.inner.v8_isolate); let tc_scope = &mut v8::TryCatch::new(scope); let module = v8::Local::new(tc_scope, &module_handle); let maybe_value = module.evaluate(tc_scope); @@ -1710,6 +1896,7 @@ impl JsRuntime { }; self + .inner .state .borrow_mut() .pending_dyn_mod_evaluate @@ -1729,11 +1916,11 @@ impl JsRuntime { /// Evaluates an already instantiated ES module. /// /// Returns a receiver handle that resolves when module promise resolves. - /// Implementors must manually call [`JsRuntime::run_event_loop`] to drive + /// Implementors must manually call [`JsRuntimeImpl::run_event_loop`] to drive /// module evaluation future. /// /// `Error` can usually be downcast to `JsError` and should be awaited and - /// checked after [`JsRuntime::run_event_loop`] completion. + /// checked after [`JsRuntimeImpl::run_event_loop`] completion. /// /// This function panics if module has not been instantiated. pub fn mod_evaluate( @@ -1741,7 +1928,7 @@ impl JsRuntime { id: ModuleId, ) -> oneshot::Receiver> { let global_realm = self.global_realm(); - let state_rc = self.state.clone(); + let state_rc = self.inner.state.clone(); let module_map_rc = self.module_map().clone(); let scope = &mut self.handle_scope(); let tc_scope = &mut v8::TryCatch::new(scope); @@ -1766,7 +1953,7 @@ impl JsRuntime { // Because that promise is created internally by V8, when error occurs during // module evaluation the promise is rejected, and since the promise has no rejection // handler it will result in call to `bindings::promise_reject_callback` adding - // the promise to pending promise rejection table - meaning JsRuntime will return + // the promise to pending promise rejection table - meaning JsRuntimeImpl will return // error on next poll(). // // This situation is not desirable as we want to manually return error at the @@ -1903,7 +2090,7 @@ impl JsRuntime { } fn dynamic_import_resolve(&mut self, id: ModuleLoadId, mod_id: ModuleId) { - let state_rc = self.state.clone(); + let state_rc = self.inner.state.clone(); let module_map_rc = self.module_map().clone(); let scope = &mut self.handle_scope(); @@ -2078,14 +2265,14 @@ impl JsRuntime { /// then another turn of event loop must be performed. fn evaluate_pending_module(&mut self) { let maybe_module_evaluation = - self.state.borrow_mut().pending_mod_evaluate.take(); + self.inner.state.borrow_mut().pending_mod_evaluate.take(); if maybe_module_evaluation.is_none() { return; } let mut module_evaluation = maybe_module_evaluation.unwrap(); - let state_rc = self.state.clone(); + let state_rc = self.inner.state.clone(); let scope = &mut self.handle_scope(); let promise_global = module_evaluation.promise.clone().unwrap(); @@ -2126,8 +2313,9 @@ impl JsRuntime { // Returns true if some dynamic import was resolved. fn evaluate_dyn_imports(&mut self) -> bool { - let pending = - std::mem::take(&mut self.state.borrow_mut().pending_dyn_mod_evaluate); + let pending = std::mem::take( + &mut self.inner.state.borrow_mut().pending_dyn_mod_evaluate, + ); if pending.is_empty() { return false; } @@ -2170,7 +2358,7 @@ impl JsRuntime { } } } - self.state.borrow_mut().pending_dyn_mod_evaluate = still_pending; + self.inner.state.borrow_mut().pending_dyn_mod_evaluate = still_pending; resolved_any } @@ -2179,7 +2367,7 @@ impl JsRuntime { /// The module will be marked as "main", and because of that /// "import.meta.main" will return true when checked inside that module. /// - /// User must call [`JsRuntime::mod_evaluate`] with returned `ModuleId` + /// User must call [`JsRuntimeImpl::mod_evaluate`] with returned `ModuleId` /// manually after load is finished. pub async fn load_main_module( &mut self, @@ -2234,7 +2422,7 @@ impl JsRuntime { /// This method is meant to be used when loading some utility code that /// might be later imported by the main module (ie. an entry point module). /// - /// User must call [`JsRuntime::mod_evaluate`] with returned `ModuleId` + /// User must call [`JsRuntimeImpl::mod_evaluate`] with returned `ModuleId` /// manually after load is finished. pub async fn load_side_module( &mut self, @@ -2285,7 +2473,7 @@ impl JsRuntime { } fn check_promise_rejections(&mut self) -> Result<(), Error> { - let state = self.state.clone(); + let state = self.inner.state.clone(); let scope = &mut self.handle_scope(); let state = state.borrow(); for realm in &state.known_realms { @@ -2298,21 +2486,16 @@ impl JsRuntime { fn do_js_event_loop_tick(&mut self, cx: &mut Context) -> Result<(), Error> { // Now handle actual ops. { - let mut state = self.state.borrow_mut(); + let mut state = self.inner.state.borrow_mut(); state.have_unpolled_ops = false; } // Handle responses for each realm. - let isolate = self.v8_isolate.as_mut().unwrap(); - let realm_count = self.state.clone().borrow().known_realms.len(); + let state = self.inner.state.clone(); + let isolate = &mut self.inner.v8_isolate; + let realm_count = state.borrow().known_realms.len(); for realm_idx in 0..realm_count { - let realm = self - .state - .borrow() - .known_realms - .get(realm_idx) - .unwrap() - .clone(); + let realm = 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); @@ -2334,8 +2517,7 @@ impl JsRuntime { context_state.pending_ops.poll_next_unpin(cx) { let (promise_id, op_id, mut resp) = item; - self - .state + state .borrow() .op_state .borrow() @@ -2352,7 +2534,7 @@ impl JsRuntime { } let has_tick_scheduled = - v8::Boolean::new(scope, self.state.borrow().has_tick_scheduled); + v8::Boolean::new(scope, self.inner.state.borrow().has_tick_scheduled); args.push(has_tick_scheduled.into()); let js_event_loop_tick_cb_handle = @@ -2386,7 +2568,7 @@ pub fn queue_fast_async_op( ) { let runtime_state = match ctx.runtime_state.upgrade() { Some(rc_state) => rc_state, - // atleast 1 Rc is held by the JsRuntime. + // at least 1 Rc is held by the JsRuntimeImpl. None => unreachable!(), }; let get_class = { @@ -2484,7 +2666,7 @@ pub fn queue_async_op<'s>( ) -> Option> { let runtime_state = match ctx.runtime_state.upgrade() { Some(rc_state) => rc_state, - // atleast 1 Rc is held by the JsRuntime. + // at least 1 Rc is held by the JsRuntimeImpl. None => unreachable!(), }; @@ -3053,13 +3235,24 @@ pub mod tests { .await; } + /// Ensure that putting the inspector into OpState doesn't cause crashes. The only valid place we currently allow + /// the inspector to be stashed without cleanup is the OpState, and this should not actually cause crashes. + #[test] + fn inspector() { + let mut runtime = JsRuntime::new(RuntimeOptions { + inspector: true, + ..Default::default() + }); + // This was causing a crash + runtime.op_state().borrow_mut().put(runtime.inspector()); + runtime.execute_script_static("check.js", "null").unwrap(); + } + #[test] fn will_snapshot() { let snapshot = { - let mut runtime = JsRuntime::new(RuntimeOptions { - will_snapshot: true, - ..Default::default() - }); + let mut runtime = + JsRuntimeForSnapshot::new(Default::default(), Default::default()); runtime.execute_script_static("a.js", "a = 1 + 2").unwrap(); runtime.snapshot() }; @@ -3077,10 +3270,8 @@ pub mod tests { #[test] fn will_snapshot2() { let startup_data = { - let mut runtime = JsRuntime::new(RuntimeOptions { - will_snapshot: true, - ..Default::default() - }); + let mut runtime = + JsRuntimeForSnapshot::new(Default::default(), Default::default()); runtime .execute_script_static("a.js", "let a = 1 + 2") .unwrap(); @@ -3088,11 +3279,13 @@ pub mod tests { }; let snapshot = Snapshot::JustCreated(startup_data); - let mut runtime = JsRuntime::new(RuntimeOptions { - will_snapshot: true, - startup_snapshot: Some(snapshot), - ..Default::default() - }); + let mut runtime = JsRuntimeForSnapshot::new( + RuntimeOptions { + startup_snapshot: Some(snapshot), + ..Default::default() + }, + Default::default(), + ); let startup_data = { runtime @@ -3120,10 +3313,8 @@ pub mod tests { #[test] fn test_snapshot_callbacks() { let snapshot = { - let mut runtime = JsRuntime::new(RuntimeOptions { - will_snapshot: true, - ..Default::default() - }); + let mut runtime = + JsRuntimeForSnapshot::new(Default::default(), Default::default()); runtime .execute_script_static( "a.js", @@ -3157,10 +3348,8 @@ pub mod tests { #[test] fn test_from_boxed_snapshot() { let snapshot = { - let mut runtime = JsRuntime::new(RuntimeOptions { - will_snapshot: true, - ..Default::default() - }); + let mut runtime = + JsRuntimeForSnapshot::new(Default::default(), Default::default()); runtime.execute_script_static("a.js", "a = 1 + 2").unwrap(); let snap: &[u8] = &runtime.snapshot(); Vec::from(snap).into_boxed_slice() @@ -3375,8 +3564,8 @@ pub mod tests { } } - fn create_module( - runtime: &mut JsRuntime, + fn create_module( + runtime: &mut JsRuntimeImpl, i: usize, main: bool, ) -> ModuleInfo { @@ -3419,7 +3608,10 @@ pub mod tests { } } - fn assert_module_map(runtime: &mut JsRuntime, modules: &Vec) { + fn assert_module_map( + runtime: &mut JsRuntimeImpl, + modules: &Vec, + ) { let module_map_rc = runtime.module_map(); let module_map = module_map_rc.borrow(); assert_eq!(module_map.handles.len(), modules.len()); @@ -3453,14 +3645,16 @@ pub mod tests { } let loader = Rc::new(ModsLoader::default()); - let mut runtime = JsRuntime::new(RuntimeOptions { - module_loader: Some(loader.clone()), - will_snapshot: true, - extensions: vec![Extension::builder("text_ext") - .ops(vec![op_test::decl()]) - .build()], - ..Default::default() - }); + let mut runtime = JsRuntimeForSnapshot::new( + RuntimeOptions { + module_loader: Some(loader.clone()), + extensions: vec![Extension::builder("text_ext") + .ops(vec![op_test::decl()]) + .build()], + ..Default::default() + }, + Default::default(), + ); let specifier = crate::resolve_url("file:///0.js").unwrap(); let source_code = @@ -3489,15 +3683,17 @@ pub mod tests { let snapshot = runtime.snapshot(); - let mut runtime2 = JsRuntime::new(RuntimeOptions { - module_loader: Some(loader.clone()), - will_snapshot: true, - startup_snapshot: Some(Snapshot::JustCreated(snapshot)), - extensions: vec![Extension::builder("text_ext") - .ops(vec![op_test::decl()]) - .build()], - ..Default::default() - }); + let mut runtime2 = JsRuntimeForSnapshot::new( + RuntimeOptions { + module_loader: Some(loader.clone()), + startup_snapshot: Some(Snapshot::JustCreated(snapshot)), + extensions: vec![Extension::builder("text_ext") + .ops(vec![op_test::decl()]) + .build()], + ..Default::default() + }, + Default::default(), + ); assert_module_map(&mut runtime2, &modules); @@ -3997,8 +4193,7 @@ Deno.core.opAsync("op_async_serialize_object_with_numbers_as_keys", { assert!(matches!(runtime.poll_event_loop(cx, false), Poll::Pending)); assert_eq!(awoken_times.swap(0, Ordering::Relaxed), 1); - let state_rc = JsRuntime::state(runtime.v8_isolate()); - state_rc.borrow_mut().has_tick_scheduled = false; + runtime.inner.state.borrow_mut().has_tick_scheduled = false; assert!(matches!( runtime.poll_event_loop(cx, false), Poll::Ready(Ok(())) @@ -4479,10 +4674,8 @@ Deno.core.opAsync("op_async_serialize_object_with_numbers_as_keys", { #[test] fn js_realm_init_snapshot() { let snapshot = { - let runtime = JsRuntime::new(RuntimeOptions { - will_snapshot: true, - ..Default::default() - }); + let runtime = + JsRuntimeForSnapshot::new(Default::default(), Default::default()); let snap: &[u8] = &runtime.snapshot(); Vec::from(snap).into_boxed_slice() }; diff --git a/core/snapshot_util.rs b/core/snapshot_util.rs index 20019f5cc7..05a196f50c 100644 --- a/core/snapshot_util.rs +++ b/core/snapshot_util.rs @@ -4,9 +4,10 @@ use std::path::Path; use std::path::PathBuf; use std::time::Instant; +use crate::runtime::RuntimeSnapshotOptions; use crate::ExtModuleLoaderCb; use crate::Extension; -use crate::JsRuntime; +use crate::JsRuntimeForSnapshot; use crate::RuntimeOptions; use crate::Snapshot; @@ -21,16 +22,28 @@ pub struct CreateSnapshotOptions { pub snapshot_module_load_cb: Option, } -pub fn create_snapshot(create_snapshot_options: CreateSnapshotOptions) { +pub struct CreateSnapshotOutput { + /// Any files marked as LoadedFromFsDuringSnapshot are collected here and should be + /// printed as 'cargo:rerun-if-changed' lines from your build script. + pub files_loaded_during_snapshot: Vec, +} + +#[must_use = "The files listed by create_snapshot should be printed as 'cargo:rerun-if-changed' lines"] +pub fn create_snapshot( + create_snapshot_options: CreateSnapshotOptions, +) -> CreateSnapshotOutput { let mut mark = Instant::now(); - let js_runtime = JsRuntime::new(RuntimeOptions { - will_snapshot: true, - startup_snapshot: create_snapshot_options.startup_snapshot, - extensions: create_snapshot_options.extensions, - snapshot_module_load_cb: create_snapshot_options.snapshot_module_load_cb, - ..Default::default() - }); + let js_runtime = JsRuntimeForSnapshot::new( + RuntimeOptions { + startup_snapshot: create_snapshot_options.startup_snapshot, + extensions: create_snapshot_options.extensions, + ..Default::default() + }, + RuntimeSnapshotOptions { + snapshot_module_load_cb: create_snapshot_options.snapshot_module_load_cb, + }, + ); println!( "JsRuntime for snapshot prepared, took {:#?} ({})", Instant::now().saturating_duration_since(mark), @@ -38,6 +51,22 @@ pub fn create_snapshot(create_snapshot_options: CreateSnapshotOptions) { ); mark = Instant::now(); + let mut files_loaded_during_snapshot = vec![]; + for source in js_runtime + .extensions() + .iter() + .flat_map(|e| vec![e.get_esm_sources(), e.get_js_sources()]) + .flatten() + .flatten() + { + use crate::ExtensionFileSourceCode; + if let ExtensionFileSourceCode::LoadedFromFsDuringSnapshot(path) = + &source.code + { + files_loaded_during_snapshot.push(path.clone()); + } + } + let snapshot = js_runtime.snapshot(); let snapshot_slice: &[u8] = &snapshot; println!( @@ -83,6 +112,9 @@ pub fn create_snapshot(create_snapshot_options: CreateSnapshotOptions) { Instant::now().saturating_duration_since(mark), create_snapshot_options.snapshot_path.display(), ); + CreateSnapshotOutput { + files_loaded_during_snapshot, + } } pub type FilterFn = Box bool>; @@ -121,29 +153,36 @@ fn data_error_to_panic(err: v8::DataError) -> ! { } } -#[derive(Copy, Clone, Debug, PartialEq, Eq)] pub(crate) enum SnapshotOptions { - Load, - CreateFromExisting, + Load(Snapshot), + CreateFromExisting(Snapshot), Create, None, } impl SnapshotOptions { + pub fn new_from(snapshot: Option, will_snapshot: bool) -> Self { + match (snapshot, will_snapshot) { + (Some(snapshot), true) => Self::CreateFromExisting(snapshot), + (None, true) => Self::Create, + (Some(snapshot), false) => Self::Load(snapshot), + (None, false) => Self::None, + } + } + pub fn loaded(&self) -> bool { - matches!(self, Self::Load | Self::CreateFromExisting) + matches!(self, Self::Load(_) | Self::CreateFromExisting(_)) } pub fn will_snapshot(&self) -> bool { - matches!(self, Self::Create | Self::CreateFromExisting) + matches!(self, Self::Create | Self::CreateFromExisting(_)) } - pub fn from_bools(snapshot_loaded: bool, will_snapshot: bool) -> Self { - match (snapshot_loaded, will_snapshot) { - (true, true) => Self::CreateFromExisting, - (false, true) => Self::Create, - (true, false) => Self::Load, - (false, false) => Self::None, + pub fn snapshot(self) -> Option { + match self { + Self::CreateFromExisting(snapshot) => Some(snapshot), + Self::Load(snapshot) => Some(snapshot), + _ => None, } } } @@ -218,9 +257,9 @@ pub(crate) fn set_snapshotted_data( /// Returns an isolate set up for snapshotting. pub(crate) fn create_snapshot_creator( external_refs: &'static v8::ExternalReferences, - maybe_startup_snapshot: Option, + maybe_startup_snapshot: SnapshotOptions, ) -> v8::OwnedIsolate { - if let Some(snapshot) = maybe_startup_snapshot { + if let Some(snapshot) = maybe_startup_snapshot.snapshot() { match snapshot { Snapshot::Static(data) => { v8::Isolate::snapshot_creator_from_existing_snapshot( diff --git a/runtime/build.rs b/runtime/build.rs index 334c3b11a9..f656682a1d 100644 --- a/runtime/build.rs +++ b/runtime/build.rs @@ -337,7 +337,7 @@ mod startup_snapshot { runtime_main::init_ops_and_esm(), ]; - create_snapshot(CreateSnapshotOptions { + let output = create_snapshot(CreateSnapshotOptions { cargo_manifest_dir: env!("CARGO_MANIFEST_DIR"), snapshot_path, startup_snapshot: None, @@ -345,6 +345,9 @@ mod startup_snapshot { compression_cb: None, snapshot_module_load_cb: Some(Box::new(transpile_ts_for_snapshotting)), }); + for path in output.files_loaded_during_snapshot { + println!("cargo:rerun-if-changed={}", path.display()); + } } }