Repository: denoland/deno_core
Pull request #655 (2024-03-13 23:55:46)

feat: support for v8 code cache
This PR adds V8 code cache support for ES modules loaded through `ModuleLoader`, and for scripts evaluated through `evalContext` (used by `require` in Deno). Embedders have full control over whether code caching is enabled, and how code cache is retrieved/stored.


Patch:
diff --git a/Cargo.lock b/Cargo.lock
index 94a5b77..71592a0 100644
--- a/Cargo.lock
+++ b/Cargo.lock
@@ -2497,9 +2497,9 @@ dependencies = [
 
 [[package]]
 name = "v8"
-version = "0.85.0"
+version = "0.86.0"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "ec8e09551fa5c3500b47f08912b4a39e07ae20a3874051941408fbd52e3e5190"
+checksum = "0d30d72faeef07020ec4428dbfa2909801e5e36becf650c1b49c92b70f6771d8"
 dependencies = [
  "bitflags 2.4.2",
  "fslock",
diff --git a/Cargo.toml b/Cargo.toml
index 6af3926..328324e 100644
--- a/Cargo.toml
+++ b/Cargo.toml
@@ -23,7 +23,7 @@ deno_core = { version = "0.270.0", path = "./core" }
 deno_ops = { version = "0.146.0", path = "./ops" }
 serde_v8 = { version = "0.179.0", path = "./serde_v8" }
 
-v8 = { version = "0.85.0", default-features = false }
+v8 = { version = "0.86.0", default-features = false }
 deno_ast = { version = "=0.32.0", features = ["transpiling"] }
 deno_unsync = "0.3.2"
 deno_core_icudata = "0.0.73"
diff --git a/core/examples/ts_module_loader.rs b/core/examples/ts_module_loader.rs
index 44d63dd..436ec52 100644
--- a/core/examples/ts_module_loader.rs
+++ b/core/examples/ts_module_loader.rs
@@ -123,6 +123,7 @@ impl ModuleLoader for TypescriptModuleLoader {
         module_type,
         ModuleSourceCode::String(code.into()),
         module_specifier,
+        None,
       ))
     }
 
diff --git a/core/modules/loaders.rs b/core/modules/loaders.rs
index fa3313d..115a49a 100644
--- a/core/modules/loaders.rs
+++ b/core/modules/loaders.rs
@@ -82,6 +82,18 @@ pub trait ModuleLoader {
   ) -> Pin<Box<dyn Future<Output = Result<(), Error>>>> {
     async { Ok(()) }.boxed_local()
   }
+
+  /// Called when new v8 code cache is available for this module. Implementors
+  /// can store the provided code cache for future executions of the same module.
+  ///
+  /// It's not required to implement this method.
+  fn code_cache_ready(
+    &self,
+    _module_specifier: &ModuleSpecifier,
+    _code_cache: &[u8],
+  ) -> Pin<Box<dyn Future<Output = ()>>> {
+    async {}.boxed_local()
+  }
 }
 
 /// Placeholder structure used when creating
@@ -186,6 +198,7 @@ impl ModuleLoader for ExtModuleLoader {
       ModuleType::JavaScript,
       ModuleSourceCode::String(source),
       specifier,
+      None,
     )))
   }
 
@@ -240,6 +253,7 @@ impl ModuleLoader for LazyEsmModuleLoader {
       ModuleType::JavaScript,
       ModuleSourceCode::String(source),
       specifier,
+      None,
     )))
   }
 
@@ -316,6 +330,7 @@ impl ModuleLoader for FsModuleLoader {
         module_type,
         ModuleSourceCode::Bytes(code.into_boxed_slice().into()),
         &module_specifier,
+        None,
       );
       Ok(module)
     }
@@ -377,6 +392,7 @@ impl ModuleLoader for StaticModuleLoader {
         ModuleType::JavaScript,
         ModuleSourceCode::String(code.try_clone().unwrap()),
         module_specifier,
+        None,
       ))
     } else {
       Err(generic_error("Module not found"))
diff --git a/core/modules/map.rs b/core/modules/map.rs
index 57ca421..b6646d4 100644
--- a/core/modules/map.rs
+++ b/core/modules/map.rs
@@ -45,6 +45,7 @@ use log::debug;
 use v8::Function;
 use v8::PromiseState;
 
+use std::borrow::Cow;
 use std::cell::Cell;
 use std::cell::RefCell;
 use std::collections::HashMap;
@@ -63,6 +64,8 @@ use super::RequestedModuleType;
 type PrepareLoadFuture =
   dyn Future<Output = (ModuleLoadId, Result<RecursiveModuleLoad, Error>)>;
 
+type CodeCacheReadyFuture = dyn Future<Output = ()>;
+
 use super::ImportMetaResolveCallback;
 
 struct ModEvaluate {
@@ -70,6 +73,13 @@ struct ModEvaluate {
   sender: Option<oneshot::Sender<Result<(), Error>>>,
 }
 
+type CodeCacheReadyCallback =
+  Box<dyn Fn(&[u8]) -> Pin<Box<dyn Future<Output = ()>>>>;
+pub(crate) struct CodeCacheInfo {
+  code_cache: Option<Cow<'static, [u8]>>,
+  ready_callback: CodeCacheReadyCallback,
+}
+
 pub const BOM_CHAR: &[u8] = &[0xef, 0xbb, 0xbf];
 
 /// Strips the byte order mark from the provided text if it exists.
@@ -107,8 +117,12 @@ pub(crate) struct ModuleMap {
   pending_dyn_mod_evaluations: RefCell<Vec<DynImportModEvaluate>>,
   pending_dyn_mod_evaluations_pending: Cell<bool>,
   pending_mod_evaluation: Cell<bool>,
+  code_cache_ready_futs:
+    RefCell<FuturesUnordered<Pin<Box<CodeCacheReadyFuture>>>>,
+  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.
@@ -162,6 +176,7 @@ impl ModuleMap {
     loader: Rc<dyn ModuleLoader>,
     exception_state: Rc<ExceptionState>,
     import_meta_resolve_cb: ImportMetaResolveCallback,
+    enable_code_cache: bool,
   ) -> Self {
     Self {
       loader: loader.into(),
@@ -176,8 +191,11 @@ impl ModuleMap {
       pending_dyn_mod_evaluations: Default::default(),
       pending_dyn_mod_evaluations_pending: Default::default(),
       pending_mod_evaluation: Default::default(),
+      code_cache_ready_futs: Default::default(),
+      pending_code_cache_ready: Default::default(),
       module_waker: Default::default(),
       data: Default::default(),
+      enable_code_cache,
     }
   }
 
@@ -271,6 +289,7 @@ impl ModuleMap {
       module_type,
       module_url_found,
       module_url_specified,
+      code_cache,
     } = module_source;
 
     // Register the module in the module map unless it's already there. If the
@@ -310,6 +329,26 @@ impl ModuleMap {
         let code =
           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)
+              }),
+            }),
+            module_url_found2,
+          )
+        } else {
+          (None, module_url_found)
+        };
+
         self.new_module_from_js_source(
           scope,
           main,
@@ -317,6 +356,7 @@ impl ModuleMap {
           module_url_found,
           code,
           dynamic,
+          code_cache_info,
         )?
       }
       ModuleType::Wasm => {
@@ -384,6 +424,25 @@ impl ModuleMap {
               synthetic_module_type,
               exports,
             )?;
+
+            let (code_cache_info, url2) = if self.enable_code_cache {
+              let (url1, url2) = url2.into_cheap_copy();
+              let loader = self.loader.borrow().clone();
+              (
+                Some(CodeCacheInfo {
+                  code_cache,
+                  ready_callback: Box::new(move |cache| {
+                    let specifier =
+                      ModuleSpecifier::parse(url1.as_str()).unwrap();
+                    loader.code_cache_ready(&specifier, cache)
+                  }),
+                }),
+                url2,
+              )
+            } else {
+              (None, url2)
+            };
+
             self.new_module_from_js_source(
               scope,
               main,
@@ -391,6 +450,7 @@ impl ModuleMap {
               url2,
               computed_src,
               dynamic,
+              code_cache_info,
             )?
           }
         }
@@ -464,6 +524,7 @@ impl ModuleMap {
     name: impl IntoModuleName,
     source: impl IntoModuleCodeString,
     is_dynamic_import: bool,
+    code_cache_info: Option<CodeCacheInfo>,
   ) -> Result<ModuleId, ModuleError> {
     let name = name.into_module_name();
     self.new_module_from_js_source(
@@ -473,6 +534,7 @@ impl ModuleMap {
       name.into_module_name(),
       source.into_module_code(),
       is_dynamic_import,
+      code_cache_info,
     )
   }
 
@@ -488,6 +550,7 @@ impl ModuleMap {
   /// and attached to associated [`ModuleInfo`].
   ///
   /// Returns an ID of newly created module.
+  #[allow(clippy::too_many_arguments)]
   pub(crate) fn new_module_from_js_source(
     &self,
     scope: &mut v8::HandleScope,
@@ -496,6 +559,7 @@ impl ModuleMap {
     name: ModuleName,
     source: ModuleCodeString,
     is_dynamic_import: bool,
+    mut code_cache_info: Option<CodeCacheInfo>,
   ) -> Result<ModuleId, ModuleError> {
     if main {
       let data = self.data.borrow();
@@ -513,11 +577,37 @@ impl ModuleMap {
     let source_str = source.v8_string(scope);
 
     let origin = module_origin(scope, name_str);
-    let source = v8::script_compiler::Source::new(source_str, Some(&origin));
 
     let tc_scope = &mut v8::TryCatch::new(scope);
 
-    let maybe_module = v8::script_compiler::compile_module(tc_scope, source);
+    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| {
+          let mut source = v8::script_compiler::Source::new_with_cached_data(
+            source_str,
+            Some(&origin),
+            v8::CachedData::new(cache),
+          );
+          let maybe_module = v8::script_compiler::compile_module2(
+            tc_scope,
+            &mut source,
+            v8::script_compiler::CompileOptions::ConsumeCodeCache,
+            v8::script_compiler::NoCacheReason::NoReason,
+          );
+          // Check if the provided code cache is rejected by V8.
+          let rejected = match source.get_cached_data() {
+            Some(cached_data) => cached_data.rejected(),
+            _ => true,
+          };
+          (maybe_module, rejected)
+        })
+      })
+      .unwrap_or_else(|| {
+        let source =
+          v8::script_compiler::Source::new(source_str, Some(&origin));
+        (v8::script_compiler::compile_module(tc_scope, source), true)
+      });
 
     if tc_scope.has_caught() {
       assert!(maybe_module.is_none());
@@ -529,6 +619,23 @@ impl ModuleMap {
 
     let module = maybe_module.unwrap();
 
+    if try_store_code_cache {
+      if let Some(code_cache_info) = code_cache_info.take() {
+        let unbound_module_script = module.get_unbound_module_script(tc_scope);
+        let code_cache =
+          unbound_module_script.create_code_cache().ok_or_else(|| {
+            ModuleError::Other(generic_error(
+              "Unable to get code cache from unbound module script",
+            ))
+          })?;
+        let fut =
+          async move { (code_cache_info.ready_callback)(&code_cache).await }
+            .boxed_local();
+        self.code_cache_ready_futs.borrow_mut().push(fut);
+        self.pending_code_cache_ready.set(true);
+      }
+    }
+
     // TODO(bartlomieju): maybe move to a helper function?
     let module_requests = module.get_module_requests();
     let requests_len = module_requests.length();
@@ -1228,6 +1335,9 @@ impl ModuleMap {
         let poll_imports = self.poll_dyn_imports(cx, scope)?;
         assert!(poll_imports.is_ready());
 
+        let poll_code_cache_ready = self.poll_code_cache_ready(cx)?;
+        assert!(poll_code_cache_ready.is_ready());
+
         if self.evaluate_dyn_imports(scope) {
           has_evaluated = true;
         } else {
@@ -1366,6 +1476,26 @@ impl ModuleMap {
     }
   }
 
+  fn poll_code_cache_ready(&self, cx: &mut Context) -> Poll<Result<(), Error>> {
+    if !self.pending_code_cache_ready.get() {
+      return Poll::Ready(Ok(()));
+    }
+
+    loop {
+      let poll_result =
+        self.code_cache_ready_futs.borrow_mut().poll_next_unpin(cx);
+
+      if let Poll::Ready(Some(_)) = poll_result {
+        continue;
+      }
+
+      self
+        .pending_code_cache_ready
+        .set(!self.code_cache_ready_futs.borrow().is_empty());
+      return Poll::Ready(Ok(()));
+    }
+  }
+
   /// Returns the namespace object of a module.
   ///
   /// This is only available after module evaluation has completed.
@@ -1470,10 +1600,18 @@ impl ModuleMap {
     scope: &mut v8::HandleScope,
     module_specifier: &str,
     source_code: ModuleCodeString,
+    code_cache_info: Option<CodeCacheInfo>,
   ) -> Result<v8::Global<v8::Value>, Error> {
     let specifier = ModuleSpecifier::parse(module_specifier)?;
     let mod_id = self
-      .new_es_module(scope, false, specifier, source_code, false)
+      .new_es_module(
+        scope,
+        false,
+        specifier.clone(),
+        source_code,
+        false,
+        code_cache_info,
+      )
       .map_err(|e| e.into_any_error(scope, false, true))?;
 
     self.instantiate_module(scope, mod_id).map_err(|e| {
@@ -1556,6 +1694,17 @@ impl ModuleMap {
       scope,
       module_specifier,
       ModuleSource::get_string_source(specifier.as_str(), source.code)?,
+      if self.enable_code_cache {
+        let loader = self.loader.borrow().clone();
+        Some(CodeCacheInfo {
+          code_cache: source.code_cache,
+          ready_callback: Box::new(move |cache| {
+            loader.code_cache_ready(&specifier, cache)
+          }),
+        })
+      } else {
+        None
+      },
     )
   }
 }
diff --git a/core/modules/mod.rs b/core/modules/mod.rs
index be6163d..d33aef6 100644
--- a/core/modules/mod.rs
+++ b/core/modules/mod.rs
@@ -202,6 +202,11 @@ pub type CustomModuleEvaluationCb = Box<
   ) -> Result<CustomModuleEvaluationKind, AnyError>,
 >;
 
+pub type EvalContextGetCodeCacheCb =
+  Box<dyn Fn(&str) -> Result<Option<Cow<'static, [u8]>>, AnyError>>;
+
+pub type EvalContextCodeCacheReadyCb = Box<dyn Fn(&str, &[u8])>;
+
 pub enum CustomModuleEvaluationKind {
   /// This evaluation results in a single, "synthetic" module.
   Synthetic(v8::Global<v8::Value>),
@@ -353,6 +358,7 @@ impl ModuleType {
 pub struct ModuleSource {
   pub code: ModuleSourceCode,
   pub module_type: ModuleType,
+  pub code_cache: Option<Cow<'static, [u8]>>,
   module_url_specified: ModuleName,
   /// If the module was found somewhere other than the specified address, this will be [`Some`].
   module_url_found: Option<ModuleName>,
@@ -364,11 +370,13 @@ impl ModuleSource {
     module_type: impl Into<ModuleType>,
     code: ModuleSourceCode,
     specifier: &ModuleSpecifier,
+    code_cache: Option<Cow<'static, [u8]>>,
   ) -> Self {
     let module_url_specified = specifier.as_ref().to_owned().into();
     Self {
       code,
       module_type: module_type.into(),
+      code_cache,
       module_url_specified,
       module_url_found: None,
     }
@@ -381,6 +389,7 @@ impl ModuleSource {
     code: ModuleSourceCode,
     specifier: &ModuleSpecifier,
     specifier_found: &ModuleSpecifier,
+    code_cache: Option<Cow<'static, [u8]>>,
   ) -> Self {
     let module_url_found = if specifier == specifier_found {
       None
@@ -391,6 +400,7 @@ impl ModuleSource {
     Self {
       code,
       module_type: module_type.into(),
+      code_cache,
       module_url_specified,
       module_url_found,
     }
@@ -401,6 +411,7 @@ impl ModuleSource {
     Self {
       code: ModuleSourceCode::String(code.into_module_code()),
       module_type: ModuleType::JavaScript,
+      code_cache: None,
       module_url_specified: file.as_ref().to_owned().into(),
       module_url_found: None,
     }
@@ -412,6 +423,7 @@ impl ModuleSource {
     code: &'static str,
     specified: impl AsRef<str>,
     found: impl AsRef<str>,
+    code_cache: Option<Cow<'static, [u8]>>,
   ) -> Self {
     let specified = specified.as_ref().to_string();
     let found = found.as_ref().to_string();
@@ -423,6 +435,7 @@ impl ModuleSource {
     Self {
       code: ModuleSourceCode::String(code.into_module_code()),
       module_type: ModuleType::JavaScript,
+      code_cache,
       module_url_specified: specified.into(),
       module_url_found: found,
     }
diff --git a/core/ops_builtin_v8.rs b/core/ops_builtin_v8.rs
index 956fb38..493cf19 100644
--- a/core/ops_builtin_v8.rs
+++ b/core/ops_builtin_v8.rs
@@ -245,14 +245,43 @@ pub fn op_eval_context<'a>(
   source: v8::Local<'a, v8::Value>,
   #[string] specifier: String,
 ) -> Result<EvalContextResult<'a>, Error> {
+  let state = JsRuntime::state_from(scope);
   let tc_scope = &mut v8::TryCatch::new(scope);
   let source = v8::Local::<v8::String>::try_from(source)
     .map_err(|_| type_error("Invalid source"))?;
   let specifier = resolve_url(&specifier)?.to_string();
-  let specifier = v8::String::new(tc_scope, &specifier).unwrap();
-  let origin = script_origin(tc_scope, specifier);
-
-  let script = match v8::Script::compile(tc_scope, source, Some(&origin)) {
+  let specifier_v8 = v8::String::new(tc_scope, &specifier).unwrap();
+  let origin = script_origin(tc_scope, specifier_v8);
+
+  let (script, try_store_code_cache) = state
+    .eval_context_get_code_cache_cb
+    .as_ref()
+    .and_then(|cb| {
+      cb(&specifier).unwrap().map(|code_cache| {
+        let mut source = v8::script_compiler::Source::new_with_cached_data(
+          source,
+          Some(&origin),
+          v8::CachedData::new(&code_cache),
+        );
+        let script = v8::script_compiler::compile(
+          tc_scope,
+          &mut source,
+          v8::script_compiler::CompileOptions::ConsumeCodeCache,
+          v8::script_compiler::NoCacheReason::NoReason,
+        );
+        // Check if the provided code cache is rejected by V8.
+        let rejected = match source.get_cached_data() {
+          Some(cached_data) => cached_data.rejected(),
+          _ => true,
+        };
+        (script, rejected)
+      })
+    })
+    .unwrap_or_else(|| {
+      (v8::Script::compile(tc_scope, source, Some(&origin)), true)
+    });
+
+  let script = match script {
     Some(s) => s,
     None => {
       assert!(tc_scope.has_caught());
@@ -268,6 +297,16 @@ pub fn op_eval_context<'a>(
     }
   };
 
+  if try_store_code_cache {
+    if let Some(cb) = state.eval_context_code_cache_ready_cb.as_ref() {
+      let unbound_script = script.get_unbound_script(tc_scope);
+      let code_cache = unbound_script.create_code_cache().ok_or_else(|| {
+        type_error("Unable to get code cache from unbound module script")
+      })?;
+      cb(&specifier, &code_cache);
+    }
+  }
+
   match script.run(tc_scope) {
     Some(result) => Ok(EvalContextResult(Some(result.into()), None)),
     None => {
diff --git a/core/runtime/jsrealm.rs b/core/runtime/jsrealm.rs
index 98a2a59..c2a85be 100644
--- a/core/runtime/jsrealm.rs
+++ b/core/runtime/jsrealm.rs
@@ -349,7 +349,7 @@ impl JsRealm {
       let scope = &mut self.handle_scope(isolate);
       // true for main module
       module_map_rc
-        .new_es_module(scope, true, specifier.to_owned(), code, false)
+        .new_es_module(scope, true, specifier.to_owned(), code, false, None)
         .map_err(|e| e.into_any_error(scope, false, false))?;
     }
 
@@ -395,7 +395,7 @@ impl JsRealm {
       let scope = &mut self.handle_scope(isolate);
       // false for side module (not main module)
       module_map_rc
-        .new_es_module(scope, false, specifier, code, false)
+        .new_es_module(scope, false, specifier, code, false, None)
         .map_err(|e| e.into_any_error(scope, false, false))?;
     }
 
@@ -438,6 +438,7 @@ impl JsRealm {
       scope,
       module_specifier.as_str(),
       code,
+      None,
     )
   }
 }
diff --git a/core/runtime/jsruntime.rs b/core/runtime/jsruntime.rs
index eefc13d..717bb53 100644
--- a/core/runtime/jsruntime.rs
+++ b/core/runtime/jsruntime.rs
@@ -26,6 +26,8 @@ use crate::inspector::JsRuntimeInspector;
 use crate::module_specifier::ModuleSpecifier;
 use crate::modules::default_import_meta_resolve_cb;
 use crate::modules::CustomModuleEvaluationCb;
+use crate::modules::EvalContextCodeCacheReadyCb;
+use crate::modules::EvalContextGetCodeCacheCb;
 use crate::modules::ExtModuleLoader;
 use crate::modules::ImportMetaResolveCallback;
 use crate::modules::IntoModuleCodeString;
@@ -412,6 +414,9 @@ pub struct JsRuntimeState {
     Option<WaitForInspectorDisconnectCallback>,
   pub(crate) validate_import_attributes_cb: Option<ValidateImportAttributesCb>,
   pub(crate) custom_module_evaluation_cb: Option<CustomModuleEvaluationCb>,
+  pub(crate) eval_context_get_code_cache_cb: Option<EvalContextGetCodeCacheCb>,
+  pub(crate) eval_context_code_cache_ready_cb:
+    Option<EvalContextCodeCacheReadyCb>,
   waker: Arc<AtomicWaker>,
   /// Accessed through [`JsRuntimeState::with_inspector`].
   inspector: RefCell<Option<Rc<RefCell<JsRuntimeInspector>>>>,
@@ -526,6 +531,16 @@ pub struct RuntimeOptions {
   /// A callback that allows to evaluate a custom type of a module - eg.
   /// embedders might implement loading WASM or test modules.
   pub custom_module_evaluation_cb: Option<CustomModuleEvaluationCb>,
+
+  // Controls whether V8 code cache is enabled. Code cache can be applied
+  // to ES modules (loaded through `ModuleLoader`) and to scripts evaluated
+  // through `Deno.core.evalContext`.
+  pub enable_code_cache: bool,
+
+  /// Callbacks to retrieve and store code cache for scripts evaluated
+  /// through evalContext.
+  pub eval_context_code_cache_cbs:
+    Option<(EvalContextGetCodeCacheCb, EvalContextCodeCacheReadyCb)>,
 }
 
 impl RuntimeOptions {
@@ -655,6 +670,15 @@ impl JsRuntime {
     // later, after `JsRuntime` is all set up...
     let waker = op_state.waker.clone();
     let op_state = Rc::new(RefCell::new(op_state));
+    let (eval_context_get_code_cache_cb, eval_context_set_code_cache_cb) =
+      if options.enable_code_cache {
+        options
+          .eval_context_code_cache_cbs
+          .map(|cbs| (Some(cbs.0), Some(cbs.1)))
+          .unwrap_or_default()
+      } else {
+        (None, None)
+      };
     let state_rc = Rc::new(JsRuntimeState {
       source_mapper: RefCell::new(source_mapper),
       shared_array_buffer_store: options.shared_array_buffer_store,
@@ -664,6 +688,8 @@ impl JsRuntime {
       op_state: op_state.clone(),
       validate_import_attributes_cb: options.validate_import_attributes_cb,
       custom_module_evaluation_cb: options.custom_module_evaluation_cb,
+      eval_context_get_code_cache_cb,
+      eval_context_code_cache_ready_cb: eval_context_set_code_cache_cb,
       waker,
       // Some fields are initialized later after isolate is created
       inspector: None.into(),
@@ -849,6 +875,7 @@ impl JsRuntime {
       loader,
       exception_state.clone(),
       import_meta_resolve_cb,
+      options.enable_code_cache,
     ));
 
     if let Some((snapshotted_data, mut data_store)) = snapshotted_data {
@@ -1087,6 +1114,7 @@ impl JsRuntime {
         scope,
         file_source.specifier,
         file_source.load()?,
+        None,
       )?;
     }
 


Test patch:
diff --git a/core/modules/tests.rs b/core/modules/tests.rs
index c99c984..5229cd8 100644
--- a/core/modules/tests.rs
+++ b/core/modules/tests.rs
@@ -51,12 +51,21 @@ use crate::deno_core;
 #[derive(Default)]
 struct MockLoader {
   pub loads: Arc<Mutex<Vec<String>>>,
+  pub code_cache: HashMap<String, Vec<u8>>,
+  pub updated_code_cache: Arc<Mutex<HashMap<String, Vec<u8>>>>,
 }
 
 impl MockLoader {
   fn new() -> Rc<Self> {
     Default::default()
   }
+
+  fn new_with_code_cache(code_cache: HashMap<String, Vec<u8>>) -> Rc<Self> {
+    Rc::new(Self {
+      code_cache,
+      ..Default::default()
+    })
+  }
 }
 
 fn mock_source_code(url: &str) -> Option<(&'static str, &'static str)> {
@@ -182,6 +191,7 @@ impl std::error::Error for MockError {
 struct DelayedSourceCodeFuture {
   url: String,
   counter: u32,
+  code_cache: Option<Vec<u8>>,
 }
 
 impl Future for DelayedSourceCodeFuture {
@@ -204,6 +214,10 @@ impl Future for DelayedSourceCodeFuture {
         src.0,
         inner.url.as_str(),
         src.1,
+        inner
+          .code_cache
+          .as_ref()
+          .map(|code_cache| Cow::Owned(code_cache.clone())),
       ))),
       None => Poll::Ready(Err(MockError::LoadErr.into())),
     }
@@ -245,10 +259,27 @@ impl ModuleLoader for MockLoader {
     let mut loads = self.loads.lock();
     loads.push(module_specifier.to_string());
     let url = module_specifier.to_string();
+    let code_cache = self.code_cache.get(&url);
     ModuleLoadResponse::Async(
-      DelayedSourceCodeFuture { url, counter: 0 }.boxed(),
+      DelayedSourceCodeFuture {
+        url,
+        counter: 0,
+        code_cache: code_cache.cloned(),
+      }
+      .boxed(),
     )
   }
+
+  fn code_cache_ready(
+    &self,
+    module_specifier: &ModuleSpecifier,
+    code_cache: &[u8],
+  ) -> Pin<Box<dyn Future<Output = ()>>> {
+    let mut updated_code_cache = self.updated_code_cache.lock();
+    updated_code_cache
+      .insert(module_specifier.to_string(), code_cache.to_vec());
+    async {}.boxed_local()
+  }
 }
 
 #[test]
@@ -375,6 +406,7 @@ fn test_mods() {
           Deno.core.ops.op_test(control);
         "#,
         false,
+        None,
       )
       .unwrap();
 
@@ -395,6 +427,7 @@ fn test_mods() {
         ascii_str!("file:///b.js"),
         ascii_str!("export function b() { return 'b' }"),
         false,
+        None,
       )
       .unwrap();
     let imports = module_map.get_requested_modules(mod_b).unwrap();
@@ -478,6 +511,7 @@ fn test_json_module() {
         "#
         ),
         false,
+        None,
       )
       .unwrap();
 
@@ -503,6 +537,7 @@ fn test_json_module() {
         "#
         ),
         false,
+        None,
       )
       .unwrap();
 
@@ -565,6 +600,7 @@ fn test_validate_import_attributes_default() {
       ascii_str!("file:///a.js"),
       ascii_str!(r#"import jsonData from './c.json' with {foo: "bar"};"#),
       false,
+      None,
     )
     .unwrap();
 
@@ -575,6 +611,7 @@ fn test_validate_import_attributes_default() {
       ascii_str!("file:///a.js"),
       ascii_str!(r#"import jsonData from './c.json' with {type: "bar"};"#),
       false,
+      None,
     )
     .unwrap();
 }
@@ -626,6 +663,7 @@ fn test_validate_import_attributes_callback() {
         ascii_str!("file:///a.js"),
         ascii_str!(r#"import jsonData from './c.json' with {foo: "bar"};"#),
         false,
+        None,
       )
       .unwrap_err();
 
@@ -650,6 +688,7 @@ fn test_validate_import_attributes_callback() {
         ascii_str!("file:///a.js"),
         ascii_str!(r#"import jsonData from './c.json' with {type: "bar"};"#),
         false,
+        None,
       )
       .unwrap_err();
 
@@ -695,6 +734,7 @@ fn test_validate_import_attributes_callback2() {
         ascii_str!("file:///a.js"),
         ascii_str!(r#"import jsonData from './c.json' with {foo: "bar"};"#),
         false,
+        None,
       )
       .unwrap_err();
 
@@ -729,6 +769,7 @@ fn test_custom_module_type_default() {
         ModuleSource {
           code: ModuleSourceCode::Bytes(ModuleCodeBytes::Static(&[])),
           module_url_found: None,
+          code_cache: None,
           module_url_specified: specifier_a,
           module_type: ModuleType::Other("bytes".into()),
         },
@@ -796,6 +837,7 @@ fn test_custom_module_type_callback_synthetic() {
         ModuleSource {
           code: ModuleSourceCode::Bytes(ModuleCodeBytes::Static(&[])),
           module_url_found: None,
+          code_cache: None,
           module_url_specified: specifier_a,
           module_type: ModuleType::Other("foo".into()),
         },
@@ -821,6 +863,7 @@ fn test_custom_module_type_callback_synthetic() {
         ModuleSource {
           code: ModuleSourceCode::Bytes(ModuleCodeBytes::Static(&[])),
           module_url_found: None,
+          code_cache: None,
           module_url_specified: specifier_a,
           module_type: ModuleType::Other("bytes".into()),
         },
@@ -891,6 +934,7 @@ export const foo = bytes;
         ModuleSource {
           code: ModuleSourceCode::Bytes(ModuleCodeBytes::Static(&[])),
           module_url_found: None,
+          code_cache: None,
           module_url_specified: specifier_a,
           module_type: ModuleType::Other("foobar".into()),
         },
@@ -1556,6 +1600,7 @@ async fn no_duplicate_loads() {
       let module_source = ModuleSource {
         code: ModuleSourceCode::String(ModuleCodeString::from(source_code)),
         module_type: ModuleType::JavaScript,
+        code_cache: None,
         module_url_specified: module_specifier.clone().into(),
         module_url_found: found_specifier.map(|s| s.into()),
       };
@@ -1685,3 +1730,122 @@ fn import_meta_filename_dirname() {
   futures::executor::block_on(runtime.run_event_loop(Default::default()))
     .unwrap();
 }
+
+#[test]
+fn test_load_with_code_cache() {
+  let mut code_cache = {
+    let loader = MockLoader::new();
+    let loads = loader.loads.clone();
+    let updated_code_cache = loader.updated_code_cache.clone();
+    let mut runtime = JsRuntime::new(RuntimeOptions {
+      module_loader: Some(loader),
+      enable_code_cache: true,
+      ..Default::default()
+    });
+    let spec = resolve_url("file:///a.js").unwrap();
+    let a_id_fut = runtime.load_main_es_module(&spec);
+    let a_id = futures::executor::block_on(a_id_fut).unwrap();
+
+    #[allow(clippy::let_underscore_future)]
+    let _ = runtime.mod_evaluate(a_id);
+    futures::executor::block_on(runtime.run_event_loop(Default::default()))
+      .unwrap();
+    let l = loads.lock();
+    assert_eq!(
+      l.to_vec(),
+      vec![
+        "file:///a.js",
+        "file:///b.js",
+        "file:///c.js",
+        "file:///d.js"
+      ]
+    );
+
+    let c = updated_code_cache.lock();
+    let mut keys = c.keys().collect::<Vec<_>>();
+    keys.sort();
+    assert_eq!(
+      keys,
+      vec![
+        "file:///a.js",
+        "file:///b.js",
+        "file:///c.js",
+        "file:///d.js"
+      ]
+    );
+    c.clone()
+  };
+
+  {
+    // Create another runtime and try to use the code cache.
+    let loader = MockLoader::new_with_code_cache(code_cache.clone());
+    let loads = loader.loads.clone();
+    let updated_code_cache = loader.updated_code_cache.clone();
+    let mut runtime = JsRuntime::new(RuntimeOptions {
+      module_loader: Some(loader),
+      enable_code_cache: true,
+      ..Default::default()
+    });
+    let spec = resolve_url("file:///a.js").unwrap();
+    let a_id_fut = runtime.load_main_es_module(&spec);
+    let a_id = futures::executor::block_on(a_id_fut).unwrap();
+
+    #[allow(clippy::let_underscore_future)]
+    let _ = runtime.mod_evaluate(a_id);
+    futures::executor::block_on(runtime.run_event_loop(Default::default()))
+      .unwrap();
+    let l = loads.lock();
+    assert_eq!(
+      l.to_vec(),
+      vec![
+        "file:///a.js",
+        "file:///b.js",
+        "file:///c.js",
+        "file:///d.js"
+      ]
+    );
+
+    // Verify that code cache was not updated, which means that provided code cache was used.
+    let c = updated_code_cache.lock();
+    assert!(c.is_empty());
+  }
+
+  {
+    // Create another runtime, and only use code cache for c.js and d.js.
+    code_cache.remove("file:///a.js");
+    code_cache.remove("file:///b.js");
+
+    let loader = MockLoader::new_with_code_cache(code_cache);
+    let loads = loader.loads.clone();
+    let updated_code_cache = loader.updated_code_cache.clone();
+    let mut runtime = JsRuntime::new(RuntimeOptions {
+      module_loader: Some(loader),
+      enable_code_cache: true,
+      ..Default::default()
+    });
+    let spec = resolve_url("file:///a.js").unwrap();
+    let a_id_fut = runtime.load_main_es_module(&spec);
+    let a_id = futures::executor::block_on(a_id_fut).unwrap();
+
+    #[allow(clippy::let_underscore_future)]
+    let _ = runtime.mod_evaluate(a_id);
+    futures::executor::block_on(runtime.run_event_loop(Default::default()))
+      .unwrap();
+    let l = loads.lock();
+    assert_eq!(
+      l.to_vec(),
+      vec![
+        "file:///a.js",
+        "file:///b.js",
+        "file:///c.js",
+        "file:///d.js"
+      ]
+    );
+
+    // Verify that we only used code cache for c.js and d.js, and created new cache for a.js and b.js.
+    let c = updated_code_cache.lock();
+    let mut keys = c.keys().collect::<Vec<_>>();
+    keys.sort();
+    assert_eq!(keys, vec!["file:///a.js", "file:///b.js",]);
+  }
+}
diff --git a/core/runtime/tests/misc.rs b/core/runtime/tests/misc.rs
index cb96ec4..dd3c6ef 100644
--- a/core/runtime/tests/misc.rs
+++ b/core/runtime/tests/misc.rs
@@ -10,7 +10,10 @@ use cooked_waker::IntoWaker;
 use cooked_waker::Wake;
 use cooked_waker::WakeRef;
 use futures::future::poll_fn;
+use parking_lot::Mutex;
 use rstest::rstest;
+use std::borrow::Cow;
+use std::collections::HashMap;
 use std::collections::HashSet;
 use std::rc::Rc;
 use std::sync::atomic::AtomicBool;
@@ -1307,3 +1310,74 @@ async fn global_template_middleware() {
   assert!(calls_set.contains("setter"));
   assert!(calls_set.contains("descriptor"));
 }
+
+#[test]
+fn eval_context_with_code_cache() {
+  let code_cache = {
+    let updated_code_cache = Arc::new(Mutex::new(HashMap::new()));
+
+    let get_code_cache_cb = Box::new(|_: &str| Ok(None));
+
+    let updated_code_cache_clone = updated_code_cache.clone();
+    let set_code_cache_cb =
+      Box::new(move |specifier: &str, code_cache: &[u8]| {
+        let mut c = updated_code_cache_clone.lock();
+        c.insert(specifier.to_string(), code_cache.to_vec());
+      });
+
+    let mut runtime = JsRuntime::new(RuntimeOptions {
+      enable_code_cache: true,
+      eval_context_code_cache_cbs: Some((get_code_cache_cb, set_code_cache_cb)),
+      ..Default::default()
+    });
+    runtime
+      .execute_script(
+        "",
+        ascii_str!("Deno.core.evalContext('const i = 10;', 'file:///foo.js');"),
+      )
+      .unwrap();
+
+    let c = updated_code_cache.lock();
+    let mut keys = c.keys().collect::<Vec<_>>();
+    keys.sort();
+    assert_eq!(keys, vec!["file:///foo.js",]);
+    c.clone()
+  };
+
+  {
+    // Create another runtime and try to use the code cache.
+    let updated_code_cache = Arc::new(Mutex::new(HashMap::new()));
+
+    let code_cache_clone = code_cache.clone();
+    let get_code_cache_cb = Box::new(move |specifier: &str| {
+      Ok(
+        code_cache_clone
+          .get(specifier)
+          .map(|code_cache| Cow::Owned(code_cache.clone())),
+      )
+    });
+
+    let updated_code_cache_clone = updated_code_cache.clone();
+    let set_code_cache_cb =
+      Box::new(move |specifier: &str, code_cache: &[u8]| {
+        let mut c = updated_code_cache_clone.lock();
+        c.insert(specifier.to_string(), code_cache.to_vec());
+      });
+
+    let mut runtime = JsRuntime::new(RuntimeOptions {
+      enable_code_cache: true,
+      eval_context_code_cache_cbs: Some((get_code_cache_cb, set_code_cache_cb)),
+      ..Default::default()
+    });
+    runtime
+      .execute_script(
+        "",
+        ascii_str!("Deno.core.evalContext('const i = 10;', 'file:///foo.js');"),
+      )
+      .unwrap();
+
+    // Verify that code cache was not updated, which means that provided code cache was used.
+    let c = updated_code_cache.lock();
+    assert!(c.is_empty());
+  }
+}
diff --git a/testing/checkin/runner/ts_module_loader.rs b/testing/checkin/runner/ts_module_loader.rs
index b67c7d2..77f4feb 100644
--- a/testing/checkin/runner/ts_module_loader.rs
+++ b/testing/checkin/runner/ts_module_loader.rs
@@ -124,6 +124,7 @@ impl ModuleLoader for TypescriptModuleLoader {
         module_type,
         ModuleSourceCode::String(code.into()),
         module_specifier,
+        None,
       ))
     }