diff options
author | David Sherret <dsherret@users.noreply.github.com> | 2023-10-25 18:13:22 -0400 |
---|---|---|
committer | GitHub <noreply@github.com> | 2023-10-25 18:13:22 -0400 |
commit | 842e29057d6e545c6b498c584a5366fff34f6aa7 (patch) | |
tree | aba1bb9d767945ba72be5f11c5c87027f65c5678 /cli/cache/module_info.rs | |
parent | 79a9f2a77c1c517282a0e3ac77f8a1252b6c50b9 (diff) |
refactor: break out ModuleInfoCache from ParsedSourceCache (#20977)
As title. This will help use the two independently from the other, which
will help in an upcoming deno doc PR where I need to parse the source
files with scope analysis.
Diffstat (limited to 'cli/cache/module_info.rs')
-rw-r--r-- | cli/cache/module_info.rs | 291 |
1 files changed, 291 insertions, 0 deletions
diff --git a/cli/cache/module_info.rs b/cli/cache/module_info.rs new file mode 100644 index 000000000..afdb8349c --- /dev/null +++ b/cli/cache/module_info.rs @@ -0,0 +1,291 @@ +// Copyright 2018-2023 the Deno authors. All rights reserved. MIT license. + +use std::sync::Arc; + +use deno_ast::MediaType; +use deno_ast::ModuleSpecifier; +use deno_core::error::AnyError; +use deno_core::serde_json; +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 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 MODULE_INFO_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, +}; + +/// 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. +pub struct ModuleInfoCache { + conn: CacheDB, +} + +impl ModuleInfoCache { + #[cfg(test)] + pub fn new_in_memory(version: &'static str) -> Self { + Self::new(CacheDB::in_memory(&MODULE_INFO_CACHE_DB, version)) + } + + pub fn new(conn: CacheDB) -> Self { + Self { conn } + } + + /// Useful for testing: re-create this cache DB with a different current version. + #[cfg(test)] + pub(crate) fn recreate_with_version(self, version: &'static str) -> Self { + Self { + conn: self.conn.recreate_with_version(version), + } + } + + pub fn get_module_info( + &self, + specifier: &ModuleSpecifier, + media_type: MediaType, + expected_source_hash: &str, + ) -> Result<Option<ModuleInfo>, AnyError> { + 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( + &self, + specifier: &ModuleSpecifier, + media_type: MediaType, + source_hash: &str, + module_info: &ModuleInfo, + ) -> Result<(), AnyError> { + let sql = " + INSERT OR REPLACE INTO + moduleinfocache (specifier, media_type, source_hash, module_info) + VALUES + (?1, ?2, ?3, ?4)"; + self.conn.execute( + sql, + params![ + specifier.as_str(), + serialize_media_type(media_type), + &source_hash, + &serde_json::to_string(&module_info)?, + ], + )?; + Ok(()) + } + + pub fn as_module_analyzer<'a>( + &'a self, + parser: Option<&'a dyn ModuleParser>, + store: &'a dyn ParsedSourceStore, + ) -> ModuleInfoCacheModuleAnalyzer<'a> { + ModuleInfoCacheModuleAnalyzer { + module_info_cache: self, + parser: CapturingModuleParser::new(parser, store), + } + } +} + +pub struct ModuleInfoCacheModuleAnalyzer<'a> { + module_info_cache: &'a ModuleInfoCache, + parser: CapturingModuleParser<'a>, +} + +impl<'a> deno_graph::ModuleAnalyzer for ModuleInfoCacheModuleAnalyzer<'a> { + fn analyze( + &self, + specifier: &ModuleSpecifier, + source: Arc<str>, + media_type: MediaType, + ) -> Result<ModuleInfo, deno_ast::Diagnostic> { + // attempt to load from the cache + let source_hash = FastInsecureHasher::hash(source.as_bytes()).to_string(); + match self.module_info_cache.get_module_info( + specifier, + media_type, + &source_hash, + ) { + Ok(Some(info)) => return Ok(info), + Ok(None) => {} + Err(err) => { + log::debug!( + "Error loading module cache info for {}. {:#}", + specifier, + err + ); + } + } + + // otherwise, get the module info from the parsed source cache + let analyzer = DefaultModuleAnalyzer::new(&self.parser); + let module_info = analyzer.analyze(specifier, source, media_type)?; + + // then attempt to cache it + if let Err(err) = self.module_info_cache.set_module_info( + specifier, + media_type, + &source_hash, + &module_info, + ) { + log::debug!( + "Error saving module cache info for {}. {:#}", + specifier, + err + ); + } + + Ok(module_info) + } +} + +// todo(dsherret): change this to be stored as an integer next time +// the cache version is bumped +fn serialize_media_type(media_type: MediaType) -> &'static str { + use MediaType::*; + match media_type { + JavaScript => "1", + Jsx => "2", + Mjs => "3", + Cjs => "4", + TypeScript => "5", + Mts => "6", + Cts => "7", + Dts => "8", + Dmts => "9", + Dcts => "10", + Tsx => "11", + Json => "12", + Wasm => "13", + TsBuildInfo => "14", + SourceMap => "15", + Unknown => "16", + } +} + +#[cfg(test)] +mod test { + use deno_graph::PositionRange; + use deno_graph::SpecifierWithRange; + + use super::*; + + #[test] + pub fn module_info_cache_general_use() { + let cache = ModuleInfoCache::new_in_memory("1.0.0"); + let specifier1 = + ModuleSpecifier::parse("https://localhost/mod.ts").unwrap(); + let specifier2 = + ModuleSpecifier::parse("https://localhost/mod2.ts").unwrap(); + assert_eq!( + cache + .get_module_info(&specifier1, MediaType::JavaScript, "1") + .unwrap(), + None + ); + + let mut module_info = ModuleInfo::default(); + module_info.jsdoc_imports.push(SpecifierWithRange { + range: PositionRange { + start: deno_graph::Position { + line: 0, + character: 3, + }, + end: deno_graph::Position { + line: 1, + character: 2, + }, + }, + text: "test".to_string(), + }); + cache + .set_module_info(&specifier1, MediaType::JavaScript, "1", &module_info) + .unwrap(); + assert_eq!( + cache + .get_module_info(&specifier1, MediaType::JavaScript, "1") + .unwrap(), + Some(module_info.clone()) + ); + assert_eq!( + cache + .get_module_info(&specifier2, MediaType::JavaScript, "1") + .unwrap(), + None, + ); + // different media type + assert_eq!( + cache + .get_module_info(&specifier1, MediaType::TypeScript, "1") + .unwrap(), + None, + ); + // different source hash + assert_eq!( + cache + .get_module_info(&specifier1, MediaType::JavaScript, "2") + .unwrap(), + None, + ); + + // try recreating with the same version + let cache = cache.recreate_with_version("1.0.0"); + + // should get it + assert_eq!( + cache + .get_module_info(&specifier1, MediaType::JavaScript, "1") + .unwrap(), + Some(module_info) + ); + + // try recreating with a different version + let cache = cache.recreate_with_version("1.0.1"); + + // should no longer exist + assert_eq!( + cache + .get_module_info(&specifier1, MediaType::JavaScript, "1") + .unwrap(), + None, + ); + } +} |