perf(compile): code cache (#26528)

Adds a lazily created code cache to `deno compile` by default.

The code cache is created on first run to a single file in the temp
directory and is only written once. After it's been written, the code
cache becomes read only on subsequent runs. Only the modules loaded
during startup are cached (dynamic imports are not code cached).

The code cache can be disabled by compiling with `--no-code-cache`.
This commit is contained in:
David Sherret 2024-11-18 15:09:28 -05:00 committed by GitHub
parent 3ba464dbc4
commit dd4570ed85
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
14 changed files with 703 additions and 20 deletions

View File

@ -1939,6 +1939,7 @@ On the first invocation with deno will download the proper binary and cache it i
]) ])
.help_heading(COMPILE_HEADING), .help_heading(COMPILE_HEADING),
) )
.arg(no_code_cache_arg())
.arg( .arg(
Arg::new("no-terminal") Arg::new("no-terminal")
.long("no-terminal") .long("no-terminal")
@ -4431,6 +4432,8 @@ fn compile_parse(
}; };
ext_arg_parse(flags, matches); ext_arg_parse(flags, matches);
flags.code_cache_enabled = !matches.get_flag("no-code-cache");
flags.subcommand = DenoSubcommand::Compile(CompileFlags { flags.subcommand = DenoSubcommand::Compile(CompileFlags {
source_file, source_file,
output, output,
@ -10040,6 +10043,7 @@ mod tests {
include: vec![] include: vec![]
}), }),
type_check_mode: TypeCheckMode::Local, type_check_mode: TypeCheckMode::Local,
code_cache_enabled: true,
..Flags::default() ..Flags::default()
} }
); );
@ -10048,7 +10052,7 @@ mod tests {
#[test] #[test]
fn compile_with_flags() { fn compile_with_flags() {
#[rustfmt::skip] #[rustfmt::skip]
let r = flags_from_vec(svec!["deno", "compile", "--import-map", "import_map.json", "--no-remote", "--config", "tsconfig.json", "--no-check", "--unsafely-ignore-certificate-errors", "--reload", "--lock", "lock.json", "--cert", "example.crt", "--cached-only", "--location", "https:foo", "--allow-read", "--allow-net", "--v8-flags=--help", "--seed", "1", "--no-terminal", "--icon", "favicon.ico", "--output", "colors", "--env=.example.env", "https://examples.deno.land/color-logging.ts", "foo", "bar", "-p", "8080"]); let r = flags_from_vec(svec!["deno", "compile", "--import-map", "import_map.json", "--no-code-cache", "--no-remote", "--config", "tsconfig.json", "--no-check", "--unsafely-ignore-certificate-errors", "--reload", "--lock", "lock.json", "--cert", "example.crt", "--cached-only", "--location", "https:foo", "--allow-read", "--allow-net", "--v8-flags=--help", "--seed", "1", "--no-terminal", "--icon", "favicon.ico", "--output", "colors", "--env=.example.env", "https://examples.deno.land/color-logging.ts", "foo", "bar", "-p", "8080"]);
assert_eq!( assert_eq!(
r.unwrap(), r.unwrap(),
Flags { Flags {
@ -10064,6 +10068,7 @@ mod tests {
}), }),
import_map_path: Some("import_map.json".to_string()), import_map_path: Some("import_map.json".to_string()),
no_remote: true, no_remote: true,
code_cache_enabled: false,
config_flag: ConfigFlag::Path("tsconfig.json".to_owned()), config_flag: ConfigFlag::Path("tsconfig.json".to_owned()),
type_check_mode: TypeCheckMode::None, type_check_mode: TypeCheckMode::None,
reload: true, reload: true,

View File

@ -1,10 +1,14 @@
// Copyright 2018-2024 the Deno authors. All rights reserved. MIT license. // Copyright 2018-2024 the Deno authors. All rights reserved. MIT license.
use std::sync::Arc;
use deno_ast::ModuleSpecifier; use deno_ast::ModuleSpecifier;
use deno_core::error::AnyError; use deno_core::error::AnyError;
use deno_runtime::code_cache; use deno_runtime::code_cache;
use deno_runtime::deno_webstorage::rusqlite::params; use deno_runtime::deno_webstorage::rusqlite::params;
use crate::worker::CliCodeCache;
use super::cache_db::CacheDB; use super::cache_db::CacheDB;
use super::cache_db::CacheDBConfiguration; use super::cache_db::CacheDBConfiguration;
use super::cache_db::CacheDBHash; use super::cache_db::CacheDBHash;
@ -82,6 +86,12 @@ impl CodeCache {
} }
} }
impl CliCodeCache for CodeCache {
fn as_code_cache(self: Arc<Self>) -> Arc<dyn code_cache::CodeCache> {
self
}
}
impl code_cache::CodeCache for CodeCache { impl code_cache::CodeCache for CodeCache {
fn get_sync( fn get_sync(
&self, &self,

View File

@ -64,6 +64,7 @@ use crate::args::NpmInstallDepsProvider;
use crate::args::PermissionFlags; use crate::args::PermissionFlags;
use crate::args::UnstableConfig; use crate::args::UnstableConfig;
use crate::cache::DenoDir; use crate::cache::DenoDir;
use crate::cache::FastInsecureHasher;
use crate::emit::Emitter; use crate::emit::Emitter;
use crate::file_fetcher::FileFetcher; use crate::file_fetcher::FileFetcher;
use crate::http_util::HttpClientProvider; use crate::http_util::HttpClientProvider;
@ -174,6 +175,7 @@ pub struct SerializedWorkspaceResolver {
pub struct Metadata { pub struct Metadata {
pub argv: Vec<String>, pub argv: Vec<String>,
pub seed: Option<u64>, pub seed: Option<u64>,
pub code_cache_key: Option<u64>,
pub permissions: PermissionFlags, pub permissions: PermissionFlags,
pub location: Option<Url>, pub location: Option<Url>,
pub v8_flags: Vec<String>, pub v8_flags: Vec<String>,
@ -604,10 +606,21 @@ impl<'a> DenoCompileBinaryWriter<'a> {
VfsBuilder::new(root_path.clone())? VfsBuilder::new(root_path.clone())?
}; };
let mut remote_modules_store = RemoteModulesStoreBuilder::default(); let mut remote_modules_store = RemoteModulesStoreBuilder::default();
let mut code_cache_key_hasher = if cli_options.code_cache_enabled() {
Some(FastInsecureHasher::new_deno_versioned())
} else {
None
};
for module in graph.modules() { for module in graph.modules() {
if module.specifier().scheme() == "data" { if module.specifier().scheme() == "data" {
continue; // don't store data urls as an entry as they're in the code continue; // don't store data urls as an entry as they're in the code
} }
if let Some(hasher) = &mut code_cache_key_hasher {
if let Some(source) = module.source() {
hasher.write(module.specifier().as_str().as_bytes());
hasher.write(source.as_bytes());
}
}
let (maybe_source, media_type) = match module { let (maybe_source, media_type) = match module {
deno_graph::Module::Js(m) => { deno_graph::Module::Js(m) => {
let source = if m.media_type.is_emittable() { let source = if m.media_type.is_emittable() {
@ -675,6 +688,7 @@ impl<'a> DenoCompileBinaryWriter<'a> {
let metadata = Metadata { let metadata = Metadata {
argv: compile_flags.args.clone(), argv: compile_flags.args.clone(),
seed: cli_options.seed(), seed: cli_options.seed(),
code_cache_key: code_cache_key_hasher.map(|h| h.finish()),
location: cli_options.location_flag().clone(), location: cli_options.location_flag().clone(),
permissions: cli_options.permission_flags().clone(), permissions: cli_options.permission_flags().clone(),
v8_flags: cli_options.v8_flags().clone(), v8_flags: cli_options.v8_flags().clone(),

View File

@ -0,0 +1,514 @@
// Copyright 2018-2024 the Deno authors. All rights reserved. MIT license.
use std::collections::BTreeMap;
use std::collections::HashMap;
use std::io::BufReader;
use std::io::BufWriter;
use std::io::Read;
use std::io::Write;
use std::path::Path;
use std::path::PathBuf;
use std::sync::Arc;
use deno_ast::ModuleSpecifier;
use deno_core::anyhow::bail;
use deno_core::error::AnyError;
use deno_core::parking_lot::Mutex;
use deno_core::unsync::sync::AtomicFlag;
use deno_runtime::code_cache::CodeCache;
use deno_runtime::code_cache::CodeCacheType;
use crate::cache::FastInsecureHasher;
use crate::util::path::get_atomic_file_path;
use crate::worker::CliCodeCache;
enum CodeCacheStrategy {
FirstRun(FirstRunCodeCacheStrategy),
SubsequentRun(SubsequentRunCodeCacheStrategy),
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct DenoCompileCodeCacheEntry {
pub source_hash: u64,
pub data: Vec<u8>,
}
pub struct DenoCompileCodeCache {
strategy: CodeCacheStrategy,
}
impl DenoCompileCodeCache {
pub fn new(file_path: PathBuf, cache_key: u64) -> Self {
// attempt to deserialize the cache data
match deserialize(&file_path, cache_key) {
Ok(data) => {
log::debug!("Loaded {} code cache entries", data.len());
Self {
strategy: CodeCacheStrategy::SubsequentRun(
SubsequentRunCodeCacheStrategy {
is_finished: AtomicFlag::lowered(),
data: Mutex::new(data),
},
),
}
}
Err(err) => {
log::debug!("Failed to deserialize code cache: {:#}", err);
Self {
strategy: CodeCacheStrategy::FirstRun(FirstRunCodeCacheStrategy {
cache_key,
file_path,
is_finished: AtomicFlag::lowered(),
data: Mutex::new(FirstRunCodeCacheData {
cache: HashMap::new(),
add_count: 0,
}),
}),
}
}
}
}
}
impl CodeCache for DenoCompileCodeCache {
fn get_sync(
&self,
specifier: &ModuleSpecifier,
code_cache_type: CodeCacheType,
source_hash: u64,
) -> Option<Vec<u8>> {
match &self.strategy {
CodeCacheStrategy::FirstRun(strategy) => {
if !strategy.is_finished.is_raised() {
// we keep track of how many times the cache is requested
// then serialize the cache when we get that number of
// "set" calls
strategy.data.lock().add_count += 1;
}
None
}
CodeCacheStrategy::SubsequentRun(strategy) => {
if strategy.is_finished.is_raised() {
return None;
}
strategy.take_from_cache(specifier, code_cache_type, source_hash)
}
}
}
fn set_sync(
&self,
specifier: ModuleSpecifier,
code_cache_type: CodeCacheType,
source_hash: u64,
bytes: &[u8],
) {
match &self.strategy {
CodeCacheStrategy::FirstRun(strategy) => {
if strategy.is_finished.is_raised() {
return;
}
let data_to_serialize = {
let mut data = strategy.data.lock();
data.cache.insert(
(specifier.to_string(), code_cache_type),
DenoCompileCodeCacheEntry {
source_hash,
data: bytes.to_vec(),
},
);
if data.add_count != 0 {
data.add_count -= 1;
}
if data.add_count == 0 {
// don't allow using the cache anymore
strategy.is_finished.raise();
if data.cache.is_empty() {
None
} else {
Some(std::mem::take(&mut data.cache))
}
} else {
None
}
};
if let Some(cache_data) = &data_to_serialize {
strategy.write_cache_data(cache_data);
}
}
CodeCacheStrategy::SubsequentRun(_) => {
// do nothing
}
}
}
}
impl CliCodeCache for DenoCompileCodeCache {
fn enabled(&self) -> bool {
match &self.strategy {
CodeCacheStrategy::FirstRun(strategy) => {
!strategy.is_finished.is_raised()
}
CodeCacheStrategy::SubsequentRun(strategy) => {
!strategy.is_finished.is_raised()
}
}
}
fn as_code_cache(self: Arc<Self>) -> Arc<dyn CodeCache> {
self
}
}
type CodeCacheKey = (String, CodeCacheType);
struct FirstRunCodeCacheData {
cache: HashMap<CodeCacheKey, DenoCompileCodeCacheEntry>,
add_count: usize,
}
struct FirstRunCodeCacheStrategy {
cache_key: u64,
file_path: PathBuf,
is_finished: AtomicFlag,
data: Mutex<FirstRunCodeCacheData>,
}
impl FirstRunCodeCacheStrategy {
fn write_cache_data(
&self,
cache_data: &HashMap<CodeCacheKey, DenoCompileCodeCacheEntry>,
) {
let count = cache_data.len();
let temp_file = get_atomic_file_path(&self.file_path);
match serialize(&temp_file, self.cache_key, cache_data) {
Ok(()) => {
if let Err(err) = std::fs::rename(&temp_file, &self.file_path) {
log::debug!("Failed to rename code cache: {}", err);
} else {
log::debug!("Serialized {} code cache entries", count);
}
}
Err(err) => {
let _ = std::fs::remove_file(&temp_file);
log::debug!("Failed to serialize code cache: {}", err);
}
}
}
}
struct SubsequentRunCodeCacheStrategy {
is_finished: AtomicFlag,
data: Mutex<HashMap<CodeCacheKey, DenoCompileCodeCacheEntry>>,
}
impl SubsequentRunCodeCacheStrategy {
fn take_from_cache(
&self,
specifier: &ModuleSpecifier,
code_cache_type: CodeCacheType,
source_hash: u64,
) -> Option<Vec<u8>> {
let mut data = self.data.lock();
// todo(dsherret): how to avoid the clone here?
let entry = data.remove(&(specifier.to_string(), code_cache_type))?;
if entry.source_hash != source_hash {
return None;
}
if data.is_empty() {
self.is_finished.raise();
}
Some(entry.data)
}
}
/// File format:
/// - <header>
/// - <cache key>
/// - <u32: number of entries>
/// - <[entry length]> - u64 * number of entries
/// - <[entry]>
/// - <[u8]: entry data>
/// - <String: specifier>
/// - <u8>: code cache type
/// - <u32: specifier length>
/// - <u64: source hash>
/// - <u64: entry data hash>
fn serialize(
file_path: &Path,
cache_key: u64,
cache: &HashMap<CodeCacheKey, DenoCompileCodeCacheEntry>,
) -> Result<(), AnyError> {
let cache_file = std::fs::OpenOptions::new()
.create(true)
.truncate(true)
.write(true)
.open(file_path)?;
let mut writer = BufWriter::new(cache_file);
serialize_with_writer(&mut writer, cache_key, cache)
}
fn serialize_with_writer<T: Write>(
writer: &mut BufWriter<T>,
cache_key: u64,
cache: &HashMap<CodeCacheKey, DenoCompileCodeCacheEntry>,
) -> Result<(), AnyError> {
// header
writer.write_all(&cache_key.to_le_bytes())?;
writer.write_all(&(cache.len() as u32).to_le_bytes())?;
// lengths of each entry
for ((specifier, _), entry) in cache {
let len: u64 =
entry.data.len() as u64 + specifier.len() as u64 + 1 + 4 + 8 + 8;
writer.write_all(&len.to_le_bytes())?;
}
// entries
for ((specifier, code_cache_type), entry) in cache {
writer.write_all(&entry.data)?;
writer.write_all(&[match code_cache_type {
CodeCacheType::EsModule => 0,
CodeCacheType::Script => 1,
}])?;
writer.write_all(specifier.as_bytes())?;
writer.write_all(&(specifier.len() as u32).to_le_bytes())?;
writer.write_all(&entry.source_hash.to_le_bytes())?;
let hash: u64 = FastInsecureHasher::new_without_deno_version()
.write(&entry.data)
.finish();
writer.write_all(&hash.to_le_bytes())?;
}
writer.flush()?;
Ok(())
}
fn deserialize(
file_path: &Path,
expected_cache_key: u64,
) -> Result<HashMap<CodeCacheKey, DenoCompileCodeCacheEntry>, AnyError> {
let cache_file = std::fs::File::open(file_path)?;
let mut reader = BufReader::new(cache_file);
deserialize_with_reader(&mut reader, expected_cache_key)
}
fn deserialize_with_reader<T: Read>(
reader: &mut BufReader<T>,
expected_cache_key: u64,
) -> Result<HashMap<CodeCacheKey, DenoCompileCodeCacheEntry>, AnyError> {
// it's very important to use this below so that a corrupt cache file
// doesn't cause a memory allocation error
fn new_vec_sized<T: Clone>(
capacity: usize,
default_value: T,
) -> Result<Vec<T>, AnyError> {
let mut vec = Vec::new();
vec.try_reserve(capacity)?;
vec.resize(capacity, default_value);
Ok(vec)
}
fn try_subtract(a: usize, b: usize) -> Result<usize, AnyError> {
if a < b {
bail!("Integer underflow");
}
Ok(a - b)
}
let mut header_bytes = vec![0; 8 + 4];
reader.read_exact(&mut header_bytes)?;
let actual_cache_key = u64::from_le_bytes(header_bytes[..8].try_into()?);
if actual_cache_key != expected_cache_key {
// cache bust
bail!("Cache key mismatch");
}
let len = u32::from_le_bytes(header_bytes[8..].try_into()?) as usize;
// read the lengths for each entry found in the file
let entry_len_bytes_capacity = len * 8;
let mut entry_len_bytes = new_vec_sized(entry_len_bytes_capacity, 0)?;
reader.read_exact(&mut entry_len_bytes)?;
let mut lengths = Vec::new();
lengths.try_reserve(len)?;
for i in 0..len {
let pos = i * 8;
lengths.push(
u64::from_le_bytes(entry_len_bytes[pos..pos + 8].try_into()?) as usize,
);
}
let mut map = HashMap::new();
map.try_reserve(len)?;
for len in lengths {
let mut buffer = new_vec_sized(len, 0)?;
reader.read_exact(&mut buffer)?;
let entry_data_hash_start_pos = try_subtract(buffer.len(), 8)?;
let expected_entry_data_hash =
u64::from_le_bytes(buffer[entry_data_hash_start_pos..].try_into()?);
let source_hash_start_pos = try_subtract(entry_data_hash_start_pos, 8)?;
let source_hash = u64::from_le_bytes(
buffer[source_hash_start_pos..entry_data_hash_start_pos].try_into()?,
);
let specifier_end_pos = try_subtract(source_hash_start_pos, 4)?;
let specifier_len = u32::from_le_bytes(
buffer[specifier_end_pos..source_hash_start_pos].try_into()?,
) as usize;
let specifier_start_pos = try_subtract(specifier_end_pos, specifier_len)?;
let specifier = String::from_utf8(
buffer[specifier_start_pos..specifier_end_pos].to_vec(),
)?;
let code_cache_type_pos = try_subtract(specifier_start_pos, 1)?;
let code_cache_type = match buffer[code_cache_type_pos] {
0 => CodeCacheType::EsModule,
1 => CodeCacheType::Script,
_ => bail!("Invalid code cache type"),
};
buffer.truncate(code_cache_type_pos);
let actual_entry_data_hash: u64 =
FastInsecureHasher::new_without_deno_version()
.write(&buffer)
.finish();
if expected_entry_data_hash != actual_entry_data_hash {
bail!("Hash mismatch.")
}
map.insert(
(specifier, code_cache_type),
DenoCompileCodeCacheEntry {
source_hash,
data: buffer,
},
);
}
Ok(map)
}
#[cfg(test)]
mod test {
use test_util::TempDir;
use super::*;
use std::fs::File;
#[test]
fn serialize_deserialize() {
let cache_key = 123456;
let cache = {
let mut cache = HashMap::new();
cache.insert(
("specifier1".to_string(), CodeCacheType::EsModule),
DenoCompileCodeCacheEntry {
source_hash: 1,
data: vec![1, 2, 3],
},
);
cache.insert(
("specifier2".to_string(), CodeCacheType::EsModule),
DenoCompileCodeCacheEntry {
source_hash: 2,
data: vec![4, 5, 6],
},
);
cache.insert(
("specifier2".to_string(), CodeCacheType::Script),
DenoCompileCodeCacheEntry {
source_hash: 2,
data: vec![6, 5, 1],
},
);
cache
};
let mut buffer = Vec::new();
serialize_with_writer(&mut BufWriter::new(&mut buffer), cache_key, &cache)
.unwrap();
let deserialized =
deserialize_with_reader(&mut BufReader::new(&buffer[..]), cache_key)
.unwrap();
assert_eq!(cache, deserialized);
}
#[test]
fn serialize_deserialize_empty() {
let cache_key = 1234;
let cache = HashMap::new();
let mut buffer = Vec::new();
serialize_with_writer(&mut BufWriter::new(&mut buffer), cache_key, &cache)
.unwrap();
let deserialized =
deserialize_with_reader(&mut BufReader::new(&buffer[..]), cache_key)
.unwrap();
assert_eq!(cache, deserialized);
}
#[test]
fn serialize_deserialize_corrupt() {
let buffer = "corrupttestingtestingtesting".as_bytes().to_vec();
let err = deserialize_with_reader(&mut BufReader::new(&buffer[..]), 1234)
.unwrap_err();
assert_eq!(err.to_string(), "Cache key mismatch");
}
#[test]
fn code_cache() {
let temp_dir = TempDir::new();
let file_path = temp_dir.path().join("cache.bin").to_path_buf();
let url1 = ModuleSpecifier::parse("https://deno.land/example1.js").unwrap();
let url2 = ModuleSpecifier::parse("https://deno.land/example2.js").unwrap();
// first run
{
let code_cache = DenoCompileCodeCache::new(file_path.clone(), 1234);
assert!(code_cache
.get_sync(&url1, CodeCacheType::EsModule, 0)
.is_none());
assert!(code_cache
.get_sync(&url2, CodeCacheType::EsModule, 1)
.is_none());
assert!(code_cache.enabled());
code_cache.set_sync(url1.clone(), CodeCacheType::EsModule, 0, &[1, 2, 3]);
assert!(code_cache.enabled());
assert!(!file_path.exists());
code_cache.set_sync(url2.clone(), CodeCacheType::EsModule, 1, &[2, 1, 3]);
assert!(file_path.exists()); // now the new code cache exists
assert!(!code_cache.enabled()); // no longer enabled
}
// second run
{
let code_cache = DenoCompileCodeCache::new(file_path.clone(), 1234);
assert!(code_cache.enabled());
let result1 = code_cache
.get_sync(&url1, CodeCacheType::EsModule, 0)
.unwrap();
assert!(code_cache.enabled());
let result2 = code_cache
.get_sync(&url2, CodeCacheType::EsModule, 1)
.unwrap();
assert!(!code_cache.enabled()); // no longer enabled
assert_eq!(result1, vec![1, 2, 3]);
assert_eq!(result2, vec![2, 1, 3]);
}
// new cache key first run
{
let code_cache = DenoCompileCodeCache::new(file_path.clone(), 54321);
assert!(code_cache
.get_sync(&url1, CodeCacheType::EsModule, 0)
.is_none());
assert!(code_cache
.get_sync(&url2, CodeCacheType::EsModule, 1)
.is_none());
code_cache.set_sync(url1.clone(), CodeCacheType::EsModule, 0, &[2, 2, 3]);
code_cache.set_sync(url2.clone(), CodeCacheType::EsModule, 1, &[3, 2, 3]);
}
// new cache key second run
{
let code_cache = DenoCompileCodeCache::new(file_path.clone(), 54321);
let result1 = code_cache
.get_sync(&url1, CodeCacheType::EsModule, 0)
.unwrap();
assert_eq!(result1, vec![2, 2, 3]);
assert!(code_cache
.get_sync(&url2, CodeCacheType::EsModule, 5) // different hash will cause none
.is_none());
}
}
}

View File

@ -7,6 +7,7 @@
use binary::StandaloneData; use binary::StandaloneData;
use binary::StandaloneModules; use binary::StandaloneModules;
use code_cache::DenoCompileCodeCache;
use deno_ast::MediaType; use deno_ast::MediaType;
use deno_cache_dir::npm::NpmCacheDir; use deno_cache_dir::npm::NpmCacheDir;
use deno_config::workspace::MappedResolution; use deno_config::workspace::MappedResolution;
@ -17,6 +18,7 @@ use deno_core::anyhow::Context;
use deno_core::error::generic_error; use deno_core::error::generic_error;
use deno_core::error::type_error; use deno_core::error::type_error;
use deno_core::error::AnyError; use deno_core::error::AnyError;
use deno_core::futures::future::LocalBoxFuture;
use deno_core::futures::FutureExt; use deno_core::futures::FutureExt;
use deno_core::v8_set_flags; use deno_core::v8_set_flags;
use deno_core::FastString; use deno_core::FastString;
@ -27,6 +29,7 @@ use deno_core::ModuleSpecifier;
use deno_core::ModuleType; use deno_core::ModuleType;
use deno_core::RequestedModuleType; use deno_core::RequestedModuleType;
use deno_core::ResolutionKind; use deno_core::ResolutionKind;
use deno_core::SourceCodeCacheInfo;
use deno_npm::npm_rc::ResolvedNpmRc; use deno_npm::npm_rc::ResolvedNpmRc;
use deno_package_json::PackageJsonDepValue; use deno_package_json::PackageJsonDepValue;
use deno_resolver::npm::NpmReqResolverOptions; use deno_resolver::npm::NpmReqResolverOptions;
@ -64,6 +67,7 @@ use crate::args::StorageKeyResolver;
use crate::cache::Caches; use crate::cache::Caches;
use crate::cache::DenoCacheEnvFsAdapter; use crate::cache::DenoCacheEnvFsAdapter;
use crate::cache::DenoDirProvider; use crate::cache::DenoDirProvider;
use crate::cache::FastInsecureHasher;
use crate::cache::NodeAnalysisCache; use crate::cache::NodeAnalysisCache;
use crate::cache::RealDenoCacheEnv; use crate::cache::RealDenoCacheEnv;
use crate::http_util::HttpClientProvider; use crate::http_util::HttpClientProvider;
@ -86,12 +90,14 @@ use crate::resolver::NpmModuleLoader;
use crate::util::progress_bar::ProgressBar; use crate::util::progress_bar::ProgressBar;
use crate::util::progress_bar::ProgressBarStyle; use crate::util::progress_bar::ProgressBarStyle;
use crate::util::v8::construct_v8_flags; use crate::util::v8::construct_v8_flags;
use crate::worker::CliCodeCache;
use crate::worker::CliMainWorkerFactory; use crate::worker::CliMainWorkerFactory;
use crate::worker::CliMainWorkerOptions; use crate::worker::CliMainWorkerOptions;
use crate::worker::CreateModuleLoaderResult; use crate::worker::CreateModuleLoaderResult;
use crate::worker::ModuleLoaderFactory; use crate::worker::ModuleLoaderFactory;
pub mod binary; pub mod binary;
mod code_cache;
mod file_system; mod file_system;
mod serialization; mod serialization;
mod virtual_fs; mod virtual_fs;
@ -113,6 +119,35 @@ struct SharedModuleLoaderState {
npm_req_resolver: Arc<CliNpmReqResolver>, npm_req_resolver: Arc<CliNpmReqResolver>,
npm_resolver: Arc<dyn CliNpmResolver>, npm_resolver: Arc<dyn CliNpmResolver>,
workspace_resolver: WorkspaceResolver, workspace_resolver: WorkspaceResolver,
code_cache: Option<Arc<dyn CliCodeCache>>,
}
impl SharedModuleLoaderState {
fn get_code_cache(
&self,
specifier: &ModuleSpecifier,
source: &[u8],
) -> Option<SourceCodeCacheInfo> {
let Some(code_cache) = &self.code_cache else {
return None;
};
if !code_cache.enabled() {
return None;
}
// deno version is already included in the root cache key
let hash = FastInsecureHasher::new_without_deno_version()
.write_hashable(source)
.finish();
let data = code_cache.get_sync(
specifier,
deno_runtime::code_cache::CodeCacheType::EsModule,
hash,
);
Some(SourceCodeCacheInfo {
hash,
data: data.map(Cow::Owned),
})
}
} }
#[derive(Clone)] #[derive(Clone)]
@ -329,14 +364,19 @@ impl ModuleLoader for EmbeddedModuleLoader {
} }
if self.shared.node_resolver.in_npm_package(original_specifier) { if self.shared.node_resolver.in_npm_package(original_specifier) {
let npm_module_loader = self.shared.npm_module_loader.clone(); let shared = self.shared.clone();
let original_specifier = original_specifier.clone(); let original_specifier = original_specifier.clone();
let maybe_referrer = maybe_referrer.cloned(); let maybe_referrer = maybe_referrer.cloned();
return deno_core::ModuleLoadResponse::Async( return deno_core::ModuleLoadResponse::Async(
async move { async move {
let code_source = npm_module_loader let code_source = shared
.npm_module_loader
.load(&original_specifier, maybe_referrer.as_ref()) .load(&original_specifier, maybe_referrer.as_ref())
.await?; .await?;
let code_cache_entry = shared.get_code_cache(
&code_source.found_url,
code_source.code.as_bytes(),
);
Ok(deno_core::ModuleSource::new_with_redirect( Ok(deno_core::ModuleSource::new_with_redirect(
match code_source.media_type { match code_source.media_type {
MediaType::Json => ModuleType::Json, MediaType::Json => ModuleType::Json,
@ -345,7 +385,7 @@ impl ModuleLoader for EmbeddedModuleLoader {
code_source.code, code_source.code,
&original_specifier, &original_specifier,
&code_source.found_url, &code_source.found_url,
None, code_cache_entry,
)) ))
} }
.boxed_local(), .boxed_local(),
@ -398,25 +438,30 @@ impl ModuleLoader for EmbeddedModuleLoader {
ModuleSourceCode::String(FastString::from_static(source)) ModuleSourceCode::String(FastString::from_static(source))
} }
}; };
let code_cache_entry = shared
.get_code_cache(&module_specifier, module_source.as_bytes());
Ok(deno_core::ModuleSource::new_with_redirect( Ok(deno_core::ModuleSource::new_with_redirect(
module_type, module_type,
module_source, module_source,
&original_specifier, &original_specifier,
&module_specifier, &module_specifier,
None, code_cache_entry,
)) ))
} }
.boxed_local(), .boxed_local(),
) )
} else { } else {
let module_source = module_source.into_for_v8(); let module_source = module_source.into_for_v8();
let code_cache_entry = self
.shared
.get_code_cache(module_specifier, module_source.as_bytes());
deno_core::ModuleLoadResponse::Sync(Ok( deno_core::ModuleLoadResponse::Sync(Ok(
deno_core::ModuleSource::new_with_redirect( deno_core::ModuleSource::new_with_redirect(
module_type, module_type,
module_source, module_source,
original_specifier, original_specifier,
module_specifier, module_specifier,
None, code_cache_entry,
), ),
)) ))
} }
@ -429,6 +474,23 @@ impl ModuleLoader for EmbeddedModuleLoader {
))), ))),
} }
} }
fn code_cache_ready(
&self,
specifier: ModuleSpecifier,
source_hash: u64,
code_cache_data: &[u8],
) -> LocalBoxFuture<'static, ()> {
if let Some(code_cache) = &self.shared.code_cache {
code_cache.set_sync(
specifier,
deno_runtime::code_cache::CodeCacheType::EsModule,
source_hash,
code_cache_data,
);
}
std::future::ready(()).boxed_local()
}
} }
impl NodeRequireLoader for EmbeddedModuleLoader { impl NodeRequireLoader for EmbeddedModuleLoader {
@ -739,6 +801,19 @@ pub async fn run(data: StandaloneData) -> Result<i32, AnyError> {
metadata.workspace_resolver.pkg_json_resolution, metadata.workspace_resolver.pkg_json_resolution,
) )
}; };
let code_cache = match metadata.code_cache_key {
Some(code_cache_key) => Some(Arc::new(DenoCompileCodeCache::new(
root_path.with_file_name(format!(
"{}.cache",
root_path.file_name().unwrap().to_string_lossy()
)),
code_cache_key,
)) as Arc<dyn CliCodeCache>),
None => {
log::debug!("Code cache disabled.");
None
}
};
let module_loader_factory = StandaloneModuleLoaderFactory { let module_loader_factory = StandaloneModuleLoaderFactory {
shared: Arc::new(SharedModuleLoaderState { shared: Arc::new(SharedModuleLoaderState {
cjs_tracker: cjs_tracker.clone(), cjs_tracker: cjs_tracker.clone(),
@ -751,6 +826,7 @@ pub async fn run(data: StandaloneData) -> Result<i32, AnyError> {
fs.clone(), fs.clone(),
node_code_translator, node_code_translator,
)), )),
code_cache: code_cache.clone(),
npm_resolver: npm_resolver.clone(), npm_resolver: npm_resolver.clone(),
workspace_resolver, workspace_resolver,
npm_req_resolver, npm_req_resolver,
@ -792,8 +868,7 @@ pub async fn run(data: StandaloneData) -> Result<i32, AnyError> {
}); });
let worker_factory = CliMainWorkerFactory::new( let worker_factory = CliMainWorkerFactory::new(
Arc::new(BlobStore::default()), Arc::new(BlobStore::default()),
// Code cache is not supported for standalone binary yet. code_cache,
None,
feature_checker, feature_checker,
fs, fs,
None, None,

View File

@ -83,6 +83,15 @@ pub trait HmrRunner: Send + Sync {
async fn run(&mut self) -> Result<(), AnyError>; async fn run(&mut self) -> Result<(), AnyError>;
} }
pub trait CliCodeCache: code_cache::CodeCache {
/// Gets if the code cache is still enabled.
fn enabled(&self) -> bool {
true
}
fn as_code_cache(self: Arc<Self>) -> Arc<dyn code_cache::CodeCache>;
}
#[async_trait::async_trait(?Send)] #[async_trait::async_trait(?Send)]
pub trait CoverageCollector: Send + Sync { pub trait CoverageCollector: Send + Sync {
async fn start_collecting(&mut self) -> Result<(), AnyError>; async fn start_collecting(&mut self) -> Result<(), AnyError>;
@ -127,7 +136,7 @@ pub struct CliMainWorkerOptions {
struct SharedWorkerState { struct SharedWorkerState {
blob_store: Arc<BlobStore>, blob_store: Arc<BlobStore>,
broadcast_channel: InMemoryBroadcastChannel, broadcast_channel: InMemoryBroadcastChannel,
code_cache: Option<Arc<dyn code_cache::CodeCache>>, code_cache: Option<Arc<dyn CliCodeCache>>,
compiled_wasm_module_store: CompiledWasmModuleStore, compiled_wasm_module_store: CompiledWasmModuleStore,
feature_checker: Arc<FeatureChecker>, feature_checker: Arc<FeatureChecker>,
fs: Arc<dyn deno_fs::FileSystem>, fs: Arc<dyn deno_fs::FileSystem>,
@ -393,7 +402,7 @@ impl CliMainWorkerFactory {
#[allow(clippy::too_many_arguments)] #[allow(clippy::too_many_arguments)]
pub fn new( pub fn new(
blob_store: Arc<BlobStore>, blob_store: Arc<BlobStore>,
code_cache: Option<Arc<dyn code_cache::CodeCache>>, code_cache: Option<Arc<dyn CliCodeCache>>,
feature_checker: Arc<FeatureChecker>, feature_checker: Arc<FeatureChecker>,
fs: Arc<dyn deno_fs::FileSystem>, fs: Arc<dyn deno_fs::FileSystem>,
maybe_file_watcher_communicator: Option<Arc<WatcherCommunicator>>, maybe_file_watcher_communicator: Option<Arc<WatcherCommunicator>>,
@ -554,7 +563,7 @@ impl CliMainWorkerFactory {
), ),
feature_checker, feature_checker,
permissions, permissions,
v8_code_cache: shared.code_cache.clone(), v8_code_cache: shared.code_cache.clone().map(|c| c.as_code_cache()),
}; };
let options = WorkerOptions { let options = WorkerOptions {

View File

@ -2,20 +2,12 @@
use deno_core::ModuleSpecifier; use deno_core::ModuleSpecifier;
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
pub enum CodeCacheType { pub enum CodeCacheType {
EsModule, EsModule,
Script, Script,
} }
impl CodeCacheType {
pub fn as_str(&self) -> &str {
match self {
Self::EsModule => "esmodule",
Self::Script => "script",
}
}
}
pub trait CodeCache: Send + Sync { pub trait CodeCache: Send + Sync {
fn get_sync( fn get_sync(
&self, &self,
@ -23,6 +15,7 @@ pub trait CodeCache: Send + Sync {
code_cache_type: CodeCacheType, code_cache_type: CodeCacheType,
source_hash: u64, source_hash: u64,
) -> Option<Vec<u8>>; ) -> Option<Vec<u8>>;
fn set_sync( fn set_sync(
&self, &self,
specifier: ModuleSpecifier, specifier: ModuleSpecifier,

View File

@ -0,0 +1,32 @@
{
"tempDir": true,
"steps": [{
"if": "unix",
"args": "compile --output using_code_cache --log-level=debug main.ts",
"output": "[WILDCARD]"
}, {
"if": "unix",
"commandName": "./using_code_cache",
"args": [],
"output": "first_run.out"
}, {
"if": "unix",
"commandName": "./using_code_cache",
"args": [],
"output": "second_run.out"
}, {
"if": "windows",
"args": "compile --output using_code_cache.exe --log-level=debug main.ts",
"output": "[WILDCARD]"
}, {
"if": "windows",
"commandName": "./using_code_cache.exe",
"args": [],
"output": "first_run.out"
}, {
"if": "windows",
"commandName": "./using_code_cache.exe",
"args": [],
"output": "second_run.out"
}]
}

View File

@ -0,0 +1 @@
[WILDCARD]Serialized 9 code cache entries[WILDCARD]

View File

@ -0,0 +1,3 @@
import { join } from "jsr:@std/url@0.220/join";
console.log(join);

View File

@ -0,0 +1 @@
[WILDCARD]Loaded 9 code cache entries[WILDCARD][Function: join][WILDCARD]

View File

@ -0,0 +1,22 @@
{
"tempDir": true,
"steps": [{
"if": "unix",
"args": "compile --output no_code_cache --no-code-cache --log-level=debug main.ts",
"output": "[WILDCARD]"
}, {
"if": "unix",
"commandName": "./no_code_cache",
"args": [],
"output": "main.out"
}, {
"if": "windows",
"args": "compile --output no_code_cache.exe --no-code-cache --log-level=debug main.ts",
"output": "[WILDCARD]"
}, {
"if": "windows",
"commandName": "./no_code_cache.exe",
"args": [],
"output": "main.out"
}]
}

View File

@ -0,0 +1 @@
[WILDCARD]Code cache disabled.[WILDCARD]

View File

@ -0,0 +1,3 @@
import { join } from "jsr:@std/url@0.220/join";
console.log(join);