From b3d7df55357ea6fc6f5141b64a9638ddb39b0f63 Mon Sep 17 00:00:00 2001 From: Igor Zinkovsky Date: Wed, 17 Apr 2024 07:19:55 -0700 Subject: [PATCH] perf: v8 code cache (#23081) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit This PR enables V8 code cache for ES modules and for `require` scripts through `op_eval_context`. Code cache artifacts are transparently stored and fetched using sqlite db and are passed to V8. `--no-code-cache` can be used to disable. --------- Co-authored-by: Bartek IwaƄczuk --- Cargo.lock | 1 + cli/args/flags.rs | 106 +++++++++++ cli/args/mod.rs | 4 + cli/cache/caches.rs | 15 ++ cli/cache/code_cache.rs | 231 ++++++++++++++++++++++++ cli/cache/deno_dir.rs | 6 + cli/cache/mod.rs | 2 + cli/cache/module_info.rs | 24 +++ cli/factory.rs | 22 +++ cli/graph_util.rs | 2 +- cli/lsp/analysis.rs | 2 +- cli/lsp/cache.rs | 2 +- cli/lsp/completions.rs | 2 +- cli/lsp/config.rs | 2 +- cli/lsp/documents.rs | 2 +- cli/lsp/language_server.rs | 2 +- cli/lsp/tsc.rs | 2 +- cli/module_loader.rs | 86 ++++++++- cli/npm/byonm.rs | 2 +- cli/resolver.rs | 2 +- cli/standalone/mod.rs | 2 + cli/tools/vendor/mod.rs | 2 +- cli/util/path.rs | 63 ------- cli/worker.rs | 5 + runtime/Cargo.toml | 1 + runtime/code_cache.rs | 31 ++++ runtime/fs_util.rs | 74 ++++++++ runtime/lib.rs | 1 + runtime/worker.rs | 65 +++++++ tests/integration/run_tests.rs | 202 +++++++++++++++++++++ tests/testdata/run/rejection_handled.ts | 2 +- 31 files changed, 889 insertions(+), 76 deletions(-) create mode 100644 cli/cache/code_cache.rs create mode 100644 runtime/code_cache.rs diff --git a/Cargo.lock b/Cargo.lock index 6477eb63e1..f5760f3ddd 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -1836,6 +1836,7 @@ dependencies = [ "notify", "ntapi", "once_cell", + "percent-encoding", "regex", "ring", "rustyline", diff --git a/cli/args/flags.rs b/cli/args/flags.rs index 9384dacf8a..d57f78aff3 100644 --- a/cli/args/flags.rs +++ b/cli/args/flags.rs @@ -517,6 +517,7 @@ pub struct Flags { pub unstable_config: UnstableConfig, pub unsafely_ignore_certificate_errors: Option>, pub v8_flags: Vec, + pub code_cache_enabled: bool, } fn join_paths(allowlist: &[String], d: &str) -> String { @@ -2236,6 +2237,7 @@ fn run_subcommand() -> Command { .trailing_var_arg(true), ) .arg(env_file_arg()) + .arg(no_code_cache_arg()) .about("Run a JavaScript or TypeScript program") .long_about( "Run a JavaScript or TypeScript program @@ -3222,6 +3224,13 @@ fn no_clear_screen_arg() -> Arg { .help("Do not clear terminal screen when under watch mode") } +fn no_code_cache_arg() -> Arg { + Arg::new("no-code-cache") + .long("no-code-cache") + .help("Disable V8 code cache feature") + .action(ArgAction::SetTrue) +} + fn watch_exclude_arg() -> Arg { Arg::new("watch-exclude") .long("watch-exclude") @@ -3829,6 +3838,8 @@ fn run_parse( ) -> clap::error::Result<()> { runtime_args_parse(flags, matches, true, true); + flags.code_cache_enabled = !matches.get_flag("no-code-cache"); + let mut script_arg = matches.remove_many::("script_arg").ok_or_else(|| { let mut app = app; @@ -4469,6 +4480,7 @@ mod tests { ..Default::default() }, log_level: Some(Level::Error), + code_cache_enabled: true, ..Flags::default() } ); @@ -4540,6 +4552,7 @@ mod tests { "script.ts".to_string() )), reload: true, + code_cache_enabled: true, ..Flags::default() } ); @@ -4561,6 +4574,7 @@ mod tests { exclude: vec![], }), }), + code_cache_enabled: true, ..Flags::default() } ); @@ -4585,6 +4599,7 @@ mod tests { exclude: vec![], }), }), + code_cache_enabled: true, ..Flags::default() } ); @@ -4609,6 +4624,7 @@ mod tests { exclude: vec![], }), }), + code_cache_enabled: true, ..Flags::default() } ); @@ -4633,6 +4649,7 @@ mod tests { exclude: vec![], }), }), + code_cache_enabled: true, ..Flags::default() } ); @@ -4659,6 +4676,7 @@ mod tests { exclude: vec![], }), }), + code_cache_enabled: true, ..Flags::default() } ); @@ -4687,6 +4705,7 @@ mod tests { exclude: vec![], }), }), + code_cache_enabled: true, ..Flags::default() } ); @@ -4715,6 +4734,7 @@ mod tests { exclude: vec![String::from("foo")], }), }), + code_cache_enabled: true, ..Flags::default() } ); @@ -4739,6 +4759,7 @@ mod tests { exclude: vec![String::from("bar")], }), }), + code_cache_enabled: true, ..Flags::default() } ); @@ -4764,6 +4785,7 @@ mod tests { exclude: vec![String::from("foo"), String::from("bar")], }), }), + code_cache_enabled: true, ..Flags::default() } ); @@ -4789,6 +4811,7 @@ mod tests { exclude: vec![String::from("baz"), String::from("qux"),], }), }), + code_cache_enabled: true, ..Flags::default() } ); @@ -4806,6 +4829,7 @@ mod tests { "script.ts".to_string() )), allow_write: Some(vec![]), + code_cache_enabled: true, ..Flags::default() } ); @@ -4819,6 +4843,7 @@ mod tests { Flags { subcommand: DenoSubcommand::Run(RunFlags::new_default("_".to_string())), v8_flags: svec!["--help"], + code_cache_enabled: true, ..Flags::default() } ); @@ -4836,6 +4861,7 @@ mod tests { "script.ts".to_string() )), v8_flags: svec!["--expose-gc", "--gc-stats=1"], + code_cache_enabled: true, ..Flags::default() } ); @@ -4889,6 +4915,7 @@ mod tests { )), argv: svec!["--title", "X"], allow_net: Some(vec![]), + code_cache_enabled: true, ..Flags::default() } ); @@ -4912,6 +4939,7 @@ mod tests { allow_write: Some(vec![]), allow_ffi: Some(vec![]), allow_hrtime: true, + code_cache_enabled: true, ..Flags::default() } ); @@ -4927,6 +4955,7 @@ mod tests { "gist.ts".to_string() )), allow_read: Some(vec![]), + code_cache_enabled: true, ..Flags::default() } ); @@ -4942,6 +4971,7 @@ mod tests { "gist.ts".to_string() )), deny_read: Some(vec![]), + code_cache_enabled: true, ..Flags::default() } ); @@ -4957,6 +4987,7 @@ mod tests { "gist.ts".to_string(), )), allow_hrtime: true, + code_cache_enabled: true, ..Flags::default() } ); @@ -4972,6 +5003,7 @@ mod tests { "gist.ts".to_string(), )), deny_hrtime: true, + code_cache_enabled: true, ..Flags::default() } ); @@ -4999,6 +5031,7 @@ mod tests { )), argv: svec!["--", "-D", "--allow-net"], allow_write: Some(vec![]), + code_cache_enabled: true, ..Flags::default() } ); @@ -5713,6 +5746,7 @@ mod tests { "script.ts".to_string(), )), config_flag: ConfigFlag::Path("tsconfig.json".to_owned()), + code_cache_enabled: true, ..Flags::default() } ); @@ -6007,6 +6041,7 @@ mod tests { subcommand: DenoSubcommand::Run(RunFlags::new_default( "script.ts".to_string(), )), + code_cache_enabled: true, ..Flags::default() } ); @@ -6031,6 +6066,7 @@ mod tests { subcommand: DenoSubcommand::Run(RunFlags::new_default( "script.ts".to_string(), )), + code_cache_enabled: true, ..Flags::default() } ); @@ -6055,6 +6091,7 @@ mod tests { subcommand: DenoSubcommand::Run(RunFlags::new_default( "script.ts".to_string(), )), + code_cache_enabled: true, ..Flags::default() } ); @@ -6079,6 +6116,7 @@ mod tests { subcommand: DenoSubcommand::Run(RunFlags::new_default( "script.ts".to_string(), )), + code_cache_enabled: true, ..Flags::default() } ); @@ -6099,6 +6137,7 @@ mod tests { "script.ts".to_string(), )), allow_net: Some(svec!["127.0.0.1"]), + code_cache_enabled: true, ..Flags::default() } ); @@ -6115,6 +6154,7 @@ mod tests { "script.ts".to_string(), )), deny_net: Some(svec!["127.0.0.1"]), + code_cache_enabled: true, ..Flags::default() } ); @@ -6131,6 +6171,7 @@ mod tests { "script.ts".to_string(), )), allow_env: Some(svec!["HOME"]), + code_cache_enabled: true, ..Flags::default() } ); @@ -6147,6 +6188,7 @@ mod tests { "script.ts".to_string(), )), deny_env: Some(svec!["HOME"]), + code_cache_enabled: true, ..Flags::default() } ); @@ -6167,6 +6209,7 @@ mod tests { "script.ts".to_string(), )), allow_env: Some(svec!["HOME", "PATH"]), + code_cache_enabled: true, ..Flags::default() } ); @@ -6183,6 +6226,7 @@ mod tests { "script.ts".to_string(), )), deny_env: Some(svec!["HOME", "PATH"]), + code_cache_enabled: true, ..Flags::default() } ); @@ -6224,6 +6268,7 @@ mod tests { "script.ts".to_string(), )), allow_sys: Some(vec![]), + code_cache_enabled: true, ..Flags::default() } ); @@ -6239,6 +6284,7 @@ mod tests { "script.ts".to_string(), )), deny_sys: Some(vec![]), + code_cache_enabled: true, ..Flags::default() } ); @@ -6255,6 +6301,7 @@ mod tests { "script.ts".to_string(), )), allow_sys: Some(svec!["hostname"]), + code_cache_enabled: true, ..Flags::default() } ); @@ -6271,6 +6318,7 @@ mod tests { "script.ts".to_string(), )), deny_sys: Some(svec!["hostname"]), + code_cache_enabled: true, ..Flags::default() } ); @@ -6291,6 +6339,7 @@ mod tests { "script.ts".to_string(), )), allow_sys: Some(svec!["hostname", "osRelease"]), + code_cache_enabled: true, ..Flags::default() } ); @@ -6311,6 +6360,7 @@ mod tests { "script.ts".to_string(), )), deny_sys: Some(svec!["hostname", "osRelease"]), + code_cache_enabled: true, ..Flags::default() } ); @@ -6618,6 +6668,7 @@ mod tests { "script.ts".to_string(), )), import_map_path: Some("import_map.json".to_owned()), + code_cache_enabled: true, ..Flags::default() } ); @@ -6699,6 +6750,22 @@ mod tests { "script.ts".to_string(), )), env_file: Some(".env".to_owned()), + code_cache_enabled: true, + ..Flags::default() + } + ); + } + + #[test] + fn run_no_code_cache() { + let r = + flags_from_vec(svec!["deno", "run", "--no-code-cache", "script.ts"]); + assert_eq!( + r.unwrap(), + Flags { + subcommand: DenoSubcommand::Run(RunFlags::new_default( + "script.ts".to_string(), + )), ..Flags::default() } ); @@ -6715,6 +6782,7 @@ mod tests { "script.ts".to_string(), )), env_file: Some(".another_env".to_owned()), + code_cache_enabled: true, ..Flags::default() } ); @@ -6746,6 +6814,7 @@ mod tests { )), seed: Some(250_u64), v8_flags: svec!["--random-seed=250"], + code_cache_enabled: true, ..Flags::default() } ); @@ -6769,6 +6838,7 @@ mod tests { )), seed: Some(250_u64), v8_flags: svec!["--expose-gc", "--random-seed=250"], + code_cache_enabled: true, ..Flags::default() } ); @@ -6907,6 +6977,7 @@ mod tests { "script.ts".to_string(), )), log_level: Some(Level::Debug), + code_cache_enabled: true, ..Flags::default() } ); @@ -6922,6 +6993,7 @@ mod tests { "script.ts".to_string(), )), log_level: Some(Level::Error), + code_cache_enabled: true, ..Flags::default() } ); @@ -6955,6 +7027,7 @@ mod tests { "script.ts".to_string(), )), argv: svec!["--allow-read", "--allow-net"], + code_cache_enabled: true, ..Flags::default() } ); @@ -6980,6 +7053,7 @@ mod tests { location: Some(Url::parse("https://foo/").unwrap()), allow_read: Some(vec![]), argv: svec!["--allow-net", "-r", "--help", "--foo", "bar"], + code_cache_enabled: true, ..Flags::default() } ); @@ -6992,6 +7066,7 @@ mod tests { "script.ts".to_string(), )), argv: svec!["foo", "bar"], + code_cache_enabled: true, ..Flags::default() } ); @@ -7003,6 +7078,7 @@ mod tests { "script.ts".to_string(), )), argv: svec!["-"], + code_cache_enabled: true, ..Flags::default() } ); @@ -7016,6 +7092,7 @@ mod tests { "script.ts".to_string(), )), argv: svec!["-", "foo", "bar"], + code_cache_enabled: true, ..Flags::default() } ); @@ -7031,6 +7108,7 @@ mod tests { "script.ts".to_string(), )), type_check_mode: TypeCheckMode::None, + code_cache_enabled: true, ..Flags::default() } ); @@ -7047,6 +7125,7 @@ mod tests { "script.ts".to_string(), )), type_check_mode: TypeCheckMode::Local, + code_cache_enabled: true, ..Flags::default() } ); @@ -7091,6 +7170,7 @@ mod tests { "script.ts".to_string(), )), unsafely_ignore_certificate_errors: Some(vec![]), + code_cache_enabled: true, ..Flags::default() } ); @@ -7118,6 +7198,7 @@ mod tests { "[::1]", "1.2.3.4" ]), + code_cache_enabled: true, ..Flags::default() } ); @@ -7161,6 +7242,7 @@ mod tests { "script.ts".to_string(), )), no_remote: true, + code_cache_enabled: true, ..Flags::default() } ); @@ -7176,6 +7258,7 @@ mod tests { "script.ts".to_string(), )), no_npm: true, + code_cache_enabled: true, ..Flags::default() } ); @@ -7192,6 +7275,7 @@ mod tests { "script.ts".to_string(), )), node_modules_dir: Some(true), + code_cache_enabled: true, ..Flags::default() } ); @@ -7209,6 +7293,7 @@ mod tests { "script.ts".to_string(), )), node_modules_dir: Some(false), + code_cache_enabled: true, ..Flags::default() } ); @@ -7224,6 +7309,7 @@ mod tests { "script.ts".to_string(), )), vendor: Some(true), + code_cache_enabled: true, ..Flags::default() } ); @@ -7236,6 +7322,7 @@ mod tests { "script.ts".to_string(), )), vendor: Some(false), + code_cache_enabled: true, ..Flags::default() } ); @@ -7251,6 +7338,7 @@ mod tests { "script.ts".to_string(), )), cached_only: true, + code_cache_enabled: true, ..Flags::default() } ); @@ -7279,6 +7367,7 @@ mod tests { "127.0.0.1:4545", "localhost:4545" ]), + code_cache_enabled: true, ..Flags::default() } ); @@ -7307,6 +7396,7 @@ mod tests { "127.0.0.1:4545", "localhost:4545" ]), + code_cache_enabled: true, ..Flags::default() } ); @@ -7338,6 +7428,7 @@ mod tests { "localhost:5678", "[::1]:8080" ]), + code_cache_enabled: true, ..Flags::default() } ); @@ -7369,6 +7460,7 @@ mod tests { "localhost:5678", "[::1]:8080" ]), + code_cache_enabled: true, ..Flags::default() } ); @@ -7391,6 +7483,7 @@ mod tests { )), lock_write: true, lock: Some(String::from("lock.json")), + code_cache_enabled: true, ..Flags::default() } ); @@ -7403,6 +7496,7 @@ mod tests { "script.ts".to_string(), )), no_lock: true, + code_cache_enabled: true, ..Flags::default() } ); @@ -7422,6 +7516,7 @@ mod tests { )), lock_write: true, lock: Some(String::from("./deno.lock")), + code_cache_enabled: true, ..Flags::default() } ); @@ -7442,6 +7537,7 @@ mod tests { )), lock_write: true, lock: Some(String::from("lock.json")), + code_cache_enabled: true, ..Flags::default() } ); @@ -7454,6 +7550,7 @@ mod tests { "script.ts".to_string(), )), lock_write: true, + code_cache_enabled: true, ..Flags::default() } ); @@ -7546,6 +7643,7 @@ mod tests { "script.ts".to_string(), )), ca_data: Some(CaData::File("example.crt".to_owned())), + code_cache_enabled: true, ..Flags::default() } ); @@ -7566,6 +7664,7 @@ mod tests { "script.ts".to_string(), )), enable_testing_features: true, + code_cache_enabled: true, ..Flags::default() } ); @@ -8244,6 +8343,7 @@ mod tests { "foo.js".to_string(), )), inspect: Some("127.0.0.1:9229".parse().unwrap()), + code_cache_enabled: true, ..Flags::default() } ); @@ -8259,6 +8359,7 @@ mod tests { "foo.js".to_string(), )), inspect_wait: Some("127.0.0.1:9229".parse().unwrap()), + code_cache_enabled: true, ..Flags::default() } ); @@ -8276,6 +8377,7 @@ mod tests { "foo.js".to_string(), )), inspect_wait: Some("127.0.0.1:3567".parse().unwrap()), + code_cache_enabled: true, ..Flags::default() } ); @@ -8802,6 +8904,7 @@ mod tests { "script.ts".to_string(), )), type_check_mode: TypeCheckMode::Local, + code_cache_enabled: true, ..Flags::default() } ); @@ -8814,6 +8917,7 @@ mod tests { "script.ts".to_string(), )), type_check_mode: TypeCheckMode::All, + code_cache_enabled: true, ..Flags::default() } ); @@ -8826,6 +8930,7 @@ mod tests { "script.ts".to_string(), )), type_check_mode: TypeCheckMode::None, + code_cache_enabled: true, ..Flags::default() } ); @@ -8850,6 +8955,7 @@ mod tests { "script.ts".to_string(), )), config_flag: ConfigFlag::Disabled, + code_cache_enabled: true, ..Flags::default() } ); diff --git a/cli/args/mod.rs b/cli/args/mod.rs index 9bc4093072..dbb3e6e466 100644 --- a/cli/args/mod.rs +++ b/cli/args/mod.rs @@ -1653,6 +1653,10 @@ impl CliOptions { &self.flags.v8_flags } + pub fn code_cache_enabled(&self) -> bool { + self.flags.code_cache_enabled + } + pub fn watch_paths(&self) -> Vec { let mut full_paths = Vec::new(); if let DenoSubcommand::Run(RunFlags { diff --git a/cli/cache/caches.rs b/cli/cache/caches.rs index dc97f02d55..1be14b53bb 100644 --- a/cli/cache/caches.rs +++ b/cli/cache/caches.rs @@ -8,6 +8,7 @@ use once_cell::sync::OnceCell; use super::cache_db::CacheDB; use super::cache_db::CacheDBConfiguration; use super::check::TYPE_CHECK_CACHE_DB; +use super::code_cache::CODE_CACHE_DB; use super::deno_dir::DenoDirProvider; use super::fast_check::FAST_CHECK_CACHE_DB; use super::incremental::INCREMENTAL_CACHE_DB; @@ -22,6 +23,7 @@ pub struct Caches { fast_check_db: OnceCell, node_analysis_db: OnceCell, type_checking_cache_db: OnceCell, + code_cache_db: OnceCell, } impl Caches { @@ -34,6 +36,7 @@ impl Caches { fast_check_db: Default::default(), node_analysis_db: Default::default(), type_checking_cache_db: Default::default(), + code_cache_db: Default::default(), } } @@ -124,4 +127,16 @@ impl Caches { .map(|dir| dir.type_checking_cache_db_file_path()), ) } + + pub fn code_cache_db(&self) -> CacheDB { + Self::make_db( + &self.code_cache_db, + &CODE_CACHE_DB, + self + .dir_provider + .get_or_create() + .ok() + .map(|dir| dir.code_cache_db_file_path()), + ) + } } diff --git a/cli/cache/code_cache.rs b/cli/cache/code_cache.rs new file mode 100644 index 0000000000..5e44c366e7 --- /dev/null +++ b/cli/cache/code_cache.rs @@ -0,0 +1,231 @@ +// Copyright 2018-2024 the Deno authors. All rights reserved. MIT license. + +use deno_core::error::AnyError; +use deno_runtime::code_cache; +use deno_runtime::deno_webstorage::rusqlite::params; + +use super::cache_db::CacheDB; +use super::cache_db::CacheDBConfiguration; +use super::cache_db::CacheFailure; + +pub static CODE_CACHE_DB: CacheDBConfiguration = CacheDBConfiguration { + table_initializer: "CREATE TABLE IF NOT EXISTS codecache ( + specifier TEXT NOT NULL, + type TEXT NOT NULL, + source_hash TEXT NOT NULL, + data BLOB NOT NULL, + PRIMARY KEY (specifier, type) + );", + on_version_change: "DELETE FROM codecache;", + preheat_queries: &[], + on_failure: CacheFailure::Blackhole, +}; + +#[derive(Clone)] +pub struct CodeCache { + inner: CodeCacheInner, +} + +impl CodeCache { + pub fn new(db: CacheDB) -> Self { + Self { + inner: CodeCacheInner::new(db), + } + } + + fn ensure_ok(res: Result) -> T { + match res { + Ok(x) => x, + Err(err) => { + // TODO(mmastrac): This behavior was inherited from before the refactoring but it probably makes sense to move it into the cache + // at some point. + // should never error here, but if it ever does don't fail + if cfg!(debug_assertions) { + panic!("Error using code cache: {err:#}"); + } else { + log::debug!("Error using code cache: {:#}", err); + } + T::default() + } + } + } + + pub fn get_sync( + &self, + specifier: &str, + code_cache_type: code_cache::CodeCacheType, + source_hash: &str, + ) -> Option> { + Self::ensure_ok(self.inner.get_sync( + specifier, + code_cache_type, + source_hash, + )) + } + + pub fn set_sync( + &self, + specifier: &str, + code_cache_type: code_cache::CodeCacheType, + source_hash: &str, + data: &[u8], + ) { + Self::ensure_ok(self.inner.set_sync( + specifier, + code_cache_type, + source_hash, + data, + )); + } +} + +impl code_cache::CodeCache for CodeCache { + fn get_sync( + &self, + specifier: &str, + code_cache_type: code_cache::CodeCacheType, + source_hash: &str, + ) -> Option> { + self.get_sync(specifier, code_cache_type, source_hash) + } + + fn set_sync( + &self, + specifier: &str, + code_cache_type: code_cache::CodeCacheType, + source_hash: &str, + data: &[u8], + ) { + self.set_sync(specifier, code_cache_type, source_hash, data); + } +} + +#[derive(Clone)] +struct CodeCacheInner { + conn: CacheDB, +} + +impl CodeCacheInner { + pub fn new(conn: CacheDB) -> Self { + Self { conn } + } + + pub fn get_sync( + &self, + specifier: &str, + code_cache_type: code_cache::CodeCacheType, + source_hash: &str, + ) -> Result>, AnyError> { + let query = " + SELECT + data + FROM + codecache + WHERE + specifier=?1 AND type=?2 AND source_hash=?3 + LIMIT 1"; + let params = params![specifier, code_cache_type.as_str(), source_hash,]; + self.conn.query_row(query, params, |row| { + let value: Vec = row.get(0)?; + Ok(value) + }) + } + + pub fn set_sync( + &self, + specifier: &str, + code_cache_type: code_cache::CodeCacheType, + source_hash: &str, + data: &[u8], + ) -> Result<(), AnyError> { + let sql = " + INSERT OR REPLACE INTO + codecache (specifier, type, source_hash, data) + VALUES + (?1, ?2, ?3, ?4)"; + let params = + params![specifier, code_cache_type.as_str(), source_hash, data]; + self.conn.execute(sql, params)?; + Ok(()) + } +} + +#[cfg(test)] +mod test { + use super::*; + + #[test] + pub fn end_to_end() { + let conn = CacheDB::in_memory(&CODE_CACHE_DB, "1.0.0"); + let cache = CodeCacheInner::new(conn); + + assert!(cache + .get_sync( + "file:///foo/bar.js", + code_cache::CodeCacheType::EsModule, + "hash", + ) + .unwrap() + .is_none()); + let data_esm = vec![1, 2, 3]; + cache + .set_sync( + "file:///foo/bar.js", + code_cache::CodeCacheType::EsModule, + "hash", + &data_esm, + ) + .unwrap(); + assert_eq!( + cache + .get_sync( + "file:///foo/bar.js", + code_cache::CodeCacheType::EsModule, + "hash", + ) + .unwrap() + .unwrap(), + data_esm + ); + + assert!(cache + .get_sync( + "file:///foo/bar.js", + code_cache::CodeCacheType::Script, + "hash", + ) + .unwrap() + .is_none()); + let data_script = vec![4, 5, 6]; + cache + .set_sync( + "file:///foo/bar.js", + code_cache::CodeCacheType::Script, + "hash", + &data_script, + ) + .unwrap(); + assert_eq!( + cache + .get_sync( + "file:///foo/bar.js", + code_cache::CodeCacheType::Script, + "hash", + ) + .unwrap() + .unwrap(), + data_script + ); + assert_eq!( + cache + .get_sync( + "file:///foo/bar.js", + code_cache::CodeCacheType::EsModule, + "hash", + ) + .unwrap() + .unwrap(), + data_esm + ); + } +} diff --git a/cli/cache/deno_dir.rs b/cli/cache/deno_dir.rs index ee8c35684e..b56dfbc893 100644 --- a/cli/cache/deno_dir.rs +++ b/cli/cache/deno_dir.rs @@ -142,6 +142,12 @@ impl DenoDir { self.root.join("npm") } + /// Path for the V8 code cache. + pub fn code_cache_db_file_path(&self) -> PathBuf { + // bump this version name to invalidate the entire cache + self.root.join("v8_code_cache_v1") + } + /// Path used for the REPL history file. /// Can be overridden or disabled by setting `DENO_REPL_HISTORY` environment variable. pub fn repl_history_file_path(&self) -> Option { diff --git a/cli/cache/mod.rs b/cli/cache/mod.rs index 229a9cb544..a511792132 100644 --- a/cli/cache/mod.rs +++ b/cli/cache/mod.rs @@ -25,6 +25,7 @@ use std::time::SystemTime; mod cache_db; mod caches; mod check; +mod code_cache; mod common; mod deno_dir; mod disk_cache; @@ -37,6 +38,7 @@ mod parsed_source; pub use caches::Caches; pub use check::TypeCheckCache; +pub use code_cache::CodeCache; pub use common::FastInsecureHasher; pub use deno_dir::DenoDir; pub use deno_dir::DenoDirProvider; diff --git a/cli/cache/module_info.rs b/cli/cache/module_info.rs index 6d317b2165..2e9274160c 100644 --- a/cli/cache/module_info.rs +++ b/cli/cache/module_info.rs @@ -39,6 +39,7 @@ pub static MODULE_INFO_CACHE_DB: CacheDBConfiguration = CacheDBConfiguration { on_failure: CacheFailure::InMemory, }; +#[derive(Debug)] pub struct ModuleInfoCacheSourceHash(String); impl ModuleInfoCacheSourceHash { @@ -55,6 +56,12 @@ impl ModuleInfoCacheSourceHash { } } +impl From for String { + fn from(source_hash: ModuleInfoCacheSourceHash) -> String { + source_hash.0 + } +} + /// A cache of `deno_graph::ModuleInfo` objects. Using this leads to a considerable /// performance improvement because when it exists we can skip parsing a module for /// deno_graph. @@ -80,6 +87,23 @@ impl ModuleInfoCache { } } + pub fn get_module_source_hash( + &self, + specifier: &ModuleSpecifier, + media_type: MediaType, + ) -> Result, AnyError> { + let query = "SELECT source_hash FROM moduleinfocache WHERE specifier=?1 AND media_type=?2"; + let res = self.conn.query_row( + query, + params![specifier.as_str(), serialize_media_type(media_type)], + |row| { + let source_hash: String = row.get(0)?; + Ok(ModuleInfoCacheSourceHash(source_hash)) + }, + )?; + Ok(res) + } + pub fn get_module_info( &self, specifier: &ModuleSpecifier, diff --git a/cli/factory.rs b/cli/factory.rs index fd33d295c8..2d685ce76e 100644 --- a/cli/factory.rs +++ b/cli/factory.rs @@ -9,6 +9,7 @@ use crate::args::PackageJsonDepsProvider; use crate::args::StorageKeyResolver; use crate::args::TsConfigType; use crate::cache::Caches; +use crate::cache::CodeCache; use crate::cache::DenoDir; use crate::cache::DenoDirProvider; use crate::cache::EmitCache; @@ -178,6 +179,7 @@ struct CliFactoryServices { cjs_resolutions: Deferred>, cli_node_resolver: Deferred>, feature_checker: Deferred>, + code_cache: Deferred>, } pub struct CliFactory { @@ -226,6 +228,9 @@ impl CliFactory { _ = caches.fast_check_db(); _ = caches.type_checking_cache_db(); } + if self.options.code_cache_enabled() { + _ = caches.code_cache_db(); + } } _ => {} } @@ -534,6 +539,12 @@ impl CliFactory { }) } + pub fn code_cache(&self) -> Result<&Arc, AnyError> { + self.services.code_cache.get_or_try_init(|| { + Ok(Arc::new(CodeCache::new(self.caches()?.code_cache_db()))) + }) + } + pub fn parsed_source_cache(&self) -> &Arc { self .services @@ -790,6 +801,12 @@ impl CliFactory { fs.clone(), cli_node_resolver.clone(), ), + if self.options.code_cache_enabled() { + Some(self.code_cache()?.clone()) + } else { + None + }, + self.module_info_cache()?.clone(), )), self.root_cert_store_provider().clone(), self.fs().clone(), @@ -804,6 +821,11 @@ impl CliFactory { // self.options.disable_deprecated_api_warning, true, self.options.verbose_deprecated_api_warning, + if self.options.code_cache_enabled() { + Some(self.code_cache()?.clone()) + } else { + None + }, )) } diff --git a/cli/graph_util.rs b/cli/graph_util.rs index f3b69b2434..7363358f15 100644 --- a/cli/graph_util.rs +++ b/cli/graph_util.rs @@ -18,9 +18,9 @@ use crate::tools::check; use crate::tools::check::TypeChecker; use crate::util::file_watcher::WatcherCommunicator; use crate::util::fs::canonicalize_path; -use crate::util::path::specifier_to_file_path; use crate::util::sync::TaskQueue; use crate::util::sync::TaskQueuePermit; +use deno_runtime::fs_util::specifier_to_file_path; use deno_config::WorkspaceMemberConfig; use deno_core::anyhow::bail; diff --git a/cli/lsp/analysis.rs b/cli/lsp/analysis.rs index 9da3fcad7d..9ea8d710e0 100644 --- a/cli/lsp/analysis.rs +++ b/cli/lsp/analysis.rs @@ -10,7 +10,7 @@ use crate::args::jsr_url; use crate::npm::CliNpmResolver; use crate::resolver::CliNodeResolver; use crate::tools::lint::create_linter; -use crate::util::path::specifier_to_file_path; +use deno_runtime::fs_util::specifier_to_file_path; use deno_ast::SourceRange; use deno_ast::SourceRangedForSpanned; diff --git a/cli/lsp/cache.rs b/cli/lsp/cache.rs index e0034207d3..a1048dace7 100644 --- a/cli/lsp/cache.rs +++ b/cli/lsp/cache.rs @@ -1,7 +1,7 @@ // Copyright 2018-2024 the Deno authors. All rights reserved. MIT license. use crate::cache::HttpCache; -use crate::util::path::specifier_to_file_path; +use deno_runtime::fs_util::specifier_to_file_path; use deno_core::parking_lot::Mutex; use deno_core::ModuleSpecifier; diff --git a/cli/lsp/completions.rs b/cli/lsp/completions.rs index 164b3b8c3d..a4a7c81c16 100644 --- a/cli/lsp/completions.rs +++ b/cli/lsp/completions.rs @@ -15,7 +15,7 @@ use super::tsc; use crate::jsr::JsrFetchResolver; use crate::util::path::is_importable_ext; use crate::util::path::relative_specifier; -use crate::util::path::specifier_to_file_path; +use deno_runtime::fs_util::specifier_to_file_path; use deno_ast::LineAndColumnIndex; use deno_ast::SourceTextInfo; diff --git a/cli/lsp/config.rs b/cli/lsp/config.rs index 3e5460a1d3..15bd93ced2 100644 --- a/cli/lsp/config.rs +++ b/cli/lsp/config.rs @@ -10,7 +10,6 @@ use crate::lsp::logging::lsp_warn; use crate::tools::lint::get_configured_rules; use crate::tools::lint::ConfiguredRules; use crate::util::fs::canonicalize_path_maybe_not_exists; -use crate::util::path::specifier_to_file_path; use deno_ast::MediaType; use deno_config::FmtOptionsConfig; use deno_config::TsConfig; @@ -25,6 +24,7 @@ use deno_core::serde_json::Value; use deno_core::ModuleSpecifier; use deno_lockfile::Lockfile; use deno_runtime::deno_node::PackageJson; +use deno_runtime::fs_util::specifier_to_file_path; use deno_runtime::permissions::PermissionsContainer; use import_map::ImportMap; use lsp::Url; diff --git a/cli/lsp/documents.rs b/cli/lsp/documents.rs index e7ef048cfd..154dfb5dc3 100644 --- a/cli/lsp/documents.rs +++ b/cli/lsp/documents.rs @@ -20,7 +20,7 @@ use crate::resolver::CliNodeResolver; use crate::resolver::SloppyImportsFsEntry; use crate::resolver::SloppyImportsResolution; use crate::resolver::SloppyImportsResolver; -use crate::util::path::specifier_to_file_path; +use deno_runtime::fs_util::specifier_to_file_path; use dashmap::DashMap; use deno_ast::MediaType; diff --git a/cli/lsp/language_server.rs b/cli/lsp/language_server.rs index 73847807b9..86d7d65c51 100644 --- a/cli/lsp/language_server.rs +++ b/cli/lsp/language_server.rs @@ -120,10 +120,10 @@ use crate::tools::upgrade::check_for_upgrades_for_lsp; use crate::tools::upgrade::upgrade_check_enabled; use crate::util::fs::remove_dir_all_if_exists; use crate::util::path::is_importable_ext; -use crate::util::path::specifier_to_file_path; use crate::util::path::to_percent_decoded_str; use crate::util::progress_bar::ProgressBar; use crate::util::progress_bar::ProgressBarStyle; +use deno_runtime::fs_util::specifier_to_file_path; struct LspRootCertStoreProvider(RootCertStore); diff --git a/cli/lsp/tsc.rs b/cli/lsp/tsc.rs index 13da932f7f..d36b598210 100644 --- a/cli/lsp/tsc.rs +++ b/cli/lsp/tsc.rs @@ -30,8 +30,8 @@ use crate::tsc; use crate::tsc::ResolveArgs; use crate::tsc::MISSING_DEPENDENCY_SPECIFIER; use crate::util::path::relative_specifier; -use crate::util::path::specifier_to_file_path; use crate::util::path::to_percent_decoded_str; +use deno_runtime::fs_util::specifier_to_file_path; use dashmap::DashMap; use deno_ast::MediaType; diff --git a/cli/module_loader.rs b/cli/module_loader.rs index 940cfbd8eb..a6c8d13381 100644 --- a/cli/module_loader.rs +++ b/cli/module_loader.rs @@ -4,6 +4,8 @@ use crate::args::jsr_url; use crate::args::CliOptions; use crate::args::DenoSubcommand; use crate::args::TsTypeLib; +use crate::cache::CodeCache; +use crate::cache::ModuleInfoCache; use crate::cache::ParsedSourceCache; use crate::emit::Emitter; use crate::graph_util::graph_lock_or_exit; @@ -50,7 +52,9 @@ use deno_graph::JsonModule; use deno_graph::Module; use deno_graph::Resolution; use deno_lockfile::Lockfile; +use deno_runtime::code_cache; use deno_runtime::deno_node::NodeResolutionMode; +use deno_runtime::fs_util::code_timestamp; use deno_runtime::permissions::PermissionsContainer; use deno_semver::npm::NpmPackageReqReference; use deno_terminal::colors; @@ -311,6 +315,8 @@ struct SharedCliModuleLoaderState { resolver: Arc, node_resolver: Arc, npm_module_loader: NpmModuleLoader, + code_cache: Option>, + module_info_cache: Arc, } pub struct CliModuleLoaderFactory { @@ -328,6 +334,8 @@ impl CliModuleLoaderFactory { resolver: Arc, node_resolver: Arc, npm_module_loader: NpmModuleLoader, + code_cache: Option>, + module_info_cache: Arc, ) -> Self { Self { shared: Arc::new(SharedCliModuleLoaderState { @@ -348,6 +356,8 @@ impl CliModuleLoaderFactory { resolver, node_resolver, npm_module_loader, + code_cache, + module_info_cache, }), } } @@ -458,12 +468,40 @@ impl CliModuleLoader { return Err(generic_error("Attempted to load JSON module without specifying \"type\": \"json\" attribute in the import statement.")); } + let code_cache = if module_type == ModuleType::JavaScript { + self.shared.code_cache.as_ref().and_then(|cache| { + let code_hash = self + .get_code_hash_or_timestamp(specifier, code_source.media_type) + .ok() + .flatten(); + if let Some(code_hash) = code_hash { + cache + .get_sync( + specifier.as_str(), + code_cache::CodeCacheType::EsModule, + &code_hash, + ) + .map(Cow::from) + .inspect(|_| { + // This log line is also used by tests. + log::debug!( + "V8 code cache hit for ES module: {specifier}, [{code_hash:?}]" + ); + }) + } else { + None + } + }) + } else { + None + }; + Ok(ModuleSource::new_with_redirect( module_type, ModuleSourceCode::String(code), specifier, &code_source.found_url, - None, + code_cache, )) } @@ -603,6 +641,25 @@ impl CliModuleLoader { resolution.map_err(|err| err.into()) } + + fn get_code_hash_or_timestamp( + &self, + specifier: &ModuleSpecifier, + media_type: MediaType, + ) -> Result, AnyError> { + let hash = self + .shared + .module_info_cache + .get_module_source_hash(specifier, media_type)?; + if let Some(hash) = hash { + return Ok(Some(hash.into())); + } + + // Use the modified timestamp from the local file system if we don't have a hash. + let timestamp = code_timestamp(specifier.as_str()) + .map(|timestamp| timestamp.to_string())?; + Ok(Some(timestamp)) + } } impl ModuleLoader for CliModuleLoader { @@ -678,6 +735,33 @@ impl ModuleLoader for CliModuleLoader { } .boxed_local() } + + fn code_cache_ready( + &self, + specifier: &ModuleSpecifier, + code_cache: &[u8], + ) -> Pin>> { + if let Some(cache) = self.shared.code_cache.as_ref() { + let media_type = MediaType::from_specifier(specifier); + let code_hash = self + .get_code_hash_or_timestamp(specifier, media_type) + .ok() + .flatten(); + if let Some(code_hash) = code_hash { + // This log line is also used by tests. + log::debug!( + "Updating V8 code cache for ES module: {specifier}, [{code_hash:?}]" + ); + cache.set_sync( + specifier.as_str(), + code_cache::CodeCacheType::EsModule, + &code_hash, + code_cache, + ); + } + } + async {}.boxed_local() + } } struct CliSourceMapGetter { diff --git a/cli/npm/byonm.rs b/cli/npm/byonm.rs index 1e61ce885e..9317455378 100644 --- a/cli/npm/byonm.rs +++ b/cli/npm/byonm.rs @@ -21,7 +21,7 @@ use crate::args::package_json::get_local_package_json_version_reqs; use crate::args::NpmProcessState; use crate::args::NpmProcessStateKind; use crate::util::fs::canonicalize_path_maybe_not_exists_with_fs; -use crate::util::path::specifier_to_file_path; +use deno_runtime::fs_util::specifier_to_file_path; use super::common::types_package_name; use super::CliNpmResolver; diff --git a/cli/resolver.rs b/cli/resolver.rs index d5a85e0012..ea12a6687a 100644 --- a/cli/resolver.rs +++ b/cli/resolver.rs @@ -27,6 +27,7 @@ use deno_runtime::deno_node::NodeResolutionMode; use deno_runtime::deno_node::NodeResolver; use deno_runtime::deno_node::NpmResolver as DenoNodeNpmResolver; use deno_runtime::deno_node::PackageJson; +use deno_runtime::fs_util::specifier_to_file_path; use deno_runtime::permissions::PermissionsContainer; use deno_semver::npm::NpmPackageReqReference; use deno_semver::package::PackageReq; @@ -48,7 +49,6 @@ use crate::node::CliNodeCodeTranslator; use crate::npm::ByonmCliNpmResolver; use crate::npm::CliNpmResolver; use crate::npm::InnerCliNpmResolverRef; -use crate::util::path::specifier_to_file_path; use crate::util::sync::AtomicFlag; pub fn format_range_with_colors(range: &deno_graph::Range) -> String { diff --git a/cli/standalone/mod.rs b/cli/standalone/mod.rs index 043f05e13a..003e2bf793 100644 --- a/cli/standalone/mod.rs +++ b/cli/standalone/mod.rs @@ -572,6 +572,8 @@ pub async fn run( // metadata.disable_deprecated_api_warning, true, false, + // Code cache is not supported for standalone binary yet. + None, ); // Initialize v8 once from the main thread. diff --git a/cli/tools/vendor/mod.rs b/cli/tools/vendor/mod.rs index 5a76365eed..2abdf6e99c 100644 --- a/cli/tools/vendor/mod.rs +++ b/cli/tools/vendor/mod.rs @@ -24,7 +24,7 @@ use crate::tools::fmt::format_json; use crate::util::fs::canonicalize_path; use crate::util::fs::resolve_from_cwd; use crate::util::path::relative_specifier; -use crate::util::path::specifier_to_file_path; +use deno_runtime::fs_util::specifier_to_file_path; mod analyze; mod build; diff --git a/cli/util/path.rs b/cli/util/path.rs index 144676c015..a3109ad04a 100644 --- a/cli/util/path.rs +++ b/cli/util/path.rs @@ -9,8 +9,6 @@ use deno_ast::ModuleSpecifier; use deno_config::glob::PathGlobMatch; use deno_config::glob::PathOrPattern; use deno_config::glob::PathOrPatternSet; -use deno_core::error::uri_error; -use deno_core::error::AnyError; /// Checks if the path has an extension Deno supports for script execution. pub fn is_script_ext(path: &Path) -> bool { @@ -82,49 +80,6 @@ pub fn mapped_specifier_for_tsc( } } -/// Attempts to convert a specifier to a file path. By default, uses the Url -/// crate's `to_file_path()` method, but falls back to try and resolve unix-style -/// paths on Windows. -pub fn specifier_to_file_path( - specifier: &ModuleSpecifier, -) -> Result { - let result = if specifier.scheme() != "file" { - Err(()) - } else if cfg!(windows) { - match specifier.to_file_path() { - Ok(path) => Ok(path), - Err(()) => { - // This might be a unix-style path which is used in the tests even on Windows. - // Attempt to see if we can convert it to a `PathBuf`. This code should be removed - // once/if https://github.com/servo/rust-url/issues/730 is implemented. - if specifier.scheme() == "file" - && specifier.host().is_none() - && specifier.port().is_none() - && specifier.path_segments().is_some() - { - let path_str = specifier.path(); - match String::from_utf8( - percent_encoding::percent_decode(path_str.as_bytes()).collect(), - ) { - Ok(path_str) => Ok(PathBuf::from(path_str)), - Err(_) => Err(()), - } - } else { - Err(()) - } - } - } - } else { - specifier.to_file_path() - }; - match result { - Ok(path) => Ok(path), - Err(()) => Err(uri_error(format!( - "Invalid file path.\n Specifier: {specifier}" - ))), - } -} - /// `from.make_relative(to)` but with fixes. pub fn relative_specifier( from: &ModuleSpecifier, @@ -330,24 +285,6 @@ mod test { assert!(!is_importable_ext(Path::new("foo.mjsx"))); } - #[test] - fn test_specifier_to_file_path() { - run_success_test("file:///", "/"); - run_success_test("file:///test", "/test"); - run_success_test("file:///dir/test/test.txt", "/dir/test/test.txt"); - run_success_test( - "file:///dir/test%20test/test.txt", - "/dir/test test/test.txt", - ); - - fn run_success_test(specifier: &str, expected_path: &str) { - let result = - specifier_to_file_path(&ModuleSpecifier::parse(specifier).unwrap()) - .unwrap(); - assert_eq!(result, PathBuf::from(expected_path)); - } - } - #[test] fn test_relative_specifier() { let fixtures: Vec<(&str, &str, Option<&str>)> = vec![ diff --git a/cli/worker.rs b/cli/worker.rs index 070671e60b..7dbb9b177a 100644 --- a/cli/worker.rs +++ b/cli/worker.rs @@ -22,6 +22,7 @@ use deno_core::PollEventLoopOptions; use deno_core::SharedArrayBufferStore; use deno_core::SourceMapGetter; use deno_lockfile::Lockfile; +use deno_runtime::code_cache; use deno_runtime::deno_broadcast_channel::InMemoryBroadcastChannel; use deno_runtime::deno_fs; use deno_runtime::deno_node; @@ -140,6 +141,7 @@ struct SharedWorkerState { enable_future_features: bool, disable_deprecated_api_warning: bool, verbose_deprecated_api_warning: bool, + code_cache: Option>, } impl SharedWorkerState { @@ -411,6 +413,7 @@ impl CliMainWorkerFactory { enable_future_features: bool, disable_deprecated_api_warning: bool, verbose_deprecated_api_warning: bool, + code_cache: Option>, ) -> Self { Self { shared: Arc::new(SharedWorkerState { @@ -434,6 +437,7 @@ impl CliMainWorkerFactory { enable_future_features, disable_deprecated_api_warning, verbose_deprecated_api_warning, + code_cache, }), } } @@ -628,6 +632,7 @@ impl CliMainWorkerFactory { stdio, feature_checker, skip_op_registration: shared.options.skip_op_registration, + v8_code_cache: shared.code_cache.clone(), }; let mut worker = MainWorker::bootstrap_from_options( diff --git a/runtime/Cargo.toml b/runtime/Cargo.toml index 97a6d13976..8028a570de 100644 --- a/runtime/Cargo.toml +++ b/runtime/Cargo.toml @@ -112,6 +112,7 @@ log.workspace = true netif = "0.1.6" notify.workspace = true once_cell.workspace = true +percent-encoding.workspace = true regex.workspace = true ring.workspace = true rustyline = { workspace = true, features = ["custom-bindings"] } diff --git a/runtime/code_cache.rs b/runtime/code_cache.rs new file mode 100644 index 0000000000..ccc070365b --- /dev/null +++ b/runtime/code_cache.rs @@ -0,0 +1,31 @@ +// Copyright 2018-2024 the Deno authors. All rights reserved. MIT license. + +pub enum CodeCacheType { + EsModule, + Script, +} + +impl CodeCacheType { + pub fn as_str(&self) -> &str { + match self { + Self::EsModule => "esmodule", + Self::Script => "script", + } + } +} + +pub trait CodeCache: Send + Sync { + fn get_sync( + &self, + specifier: &str, + code_cache_type: CodeCacheType, + source_hash: &str, + ) -> Option>; + fn set_sync( + &self, + specifier: &str, + code_cache_type: CodeCacheType, + source_hash: &str, + data: &[u8], + ); +} diff --git a/runtime/fs_util.rs b/runtime/fs_util.rs index f7c006a919..09b1073003 100644 --- a/runtime/fs_util.rs +++ b/runtime/fs_util.rs @@ -1,6 +1,8 @@ // Copyright 2018-2024 the Deno authors. All rights reserved. MIT license. +use deno_ast::ModuleSpecifier; use deno_core::anyhow::Context; +use deno_core::error::uri_error; use deno_core::error::AnyError; pub use deno_core::normalize_path; use std::path::Path; @@ -18,6 +20,60 @@ pub fn resolve_from_cwd(path: &Path) -> Result { } } +/// Attempts to convert a specifier to a file path. By default, uses the Url +/// crate's `to_file_path()` method, but falls back to try and resolve unix-style +/// paths on Windows. +pub fn specifier_to_file_path( + specifier: &ModuleSpecifier, +) -> Result { + let result = if specifier.scheme() != "file" { + Err(()) + } else if cfg!(windows) { + match specifier.to_file_path() { + Ok(path) => Ok(path), + Err(()) => { + // This might be a unix-style path which is used in the tests even on Windows. + // Attempt to see if we can convert it to a `PathBuf`. This code should be removed + // once/if https://github.com/servo/rust-url/issues/730 is implemented. + if specifier.scheme() == "file" + && specifier.host().is_none() + && specifier.port().is_none() + && specifier.path_segments().is_some() + { + let path_str = specifier.path(); + match String::from_utf8( + percent_encoding::percent_decode(path_str.as_bytes()).collect(), + ) { + Ok(path_str) => Ok(PathBuf::from(path_str)), + Err(_) => Err(()), + } + } else { + Err(()) + } + } + } + } else { + specifier.to_file_path() + }; + match result { + Ok(path) => Ok(path), + Err(()) => Err(uri_error(format!( + "Invalid file path.\n Specifier: {specifier}" + ))), + } +} + +pub fn code_timestamp(specifier: &str) -> Result { + let specifier = ModuleSpecifier::parse(specifier)?; + let path = specifier_to_file_path(&specifier)?; + #[allow(clippy::disallowed_methods)] + let timestamp = std::fs::metadata(path)? + .modified()? + .duration_since(std::time::UNIX_EPOCH)? + .as_millis() as u64; + Ok(timestamp) +} + #[cfg(test)] mod tests { use super::*; @@ -69,4 +125,22 @@ mod tests { let absolute_expected = cwd.join(expected); assert_eq!(resolve_from_cwd(expected).unwrap(), absolute_expected); } + + #[test] + fn test_specifier_to_file_path() { + run_success_test("file:///", "/"); + run_success_test("file:///test", "/test"); + run_success_test("file:///dir/test/test.txt", "/dir/test/test.txt"); + run_success_test( + "file:///dir/test%20test/test.txt", + "/dir/test test/test.txt", + ); + + fn run_success_test(specifier: &str, expected_path: &str) { + let result = + specifier_to_file_path(&ModuleSpecifier::parse(specifier).unwrap()) + .unwrap(); + assert_eq!(result, PathBuf::from(expected_path)); + } + } } diff --git a/runtime/lib.rs b/runtime/lib.rs index 72fa1cef85..f33e9b7e33 100644 --- a/runtime/lib.rs +++ b/runtime/lib.rs @@ -26,6 +26,7 @@ pub use deno_webidl; pub use deno_websocket; pub use deno_webstorage; +pub mod code_cache; pub mod errors; pub mod fmt_errors; pub mod fs_util; diff --git a/runtime/worker.rs b/runtime/worker.rs index 956fa2d329..ee6b256ff6 100644 --- a/runtime/worker.rs +++ b/runtime/worker.rs @@ -1,4 +1,5 @@ // Copyright 2018-2024 the Deno authors. All rights reserved. MIT license. +use std::borrow::Cow; use std::collections::HashMap; use std::rc::Rc; use std::sync::atomic::AtomicBool; @@ -41,6 +42,9 @@ use deno_tls::RootCertStoreProvider; use deno_web::BlobStore; use log::debug; +use crate::code_cache::CodeCache; +use crate::code_cache::CodeCacheType; +use crate::fs_util::code_timestamp; use crate::inspector_server::InspectorServer; use crate::ops; use crate::permissions::PermissionsContainer; @@ -193,6 +197,9 @@ pub struct WorkerOptions { pub compiled_wasm_module_store: Option, pub stdio: Stdio, pub feature_checker: Arc, + + /// V8 code cache for module and script source code. + pub v8_code_cache: Option>, } impl Default for WorkerOptions { @@ -227,6 +234,7 @@ impl Default for WorkerOptions { bootstrap: Default::default(), stdio: Default::default(), feature_checker: Default::default(), + v8_code_cache: Default::default(), } } } @@ -296,6 +304,51 @@ pub fn create_op_metrics( (op_summary_metrics, op_metrics_factory_fn) } +fn get_code_cache( + code_cache: Arc, + specifier: &str, +) -> Option> { + // Code hashes are not maintained for op_eval_context scripts. Instead we use + // the modified timestamp from the local file system. + if let Ok(code_timestamp) = code_timestamp(specifier) { + code_cache + .get_sync( + specifier, + CodeCacheType::Script, + code_timestamp.to_string().as_str(), + ) + .inspect(|_| { + // This log line is also used by tests. + log::debug!( + "V8 code cache hit for script: {specifier}, [{code_timestamp}]" + ); + }) + } else { + None + } +} + +fn set_code_cache( + code_cache: Arc, + specifier: &str, + data: &[u8], +) { + // Code hashes are not maintained for op_eval_context scripts. Instead we use + // the modified timestamp from the local file system. + if let Ok(code_timestamp) = code_timestamp(specifier) { + // This log line is also used by tests. + log::debug!( + "Updating V8 code cache for script: {specifier}, [{code_timestamp}]", + ); + code_cache.set_sync( + specifier, + CodeCacheType::Script, + code_timestamp.to_string().as_str(), + data, + ); + } +} + impl MainWorker { pub fn bootstrap_from_options( main_module: ModuleSpecifier, @@ -495,6 +548,18 @@ impl MainWorker { validate_import_attributes_cb: Some(Box::new( validate_import_attributes_callback, )), + enable_code_cache: options.v8_code_cache.is_some(), + eval_context_code_cache_cbs: options.v8_code_cache.map(|cache| { + let cache_clone = cache.clone(); + ( + Box::new(move |specifier: &str| { + Ok(get_code_cache(cache.clone(), specifier).map(Cow::Owned)) + }) as Box _>, + Box::new(move |specifier: &str, data: &[u8]| { + set_code_cache(cache_clone.clone(), specifier, data); + }) as Box, + ) + }), ..Default::default() }); diff --git a/tests/integration/run_tests.rs b/tests/integration/run_tests.rs index 31596c1bba..e477a98d66 100644 --- a/tests/integration/run_tests.rs +++ b/tests/integration/run_tests.rs @@ -5177,3 +5177,205 @@ fn run_etag_delete_source_cache() { "[WILDCARD]Cache body not found. Trying again without etag.[WILDCARD]", ); } + +#[test] +fn code_cache_test() { + let deno_dir = TempDir::new(); + let test_context = TestContextBuilder::new().use_temp_cwd().build(); + let temp_dir = test_context.temp_dir(); + temp_dir.write("main.js", "console.log('Hello World - A');"); + + // First run with no prior cache. + { + let output = test_context + .new_command() + .env("DENO_DIR", deno_dir.path()) + .arg("run") + .arg("-Ldebug") + .arg("main.js") + .split_output() + .run(); + + output + .assert_stdout_matches_text("Hello World - A[WILDCARD]") + .assert_stderr_matches_text("[WILDCARD]Updating V8 code cache for ES module: file:///[WILDCARD]/main.js[WILDCARD]"); + assert!(!output.stderr().contains("V8 code cache hit")); + + // Check that the code cache database exists. + let code_cache_path = deno_dir.path().join("v8_code_cache_v1"); + assert!(code_cache_path.exists()); + } + + // 2nd run with cache. + { + let output = test_context + .new_command() + .env("DENO_DIR", deno_dir.path()) + .arg("run") + .arg("-Ldebug") + .arg("main.js") + .split_output() + .run(); + + output + .assert_stdout_matches_text("Hello World - A[WILDCARD]") + .assert_stderr_matches_text("[WILDCARD]V8 code cache hit for ES module: file:///[WILDCARD]/main.js[WILDCARD]"); + assert!(!output.stderr().contains("Updating V8 code cache")); + } + + // Rerun with --no-code-cache. + { + let output = test_context + .new_command() + .env("DENO_DIR", deno_dir.path()) + .arg("run") + .arg("-Ldebug") + .arg("--no-code-cache") + .arg("main.js") + .split_output() + .run(); + + output + .assert_stdout_matches_text("Hello World - A[WILDCARD]") + .skip_stderr_check(); + assert!(!output.stderr().contains("V8 code cache")); + } + + // Modify the script, and make sure that the cache is rejected. + temp_dir.write("main.js", "console.log('Hello World - B');"); + { + let output = test_context + .new_command() + .env("DENO_DIR", deno_dir.path()) + .arg("run") + .arg("-Ldebug") + .arg("main.js") + .split_output() + .run(); + + output + .assert_stdout_matches_text("Hello World - B[WILDCARD]") + .assert_stderr_matches_text("[WILDCARD]Updating V8 code cache for ES module: file:///[WILDCARD]/main.js[WILDCARD]"); + assert!(!output.stderr().contains("V8 code cache hit")); + } +} + +#[test] +fn code_cache_npm_test() { + let deno_dir = TempDir::new(); + let test_context = TestContextBuilder::new() + .use_temp_cwd() + .use_http_server() + .build(); + let temp_dir = test_context.temp_dir(); + temp_dir.write( + "main.js", + "import chalk from \"npm:chalk@5\";console.log(chalk('Hello World'));", + ); + + // First run with no prior cache. + { + let output = test_context + .new_command() + .env("DENO_DIR", deno_dir.path()) + .envs(env_vars_for_npm_tests()) + .arg("run") + .arg("-Ldebug") + .arg("-A") + .arg("main.js") + .split_output() + .run(); + + output + .assert_stdout_matches_text("Hello World[WILDCARD]") + .assert_stderr_matches_text("[WILDCARD]Updating V8 code cache for ES module: file:///[WILDCARD]/main.js[WILDCARD]") + .assert_stderr_matches_text("[WILDCARD]Updating V8 code cache for ES module: file:///[WILDCARD]/npm/registry/chalk/5.[WILDCARD]/source/index.js[WILDCARD]"); + assert!(!output.stderr().contains("V8 code cache hit")); + + // Check that the code cache database exists. + let code_cache_path = deno_dir.path().join("v8_code_cache_v1"); + assert!(code_cache_path.exists()); + } + + // 2nd run with cache. + { + let output = test_context + .new_command() + .env("DENO_DIR", deno_dir.path()) + .envs(env_vars_for_npm_tests()) + .arg("run") + .arg("-Ldebug") + .arg("-A") + .arg("main.js") + .split_output() + .run(); + + output + .assert_stdout_matches_text("Hello World[WILDCARD]") + .assert_stderr_matches_text("[WILDCARD]V8 code cache hit for ES module: file:///[WILDCARD]/main.js[WILDCARD]") + .assert_stderr_matches_text("[WILDCARD]V8 code cache hit for ES module: file:///[WILDCARD]/npm/registry/chalk/5.[WILDCARD]/source/index.js[WILDCARD]"); + assert!(!output.stderr().contains("Updating V8 code cache")); + } +} + +#[test] +fn code_cache_npm_with_require_test() { + let deno_dir = TempDir::new(); + let test_context = TestContextBuilder::new() + .use_temp_cwd() + .use_http_server() + .build(); + let temp_dir = test_context.temp_dir(); + temp_dir.write( + "main.js", + "import fraction from \"npm:autoprefixer\";console.log(typeof fraction);", + ); + + // First run with no prior cache. + { + let output = test_context + .new_command() + .env("DENO_DIR", deno_dir.path()) + .envs(env_vars_for_npm_tests()) + .arg("run") + .arg("-Ldebug") + .arg("-A") + .arg("main.js") + .split_output() + .run(); + + output + .assert_stdout_matches_text("function[WILDCARD]") + .assert_stderr_matches_text("[WILDCARD]Updating V8 code cache for ES module: file:///[WILDCARD]/main.js[WILDCARD]") + .assert_stderr_matches_text("[WILDCARD]Updating V8 code cache for ES module: file:///[WILDCARD]/npm/registry/autoprefixer/[WILDCARD]/autoprefixer.js[WILDCARD]") + .assert_stderr_matches_text("[WILDCARD]Updating V8 code cache for script: file:///[WILDCARD]/npm/registry/autoprefixer/[WILDCARD]/autoprefixer.js[WILDCARD]") + .assert_stderr_matches_text("[WILDCARD]Updating V8 code cache for script: file:///[WILDCARD]/npm/registry/browserslist/[WILDCARD]/index.js[WILDCARD]"); + assert!(!output.stderr().contains("V8 code cache hit")); + + // Check that the code cache database exists. + let code_cache_path = deno_dir.path().join("v8_code_cache_v1"); + assert!(code_cache_path.exists()); + } + + // 2nd run with cache. + { + let output = test_context + .new_command() + .env("DENO_DIR", deno_dir.path()) + .envs(env_vars_for_npm_tests()) + .arg("run") + .arg("-Ldebug") + .arg("-A") + .arg("main.js") + .split_output() + .run(); + + output + .assert_stdout_matches_text("function[WILDCARD]") + .assert_stderr_matches_text("[WILDCARD]V8 code cache hit for ES module: file:///[WILDCARD]/main.js[WILDCARD]") + .assert_stderr_matches_text("[WILDCARD]V8 code cache hit for ES module: file:///[WILDCARD]/npm/registry/autoprefixer/[WILDCARD]/autoprefixer.js[WILDCARD]") + .assert_stderr_matches_text("[WILDCARD]V8 code cache hit for script: file:///[WILDCARD]/npm/registry/autoprefixer/[WILDCARD]/autoprefixer.js[WILDCARD]") + .assert_stderr_matches_text("[WILDCARD]V8 code cache hit for script: file:///[WILDCARD]/npm/registry/browserslist/[WILDCARD]/index.js[WILDCARD]"); + assert!(!output.stderr().contains("Updating V8 code cache")); + } +} diff --git a/tests/testdata/run/rejection_handled.ts b/tests/testdata/run/rejection_handled.ts index f058ff9665..c29ae7089d 100644 --- a/tests/testdata/run/rejection_handled.ts +++ b/tests/testdata/run/rejection_handled.ts @@ -14,4 +14,4 @@ setTimeout(async () => { setTimeout(() => { console.log("Success"); -}, 50); +}, 200);