perf(check): use v8 code cache for extension sources in deno check (#28089)

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
```
This commit is contained in:
Nathan Whitaker 2025-02-12 18:47:00 +01:00 committed by GitHub
parent 55c5b07535
commit 33bccf9090
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
4 changed files with 235 additions and 16 deletions

View file

@ -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()
}
);

View file

@ -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

View file

@ -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)

View file

@ -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));
}
}
}
}