From 4c6db7aa1493139f5a832c1e9ebfe44a1c80af80 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bartek=20Iwa=C5=84czuk?= Date: Wed, 22 Feb 2023 01:55:31 +0100 Subject: [PATCH] perf(core, runtime): Further improve startup time (#17860) This commit further improves startup time by: - no relying on "JsRuntime::execute_script" for runtime bootstrapping, this is instead done using V8 APIs directly - registering error classes during the snapshot time, instead of on startup Further improvements can be made, mainly around removing "core.initializeAsyncOps()" which takes around 2ms. This commit should result in ~1ms startup time improvement. --- core/bindings.rs | 24 +++++-- core/runtime.rs | 35 ++++++++-- runtime/js/99_main.js | 129 ++++++++++++++++++------------------ runtime/web_worker.rs | 48 +++++++++++--- runtime/worker.rs | 35 ++++++++-- runtime/worker_bootstrap.rs | 7 +- 6 files changed, 184 insertions(+), 94 deletions(-) diff --git a/core/bindings.rs b/core/bindings.rs index 50308b9318..8ac3082508 100644 --- a/core/bindings.rs +++ b/core/bindings.rs @@ -111,12 +111,29 @@ pub fn initialize_context<'s>( let scope = &mut v8::ContextScope::new(scope, context); + let deno_str = v8::String::new(scope, "Deno").unwrap(); + let core_str = v8::String::new(scope, "core").unwrap(); + let ops_str = v8::String::new(scope, "ops").unwrap(); + // Snapshot already registered `Deno.core.ops` but // extensions may provide ops that aren't part of the snapshot. if snapshot_options.loaded() { // Grab the Deno.core.ops object & init it - let ops_obj = JsRuntime::eval::(scope, "Deno.core.ops") - .expect("Deno.core.ops to exist"); + let deno_obj: v8::Local = global + .get(scope, deno_str.into()) + .unwrap() + .try_into() + .unwrap(); + let core_obj: v8::Local = deno_obj + .get(scope, core_str.into()) + .unwrap() + .try_into() + .unwrap(); + let ops_obj: v8::Local = core_obj + .get(scope, ops_str.into()) + .expect("Deno.core.ops to exist") + .try_into() + .unwrap(); initialize_ops(scope, ops_obj, op_ctxs, snapshot_options); if snapshot_options != SnapshotOptions::CreateFromExisting { initialize_async_ops_info(scope, ops_obj, op_ctxs); @@ -126,11 +143,9 @@ pub fn initialize_context<'s>( // global.Deno = { core: { } }; let deno_obj = v8::Object::new(scope); - let deno_str = v8::String::new(scope, "Deno").unwrap(); global.set(scope, deno_str.into(), deno_obj.into()); let core_obj = v8::Object::new(scope); - let core_str = v8::String::new(scope, "core").unwrap(); deno_obj.set(scope, core_str.into(), core_obj.into()); // Bind functions to Deno.core.* @@ -144,7 +159,6 @@ pub fn initialize_context<'s>( // Bind functions to Deno.core.ops.* let ops_obj = v8::Object::new(scope); - let ops_str = v8::String::new(scope, "ops").unwrap(); core_obj.set(scope, ops_str.into(), ops_obj.into()); if !snapshot_options.will_snapshot() { diff --git a/core/runtime.rs b/core/runtime.rs index 9c6b7afeaf..c028d97c29 100644 --- a/core/runtime.rs +++ b/core/runtime.rs @@ -985,11 +985,36 @@ impl JsRuntime { fn init_cbs(&mut self, realm: &JsRealm) { let (recv_cb, build_custom_error_cb) = { let scope = &mut realm.handle_scope(self.v8_isolate()); - let recv_cb = - Self::eval::(scope, "Deno.core.opresolve").unwrap(); - let build_custom_error_cb = - Self::eval::(scope, "Deno.core.buildCustomError") - .expect("Deno.core.buildCustomError is undefined in the realm"); + let context = realm.context(); + let context_local = v8::Local::new(scope, context); + let global = context_local.global(scope); + let deno_str = v8::String::new(scope, "Deno").unwrap(); + let core_str = v8::String::new(scope, "core").unwrap(); + let opresolve_str = v8::String::new(scope, "opresolve").unwrap(); + let build_custom_error_str = + v8::String::new(scope, "buildCustomError").unwrap(); + + let deno_obj: v8::Local = global + .get(scope, deno_str.into()) + .unwrap() + .try_into() + .unwrap(); + let core_obj: v8::Local = deno_obj + .get(scope, core_str.into()) + .unwrap() + .try_into() + .unwrap(); + + let recv_cb: v8::Local = core_obj + .get(scope, opresolve_str.into()) + .unwrap() + .try_into() + .unwrap(); + let build_custom_error_cb: v8::Local = core_obj + .get(scope, build_custom_error_str.into()) + .unwrap() + .try_into() + .unwrap(); ( v8::Global::new(scope, recv_cb), v8::Global::new(scope, build_custom_error_cb), diff --git a/runtime/js/99_main.js b/runtime/js/99_main.js index fa9b0a20d2..da5b5f1b8b 100644 --- a/runtime/js/99_main.js +++ b/runtime/js/99_main.js @@ -231,6 +231,69 @@ function formatException(error) { } } +core.registerErrorClass("NotFound", errors.NotFound); +core.registerErrorClass("PermissionDenied", errors.PermissionDenied); +core.registerErrorClass("ConnectionRefused", errors.ConnectionRefused); +core.registerErrorClass("ConnectionReset", errors.ConnectionReset); +core.registerErrorClass("ConnectionAborted", errors.ConnectionAborted); +core.registerErrorClass("NotConnected", errors.NotConnected); +core.registerErrorClass("AddrInUse", errors.AddrInUse); +core.registerErrorClass("AddrNotAvailable", errors.AddrNotAvailable); +core.registerErrorClass("BrokenPipe", errors.BrokenPipe); +core.registerErrorClass("AlreadyExists", errors.AlreadyExists); +core.registerErrorClass("InvalidData", errors.InvalidData); +core.registerErrorClass("TimedOut", errors.TimedOut); +core.registerErrorClass("Interrupted", errors.Interrupted); +core.registerErrorClass("WouldBlock", errors.WouldBlock); +core.registerErrorClass("WriteZero", errors.WriteZero); +core.registerErrorClass("UnexpectedEof", errors.UnexpectedEof); +core.registerErrorClass("BadResource", errors.BadResource); +core.registerErrorClass("Http", errors.Http); +core.registerErrorClass("Busy", errors.Busy); +core.registerErrorClass("NotSupported", errors.NotSupported); +core.registerErrorBuilder( + "DOMExceptionOperationError", + function DOMExceptionOperationError(msg) { + return new DOMException(msg, "OperationError"); + }, +); +core.registerErrorBuilder( + "DOMExceptionQuotaExceededError", + function DOMExceptionQuotaExceededError(msg) { + return new DOMException(msg, "QuotaExceededError"); + }, +); +core.registerErrorBuilder( + "DOMExceptionNotSupportedError", + function DOMExceptionNotSupportedError(msg) { + return new DOMException(msg, "NotSupported"); + }, +); +core.registerErrorBuilder( + "DOMExceptionNetworkError", + function DOMExceptionNetworkError(msg) { + return new DOMException(msg, "NetworkError"); + }, +); +core.registerErrorBuilder( + "DOMExceptionAbortError", + function DOMExceptionAbortError(msg) { + return new DOMException(msg, "AbortError"); + }, +); +core.registerErrorBuilder( + "DOMExceptionInvalidCharacterError", + function DOMExceptionInvalidCharacterError(msg) { + return new DOMException(msg, "InvalidCharacterError"); + }, +); +core.registerErrorBuilder( + "DOMExceptionDataError", + function DOMExceptionDataError(msg) { + return new DOMException(msg, "DataError"); + }, +); + function runtimeStart(runtimeOptions, source) { core.setMacrotaskCallback(timers.handleTimerMacrotask); core.setMacrotaskCallback(promiseRejectMacrotaskCallback); @@ -247,72 +310,6 @@ function runtimeStart(runtimeOptions, source) { colors.setNoColor(runtimeOptions.noColor || !runtimeOptions.isTty); // deno-lint-ignore prefer-primordials Error.prepareStackTrace = core.prepareStackTrace; - registerErrors(); -} - -function registerErrors() { - core.registerErrorClass("NotFound", errors.NotFound); - core.registerErrorClass("PermissionDenied", errors.PermissionDenied); - core.registerErrorClass("ConnectionRefused", errors.ConnectionRefused); - core.registerErrorClass("ConnectionReset", errors.ConnectionReset); - core.registerErrorClass("ConnectionAborted", errors.ConnectionAborted); - core.registerErrorClass("NotConnected", errors.NotConnected); - core.registerErrorClass("AddrInUse", errors.AddrInUse); - core.registerErrorClass("AddrNotAvailable", errors.AddrNotAvailable); - core.registerErrorClass("BrokenPipe", errors.BrokenPipe); - core.registerErrorClass("AlreadyExists", errors.AlreadyExists); - core.registerErrorClass("InvalidData", errors.InvalidData); - core.registerErrorClass("TimedOut", errors.TimedOut); - core.registerErrorClass("Interrupted", errors.Interrupted); - core.registerErrorClass("WouldBlock", errors.WouldBlock); - core.registerErrorClass("WriteZero", errors.WriteZero); - core.registerErrorClass("UnexpectedEof", errors.UnexpectedEof); - core.registerErrorClass("BadResource", errors.BadResource); - core.registerErrorClass("Http", errors.Http); - core.registerErrorClass("Busy", errors.Busy); - core.registerErrorClass("NotSupported", errors.NotSupported); - core.registerErrorBuilder( - "DOMExceptionOperationError", - function DOMExceptionOperationError(msg) { - return new DOMException(msg, "OperationError"); - }, - ); - core.registerErrorBuilder( - "DOMExceptionQuotaExceededError", - function DOMExceptionQuotaExceededError(msg) { - return new DOMException(msg, "QuotaExceededError"); - }, - ); - core.registerErrorBuilder( - "DOMExceptionNotSupportedError", - function DOMExceptionNotSupportedError(msg) { - return new DOMException(msg, "NotSupported"); - }, - ); - core.registerErrorBuilder( - "DOMExceptionNetworkError", - function DOMExceptionNetworkError(msg) { - return new DOMException(msg, "NetworkError"); - }, - ); - core.registerErrorBuilder( - "DOMExceptionAbortError", - function DOMExceptionAbortError(msg) { - return new DOMException(msg, "AbortError"); - }, - ); - core.registerErrorBuilder( - "DOMExceptionInvalidCharacterError", - function DOMExceptionInvalidCharacterError(msg) { - return new DOMException(msg, "InvalidCharacterError"); - }, - ); - core.registerErrorBuilder( - "DOMExceptionDataError", - function DOMExceptionDataError(msg) { - return new DOMException(msg, "DataError"); - }, - ); } const pendingRejections = []; diff --git a/runtime/web_worker.rs b/runtime/web_worker.rs index bcf999b3b7..353c4a4429 100644 --- a/runtime/web_worker.rs +++ b/runtime/web_worker.rs @@ -318,6 +318,7 @@ pub struct WebWorker { pub worker_type: WebWorkerType, pub main_module: ModuleSpecifier, poll_for_messages_fn: Option>, + bootstrap_fn_global: Option>, } pub struct WebWorkerOptions { @@ -496,6 +497,25 @@ impl WebWorker { (internal_handle, external_handle) }; + let bootstrap_fn_global = { + let context = js_runtime.global_context(); + let scope = &mut js_runtime.handle_scope(); + let context_local = v8::Local::new(scope, context); + let global_obj = context_local.global(scope); + let bootstrap_str = v8::String::new(scope, "bootstrap").unwrap(); + let bootstrap_ns: v8::Local = global_obj + .get(scope, bootstrap_str.into()) + .unwrap() + .try_into() + .unwrap(); + let main_runtime_str = v8::String::new(scope, "workerRuntime").unwrap(); + let bootstrap_fn = + bootstrap_ns.get(scope, main_runtime_str.into()).unwrap(); + let bootstrap_fn = + v8::Local::::try_from(bootstrap_fn).unwrap(); + v8::Global::new(scope, bootstrap_fn) + }; + ( Self { id: worker_id, @@ -505,6 +525,7 @@ impl WebWorker { worker_type: options.worker_type, main_module, poll_for_messages_fn: None, + bootstrap_fn_global: Some(bootstrap_fn_global), }, external_handle, ) @@ -513,15 +534,24 @@ impl WebWorker { pub fn bootstrap(&mut self, options: &BootstrapOptions) { // Instead of using name for log we use `worker-${id}` because // WebWorkers can have empty string as name. - let script = format!( - "bootstrap.workerRuntime({}, \"{}\", \"{}\")", - options.as_json(), - self.name, - self.id - ); - self - .execute_script(&located_script_name!(), &script) - .expect("Failed to execute worker bootstrap script"); + { + let scope = &mut self.js_runtime.handle_scope(); + let options_v8 = + deno_core::serde_v8::to_v8(scope, options.as_json()).unwrap(); + let bootstrap_fn = self.bootstrap_fn_global.take().unwrap(); + let bootstrap_fn = v8::Local::new(scope, bootstrap_fn); + let undefined = v8::undefined(scope); + let name_str: v8::Local = + v8::String::new(scope, &self.name).unwrap().into(); + let id_str: v8::Local = + v8::String::new(scope, &format!("{}", self.id)) + .unwrap() + .into(); + bootstrap_fn + .call(scope, undefined.into(), &[options_v8, name_str, id_str]) + .unwrap(); + } + // TODO(bartlomieju): this could be done using V8 API, without calling `execute_script`. // Save a reference to function that will start polling for messages // from a worker host; it will be called after the user code is loaded. let script = r#" diff --git a/runtime/worker.rs b/runtime/worker.rs index d1998cd881..908f1a7ab0 100644 --- a/runtime/worker.rs +++ b/runtime/worker.rs @@ -14,7 +14,6 @@ use deno_cache::SqliteBackedCache; use deno_core::error::AnyError; use deno_core::error::JsError; use deno_core::futures::Future; -use deno_core::located_script_name; use deno_core::v8; use deno_core::CompiledWasmModuleStore; use deno_core::Extension; @@ -66,6 +65,7 @@ pub struct MainWorker { should_break_on_first_statement: bool, should_wait_for_inspector_session: bool, exit_code: ExitCode, + bootstrap_fn_global: Option>, } pub struct WorkerOptions { @@ -318,20 +318,45 @@ impl MainWorker { op_state.borrow_mut().put(inspector); } + let bootstrap_fn_global = { + let context = js_runtime.global_context(); + let scope = &mut js_runtime.handle_scope(); + let context_local = v8::Local::new(scope, context); + let global_obj = context_local.global(scope); + let bootstrap_str = v8::String::new(scope, "bootstrap").unwrap(); + let bootstrap_ns: v8::Local = global_obj + .get(scope, bootstrap_str.into()) + .unwrap() + .try_into() + .unwrap(); + let main_runtime_str = v8::String::new(scope, "mainRuntime").unwrap(); + let bootstrap_fn = + bootstrap_ns.get(scope, main_runtime_str.into()).unwrap(); + let bootstrap_fn = + v8::Local::::try_from(bootstrap_fn).unwrap(); + v8::Global::new(scope, bootstrap_fn) + }; + Self { js_runtime, should_break_on_first_statement: options.should_break_on_first_statement, should_wait_for_inspector_session: options .should_wait_for_inspector_session, exit_code, + bootstrap_fn_global: Some(bootstrap_fn_global), } } pub fn bootstrap(&mut self, options: &BootstrapOptions) { - let script = format!("bootstrap.mainRuntime({})", options.as_json()); - self - .execute_script(&located_script_name!(), &script) - .expect("Failed to execute bootstrap script"); + let scope = &mut self.js_runtime.handle_scope(); + let options_v8 = + deno_core::serde_v8::to_v8(scope, options.as_json()).unwrap(); + let bootstrap_fn = self.bootstrap_fn_global.take().unwrap(); + let bootstrap_fn = v8::Local::new(scope, bootstrap_fn); + let undefined = v8::undefined(scope); + bootstrap_fn + .call(scope, undefined.into(), &[options_v8]) + .unwrap(); } /// See [JsRuntime::execute_script](deno_core::JsRuntime::execute_script) diff --git a/runtime/worker_bootstrap.rs b/runtime/worker_bootstrap.rs index 5563b6eadb..12abceca65 100644 --- a/runtime/worker_bootstrap.rs +++ b/runtime/worker_bootstrap.rs @@ -58,8 +58,8 @@ impl Default for BootstrapOptions { } impl BootstrapOptions { - pub fn as_json(&self) -> String { - let payload = json!({ + pub fn as_json(&self) -> serde_json::Value { + json!({ // Shared bootstrap args "args": self.args, "cpuCount": self.cpu_count, @@ -80,7 +80,6 @@ impl BootstrapOptions { "v8Version": deno_core::v8_version(), "userAgent": self.user_agent, "inspectFlag": self.inspect, - }); - serde_json::to_string_pretty(&payload).unwrap() + }) } }