diff options
-rw-r--r-- | cli/cache/cache_db.rs | 486 | ||||
-rw-r--r-- | cli/cache/caches.rs | 75 | ||||
-rw-r--r-- | cli/cache/check.rs | 176 | ||||
-rw-r--r-- | cli/cache/common.rs | 12 | ||||
-rw-r--r-- | cli/cache/incremental.rs | 166 | ||||
-rw-r--r-- | cli/cache/mod.rs | 3 | ||||
-rw-r--r-- | cli/cache/node.rs | 312 | ||||
-rw-r--r-- | cli/cache/parsed_source.rs | 223 | ||||
-rw-r--r-- | cli/graph_util.rs | 2 | ||||
-rw-r--r-- | cli/node/analyze.rs | 4 | ||||
-rw-r--r-- | cli/proc_state.rs | 26 | ||||
-rw-r--r-- | cli/tests/integration/inspector_tests.rs | 6 | ||||
-rw-r--r-- | cli/tests/integration/watcher_tests.rs | 7 | ||||
-rw-r--r-- | cli/tools/fmt.rs | 6 | ||||
-rw-r--r-- | cli/tools/lint.rs | 4 | ||||
-rw-r--r-- | cli/tools/vendor/test.rs | 2 | ||||
-rw-r--r-- | test_util/src/lib.rs | 111 |
17 files changed, 999 insertions, 622 deletions
diff --git a/cli/cache/cache_db.rs b/cli/cache/cache_db.rs new file mode 100644 index 000000000..0f772091f --- /dev/null +++ b/cli/cache/cache_db.rs @@ -0,0 +1,486 @@ +// Copyright 2018-2023 the Deno authors. All rights reserved. MIT license. + +use deno_core::error::AnyError; +use deno_core::parking_lot::Mutex; +use deno_core::parking_lot::MutexGuard; +use deno_runtime::deno_webstorage::rusqlite; +use deno_runtime::deno_webstorage::rusqlite::Connection; +use deno_runtime::deno_webstorage::rusqlite::OptionalExtension; +use deno_runtime::deno_webstorage::rusqlite::Params; +use once_cell::sync::OnceCell; +use std::path::PathBuf; +use std::sync::Arc; + +/// What should the cache should do on failure? +#[derive(Default)] +pub enum CacheFailure { + /// Return errors if failure mode otherwise unspecified. + #[default] + Error, + /// Create an in-memory cache that is not persistent. + InMemory, + /// Create a blackhole cache that ignores writes and returns empty reads. + Blackhole, +} + +/// Configuration SQL and other parameters for a [`CacheDB`]. +pub struct CacheDBConfiguration { + /// SQL to run for a new database. + pub table_initializer: &'static str, + /// SQL to run when the version from [`crate::version::deno()`] changes. + pub on_version_change: &'static str, + /// Prepared statements to pre-heat while initializing the database. + pub preheat_queries: &'static [&'static str], + /// What the cache should do on failure. + pub on_failure: CacheFailure, +} + +impl CacheDBConfiguration { + fn create_combined_sql(&self) -> String { + format!( + " + PRAGMA journal_mode=OFF; + PRAGMA synchronous=NORMAL; + PRAGMA temp_store=memory; + PRAGMA page_size=4096; + PRAGMA mmap_size=6000000; + PRAGMA optimize; + + CREATE TABLE IF NOT EXISTS info ( + key TEXT PRIMARY KEY, + value TEXT NOT NULL + ); + + {} + ", + self.table_initializer + ) + } +} + +enum ConnectionState { + Connected(Connection), + Blackhole, + Error(Arc<AnyError>), +} + +/// A cache database that eagerly initializes itself off-thread, preventing initialization operations +/// from blocking the main thread. +#[derive(Clone)] +pub struct CacheDB { + // TODO(mmastrac): We can probably simplify our thread-safe implementation here + conn: Arc<Mutex<OnceCell<ConnectionState>>>, + path: Option<PathBuf>, + config: &'static CacheDBConfiguration, + version: &'static str, +} + +impl Drop for CacheDB { + fn drop(&mut self) { + // No need to clean up an in-memory cache in an way -- just drop and go. + let path = match self.path.take() { + Some(path) => path, + _ => return, + }; + + // TODO(mmastrac): we should ensure tokio runtimes are consistently available or not + if tokio::runtime::Handle::try_current().is_err() { + return; + } + + // For on-disk caches, see if we're the last holder of the Arc. + let arc = std::mem::take(&mut self.conn); + if let Ok(inner) = Arc::try_unwrap(arc) { + // Hand off SQLite connection to another thread to do the surprisingly expensive cleanup + let inner = inner.into_inner().into_inner(); + if let Some(conn) = inner { + tokio::task::spawn_blocking(move || { + drop(conn); + log::trace!( + "Cleaned up SQLite connection at {}", + path.to_string_lossy() + ); + }); + } + } + } +} + +impl CacheDB { + #[cfg(test)] + pub fn in_memory( + config: &'static CacheDBConfiguration, + version: &'static str, + ) -> Self { + CacheDB { + conn: Arc::new(Mutex::new(OnceCell::new())), + path: None, + config, + version, + } + } + + pub fn from_path( + config: &'static CacheDBConfiguration, + path: PathBuf, + version: &'static str, + ) -> Self { + log::debug!("Opening cache {}...", path.to_string_lossy()); + let new = Self { + conn: Arc::new(Mutex::new(OnceCell::new())), + path: Some(path), + config, + version, + }; + + new.spawn_eager_init_thread(); + new + } + + /// Useful for testing: re-create this cache DB with a different current version. + #[cfg(test)] + pub(crate) fn recreate_with_version(mut self, version: &'static str) -> Self { + // By taking the lock, we know there are no initialization threads alive + drop(self.conn.lock()); + + let arc = std::mem::take(&mut self.conn); + let conn = match Arc::try_unwrap(arc) { + Err(_) => panic!("Failed to unwrap connection"), + Ok(conn) => match conn.into_inner().into_inner() { + Some(ConnectionState::Connected(conn)) => conn, + _ => panic!("Connection had failed and cannot be unwrapped"), + }, + }; + + Self::initialize_connection(self.config, &conn, version).unwrap(); + + let cell = OnceCell::new(); + _ = cell.set(ConnectionState::Connected(conn)); + Self { + conn: Arc::new(Mutex::new(cell)), + path: self.path.clone(), + config: self.config, + version, + } + } + + fn spawn_eager_init_thread(&self) { + let clone = self.clone(); + // TODO(mmastrac): we should ensure tokio runtimes are consistently available or not + if tokio::runtime::Handle::try_current().is_ok() { + tokio::task::spawn_blocking(move || { + let lock = clone.conn.lock(); + clone.initialize(&lock); + }); + } + } + + /// Open the connection in memory or on disk. + fn actually_open_connection( + &self, + path: &Option<PathBuf>, + ) -> Result<Connection, rusqlite::Error> { + match path { + // This should never fail unless something is very wrong + None => Connection::open_in_memory(), + Some(path) => Connection::open(path), + } + } + + /// Attempt to initialize that connection. + fn initialize_connection( + config: &CacheDBConfiguration, + conn: &Connection, + version: &str, + ) -> Result<(), AnyError> { + let sql = config.create_combined_sql(); + conn.execute_batch(&sql)?; + + // Check the version + let existing_version = conn + .query_row( + "SELECT value FROM info WHERE key='CLI_VERSION' LIMIT 1", + [], + |row| row.get::<_, String>(0), + ) + .optional()? + .unwrap_or_default(); + + // If Deno has been upgraded, run the SQL to update the version + if existing_version != version { + conn.execute_batch(config.on_version_change)?; + let mut stmt = conn + .prepare("INSERT OR REPLACE INTO info (key, value) VALUES (?1, ?2)")?; + stmt.execute(["CLI_VERSION", version])?; + } + + // Preheat any prepared queries + for preheat in config.preheat_queries { + drop(conn.prepare_cached(preheat)?); + } + Ok(()) + } + + /// Open and initialize a connection. + fn open_connection_and_init( + &self, + path: &Option<PathBuf>, + ) -> Result<Connection, AnyError> { + let conn = self.actually_open_connection(path)?; + Self::initialize_connection(self.config, &conn, self.version)?; + Ok(conn) + } + + /// This function represents the policy for dealing with corrupted cache files. We try fairly aggressively + /// to repair the situation, and if we can't, we prefer to log noisily and continue with in-memory caches. + fn open_connection(&self) -> Result<ConnectionState, AnyError> { + // Success on first try? We hope that this is the case. + let err = match self.open_connection_and_init(&self.path) { + Ok(conn) => return Ok(ConnectionState::Connected(conn)), + Err(err) => err, + }; + + if self.path.is_none() { + // If an in-memory DB fails, that's game over + log::error!("Failed to initialize in-memory cache database."); + return Err(err); + } + + let path = self.path.as_ref().unwrap(); + + // There are rare times in the tests when we can't initialize a cache DB the first time, but it succeeds the second time, so + // we don't log these at a debug level. + log::trace!( + "Could not initialize cache database '{}', retrying... ({err:?})", + path.to_string_lossy(), + ); + + // Try a second time + let err = match self.open_connection_and_init(&self.path) { + Ok(conn) => return Ok(ConnectionState::Connected(conn)), + Err(err) => err, + }; + + // Failed, try deleting it + log::warn!( + "Could not initialize cache database '{}', deleting and retrying... ({err:?})", + path.to_string_lossy() + ); + if std::fs::remove_file(path).is_ok() { + // Try a third time if we successfully deleted it + let res = self.open_connection_and_init(&self.path); + if let Ok(conn) = res { + return Ok(ConnectionState::Connected(conn)); + }; + } + + match self.config.on_failure { + CacheFailure::InMemory => { + log::error!( + "Failed to open cache file '{}', opening in-memory cache.", + path.to_string_lossy() + ); + Ok(ConnectionState::Connected( + self.open_connection_and_init(&None)?, + )) + } + CacheFailure::Blackhole => { + log::error!( + "Failed to open cache file '{}', performance may be degraded.", + path.to_string_lossy() + ); + Ok(ConnectionState::Blackhole) + } + CacheFailure::Error => { + log::error!( + "Failed to open cache file '{}', expect further errors.", + path.to_string_lossy() + ); + Err(err) + } + } + } + + fn initialize<'a>( + &self, + lock: &'a MutexGuard<OnceCell<ConnectionState>>, + ) -> &'a ConnectionState { + lock.get_or_init(|| match self.open_connection() { + Ok(conn) => conn, + Err(e) => ConnectionState::Error(e.into()), + }) + } + + pub fn with_connection<T: Default>( + &self, + f: impl FnOnce(&Connection) -> Result<T, AnyError>, + ) -> Result<T, AnyError> { + let lock = self.conn.lock(); + let conn = self.initialize(&lock); + + match conn { + ConnectionState::Blackhole => { + // Cache is a blackhole - nothing in or out. + Ok(T::default()) + } + ConnectionState::Error(e) => { + // This isn't ideal because we lose the original underlying error + let err = AnyError::msg(e.clone().to_string()); + Err(err) + } + ConnectionState::Connected(conn) => f(conn), + } + } + + #[cfg(test)] + pub fn ensure_connected(&self) -> Result<(), AnyError> { + self.with_connection(|_| Ok(())) + } + + pub fn execute( + &self, + sql: &'static str, + params: impl Params, + ) -> Result<usize, AnyError> { + self.with_connection(|conn| { + let mut stmt = conn.prepare_cached(sql)?; + let res = stmt.execute(params)?; + Ok(res) + }) + } + + pub fn exists( + &self, + sql: &'static str, + params: impl Params, + ) -> Result<bool, AnyError> { + self.with_connection(|conn| { + let mut stmt = conn.prepare_cached(sql)?; + let res = stmt.exists(params)?; + Ok(res) + }) + } + + /// Query a row from the database with a mapping function. + pub fn query_row<T, F>( + &self, + sql: &'static str, + params: impl Params, + f: F, + ) -> Result<Option<T>, AnyError> + where + F: FnOnce(&rusqlite::Row<'_>) -> Result<T, AnyError>, + { + let res = self.with_connection(|conn| { + let mut stmt = conn.prepare_cached(sql)?; + let mut rows = stmt.query(params)?; + if let Some(row) = rows.next()? { + let res = f(row)?; + Ok(Some(res)) + } else { + Ok(None) + } + })?; + Ok(res) + } +} + +#[cfg(test)] +mod tests { + use super::*; + + static TEST_DB: CacheDBConfiguration = CacheDBConfiguration { + table_initializer: "create table if not exists test(value TEXT);", + on_version_change: "delete from test;", + preheat_queries: &[], + on_failure: CacheFailure::InMemory, + }; + + static TEST_DB_BLACKHOLE: CacheDBConfiguration = CacheDBConfiguration { + table_initializer: "create table if not exists test(value TEXT);", + on_version_change: "delete from test;", + preheat_queries: &[], + on_failure: CacheFailure::Blackhole, + }; + + static TEST_DB_ERROR: CacheDBConfiguration = CacheDBConfiguration { + table_initializer: "create table if not exists test(value TEXT);", + on_version_change: "delete from test;", + preheat_queries: &[], + on_failure: CacheFailure::Error, + }; + + static BAD_SQL_TEST_DB: CacheDBConfiguration = CacheDBConfiguration { + table_initializer: "bad sql;", + on_version_change: "delete from test;", + preheat_queries: &[], + on_failure: CacheFailure::InMemory, + }; + + static FAILURE_PATH: &str = "/tmp/this/doesnt/exist/so/will/always/fail"; + + #[tokio::test] + async fn simple_database() { + let db = CacheDB::in_memory(&TEST_DB, "1.0"); + db.ensure_connected() + .expect("Failed to initialize in-memory database"); + + db.execute("insert into test values (?1)", [1]).unwrap(); + let res = db + .query_row("select * from test", [], |row| { + Ok(row.get::<_, String>(0).unwrap()) + }) + .unwrap(); + assert_eq!(Some("1".into()), res); + } + + #[tokio::test] + async fn bad_sql() { + let db = CacheDB::in_memory(&BAD_SQL_TEST_DB, "1.0"); + db.ensure_connected() + .expect_err("Expected to fail, but succeeded"); + } + + #[tokio::test] + async fn failure_mode_in_memory() { + let db = CacheDB::from_path(&TEST_DB, FAILURE_PATH.into(), "1.0"); + db.ensure_connected() + .expect("Should have created a database"); + + db.execute("insert into test values (?1)", [1]).unwrap(); + let res = db + .query_row("select * from test", [], |row| { + Ok(row.get::<_, String>(0).unwrap()) + }) + .unwrap(); + assert_eq!(Some("1".into()), res); + } + + #[tokio::test] + async fn failure_mode_blackhole() { + let db = CacheDB::from_path(&TEST_DB_BLACKHOLE, FAILURE_PATH.into(), "1.0"); + db.ensure_connected() + .expect("Should have created a database"); + + db.execute("insert into test values (?1)", [1]).unwrap(); + let res = db + .query_row("select * from test", [], |row| { + Ok(row.get::<_, String>(0).unwrap()) + }) + .unwrap(); + assert_eq!(None, res); + } + + #[tokio::test] + async fn failure_mode_error() { + let db = CacheDB::from_path(&TEST_DB_ERROR, FAILURE_PATH.into(), "1.0"); + db.ensure_connected().expect_err("Should have failed"); + + db.execute("insert into test values (?1)", [1]) + .expect_err("Should have failed"); + db.query_row("select * from test", [], |row| { + Ok(row.get::<_, String>(0).unwrap()) + }) + .expect_err("Should have failed"); + } +} diff --git a/cli/cache/caches.rs b/cli/cache/caches.rs new file mode 100644 index 000000000..da6912f2a --- /dev/null +++ b/cli/cache/caches.rs @@ -0,0 +1,75 @@ +// Copyright 2018-2023 the Deno authors. All rights reserved. MIT license. + +use std::path::PathBuf; +use std::sync::Arc; + +use once_cell::sync::OnceCell; + +use super::cache_db::CacheDB; +use super::cache_db::CacheDBConfiguration; +use super::check::TYPE_CHECK_CACHE_DB; +use super::incremental::INCREMENTAL_CACHE_DB; +use super::node::NODE_ANALYSIS_CACHE_DB; +use super::parsed_source::PARSED_SOURCE_CACHE_DB; +use super::DenoDir; + +#[derive(Clone, Default)] +pub struct Caches { + fmt_incremental_cache_db: Arc<OnceCell<CacheDB>>, + lint_incremental_cache_db: Arc<OnceCell<CacheDB>>, + dep_analysis_db: Arc<OnceCell<CacheDB>>, + node_analysis_db: Arc<OnceCell<CacheDB>>, + type_checking_cache_db: Arc<OnceCell<CacheDB>>, +} + +impl Caches { + fn make_db( + cell: &Arc<OnceCell<CacheDB>>, + config: &'static CacheDBConfiguration, + path: PathBuf, + ) -> CacheDB { + cell + .get_or_init(|| CacheDB::from_path(config, path, crate::version::deno())) + .clone() + } + + pub fn fmt_incremental_cache_db(&self, dir: &DenoDir) -> CacheDB { + Self::make_db( + &self.fmt_incremental_cache_db, + &INCREMENTAL_CACHE_DB, + dir.fmt_incremental_cache_db_file_path(), + ) + } + + pub fn lint_incremental_cache_db(&self, dir: &DenoDir) -> CacheDB { + Self::make_db( + &self.lint_incremental_cache_db, + &INCREMENTAL_CACHE_DB, + dir.lint_incremental_cache_db_file_path(), + ) + } + + pub fn dep_analysis_db(&self, dir: &DenoDir) -> CacheDB { + Self::make_db( + &self.dep_analysis_db, + &PARSED_SOURCE_CACHE_DB, + dir.dep_analysis_db_file_path(), + ) + } + + pub fn node_analysis_db(&self, dir: &DenoDir) -> CacheDB { + Self::make_db( + &self.node_analysis_db, + &NODE_ANALYSIS_CACHE_DB, + dir.node_analysis_db_file_path(), + ) + } + + pub fn type_checking_cache_db(&self, dir: &DenoDir) -> CacheDB { + Self::make_db( + &self.type_checking_cache_db, + &TYPE_CHECK_CACHE_DB, + dir.type_checking_cache_db_file_path(), + ) + } +} diff --git a/cli/cache/check.rs b/cli/cache/check.rs index 1bd410013..bf71380cb 100644 --- a/cli/cache/check.rs +++ b/cli/cache/check.rs @@ -1,68 +1,40 @@ // Copyright 2018-2023 the Deno authors. All rights reserved. MIT license. -use std::path::Path; - +use super::cache_db::CacheDB; +use super::cache_db::CacheDBConfiguration; +use super::cache_db::CacheFailure; use deno_ast::ModuleSpecifier; use deno_core::error::AnyError; use deno_runtime::deno_webstorage::rusqlite::params; -use deno_runtime::deno_webstorage::rusqlite::Connection; -use super::common::INITIAL_PRAGMAS; +pub static TYPE_CHECK_CACHE_DB: CacheDBConfiguration = CacheDBConfiguration { + table_initializer: concat!( + "CREATE TABLE IF NOT EXISTS checkcache ( + check_hash TEXT PRIMARY KEY + );", + "CREATE TABLE IF NOT EXISTS tsbuildinfo ( + specifier TEXT PRIMARY KEY, + text TEXT NOT NULL + );", + ), + on_version_change: concat!( + "DELETE FROM checkcache;", + "DELETE FROM tsbuildinfo;" + ), + preheat_queries: &[], + // If the cache fails, just ignore all caching attempts + on_failure: CacheFailure::Blackhole, +}; /// The cache used to tell whether type checking should occur again. /// /// This simply stores a hash of the inputs of each successful type check /// and only clears them out when changing CLI versions. -pub struct TypeCheckCache(Option<Connection>); +pub struct TypeCheckCache(CacheDB); impl TypeCheckCache { - pub fn new(db_file_path: &Path) -> Self { - log::debug!("Loading type check cache."); - match Self::try_new(db_file_path) { - Ok(cache) => cache, - Err(err) => { - log::debug!( - concat!( - "Failed loading internal type checking cache. ", - "Recreating...\n\nError details:\n{:#}", - ), - err - ); - // Maybe the cache file is corrupt. Attempt to remove the cache file - // then attempt to recreate again. Otherwise, use null object pattern. - match std::fs::remove_file(db_file_path) { - Ok(_) => match Self::try_new(db_file_path) { - Ok(cache) => cache, - Err(err) => { - log::debug!( - concat!( - "Unable to load internal cache for type checking. ", - "This will reduce the performance of type checking.\n\n", - "Error details:\n{:#}", - ), - err - ); - Self(None) - } - }, - Err(_) => Self(None), - } - } - } - } - - fn try_new(db_file_path: &Path) -> Result<Self, AnyError> { - let conn = Connection::open(db_file_path)?; - Self::from_connection(conn, crate::version::deno()) - } - - fn from_connection( - conn: Connection, - cli_version: &'static str, - ) -> Result<Self, AnyError> { - initialize(&conn, cli_version)?; - - Ok(Self(Some(conn))) + pub fn new(db: CacheDB) -> Self { + Self(db) } pub fn has_check_hash(&self, hash: u64) -> bool { @@ -81,13 +53,10 @@ impl TypeCheckCache { } fn hash_check_hash_result(&self, hash: u64) -> Result<bool, AnyError> { - let conn = match &self.0 { - Some(conn) => conn, - None => return Ok(false), - }; - let query = "SELECT * FROM checkcache WHERE check_hash=?1 LIMIT 1"; - let mut stmt = conn.prepare_cached(query)?; - Ok(stmt.exists(params![hash.to_string()])?) + self.0.exists( + "SELECT * FROM checkcache WHERE check_hash=?1 LIMIT 1", + params![hash.to_string()], + ) } pub fn add_check_hash(&self, check_hash: u64) { @@ -101,32 +70,24 @@ impl TypeCheckCache { } fn add_check_hash_result(&self, check_hash: u64) -> Result<(), AnyError> { - let conn = match &self.0 { - Some(conn) => conn, - None => return Ok(()), - }; let sql = " INSERT OR REPLACE INTO checkcache (check_hash) VALUES (?1)"; - let mut stmt = conn.prepare_cached(sql)?; - stmt.execute(params![&check_hash.to_string(),])?; + self.0.execute(sql, params![&check_hash.to_string(),])?; Ok(()) } pub fn get_tsbuildinfo(&self, specifier: &ModuleSpecifier) -> Option<String> { - let conn = match &self.0 { - Some(conn) => conn, - None => return None, - }; - let mut stmt = conn - .prepare_cached("SELECT text FROM tsbuildinfo WHERE specifier=?1 LIMIT 1") - .ok()?; - let mut rows = stmt.query(params![specifier.to_string()]).ok()?; - let row = rows.next().ok().flatten()?; - - row.get(0).ok() + self + .0 + .query_row( + "SELECT text FROM tsbuildinfo WHERE specifier=?1 LIMIT 1", + params![specifier.to_string()], + |row| Ok(row.get::<_, String>(0)?), + ) + .ok()? } pub fn set_tsbuildinfo(&self, specifier: &ModuleSpecifier, text: &str) { @@ -145,67 +106,22 @@ impl TypeCheckCache { specifier: &ModuleSpecifier, text: &str, ) -> Result<(), AnyError> { - let conn = match &self.0 { - Some(conn) => conn, - None => return Ok(()), - }; - let mut stmt = conn.prepare_cached( + self.0.execute( "INSERT OR REPLACE INTO tsbuildinfo (specifier, text) VALUES (?1, ?2)", + params![specifier.to_string(), text], )?; - stmt.execute(params![specifier.to_string(), text])?; Ok(()) } } -fn initialize( - conn: &Connection, - cli_version: &'static str, -) -> Result<(), AnyError> { - // INT doesn't store up to u64, so use TEXT for check_hash - let query = format!( - "{INITIAL_PRAGMAS} - CREATE TABLE IF NOT EXISTS checkcache ( - check_hash TEXT PRIMARY KEY - ); - CREATE TABLE IF NOT EXISTS tsbuildinfo ( - specifier TEXT PRIMARY KEY, - text TEXT NOT NULL - ); - CREATE TABLE IF NOT EXISTS info ( - key TEXT PRIMARY KEY, - value TEXT NOT NULL - ); - ", - ); - conn.execute_batch(&query)?; - - // delete the cache when the CLI version changes - let data_cli_version: Option<String> = conn - .query_row( - "SELECT value FROM info WHERE key='CLI_VERSION' LIMIT 1", - [], - |row| row.get(0), - ) - .ok(); - if data_cli_version.as_deref() != Some(cli_version) { - conn.execute("DELETE FROM checkcache", params![])?; - conn.execute("DELETE FROM tsbuildinfo", params![])?; - let mut stmt = conn - .prepare("INSERT OR REPLACE INTO info (key, value) VALUES (?1, ?2)")?; - stmt.execute(params!["CLI_VERSION", cli_version])?; - } - - Ok(()) -} - #[cfg(test)] mod test { use super::*; #[test] pub fn check_cache_general_use() { - let conn = Connection::open_in_memory().unwrap(); - let cache = TypeCheckCache::from_connection(conn, "1.0.0").unwrap(); + let conn = CacheDB::in_memory(&TYPE_CHECK_CACHE_DB, "1.0.0"); + let cache = TypeCheckCache::new(conn); assert!(!cache.has_check_hash(1)); cache.add_check_hash(1); @@ -218,8 +134,9 @@ mod test { assert_eq!(cache.get_tsbuildinfo(&specifier1), Some("test".to_string())); // try changing the cli version (should clear) - let conn = cache.0.unwrap(); - let cache = TypeCheckCache::from_connection(conn, "2.0.0").unwrap(); + let conn = cache.0.recreate_with_version("2.0.0"); + let cache = TypeCheckCache::new(conn); + assert!(!cache.has_check_hash(1)); cache.add_check_hash(1); assert!(cache.has_check_hash(1)); @@ -228,8 +145,9 @@ mod test { assert_eq!(cache.get_tsbuildinfo(&specifier1), Some("test".to_string())); // recreating the cache should not remove the data because the CLI version is the same - let conn = cache.0.unwrap(); - let cache = TypeCheckCache::from_connection(conn, "2.0.0").unwrap(); + let conn = cache.0.recreate_with_version("2.0.0"); + let cache = TypeCheckCache::new(conn); + assert!(cache.has_check_hash(1)); assert!(!cache.has_check_hash(2)); assert_eq!(cache.get_tsbuildinfo(&specifier1), Some("test".to_string())); diff --git a/cli/cache/common.rs b/cli/cache/common.rs index 1e6c5aa92..93ff91d50 100644 --- a/cli/cache/common.rs +++ b/cli/cache/common.rs @@ -43,15 +43,3 @@ impl FastInsecureHasher { self.0.finish() } } - -/// Disable write-ahead-logging and tweak some other stuff. -/// We want to favor startup time over cache performance and -/// creating a WAL is expensive on startup. -pub static INITIAL_PRAGMAS: &str = " - PRAGMA journal_mode=OFF; - PRAGMA synchronous=NORMAL; - PRAGMA temp_store=memory; - PRAGMA page_size=4096; - PRAGMA mmap_size=6000000; - PRAGMA optimize; -"; diff --git a/cli/cache/incremental.rs b/cli/cache/incremental.rs index d5298071f..deb30cdd1 100644 --- a/cli/cache/incremental.rs +++ b/cli/cache/incremental.rs @@ -8,57 +8,49 @@ use deno_core::error::AnyError; use deno_core::parking_lot::Mutex; use deno_core::serde_json; use deno_runtime::deno_webstorage::rusqlite::params; -use deno_runtime::deno_webstorage::rusqlite::Connection; use serde::Serialize; use tokio::task::JoinHandle; +use super::cache_db::CacheDB; +use super::cache_db::CacheDBConfiguration; +use super::cache_db::CacheFailure; use super::common::FastInsecureHasher; -use super::common::INITIAL_PRAGMAS; + +pub static INCREMENTAL_CACHE_DB: CacheDBConfiguration = CacheDBConfiguration { + table_initializer: "CREATE TABLE IF NOT EXISTS incrementalcache ( + file_path TEXT PRIMARY KEY, + state_hash TEXT NOT NULL, + source_hash TEXT NOT NULL + );", + on_version_change: "DELETE FROM incrementalcache;", + preheat_queries: &[], + // If the cache fails, just ignore all caching attempts + on_failure: CacheFailure::Blackhole, +}; /// Cache used to skip formatting/linting a file again when we /// know it is already formatted or has no lint diagnostics. -pub struct IncrementalCache(Option<IncrementalCacheInner>); +pub struct IncrementalCache(IncrementalCacheInner); impl IncrementalCache { pub fn new<TState: Serialize>( - db_file_path: &Path, + db: CacheDB, state: &TState, initial_file_paths: &[PathBuf], ) -> Self { - // if creating the incremental cache fails, then we - // treat it as not having a cache - let result = - IncrementalCacheInner::new(db_file_path, state, initial_file_paths); - IncrementalCache(match result { - Ok(inner) => Some(inner), - Err(err) => { - log::debug!("Creating the incremental cache failed.\n{:#}", err); - // Maybe the cache file is corrupt. Attempt to remove - // the cache file for next time - let _ = std::fs::remove_file(db_file_path); - None - } - }) + IncrementalCache(IncrementalCacheInner::new(db, state, initial_file_paths)) } pub fn is_file_same(&self, file_path: &Path, file_text: &str) -> bool { - if let Some(inner) = &self.0 { - inner.is_file_same(file_path, file_text) - } else { - false - } + self.0.is_file_same(file_path, file_text) } pub fn update_file(&self, file_path: &Path, file_text: &str) { - if let Some(inner) = &self.0 { - inner.update_file(file_path, file_text) - } + self.0.update_file(file_path, file_text) } pub async fn wait_completion(&self) { - if let Some(inner) = &self.0 { - inner.wait_completion().await; - } + self.0.wait_completion().await; } } @@ -75,18 +67,15 @@ struct IncrementalCacheInner { impl IncrementalCacheInner { pub fn new<TState: Serialize>( - db_file_path: &Path, + db: CacheDB, state: &TState, initial_file_paths: &[PathBuf], - ) -> Result<Self, AnyError> { + ) -> Self { let state_hash = FastInsecureHasher::new() .write_str(&serde_json::to_string(state).unwrap()) .finish(); - let sql_cache = SqlIncrementalCache::new(db_file_path, state_hash)?; - Ok(Self::from_sql_incremental_cache( - sql_cache, - initial_file_paths, - )) + let sql_cache = SqlIncrementalCache::new(db, state_hash); + Self::from_sql_incremental_cache(sql_cache, initial_file_paths) } fn from_sql_incremental_cache( @@ -155,7 +144,7 @@ impl IncrementalCacheInner { } struct SqlIncrementalCache { - conn: Connection, + conn: CacheDB, /// A hash of the state used to produce the formatting/linting other than /// the CLI version. This state is a hash of the configuration and ensures /// we format/lint a file when the configuration changes. @@ -163,20 +152,8 @@ struct SqlIncrementalCache { } impl SqlIncrementalCache { - pub fn new(db_file_path: &Path, state_hash: u64) -> Result<Self, AnyError> { - log::debug!("Loading incremental cache."); - let conn = Connection::open(db_file_path)?; - Self::from_connection(conn, state_hash, crate::version::deno()) - } - - fn from_connection( - conn: Connection, - state_hash: u64, - cli_version: &'static str, - ) -> Result<Self, AnyError> { - initialize(&conn, cli_version)?; - - Ok(Self { conn, state_hash }) + pub fn new(conn: CacheDB, state_hash: u64) -> Self { + Self { conn, state_hash } } pub fn get_source_hash(&self, path: &Path) -> Option<u64> { @@ -206,15 +183,15 @@ impl SqlIncrementalCache { file_path=?1 AND state_hash=?2 LIMIT 1"; - let mut stmt = self.conn.prepare_cached(query)?; - let mut rows = stmt - .query(params![path.to_string_lossy(), self.state_hash.to_string()])?; - if let Some(row) = rows.next()? { - let hash: String = row.get(0)?; - Ok(Some(hash.parse::<u64>()?)) - } else { - Ok(None) - } + let res = self.conn.query_row( + query, + params![path.to_string_lossy(), self.state_hash.to_string()], + |row| { + let hash: String = row.get(0)?; + Ok(hash.parse::<u64>()?) + }, + )?; + Ok(res) } pub fn set_source_hash( @@ -227,53 +204,18 @@ impl SqlIncrementalCache { incrementalcache (file_path, state_hash, source_hash) VALUES (?1, ?2, ?3)"; - let mut stmt = self.conn.prepare_cached(sql)?; - stmt.execute(params![ - path.to_string_lossy(), - &self.state_hash.to_string(), - &source_hash, - ])?; + self.conn.execute( + sql, + params![ + path.to_string_lossy(), + &self.state_hash.to_string(), + &source_hash, + ], + )?; Ok(()) } } -fn initialize( - conn: &Connection, - cli_version: &'static str, -) -> Result<(), AnyError> { - // INT doesn't store up to u64, so use TEXT for source_hash - let query = format!( - "{INITIAL_PRAGMAS} - CREATE TABLE IF NOT EXISTS incrementalcache ( - file_path TEXT PRIMARY KEY, - state_hash TEXT NOT NULL, - source_hash TEXT NOT NULL - ); - CREATE TABLE IF NOT EXISTS info ( - key TEXT PRIMARY KEY, - value TEXT NOT NULL - );" - ); - conn.execute_batch(&query)?; - - // delete the cache when the CLI version changes - let data_cli_version: Option<String> = conn - .query_row( - "SELECT value FROM info WHERE key='CLI_VERSION' LIMIT 1", - [], - |row| row.get(0), - ) - .ok(); - if data_cli_version.as_deref() != Some(cli_version) { - conn.execute("DELETE FROM incrementalcache", params![])?; - let mut stmt = conn - .prepare("INSERT OR REPLACE INTO info (key, value) VALUES (?1, ?2)")?; - stmt.execute(params!["CLI_VERSION", cli_version])?; - } - - Ok(()) -} - #[cfg(test)] mod test { use std::path::PathBuf; @@ -282,8 +224,8 @@ mod test { #[test] pub fn sql_cache_general_use() { - let conn = Connection::open_in_memory().unwrap(); - let cache = SqlIncrementalCache::from_connection(conn, 1, "1.0.0").unwrap(); + let conn = CacheDB::in_memory(&INCREMENTAL_CACHE_DB, "1.0.0"); + let cache = SqlIncrementalCache::new(conn, 1); let path = PathBuf::from("/mod.ts"); assert_eq!(cache.get_source_hash(&path), None); @@ -291,9 +233,8 @@ mod test { assert_eq!(cache.get_source_hash(&path), Some(2)); // try changing the cli version (should clear) - let conn = cache.conn; - let mut cache = - SqlIncrementalCache::from_connection(conn, 1, "2.0.0").unwrap(); + let conn = cache.conn.recreate_with_version("2.0.0"); + let mut cache = SqlIncrementalCache::new(conn, 1); assert_eq!(cache.get_source_hash(&path), None); // add back the file to the cache @@ -309,8 +250,8 @@ mod test { assert_eq!(cache.get_source_hash(&path), Some(2)); // recreating the cache should not remove the data because the CLI version and state hash is the same - let conn = cache.conn; - let cache = SqlIncrementalCache::from_connection(conn, 1, "2.0.0").unwrap(); + let conn = cache.conn.recreate_with_version("2.0.0"); + let cache = SqlIncrementalCache::new(conn, 1); assert_eq!(cache.get_source_hash(&path), Some(2)); // now try replacing and using another path @@ -324,9 +265,8 @@ mod test { #[tokio::test] pub async fn incremental_cache_general_use() { - let conn = Connection::open_in_memory().unwrap(); - let sql_cache = - SqlIncrementalCache::from_connection(conn, 1, "1.0.0").unwrap(); + let conn = CacheDB::in_memory(&INCREMENTAL_CACHE_DB, "1.0.0"); + let sql_cache = SqlIncrementalCache::new(conn, 1); let file_path = PathBuf::from("/mod.ts"); let file_text = "test"; let file_hash = FastInsecureHasher::new().write_str(file_text).finish(); diff --git a/cli/cache/mod.rs b/cli/cache/mod.rs index 1aea67058..24712d08a 100644 --- a/cli/cache/mod.rs +++ b/cli/cache/mod.rs @@ -14,6 +14,8 @@ use deno_runtime::permissions::PermissionsContainer; use std::collections::HashMap; use std::sync::Arc; +mod cache_db; +mod caches; mod check; mod common; mod deno_dir; @@ -24,6 +26,7 @@ mod incremental; mod node; mod parsed_source; +pub use caches::Caches; pub use check::TypeCheckCache; pub use common::FastInsecureHasher; pub use deno_dir::DenoDir; diff --git a/cli/cache/node.rs b/cli/cache/node.rs index 19ac45d6b..f42f132fd 100644 --- a/cli/cache/node.rs +++ b/cli/cache/node.rs @@ -1,40 +1,58 @@ // Copyright 2018-2023 the Deno authors. All rights reserved. MIT license. -use std::path::Path; - use deno_ast::CjsAnalysis; use deno_core::error::AnyError; -use deno_core::parking_lot::Mutex; use deno_core::serde_json; use deno_runtime::deno_webstorage::rusqlite::params; -use deno_runtime::deno_webstorage::rusqlite::Connection; -use serde::Deserialize; -use serde::Serialize; -use std::path::PathBuf; -use std::sync::Arc; -use super::common::INITIAL_PRAGMAS; +use super::cache_db::CacheDB; +use super::cache_db::CacheDBConfiguration; +use super::cache_db::CacheFailure; use super::FastInsecureHasher; -// todo(dsherret): use deno_ast::CjsAnalysisData directly when upgrading deno_ast -// See https://github.com/denoland/deno_ast/pull/117 -#[derive(Serialize, Deserialize)] -struct CjsAnalysisData { - pub exports: Vec<String>, - pub reexports: Vec<String>, -} +pub static NODE_ANALYSIS_CACHE_DB: CacheDBConfiguration = + CacheDBConfiguration { + table_initializer: concat!( + "CREATE TABLE IF NOT EXISTS cjsanalysiscache ( + specifier TEXT PRIMARY KEY, + source_hash TEXT NOT NULL, + data TEXT NOT NULL + );", + "CREATE UNIQUE INDEX IF NOT EXISTS cjsanalysiscacheidx + ON cjsanalysiscache(specifier);", + "CREATE TABLE IF NOT EXISTS esmglobalscache ( + specifier TEXT PRIMARY KEY, + source_hash TEXT NOT NULL, + data TEXT NOT NULL + );", + "CREATE UNIQUE INDEX IF NOT EXISTS esmglobalscacheidx + ON esmglobalscache(specifier);", + ), + on_version_change: concat!( + "DELETE FROM cjsanalysiscache;", + "DELETE FROM esmglobalscache;", + ), + preheat_queries: &[], + on_failure: CacheFailure::InMemory, + }; #[derive(Clone)] pub struct NodeAnalysisCache { - db_file_path: Option<PathBuf>, - inner: Arc<Mutex<Option<Option<NodeAnalysisCacheInner>>>>, + inner: NodeAnalysisCacheInner, } impl NodeAnalysisCache { - pub fn new(db_file_path: Option<PathBuf>) -> Self { + #[cfg(test)] + pub fn new_in_memory() -> Self { + Self::new(CacheDB::in_memory( + &NODE_ANALYSIS_CACHE_DB, + crate::version::deno(), + )) + } + + pub fn new(db: CacheDB) -> Self { Self { - db_file_path, - inner: Default::default(), + inner: NodeAnalysisCacheInner::new(db), } } @@ -45,16 +63,31 @@ impl NodeAnalysisCache { .to_string() } + fn ensure_ok<T: Default>(res: Result<T, AnyError>) -> 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 esm analysis: {err:#}"); + } else { + log::debug!("Error using esm analysis: {:#}", err); + } + T::default() + } + } + } + pub fn get_cjs_analysis( &self, specifier: &str, expected_source_hash: &str, ) -> Option<CjsAnalysis> { - self - .with_inner(|inner| { - inner.get_cjs_analysis(specifier, expected_source_hash) - }) - .flatten() + Self::ensure_ok( + self.inner.get_cjs_analysis(specifier, expected_source_hash), + ) } pub fn set_cjs_analysis( @@ -63,9 +96,11 @@ impl NodeAnalysisCache { source_hash: &str, cjs_analysis: &CjsAnalysis, ) { - self.with_inner(|inner| { - inner.set_cjs_analysis(specifier, source_hash, cjs_analysis) - }); + Self::ensure_ok(self.inner.set_cjs_analysis( + specifier, + source_hash, + cjs_analysis, + )); } pub fn get_esm_analysis( @@ -73,11 +108,9 @@ impl NodeAnalysisCache { specifier: &str, expected_source_hash: &str, ) -> Option<Vec<String>> { - self - .with_inner(|inner| { - inner.get_esm_analysis(specifier, expected_source_hash) - }) - .flatten() + Self::ensure_ok( + self.inner.get_esm_analysis(specifier, expected_source_hash), + ) } pub fn set_esm_analysis( @@ -86,78 +119,22 @@ impl NodeAnalysisCache { source_hash: &str, top_level_decls: &Vec<String>, ) { - self.with_inner(|inner| { - inner.set_esm_analysis(specifier, source_hash, top_level_decls) - }); - } - - fn with_inner<TResult>( - &self, - action: impl FnOnce(&NodeAnalysisCacheInner) -> Result<TResult, AnyError>, - ) -> Option<TResult> { - // lazily create the cache in order to not - let mut maybe_created = self.inner.lock(); - let inner = match maybe_created.as_ref() { - Some(maybe_inner) => maybe_inner.as_ref(), - None => { - let maybe_inner = match NodeAnalysisCacheInner::new( - self.db_file_path.as_deref(), - crate::version::deno().to_string(), - ) { - Ok(cache) => Some(cache), - Err(err) => { - // should never error here, but if it ever does don't fail - if cfg!(debug_assertions) { - panic!("Error creating node analysis cache: {err:#}"); - } else { - log::debug!("Error creating node analysis cache: {:#}", err); - None - } - } - }; - *maybe_created = Some(maybe_inner); - maybe_created.as_ref().and_then(|p| p.as_ref()) - } - }?; - match action(inner) { - Ok(result) => Some(result), - Err(err) => { - // should never error here, but if it ever does don't fail - if cfg!(debug_assertions) { - panic!("Error using esm analysis: {err:#}"); - } else { - log::debug!("Error using esm analysis: {:#}", err); - } - None - } - } + Self::ensure_ok(self.inner.set_esm_analysis( + specifier, + source_hash, + top_level_decls, + )) } } +#[derive(Clone)] struct NodeAnalysisCacheInner { - conn: Connection, + conn: CacheDB, } impl NodeAnalysisCacheInner { - pub fn new( - db_file_path: Option<&Path>, - version: String, - ) -> Result<Self, AnyError> { - log::debug!("Opening node analysis cache."); - let conn = match db_file_path { - Some(path) => Connection::open(path)?, - None => Connection::open_in_memory()?, - }; - Self::from_connection(conn, version) - } - - fn from_connection( - conn: Connection, - version: String, - ) -> Result<Self, AnyError> { - initialize(&conn, &version)?; - - Ok(Self { conn }) + pub fn new(conn: CacheDB) -> Self { + Self { conn } } pub fn get_cjs_analysis( @@ -174,19 +151,15 @@ impl NodeAnalysisCacheInner { specifier=?1 AND source_hash=?2 LIMIT 1"; - let mut stmt = self.conn.prepare_cached(query)?; - let mut rows = stmt.query(params![specifier, &expected_source_hash])?; - if let Some(row) = rows.next()? { - let analysis_info: String = row.get(0)?; - let analysis_info: CjsAnalysisData = - serde_json::from_str(&analysis_info)?; - Ok(Some(CjsAnalysis { - exports: analysis_info.exports, - reexports: analysis_info.reexports, - })) - } else { - Ok(None) - } + let res = self.conn.query_row( + query, + params![specifier, &expected_source_hash], + |row| { + let analysis_info: String = row.get(0)?; + Ok(serde_json::from_str(&analysis_info)?) + }, + )?; + Ok(res) } pub fn set_cjs_analysis( @@ -200,16 +173,14 @@ impl NodeAnalysisCacheInner { cjsanalysiscache (specifier, source_hash, data) VALUES (?1, ?2, ?3)"; - let mut stmt = self.conn.prepare_cached(sql)?; - stmt.execute(params![ - specifier, - &source_hash.to_string(), - &serde_json::to_string(&CjsAnalysisData { - // temporary clones until upgrading deno_ast - exports: cjs_analysis.exports.clone(), - reexports: cjs_analysis.reexports.clone(), - })?, - ])?; + self.conn.execute( + sql, + params![ + specifier, + &source_hash.to_string(), + &serde_json::to_string(&cjs_analysis)?, + ], + )?; Ok(()) } @@ -227,15 +198,16 @@ impl NodeAnalysisCacheInner { specifier=?1 AND source_hash=?2 LIMIT 1"; - let mut stmt = self.conn.prepare_cached(query)?; - let mut rows = stmt.query(params![specifier, &expected_source_hash])?; - if let Some(row) = rows.next()? { - let top_level_decls: String = row.get(0)?; - let decls: Vec<String> = serde_json::from_str(&top_level_decls)?; - Ok(Some(decls)) - } else { - Ok(None) - } + let res = self.conn.query_row( + query, + params![specifier, &expected_source_hash], + |row| { + let top_level_decls: String = row.get(0)?; + let decls: Vec<String> = serde_json::from_str(&top_level_decls)?; + Ok(decls) + }, + )?; + Ok(res) } pub fn set_esm_analysis( @@ -249,72 +221,26 @@ impl NodeAnalysisCacheInner { esmglobalscache (specifier, source_hash, data) VALUES (?1, ?2, ?3)"; - let mut stmt = self.conn.prepare_cached(sql)?; - stmt.execute(params![ - specifier, - &source_hash, - &serde_json::to_string(top_level_decls)?, - ])?; + self.conn.execute( + sql, + params![ + specifier, + &source_hash, + &serde_json::to_string(top_level_decls)?, + ], + )?; Ok(()) } } -fn initialize(conn: &Connection, cli_version: &str) -> Result<(), AnyError> { - // INT doesn't store up to u64, so use TEXT for source_hash - let query = format!( - "{INITIAL_PRAGMAS} - CREATE TABLE IF NOT EXISTS cjsanalysiscache ( - specifier TEXT PRIMARY KEY, - source_hash TEXT NOT NULL, - data TEXT NOT NULL - ); - CREATE UNIQUE INDEX IF NOT EXISTS cjsanalysiscacheidx - ON cjsanalysiscache(specifier); - CREATE TABLE IF NOT EXISTS esmglobalscache ( - specifier TEXT PRIMARY KEY, - source_hash TEXT NOT NULL, - data TEXT NOT NULL - ); - CREATE UNIQUE INDEX IF NOT EXISTS esmglobalscacheidx - ON esmglobalscache(specifier); - CREATE TABLE IF NOT EXISTS info ( - key TEXT PRIMARY KEY, - value TEXT NOT NULL - ); - " - ); - - conn.execute_batch(&query)?; - - // delete the cache when the CLI version changes - let data_cli_version: Option<String> = conn - .query_row( - "SELECT value FROM info WHERE key='CLI_VERSION' LIMIT 1", - [], - |row| row.get(0), - ) - .ok(); - if data_cli_version.as_deref() != Some(cli_version) { - conn.execute("DELETE FROM cjsanalysiscache", params![])?; - conn.execute("DELETE FROM esmglobalscache", params![])?; - let mut stmt = conn - .prepare("INSERT OR REPLACE INTO info (key, value) VALUES (?1, ?2)")?; - stmt.execute(params!["CLI_VERSION", &cli_version])?; - } - - Ok(()) -} - #[cfg(test)] mod test { use super::*; #[test] pub fn node_analysis_cache_general_use() { - let conn = Connection::open_in_memory().unwrap(); - let cache = - NodeAnalysisCacheInner::from_connection(conn, "1.0.0".to_string()) - .unwrap(); + let conn = CacheDB::in_memory(&NODE_ANALYSIS_CACHE_DB, "1.0.0"); + let cache = NodeAnalysisCacheInner::new(conn); assert!(cache.get_cjs_analysis("file.js", "2").unwrap().is_none()); let cjs_analysis = CjsAnalysis { @@ -349,10 +275,8 @@ mod test { .unwrap(); // recreating with same cli version should still have it - let conn = cache.conn; - let cache = - NodeAnalysisCacheInner::from_connection(conn, "1.0.0".to_string()) - .unwrap(); + let conn = cache.conn.recreate_with_version("1.0.0"); + let cache = NodeAnalysisCacheInner::new(conn); let actual_analysis = cache.get_cjs_analysis("file.js", "2").unwrap().unwrap(); assert_eq!(actual_analysis.exports, cjs_analysis.exports); @@ -362,10 +286,8 @@ mod test { assert_eq!(actual_esm_analysis, esm_analysis); // now changing the cli version should clear it - let conn = cache.conn; - let cache = - NodeAnalysisCacheInner::from_connection(conn, "2.0.0".to_string()) - .unwrap(); + let conn = cache.conn.recreate_with_version("2.0.0"); + let cache = NodeAnalysisCacheInner::new(conn); assert!(cache.get_cjs_analysis("file.js", "2").unwrap().is_none()); assert!(cache.get_esm_analysis("file.js", "2").unwrap().is_none()); } diff --git a/cli/cache/parsed_source.rs b/cli/cache/parsed_source.rs index 2f27f0533..09b1c13ae 100644 --- a/cli/cache/parsed_source.rs +++ b/cli/cache/parsed_source.rs @@ -1,8 +1,6 @@ // Copyright 2018-2023 the Deno authors. All rights reserved. MIT license. use std::collections::HashMap; -use std::path::Path; -use std::path::PathBuf; use std::sync::Arc; use deno_ast::MediaType; @@ -15,13 +13,37 @@ use deno_graph::CapturingModuleParser; use deno_graph::DefaultModuleAnalyzer; use deno_graph::ModuleInfo; use deno_graph::ModuleParser; -use deno_graph::ParsedSourceStore; use deno_runtime::deno_webstorage::rusqlite::params; -use deno_runtime::deno_webstorage::rusqlite::Connection; -use super::common::INITIAL_PRAGMAS; +use super::cache_db::CacheDB; +use super::cache_db::CacheDBConfiguration; +use super::cache_db::CacheFailure; use super::FastInsecureHasher; +const SELECT_MODULE_INFO: &str = " +SELECT + module_info +FROM + moduleinfocache +WHERE + specifier=?1 + AND media_type=?2 + AND source_hash=?3 +LIMIT 1"; + +pub static PARSED_SOURCE_CACHE_DB: CacheDBConfiguration = + CacheDBConfiguration { + table_initializer: "CREATE TABLE IF NOT EXISTS moduleinfocache ( + specifier TEXT PRIMARY KEY, + media_type TEXT NOT NULL, + source_hash TEXT NOT NULL, + module_info TEXT NOT NULL + );", + on_version_change: "DELETE FROM moduleinfocache;", + preheat_queries: &[SELECT_MODULE_INFO], + on_failure: CacheFailure::InMemory, + }; + #[derive(Clone, Default)] struct ParsedSourceCacheSources( Arc<Mutex<HashMap<ModuleSpecifier, ParsedSource>>>, @@ -53,24 +75,29 @@ impl deno_graph::ParsedSourceStore for ParsedSourceCacheSources { /// for cached dependency analysis. #[derive(Clone)] pub struct ParsedSourceCache { - db_cache_path: Option<PathBuf>, - cli_version: &'static str, + db: CacheDB, sources: ParsedSourceCacheSources, } impl ParsedSourceCache { - pub fn new(sql_cache_path: Option<PathBuf>) -> Self { + #[cfg(test)] + pub fn new_in_memory() -> Self { + Self { + db: CacheDB::in_memory(&PARSED_SOURCE_CACHE_DB, crate::version::deno()), + sources: Default::default(), + } + } + + pub fn new(db: CacheDB) -> Self { Self { - db_cache_path: sql_cache_path, - cli_version: crate::version::deno(), + db, sources: Default::default(), } } pub fn reset_for_file_watcher(&self) -> Self { Self { - db_cache_path: self.db_cache_path.clone(), - cli_version: self.cli_version, + db: self.db.clone(), sources: Default::default(), } } @@ -104,31 +131,11 @@ impl ParsedSourceCache { self.sources.0.lock().remove(specifier); } - /// Gets this cache as a `deno_graph::ParsedSourceStore`. - pub fn as_store(&self) -> Box<dyn ParsedSourceStore> { - // This trait is not implemented directly on ParsedSourceCache - // in order to prevent its methods from being accidentally used. - // Generally, people should prefer the methods found that will - // lazily parse if necessary. - Box::new(self.sources.clone()) - } - pub fn as_analyzer(&self) -> Box<dyn deno_graph::ModuleAnalyzer> { - match ParsedSourceCacheModuleAnalyzer::new( - self.db_cache_path.as_deref(), - self.cli_version, + Box::new(ParsedSourceCacheModuleAnalyzer::new( + self.db.clone(), self.sources.clone(), - ) { - Ok(analyzer) => Box::new(analyzer), - Err(err) => { - log::debug!("Could not create cached module analyzer. {:#}", err); - // fallback to not caching if it can't be created - Box::new(deno_graph::CapturingModuleAnalyzer::new( - None, - Some(self.as_store()), - )) - } - } + )) } /// Creates a parser that will reuse a ParsedSource from the store @@ -139,32 +146,13 @@ impl ParsedSourceCache { } struct ParsedSourceCacheModuleAnalyzer { - conn: Connection, + conn: CacheDB, sources: ParsedSourceCacheSources, } impl ParsedSourceCacheModuleAnalyzer { - pub fn new( - db_file_path: Option<&Path>, - cli_version: &'static str, - sources: ParsedSourceCacheSources, - ) -> Result<Self, AnyError> { - log::debug!("Loading cached module analyzer."); - let conn = match db_file_path { - Some(path) => Connection::open(path)?, - None => Connection::open_in_memory()?, - }; - Self::from_connection(conn, cli_version, sources) - } - - fn from_connection( - conn: Connection, - cli_version: &'static str, - sources: ParsedSourceCacheSources, - ) -> Result<Self, AnyError> { - initialize(&conn, cli_version)?; - - Ok(Self { conn, sources }) + pub fn new(conn: CacheDB, sources: ParsedSourceCacheSources) -> Self { + Self { conn, sources } } pub fn get_module_info( @@ -173,29 +161,21 @@ impl ParsedSourceCacheModuleAnalyzer { media_type: MediaType, expected_source_hash: &str, ) -> Result<Option<ModuleInfo>, AnyError> { - let query = " - SELECT - module_info - FROM - moduleinfocache - WHERE - specifier=?1 - AND media_type=?2 - AND source_hash=?3 - LIMIT 1"; - let mut stmt = self.conn.prepare_cached(query)?; - let mut rows = stmt.query(params![ - &specifier.as_str(), - serialize_media_type(media_type), - &expected_source_hash, - ])?; - if let Some(row) = rows.next()? { - let module_info: String = row.get(0)?; - let module_info = serde_json::from_str(&module_info)?; - Ok(Some(module_info)) - } else { - Ok(None) - } + let query = SELECT_MODULE_INFO; + let res = self.conn.query_row( + query, + params![ + &specifier.as_str(), + serialize_media_type(media_type), + &expected_source_hash, + ], + |row| { + let module_info: String = row.get(0)?; + let module_info = serde_json::from_str(&module_info)?; + Ok(module_info) + }, + )?; + Ok(res) } pub fn set_module_info( @@ -210,13 +190,15 @@ impl ParsedSourceCacheModuleAnalyzer { moduleinfocache (specifier, media_type, source_hash, module_info) VALUES (?1, ?2, ?3, ?4)"; - let mut stmt = self.conn.prepare_cached(sql)?; - stmt.execute(params![ - specifier.as_str(), - serialize_media_type(media_type), - &source_hash, - &serde_json::to_string(&module_info)?, - ])?; + self.conn.execute( + sql, + params![ + specifier.as_str(), + serialize_media_type(media_type), + &source_hash, + &serde_json::to_string(&module_info)?, + ], + )?; Ok(()) } } @@ -287,46 +269,6 @@ impl deno_graph::ModuleAnalyzer for ParsedSourceCacheModuleAnalyzer { } } -fn initialize( - conn: &Connection, - cli_version: &'static str, -) -> Result<(), AnyError> { - let query = format!( - "{INITIAL_PRAGMAS} - -- INT doesn't store up to u64, so use TEXT for source_hash - CREATE TABLE IF NOT EXISTS moduleinfocache ( - specifier TEXT PRIMARY KEY, - media_type TEXT NOT NULL, - source_hash TEXT NOT NULL, - module_info TEXT NOT NULL - ); - CREATE TABLE IF NOT EXISTS info ( - key TEXT PRIMARY KEY, - value TEXT NOT NULL - ); - " - ); - - conn.execute_batch(&query)?; - - // delete the cache when the CLI version changes - let data_cli_version: Option<String> = conn - .query_row( - "SELECT value FROM info WHERE key='CLI_VERSION' LIMIT 1", - [], - |row| row.get(0), - ) - .ok(); - if data_cli_version.as_deref() != Some(cli_version) { - conn.execute("DELETE FROM moduleinfocache", params![])?; - let mut stmt = conn - .prepare("INSERT OR REPLACE INTO info (key, value) VALUES (?1, ?2)")?; - stmt.execute(params!["CLI_VERSION", &cli_version])?; - } - - Ok(()) -} - fn compute_source_hash(bytes: &[u8]) -> String { FastInsecureHasher::new().write(bytes).finish().to_string() } @@ -340,13 +282,8 @@ mod test { #[test] pub fn parsed_source_cache_module_analyzer_general_use() { - let conn = Connection::open_in_memory().unwrap(); - let cache = ParsedSourceCacheModuleAnalyzer::from_connection( - conn, - "1.0.0", - Default::default(), - ) - .unwrap(); + let conn = CacheDB::in_memory(&PARSED_SOURCE_CACHE_DB, "1.0.0"); + let cache = ParsedSourceCacheModuleAnalyzer::new(conn, Default::default()); let specifier1 = ModuleSpecifier::parse("https://localhost/mod.ts").unwrap(); let specifier2 = @@ -403,13 +340,8 @@ mod test { ); // try recreating with the same version - let conn = cache.conn; - let cache = ParsedSourceCacheModuleAnalyzer::from_connection( - conn, - "1.0.0", - Default::default(), - ) - .unwrap(); + let conn = cache.conn.recreate_with_version("1.0.0"); + let cache = ParsedSourceCacheModuleAnalyzer::new(conn, Default::default()); // should get it assert_eq!( @@ -420,13 +352,8 @@ mod test { ); // try recreating with a different version - let conn = cache.conn; - let cache = ParsedSourceCacheModuleAnalyzer::from_connection( - conn, - "1.0.1", - Default::default(), - ) - .unwrap(); + let conn = cache.conn.recreate_with_version("1.0.1"); + let cache = ParsedSourceCacheModuleAnalyzer::new(conn, Default::default()); // should no longer exist assert_eq!( diff --git a/cli/graph_util.rs b/cli/graph_util.rs index 5dc7d17d6..8ea48702a 100644 --- a/cli/graph_util.rs +++ b/cli/graph_util.rs @@ -224,7 +224,7 @@ pub async fn create_graph_and_maybe_check( log::warn!("{}", ignored_options); } let maybe_config_specifier = ps.options.maybe_config_file_specifier(); - let cache = TypeCheckCache::new(&ps.dir.type_checking_cache_db_file_path()); + let cache = TypeCheckCache::new(ps.caches.type_checking_cache_db(&ps.dir)); let check_result = check::check( graph.clone(), &cache, diff --git a/cli/node/analyze.rs b/cli/node/analyze.rs index 050cbf0c1..4040c5a2b 100644 --- a/cli/node/analyze.rs +++ b/cli/node/analyze.rs @@ -167,7 +167,7 @@ mod tests { #[test] fn test_esm_code_with_node_globals() { let r = esm_code_with_node_globals( - &NodeAnalysisCache::new(None), + &NodeAnalysisCache::new_in_memory(), &ModuleSpecifier::parse("https://example.com/foo/bar.js").unwrap(), "export const x = 1;".to_string(), ) @@ -183,7 +183,7 @@ mod tests { #[test] fn test_esm_code_with_node_globals_with_shebang() { let r = esm_code_with_node_globals( - &NodeAnalysisCache::new(None), + &NodeAnalysisCache::new_in_memory(), &ModuleSpecifier::parse("https://example.com/foo/bar.js").unwrap(), "#!/usr/bin/env node\nexport const x = 1;".to_string(), ) diff --git a/cli/proc_state.rs b/cli/proc_state.rs index 529b66070..5e55f99f3 100644 --- a/cli/proc_state.rs +++ b/cli/proc_state.rs @@ -8,6 +8,7 @@ use crate::args::TsConfigType; use crate::args::TsTypeLib; use crate::args::TypeCheckMode; use crate::cache; +use crate::cache::Caches; use crate::cache::DenoDir; use crate::cache::EmitCache; use crate::cache::FastInsecureHasher; @@ -74,6 +75,7 @@ pub struct ProcState(Arc<Inner>); pub struct Inner { pub dir: DenoDir, + pub caches: Caches, pub file_fetcher: Arc<FileFetcher>, pub http_client: HttpClient, pub options: Arc<CliOptions>, @@ -139,6 +141,7 @@ impl ProcState { self.blob_store.clear(); self.0 = Arc::new(Inner { dir: self.dir.clone(), + caches: self.caches.clone(), options: self.options.clone(), emit_cache: self.emit_cache.clone(), emit_options_hash: self.emit_options_hash, @@ -192,11 +195,25 @@ impl ProcState { cli_options: Arc<CliOptions>, maybe_sender: Option<tokio::sync::mpsc::UnboundedSender<Vec<PathBuf>>>, ) -> Result<Self, AnyError> { + let dir = cli_options.resolve_deno_dir()?; + let caches = Caches::default(); + // Warm up the caches we know we'll likely need based on the CLI mode + match cli_options.sub_command() { + DenoSubcommand::Run(_) => { + _ = caches.dep_analysis_db(&dir); + _ = caches.node_analysis_db(&dir); + } + DenoSubcommand::Check(_) => { + _ = caches.dep_analysis_db(&dir); + _ = caches.node_analysis_db(&dir); + _ = caches.type_checking_cache_db(&dir); + } + _ => {} + } let blob_store = BlobStore::default(); let broadcast_channel = InMemoryBroadcastChannel::default(); let shared_array_buffer_store = SharedArrayBufferStore::default(); let compiled_wasm_module_store = CompiledWasmModuleStore::default(); - let dir = cli_options.resolve_deno_dir()?; let deps_cache_location = dir.deps_folder_path(); let http_cache = HttpCache::new(&deps_cache_location); let root_cert_store = cli_options.resolve_root_cert_store()?; @@ -284,7 +301,7 @@ impl ProcState { } let emit_cache = EmitCache::new(dir.gen_cache.clone()); let parsed_source_cache = - ParsedSourceCache::new(Some(dir.dep_analysis_db_file_path())); + ParsedSourceCache::new(caches.dep_analysis_db(&dir)); let npm_cache = NpmCache::from_deno_dir( &dir, cli_options.cache_setting(), @@ -292,11 +309,12 @@ impl ProcState { progress_bar.clone(), ); let node_analysis_cache = - NodeAnalysisCache::new(Some(dir.node_analysis_db_file_path())); + NodeAnalysisCache::new(caches.node_analysis_db(&dir)); let emit_options: deno_ast::EmitOptions = ts_config_result.ts_config.into(); Ok(ProcState(Arc::new(Inner { dir, + caches, options: cli_options, emit_cache, emit_options_hash: FastInsecureHasher::new() @@ -430,7 +448,7 @@ impl ProcState { && !roots.iter().all(|r| reload_exclusions.contains(r)), }; let check_cache = - TypeCheckCache::new(&self.dir.type_checking_cache_db_file_path()); + TypeCheckCache::new(self.caches.type_checking_cache_db(&self.dir)); let check_result = check::check(graph, &check_cache, &self.npm_resolver, options)?; self.graph_container.set_type_checked(&roots, lib); diff --git a/cli/tests/integration/inspector_tests.rs b/cli/tests/integration/inspector_tests.rs index 17f48ba5c..067963786 100644 --- a/cli/tests/integration/inspector_tests.rs +++ b/cli/tests/integration/inspector_tests.rs @@ -11,11 +11,11 @@ use deno_runtime::deno_fetch::reqwest; use deno_runtime::deno_websocket::tokio_tungstenite; use deno_runtime::deno_websocket::tokio_tungstenite::tungstenite; use std::io::BufRead; -use std::process::Child; use test_util as util; use test_util::TempDir; use tokio::net::TcpStream; use util::http_server; +use util::DenoChild; struct InspectorTester { socket_tx: SplitSink< @@ -30,7 +30,7 @@ struct InspectorTester { >, >, notification_filter: Box<dyn FnMut(&str) -> bool + 'static>, - child: Child, + child: DenoChild, stderr_lines: Box<dyn Iterator<Item = String>>, stdout_lines: Box<dyn Iterator<Item = String>>, } @@ -40,7 +40,7 @@ fn ignore_script_parsed(msg: &str) -> bool { } impl InspectorTester { - async fn create<F>(mut child: Child, notification_filter: F) -> Self + async fn create<F>(mut child: DenoChild, notification_filter: F) -> Self where F: FnMut(&str) -> bool + 'static, { diff --git a/cli/tests/integration/watcher_tests.rs b/cli/tests/integration/watcher_tests.rs index 44907b912..edc7fc842 100644 --- a/cli/tests/integration/watcher_tests.rs +++ b/cli/tests/integration/watcher_tests.rs @@ -6,6 +6,7 @@ use std::io::BufRead; use test_util as util; use test_util::assert_contains; use test_util::TempDir; +use util::DenoChild; use util::assert_not_contains; @@ -80,7 +81,7 @@ fn read_line(s: &str, lines: &mut impl Iterator<Item = String>) -> String { lines.find(|m| m.contains(s)).unwrap() } -fn check_alive_then_kill(mut child: std::process::Child) { +fn check_alive_then_kill(mut child: DenoChild) { assert!(child.try_wait().unwrap().is_none()); child.kill().unwrap(); } @@ -1244,8 +1245,8 @@ fn run_watch_dynamic_imports() { .spawn() .unwrap(); let (mut stdout_lines, mut stderr_lines) = child_lines(&mut child); - assert_contains!(stderr_lines.next().unwrap(), "No package.json file found"); - assert_contains!(stderr_lines.next().unwrap(), "Process started"); + wait_contains("No package.json file found", &mut stderr_lines); + wait_contains("Process started", &mut stderr_lines); wait_contains( "Hopefully dynamic import will be watched...", diff --git a/cli/tools/fmt.rs b/cli/tools/fmt.rs index 547dc379b..41accacba 100644 --- a/cli/tools/fmt.rs +++ b/cli/tools/fmt.rs @@ -12,6 +12,7 @@ use crate::args::FilesConfig; use crate::args::FmtOptions; use crate::args::FmtOptionsConfig; use crate::args::ProseWrap; +use crate::cache::Caches; use crate::colors; use crate::util::diff::diff; use crate::util::file_watcher; @@ -101,9 +102,10 @@ pub async fn format( } }; let deno_dir = &cli_options.resolve_deno_dir()?; - let operation = |(paths, fmt_options): (Vec<PathBuf>, FmtOptionsConfig)| async move { + let caches = Caches::default(); + let operation = |(paths, fmt_options): (Vec<PathBuf>, FmtOptionsConfig)| async { let incremental_cache = Arc::new(IncrementalCache::new( - &deno_dir.fmt_incremental_cache_db_file_path(), + caches.fmt_incremental_cache_db(deno_dir), &fmt_options, &paths, )); diff --git a/cli/tools/lint.rs b/cli/tools/lint.rs index 821257570..eae2f1032 100644 --- a/cli/tools/lint.rs +++ b/cli/tools/lint.rs @@ -11,6 +11,7 @@ use crate::args::FilesConfig; use crate::args::LintOptions; use crate::args::LintReporterKind; use crate::args::LintRulesConfig; +use crate::cache::Caches; use crate::colors; use crate::tools::fmt::run_parallelized; use crate::util::file_watcher; @@ -98,9 +99,10 @@ pub async fn lint( let has_error = Arc::new(AtomicBool::new(false)); let deno_dir = cli_options.resolve_deno_dir()?; + let caches = Caches::default(); let operation = |paths: Vec<PathBuf>| async { let incremental_cache = Arc::new(IncrementalCache::new( - &deno_dir.lint_incremental_cache_db_file_path(), + caches.lint_incremental_cache_db(&deno_dir), // use a hash of the rule names in order to bust the cache &{ // ensure this is stable by sorting it diff --git a/cli/tools/vendor/test.rs b/cli/tools/vendor/test.rs index bf34fc185..177a80b8a 100644 --- a/cli/tools/vendor/test.rs +++ b/cli/tools/vendor/test.rs @@ -218,7 +218,7 @@ impl VendorTestBuilder { let output_dir = make_path("/vendor"); let roots = self.entry_points.clone(); let loader = self.loader.clone(); - let parsed_source_cache = ParsedSourceCache::new(None); + let parsed_source_cache = ParsedSourceCache::new_in_memory(); let analyzer = parsed_source_cache.as_analyzer(); let graph = build_test_graph( roots, diff --git a/test_util/src/lib.rs b/test_util/src/lib.rs index 33b5ae8bc..d4effd88b 100644 --- a/test_util/src/lib.rs +++ b/test_util/src/lib.rs @@ -1897,22 +1897,117 @@ pub fn new_deno_dir() -> TempDir { TempDir::new() } +/// Because we need to keep the [`TempDir`] alive for the entire run of this command, +/// we have to effectively reproduce the entire builder-pattern object for [`Command`]. pub struct DenoCmd { - // keep the deno dir directory alive for the duration of the command _deno_dir: TempDir, cmd: Command, } -impl Deref for DenoCmd { - type Target = Command; - fn deref(&self) -> &Command { - &self.cmd +impl DenoCmd { + pub fn args<I, S>(&mut self, args: I) -> &mut Self + where + I: IntoIterator<Item = S>, + S: AsRef<std::ffi::OsStr>, + { + self.cmd.args(args); + self + } + + pub fn arg<S>(&mut self, arg: S) -> &mut Self + where + S: AsRef<std::ffi::OsStr>, + { + self.cmd.arg(arg); + self + } + + pub fn envs<I, K, V>(&mut self, vars: I) -> &mut Self + where + I: IntoIterator<Item = (K, V)>, + K: AsRef<std::ffi::OsStr>, + V: AsRef<std::ffi::OsStr>, + { + self.cmd.envs(vars); + self + } + + pub fn env<K, V>(&mut self, key: K, val: V) -> &mut Self + where + K: AsRef<std::ffi::OsStr>, + V: AsRef<std::ffi::OsStr>, + { + self.cmd.env(key, val); + self + } + + pub fn env_remove<K>(&mut self, key: K) -> &mut Self + where + K: AsRef<std::ffi::OsStr>, + { + self.cmd.env_remove(key); + self + } + + pub fn stdin<T: Into<Stdio>>(&mut self, cfg: T) -> &mut Self { + self.cmd.stdin(cfg); + self + } + + pub fn stdout<T: Into<Stdio>>(&mut self, cfg: T) -> &mut Self { + self.cmd.stdout(cfg); + self + } + + pub fn stderr<T: Into<Stdio>>(&mut self, cfg: T) -> &mut Self { + self.cmd.stderr(cfg); + self + } + + pub fn current_dir<P: AsRef<Path>>(&mut self, dir: P) -> &mut Self { + self.cmd.current_dir(dir); + self + } + + pub fn output(&mut self) -> Result<std::process::Output, std::io::Error> { + self.cmd.output() + } + + pub fn status(&mut self) -> Result<std::process::ExitStatus, std::io::Error> { + self.cmd.status() + } + + pub fn spawn(&mut self) -> Result<DenoChild, std::io::Error> { + Ok(DenoChild { + _deno_dir: self._deno_dir.clone(), + child: self.cmd.spawn()?, + }) + } +} + +/// We need to keep the [`TempDir`] around until the child has finished executing, so +/// this acts as a RAII guard. +pub struct DenoChild { + _deno_dir: TempDir, + child: Child, +} + +impl Deref for DenoChild { + type Target = Child; + fn deref(&self) -> &Child { + &self.child + } +} + +impl DerefMut for DenoChild { + fn deref_mut(&mut self) -> &mut Child { + &mut self.child } } -impl DerefMut for DenoCmd { - fn deref_mut(&mut self) -> &mut Command { - &mut self.cmd +impl DenoChild { + pub fn wait_with_output(self) -> Result<Output, std::io::Error> { + self.child.wait_with_output() } } |