Repository: denoland/deno_core
Pull request #752 (2024-05-23 02:06:37)

refactor: pass hash into code cache
This avoids the cli having to re-lookup the hash again.


Patch:
diff --git a/ARCHITECTURE.md b/ARCHITECTURE.md
new file mode 100644
index 0000000..4e09752
--- /dev/null
+++ b/ARCHITECTURE.md
@@ -0,0 +1,163 @@
+# Architecture
+
+## JsRuntime
+
+The heart of Deno is the `JsRuntime`, which wraps the V8 concepts of an
+`Isolate` and a `Context`. To create a runtime, an embedder instantiates a
+`JsRuntime` using a set of `extension`s, and then poll the event loop until the
+runtime has completed.
+
+Each `extension` consists of a number of JavaScript- or TypeScript-language
+ECMAScript modules, as well as a number of `op`s. This module code is loaded
+when the runtime is created, allowing user code to access whatever system
+services an embedder has decided to provide.
+
+The `op`s provided by `extension`s allow for simple and fast JavaScript-to-Rust
+function calls. An `op` may be sync or async (which makes it sync or async in
+both the Rust and JavaScript world), and fallible or infallible. Most
+fundamental V8 and Rust types are supported for `op` calls, and the system will
+generally choose the most performant way to transport data regardless of type.
+
+The runtime also provides persistent state in a number of forms: a
+general-purpose `OpState` used by Rust code to stash state, `Resource`s that can
+be read and written by both script and Rust code, and `cppgc` objects that can
+be accessed by the Rust side and held on the JavaScript side (but not directly
+accessed).
+
+## Runtime Services
+
+By default, v8 does not provide a fully-functional runtime. Many of the required
+pieces to get a useful runtime are provided by the embedder. `deno_core`
+provides a number of these useful services for you, including:
+
+0. Extensions and `op`s
+1. Resource management
+2. Error and promise rejection handling
+3. Event loop callbacks
+4. Timers
+5. Basic console support
+6. Snapshot management
+
+## Extensions and `op`s
+
+Extensions provide additional functionality to Deno beyond the core runtime.
+They can include `op`s for interacting with the runtime, ESM modules for
+JavaScript code, and other features like configuration parameters and middleware
+functions.
+
+Extensions are defined using the `extension!` macro in `deno_core`. The options
+available for defining an extension using the `extension!` macro allow for
+fine-grained control over its behavior and dependencies.
+
+When the runtime is initialized, it creates a V8 isolate and context, and then
+instantiates the provided extension code and ops into the new context. The ops
+are provided in a special, virtual module that can be imported from the
+extension module code, allowing embedders to create fully-fledged APIs that are
+backed by a combination of Rust, JavaScript or both types of code.
+
+In general, APIs for user code are provided in one of two forms: global APIs,
+available on `globalThis` (the equivalent of the `window` object in browsers),
+and module-style APIs.
+
+In the former style of API, the modules can add APIs to `globalThis` directly:
+
+```ts
+globalThis.Deno.myAPI = function myAPI() {
+  op_do_something();
+};
+```
+
+In the latter style of API, modules can be given friendly names in the
+`extension` definition, and user code can import them directly:
+
+```rust
+extension!(
+    ...
+    esm = [ "brand:api_module": "api.js" ]
+);
+```
+
+```ts
+import { myAPI } from "brand:api_module";
+myAPI();
+```
+
+## Resource management
+
+Deno provides the equivalent of system streams in the form of `Resource`s. A
+`Resource` is a Rust struct that implements a `Resource` trait. This trait
+allows for explicit lifetime management: a unique integer value is associated
+with the struct that can be used to explicitly close the resource when not
+needed.
+
+A `Resource` is instantiated inside of an `op`, and the integer handle is
+returned to the calling code.
+
+The `close` and `tryClose` operations are available as a `Deno.core` JS API, and
+both take the integer ID of the resource.
+
+A `Resource` may or may not implement stream read and write operations. These
+are exposed via `read*` and `write*` APIs on `Deno.core`, and the APIs are
+mapped to the `Resource`'s read and write trait methods.
+
+## Error and promise rejection handling
+
+Embedders make use of two callbacks for unhandled promise rejections and
+uncaught errors: `setReportExceptionCallback`,
+`setUnhandledPromiseRejectionHandler`.
+
+When an ES Module throws an exception at the top level, or code in a callback or
+promise continuation throws an exception that is not handled, `Deno.core`
+detects this as an unhandled exception and routes it to the
+`setReportExceptionCallback` or `setUnhandledPromiseRejectionHandler` callbacks.
+
+`Deno.core` automatically handles errors and promise rejects that bubble out of
+user code and into the top-level module and rejects the promise for the module
+itself.
+
+If an error ends up bubbling up to `reportUnhandledException` or
+`reportUnhandledPromiseRejection`, that error is exposed through the runtime's
+polling interface and runtime execution is halted.
+
+Custom error classes may be registered via the `registerErrorClass` API,
+allowing for Rust code to throw strongly-typed exceptions to user JS code. The
+`custom_error` function in `deno_core` may be used to return these custom error
+classes.
+
+## Event loop callbacks
+
+In async JavaScript, forward progress is made for module and promise resolution
+through event loop callbacks. These callbacks may be async `op` resolution,
+timer resolution, module loads, or internal callbacks from NAPI or FFI code.
+
+Event loop callbacks generally happen through `eventLoopTick` for convenience,
+though some other events are dispatched directly. The `eventLoopTick` method
+will often appear in stack traces.
+
+In `JsRuntime`, `do_js_event_loop_tick_realm` polls the various possible sources
+of forward progress and routes them to the V8 engine. While `deno_core` may
+allow for these async operations to complete at any time, the results are only
+forwarded to the V8 engine during the event dispatch phase of the event loop,
+preventing callbacks into the engine at unexpected times.
+
+## Timers
+
+`deno_core` provides a number of high-level timer support APIs for embedders to
+build on. `queueUserTimer`, `refTimer` and `unrefTimer` are sufficient to
+implement both web-compatible and node-compatible timers.
+
+Timers are dispatched as part of the `eventLoopTick` infrastructure, however
+this may change in the future.
+
+## Basic console support
+
+`deno_core` provides a basic implementation of the V8 console that integrates
+with the V8 inspector. Embedders are encouraged to supply their own more
+functional console implementation, however.
+
+## Snapshot management
+
+For embedders that wish to optimize `JsRuntime` startup, `JsRuntimeForSnapshot`
+allows for a runtime to be created that will be serialized to a V8 snapshot.
+This snapshot data may be passed to a `JsRuntime` as it boots, allowing it to
+pre-initialize a V8 isolate and context from earlier initialization work.
diff --git a/Cargo.lock b/Cargo.lock
index 41ff3b3..e4ca10f 100644
--- a/Cargo.lock
+++ b/Cargo.lock
@@ -540,6 +540,7 @@ dependencies = [
  "sourcemap",
  "static_assertions",
  "tokio",
+ "twox-hash",
  "unicycle",
  "url",
  "v8",
@@ -2532,6 +2533,17 @@ dependencies = [
  "toml",
 ]
 
+[[package]]
+name = "twox-hash"
+version = "1.6.3"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "97fee6b57c6a41524a810daee9286c02d7752c4253064d0b05472833a438f675"
+dependencies = [
+ "cfg-if",
+ "rand",
+ "static_assertions",
+]
+
 [[package]]
 name = "typed-arena"
 version = "2.0.2"
diff --git a/Cargo.toml b/Cargo.toml
index f1c0d5a..50a8283 100644
--- a/Cargo.toml
+++ b/Cargo.toml
@@ -59,6 +59,7 @@ strum_macros = "0.25.0"
 testing_macros = "0.2.11"
 thiserror = "1"
 tokio = { version = "1", features = ["full"] }
+twox-hash = "1.6.3"
 url = { version = "2", features = ["serde", "expose_internals"] }
 
 # macros
diff --git a/core/Cargo.toml b/core/Cargo.toml
index 100d100..f9f9e88 100644
--- a/core/Cargo.toml
+++ b/core/Cargo.toml
@@ -54,6 +54,7 @@ deno_ast.workspace = true
 fastrand.workspace = true
 pretty_assertions.workspace = true
 rstest.workspace = true
+twox-hash.workspace = true
 unicycle = "0"
 
 [[bench]]
diff --git a/core/lib.rs b/core/lib.rs
index 88a6199..5434462 100644
--- a/core/lib.rs
+++ b/core/lib.rs
@@ -117,6 +117,7 @@ pub use crate::modules::ModuleLoader;
 pub use crate::modules::ModuleName;
 pub use crate::modules::ModuleSource;
 pub use crate::modules::ModuleSourceCode;
+pub use crate::modules::ModuleSourceCodeCache;
 pub use crate::modules::ModuleSourceFuture;
 pub use crate::modules::ModuleType;
 pub use crate::modules::NoopModuleLoader;
diff --git a/core/modules/loaders.rs b/core/modules/loaders.rs
index 4efcc4d..edab4a8 100644
--- a/core/modules/loaders.rs
+++ b/core/modules/loaders.rs
@@ -89,7 +89,8 @@ pub trait ModuleLoader {
   /// It's not required to implement this method.
   fn code_cache_ready(
     &self,
-    _module_specifier: &ModuleSpecifier,
+    _module_specifier: ModuleSpecifier,
+    _hash: u64,
     _code_cache: &[u8],
   ) -> Pin<Box<dyn Future<Output = ()>>> {
     async {}.boxed_local()
diff --git a/core/modules/map.rs b/core/modules/map.rs
index 743b73b..64f069c 100644
--- a/core/modules/map.rs
+++ b/core/modules/map.rs
@@ -91,9 +91,9 @@ impl ModEvaluate {
 }
 
 type CodeCacheReadyCallback =
-  Box<dyn Fn(&[u8]) -> Pin<Box<dyn Future<Output = ()>>>>;
+  Box<dyn FnOnce(&[u8]) -> Pin<Box<dyn Future<Output = ()>>>>;
 pub(crate) struct CodeCacheInfo {
-  code_cache: Option<Cow<'static, [u8]>>,
+  data: Option<Cow<'static, [u8]>>,
   ready_callback: CodeCacheReadyCallback,
 }
 
@@ -139,7 +139,6 @@ pub(crate) struct ModuleMap {
   pending_code_cache_ready: Cell<bool>,
   module_waker: AtomicWaker,
   data: RefCell<ModuleMapData>,
-  enable_code_cache: bool,
 
   /// A counter used to delay our dynamic import deadlock detection by one spin
   /// of the event loop.
@@ -193,7 +192,6 @@ impl ModuleMap {
     loader: Rc<dyn ModuleLoader>,
     exception_state: Rc<ExceptionState>,
     import_meta_resolve_cb: ImportMetaResolveCallback,
-    enable_code_cache: bool,
   ) -> Self {
     Self {
       loader: loader.into(),
@@ -212,7 +210,6 @@ impl ModuleMap {
       pending_code_cache_ready: Default::default(),
       module_waker: Default::default(),
       data: Default::default(),
-      enable_code_cache,
     }
   }
 
@@ -346,24 +343,25 @@ impl ModuleMap {
           ModuleSource::get_string_source(module_url_found.as_str(), code)
             .map_err(ModuleError::Other)?;
 
-        let (code_cache_info, module_url_found) = if self.enable_code_cache {
-          let (module_url_found1, module_url_found2) =
-            module_url_found.into_cheap_copy();
-          let loader = self.loader.borrow().clone();
-          (
-            Some(CodeCacheInfo {
-              code_cache,
-              ready_callback: Box::new(move |cache| {
-                let specifier =
-                  ModuleSpecifier::parse(module_url_found1.as_str()).unwrap();
-                loader.code_cache_ready(&specifier, cache)
+        let (code_cache_info, module_url_found) =
+          if let Some(code_cache) = code_cache {
+            let (module_url_found1, module_url_found2) =
+              module_url_found.into_cheap_copy();
+            let loader = self.loader.borrow().clone();
+            (
+              Some(CodeCacheInfo {
+                data: code_cache.data,
+                ready_callback: Box::new(move |cache| {
+                  let specifier =
+                    ModuleSpecifier::parse(module_url_found1.as_str()).unwrap();
+                  loader.code_cache_ready(specifier, code_cache.hash, cache)
+                }),
               }),
-            }),
-            module_url_found2,
-          )
-        } else {
-          (None, module_url_found)
-        };
+              module_url_found2,
+            )
+          } else {
+            (None, module_url_found)
+          };
 
         self.new_module_from_js_source(
           scope,
@@ -441,16 +439,16 @@ impl ModuleMap {
               exports,
             )?;
 
-            let (code_cache_info, url2) = if self.enable_code_cache {
+            let (code_cache_info, url2) = if let Some(code_cache) = code_cache {
               let (url1, url2) = url2.into_cheap_copy();
               let loader = self.loader.borrow().clone();
               (
                 Some(CodeCacheInfo {
-                  code_cache,
+                  data: code_cache.data,
                   ready_callback: Box::new(move |cache| {
                     let specifier =
                       ModuleSpecifier::parse(url1.as_str()).unwrap();
-                    loader.code_cache_ready(&specifier, cache)
+                    loader.code_cache_ready(specifier, code_cache.hash, cache)
                   }),
                 }),
                 url2,
@@ -599,7 +597,7 @@ impl ModuleMap {
     let (maybe_module, try_store_code_cache) = code_cache_info
       .as_ref()
       .and_then(|code_cache_info| {
-        code_cache_info.code_cache.as_ref().map(|cache| {
+        code_cache_info.data.as_ref().map(|cache| {
           let mut source = v8::script_compiler::Source::new_with_cached_data(
             source_str,
             Some(&origin),
@@ -1743,12 +1741,12 @@ impl ModuleMap {
       scope,
       module_specifier,
       ModuleSource::get_string_source(specifier.as_str(), source.code)?,
-      if self.enable_code_cache {
+      if let Some(code_cache) = source.code_cache {
         let loader = self.loader.borrow().clone();
         Some(CodeCacheInfo {
-          code_cache: source.code_cache,
+          data: code_cache.data,
           ready_callback: Box::new(move |cache| {
-            loader.code_cache_ready(&specifier, cache)
+            loader.code_cache_ready(specifier, code_cache.hash, cache)
           }),
         })
       } else {
diff --git a/core/modules/mod.rs b/core/modules/mod.rs
index a05565a..8e52efc 100644
--- a/core/modules/mod.rs
+++ b/core/modules/mod.rs
@@ -337,6 +337,12 @@ impl ModuleType {
   }
 }
 
+#[derive(Debug)]
+pub struct ModuleSourceCodeCache {
+  pub hash: u64,
+  pub data: Option<Cow<'static, [u8]>>,
+}
+
 /// EsModule source code that will be loaded into V8.
 ///
 /// Users can implement `Into<ModuleInfo>` for different file types that
@@ -357,7 +363,7 @@ impl ModuleType {
 pub struct ModuleSource {
   pub code: ModuleSourceCode,
   pub module_type: ModuleType,
-  pub code_cache: Option<Cow<'static, [u8]>>,
+  pub code_cache: Option<ModuleSourceCodeCache>,
   module_url_specified: ModuleName,
   /// If the module was found somewhere other than the specified address, this will be [`Some`].
   module_url_found: Option<ModuleName>,
@@ -369,7 +375,7 @@ impl ModuleSource {
     module_type: impl Into<ModuleType>,
     code: ModuleSourceCode,
     specifier: &ModuleSpecifier,
-    code_cache: Option<Cow<'static, [u8]>>,
+    code_cache: Option<ModuleSourceCodeCache>,
   ) -> Self {
     let module_url_specified = specifier.as_ref().to_owned().into();
     Self {
@@ -388,7 +394,7 @@ impl ModuleSource {
     code: ModuleSourceCode,
     specifier: &ModuleSpecifier,
     specifier_found: &ModuleSpecifier,
-    code_cache: Option<Cow<'static, [u8]>>,
+    code_cache: Option<ModuleSourceCodeCache>,
   ) -> Self {
     let module_url_found = if specifier == specifier_found {
       None
@@ -422,7 +428,7 @@ impl ModuleSource {
     code: &'static str,
     specified: impl AsRef<str>,
     found: impl AsRef<str>,
-    code_cache: Option<Cow<'static, [u8]>>,
+    code_cache: Option<ModuleSourceCodeCache>,
   ) -> Self {
     let specified = specified.as_ref().to_string();
     let found = found.as_ref().to_string();
diff --git a/core/runtime/jsruntime.rs b/core/runtime/jsruntime.rs
index 2b7bc93..7caf85e 100644
--- a/core/runtime/jsruntime.rs
+++ b/core/runtime/jsruntime.rs
@@ -892,7 +892,6 @@ impl JsRuntime {
       loader,
       exception_state.clone(),
       import_meta_resolve_cb,
-      options.enable_code_cache,
     ));
 
     if let Some((snapshotted_data, mut data_store)) = snapshotted_data {


Test patch:
diff --git a/core/modules/tests.rs b/core/modules/tests.rs
index f02f4b0..1b3e49c 100644
--- a/core/modules/tests.rs
+++ b/core/modules/tests.rs
@@ -15,6 +15,7 @@ use crate::modules::ModuleError;
 use crate::modules::ModuleInfo;
 use crate::modules::ModuleRequest;
 use crate::modules::ModuleSourceCode;
+use crate::modules::ModuleSourceCodeCache;
 use crate::modules::RequestedModuleType;
 use crate::resolve_import;
 use crate::resolve_url;
@@ -213,15 +214,27 @@ impl Future for DelayedSourceCodeFuture {
       return Poll::Pending;
     }
     match mock_source_code(&inner.url) {
-      Some(src) => Poll::Ready(Ok(ModuleSource::for_test_with_redirect(
-        src.0,
-        inner.url.as_str(),
-        src.1,
-        inner
-          .code_cache
-          .as_ref()
-          .map(|code_cache| Cow::Owned(code_cache.clone())),
-      ))),
+      Some(src) => {
+        let hash = {
+          use std::hash::Hash;
+          use std::hash::Hasher;
+          let mut hasher = twox_hash::XxHash64::default();
+          src.1.hash(&mut hasher);
+          hasher.finish()
+        };
+        Poll::Ready(Ok(ModuleSource::for_test_with_redirect(
+          src.0,
+          inner.url.as_str(),
+          src.1,
+          Some(ModuleSourceCodeCache {
+            hash,
+            data: inner
+              .code_cache
+              .as_ref()
+              .map(|code_cache| Cow::Owned(code_cache.clone())),
+          }),
+        )))
+      }
       None => Poll::Ready(Err(MockError::LoadErr.into())),
     }
   }
@@ -275,7 +288,8 @@ impl ModuleLoader for MockLoader {
 
   fn code_cache_ready(
     &self,
-    module_specifier: &ModuleSpecifier,
+    module_specifier: ModuleSpecifier,
+    _hash: u64,
     code_cache: &[u8],
   ) -> Pin<Box<dyn Future<Output = ()>>> {
     let mut updated_code_cache = self.updated_code_cache.lock();