Repository: denoland/deno
Pull request: perf(check): use v8 code cache for extension sources in `deno check`

In particular this helps startup of the TSC isolate because `00_typescript.js` can use the code cache.

Overall, this offsets a fair bit of the hit we took when we removed the TSC snapshot.

```
❯ hyperfine --warmup 5 -p "rm -rf ~/Library/Caches/deno/check_cache_v2" "./deno-this-pr check main.ts" "./deno-no-snapshot check main.ts" "./deno-with-snapshot check main.ts"
Benchmark 1: ../../deno/target/release-lite/deno check main.ts
  Time (mean ± σ):     145.7 ms ±   3.6 ms    [User: 347.6 ms, System: 36.9 ms]
  Range (min … max):   142.2 ms … 155.9 ms    19 runs

Benchmark 2: ./deno-no-snapshot check main.ts
  Time (mean ± σ):     195.4 ms ±   3.3 ms    [User: 397.7 ms, System: 34.9 ms]
  Range (min … max):   192.1 ms … 206.0 ms    15 runs

Benchmark 3: ./deno-with-snapshot check main.ts
  Time (mean ± σ):     109.0 ms ±   2.2 ms    [User: 155.9 ms, System: 19.3 ms]
  Range (min … max):   106.5 ms … 118.0 ms    26 runs

Summary
  ./deno-with-snapshot check main.ts ran
    1.34 ± 0.04 times faster than ./deno-this-pr check main.ts
    1.79 ± 0.05 times faster than ./deno-no-snapshot check main.ts
```

Changed files:
filename: cli/args/flags.rs
patch: @@ -1828,6 +1828,7 @@ Unless --reload is specified, this command will not re-download already cached d
     )
     .defer(|cmd| {
       compile_args_without_check_args(cmd)
+        .arg(no_code_cache_arg())
         .arg(
           Arg::new("all")
             .long("all")
@@ -4572,6 +4573,7 @@ fn check_parse(
     doc: matches.get_flag("doc"),
     doc_only: matches.get_flag("doc-only"),
   });
+  flags.code_cache_enabled = !matches.get_flag("no-code-cache");
   allow_import_parse(flags, matches);
   Ok(())
 }
@@ -7412,6 +7414,7 @@ mod tests {
           doc_only: false,
         }),
         type_check_mode: TypeCheckMode::Local,
+        code_cache_enabled: true,
         ..Flags::default()
       }
     );
@@ -7426,6 +7429,7 @@ mod tests {
           doc_only: false,
         }),
         type_check_mode: TypeCheckMode::Local,
+        code_cache_enabled: true,
         ..Flags::default()
       }
     );
@@ -7440,6 +7444,7 @@ mod tests {
           doc_only: true,
         }),
         type_check_mode: TypeCheckMode::Local,
+        code_cache_enabled: true,
         ..Flags::default()
       }
     );
@@ -7468,6 +7473,7 @@ mod tests {
             doc_only: false,
           }),
           type_check_mode: TypeCheckMode::All,
+          code_cache_enabled: true,
           ..Flags::default()
         }
       );

filename: cli/factory.rs
patch: @@ -873,6 +873,11 @@ impl CliFactory {
           self.npm_resolver().await?.clone(),
           self.sys(),
           self.tsconfig_resolver()?.clone(),
+          if cli_options.code_cache_enabled() {
+            Some(self.code_cache()?.clone())
+          } else {
+            None
+          },
         )))
       })
       .await

filename: cli/tools/check.rs
patch: @@ -148,6 +148,7 @@ pub struct TypeChecker {
   npm_resolver: CliNpmResolver,
   sys: CliSys,
   tsconfig_resolver: Arc<TsConfigResolver>,
+  code_cache: Option<Arc<crate::cache::CodeCache>>,
 }
 
 impl TypeChecker {
@@ -162,6 +163,7 @@ impl TypeChecker {
     npm_resolver: CliNpmResolver,
     sys: CliSys,
     tsconfig_resolver: Arc<TsConfigResolver>,
+    code_cache: Option<Arc<crate::cache::CodeCache>>,
   ) -> Self {
     Self {
       caches,
@@ -173,6 +175,7 @@ impl TypeChecker {
       npm_resolver,
       sys,
       tsconfig_resolver,
+      code_cache,
     }
   }
 
@@ -283,6 +286,7 @@ impl TypeChecker {
         grouped_roots,
         options,
         seen_diagnotics: Default::default(),
+        code_cache: self.code_cache.clone(),
       }),
     ))
   }
@@ -433,6 +437,7 @@ struct DiagnosticsByFolderRealIterator<'a> {
   npm_check_state_hash: Option<u64>,
   seen_diagnotics: HashSet<String>,
   options: CheckOptions,
+  code_cache: Option<Arc<crate::cache::CodeCache>>,
 }
 
 impl<'a> Iterator for DiagnosticsByFolderRealIterator<'a> {
@@ -550,20 +555,27 @@ impl<'a> DiagnosticsByFolderRealIterator<'a> {
     let tsconfig_hash_data = FastInsecureHasher::new_deno_versioned()
       .write_hashable(ts_config)
       .finish();
-    let response = tsc::exec(tsc::Request {
-      config: ts_config.clone(),
-      debug: self.log_level == Some(log::Level::Debug),
-      graph: self.graph.clone(),
-      hash_data: tsconfig_hash_data,
-      maybe_npm: Some(tsc::RequestNpmState {
-        cjs_tracker: self.cjs_tracker.clone(),
-        node_resolver: self.node_resolver.clone(),
-        npm_resolver: self.npm_resolver.clone(),
-      }),
-      maybe_tsbuildinfo,
-      root_names,
-      check_mode: self.options.type_check_mode,
-    })?;
+    let code_cache = self.code_cache.as_ref().map(|c| {
+      let c: Arc<dyn deno_runtime::code_cache::CodeCache> = c.clone();
+      c
+    });
+    let response = tsc::exec(
+      tsc::Request {
+        config: ts_config.clone(),
+        debug: self.log_level == Some(log::Level::Debug),
+        graph: self.graph.clone(),
+        hash_data: tsconfig_hash_data,
+        maybe_npm: Some(tsc::RequestNpmState {
+          cjs_tracker: self.cjs_tracker.clone(),
+          node_resolver: self.node_resolver.clone(),
+          npm_resolver: self.npm_resolver.clone(),
+        }),
+        maybe_tsbuildinfo,
+        root_names,
+        check_mode: self.options.type_check_mode,
+      },
+      code_cache,
+    )?;
 
     let mut response_diagnostics = response.diagnostics.filter(|d| {
       self.should_include_diagnostic(self.options.type_check_mode, d)

filename: cli/tsc/mod.rs
patch: @@ -4,6 +4,7 @@ use std::borrow::Cow;
 use std::collections::HashMap;
 use std::fmt;
 use std::path::PathBuf;
+use std::rc::Rc;
 use std::sync::Arc;
 use std::sync::OnceLock;
 
@@ -1372,10 +1373,82 @@ deno_core::extension!(deno_cli_tsc,
   }
 );
 
+pub struct TscExtCodeCache {
+  cache: Arc<dyn deno_runtime::code_cache::CodeCache>,
+}
+
+impl TscExtCodeCache {
+  pub fn new(cache: Arc<dyn deno_runtime::code_cache::CodeCache>) -> Self {
+    Self { cache }
+  }
+}
+
+impl deno_core::ExtCodeCache for TscExtCodeCache {
+  fn get_code_cache_info(
+    &self,
+    specifier: &ModuleSpecifier,
+    code: &deno_core::ModuleSourceCode,
+    esm: bool,
+  ) -> deno_core::SourceCodeCacheInfo {
+    use deno_runtime::code_cache::CodeCacheType;
+    let code_hash = FastInsecureHasher::new_deno_versioned()
+      .write_hashable(code)
+      .finish();
+    let data = self
+      .cache
+      .get_sync(
+        specifier,
+        if esm {
+          CodeCacheType::EsModule
+        } else {
+          CodeCacheType::Script
+        },
+        code_hash,
+      )
+      .map(Cow::from)
+      .inspect(|_| {
+        log::debug!(
+          "V8 code cache hit for Extension module: {specifier}, [{code_hash:?}]"
+        );
+      });
+    deno_core::SourceCodeCacheInfo {
+      hash: code_hash,
+      data,
+    }
+  }
+
+  fn code_cache_ready(
+    &self,
+    specifier: ModuleSpecifier,
+    source_hash: u64,
+    code_cache: &[u8],
+    esm: bool,
+  ) {
+    use deno_runtime::code_cache::CodeCacheType;
+
+    log::debug!(
+      "Updating V8 code cache for Extension module: {specifier}, [{source_hash:?}]"
+    );
+    self.cache.set_sync(
+      specifier,
+      if esm {
+        CodeCacheType::EsModule
+      } else {
+        CodeCacheType::Script
+      },
+      source_hash,
+      code_cache,
+    );
+  }
+}
+
 /// Execute a request on the supplied snapshot, returning a response which
 /// contains information, like any emitted files, diagnostics, statistics and
 /// optionally an updated TypeScript build info.
-pub fn exec(request: Request) -> Result<Response, ExecError> {
+pub fn exec(
+  request: Request,
+  code_cache: Option<Arc<dyn deno_runtime::code_cache::CodeCache>>,
+) -> Result<Response, ExecError> {
   // tsc cannot handle root specifiers that don't have one of the "acceptable"
   // extensions.  Therefore, we have to check the root modules against their
   // extensions and remap any that are unacceptable to tsc and add them to the
@@ -1417,10 +1490,14 @@ pub fn exec(request: Request) -> Result<Response, ExecError> {
     root_map,
     remapped_specifiers,
   ));
+  let extension_code_cache = code_cache.map(|cache| {
+    Rc::new(TscExtCodeCache::new(cache)) as Rc<dyn deno_core::ExtCodeCache>
+  });
   let mut runtime = JsRuntime::new(RuntimeOptions {
     extensions,
     create_params: create_isolate_create_params(),
     startup_snapshot: deno_snapshots::CLI_SNAPSHOT,
+    extension_code_cache,
     ..Default::default()
   });
 
@@ -1450,11 +1527,13 @@ pub fn exec(request: Request) -> Result<Response, ExecError> {
 #[cfg(test)]
 mod tests {
   use deno_core::futures::future;
+  use deno_core::parking_lot::Mutex;
   use deno_core::serde_json;
   use deno_core::OpState;
   use deno_error::JsErrorBox;
   use deno_graph::GraphKind;
   use deno_graph::ModuleGraph;
+  use deno_runtime::code_cache::CodeCacheType;
   use test_util::PathRef;
 
   use super::Diagnostic;
@@ -1529,6 +1608,12 @@ mod tests {
 
   async fn test_exec(
     specifier: &ModuleSpecifier,
+  ) -> Result<Response, ExecError> {
+    test_exec_with_cache(specifier, None).await
+  }
+  async fn test_exec_with_cache(
+    specifier: &ModuleSpecifier,
+    code_cache: Option<Arc<dyn deno_runtime::code_cache::CodeCache>>,
   ) -> Result<Response, ExecError> {
     let hash_data = 123; // something random
     let fixtures = test_util::testdata_path().join("tsc2");
@@ -1563,7 +1648,7 @@ mod tests {
       root_names: vec![(specifier.clone(), MediaType::TypeScript)],
       check_mode: TypeCheckMode::All,
     };
-    exec(request)
+    exec(request, code_cache)
   }
 
   #[tokio::test]
@@ -1784,4 +1869,115 @@ mod tests {
       .expect("exec should not have errored");
     assert!(!actual.diagnostics.has_diagnostic());
   }
+
+  pub type SpecifierWithType = (ModuleSpecifier, CodeCacheType);
+
+  #[derive(Default)]
+  struct TestExtCodeCache {
+    cache: Mutex<HashMap<(SpecifierWithType, u64), Vec<u8>>>,
+
+    hits: Mutex<HashMap<SpecifierWithType, usize>>,
+    misses: Mutex<HashMap<SpecifierWithType, usize>>,
+  }
+
+  impl deno_runtime::code_cache::CodeCache for TestExtCodeCache {
+    fn get_sync(
+      &self,
+      specifier: &ModuleSpecifier,
+      code_cache_type: CodeCacheType,
+      source_hash: u64,
+    ) -> Option<Vec<u8>> {
+      let result = self
+        .cache
+        .lock()
+        .get(&((specifier.clone(), code_cache_type), source_hash))
+        .cloned();
+      if result.is_some() {
+        *self
+          .hits
+          .lock()
+          .entry((specifier.clone(), code_cache_type))
+          .or_default() += 1;
+      } else {
+        *self
+          .misses
+          .lock()
+          .entry((specifier.clone(), code_cache_type))
+          .or_default() += 1;
+      }
+      result
+    }
+
+    fn set_sync(
+      &self,
+      specifier: ModuleSpecifier,
+      code_cache_type: CodeCacheType,
+      source_hash: u64,
+      data: &[u8],
+    ) {
+      self
+        .cache
+        .lock()
+        .insert(((specifier, code_cache_type), source_hash), data.to_vec());
+    }
+  }
+
+  #[tokio::test]
+  async fn test_exec_code_cache() {
+    let code_cache = Arc::new(TestExtCodeCache::default());
+    let specifier = ModuleSpecifier::parse("https://deno.land/x/a.ts").unwrap();
+    let actual = test_exec_with_cache(&specifier, Some(code_cache.clone()))
+      .await
+      .expect("exec should not have errored");
+    assert!(!actual.diagnostics.has_diagnostic());
+
+    let expect = [
+      (
+        "ext:deno_cli_tsc/99_main_compiler.js",
+        CodeCacheType::EsModule,
+      ),
+      ("ext:deno_cli_tsc/98_lsp.js", CodeCacheType::EsModule),
+      ("ext:deno_cli_tsc/97_ts_host.js", CodeCacheType::EsModule),
+      ("ext:deno_cli_tsc/00_typescript.js", CodeCacheType::Script),
+    ];
+
+    {
+      let mut files = HashMap::new();
+
+      for (((specifier, ty), _), _) in code_cache.cache.lock().iter() {
+        let specifier = specifier.to_string();
+        if files.contains_key(&specifier) {
+          panic!("should have only 1 entry per specifier");
+        }
+        files.insert(specifier, *ty);
+      }
+
+      // 99_main_compiler, 98_lsp, 97_ts_host, 00_typescript
+      assert_eq!(files.len(), 4);
+      assert_eq!(code_cache.hits.lock().len(), 0);
+      assert_eq!(code_cache.misses.lock().len(), 4);
+
+      for (specifier, ty) in &expect {
+        assert_eq!(files.get(*specifier), Some(ty));
+      }
+
+      code_cache.hits.lock().clear();
+      code_cache.misses.lock().clear();
+    }
+
+    {
+      let _ = test_exec_with_cache(&specifier, Some(code_cache.clone()))
+        .await
+        .expect("exec should not have errored");
+
+      // 99_main_compiler, 98_lsp, 97_ts_host, 00_typescript
+      assert_eq!(code_cache.hits.lock().len(), 4);
+      assert_eq!(code_cache.misses.lock().len(), 0);
+
+      for (specifier, ty) in expect {
+        let url = ModuleSpecifier::parse(specifier).unwrap();
+        assert_eq!(code_cache.hits.lock().get(&(url, ty)), Some(&1));
+      }
+    }
+  }
 }