summaryrefslogtreecommitdiff
path: root/cli/cache/module_info.rs
diff options
context:
space:
mode:
authorDavid Sherret <dsherret@users.noreply.github.com>2023-10-25 18:13:22 -0400
committerGitHub <noreply@github.com>2023-10-25 18:13:22 -0400
commit842e29057d6e545c6b498c584a5366fff34f6aa7 (patch)
treeaba1bb9d767945ba72be5f11c5c87027f65c5678 /cli/cache/module_info.rs
parent79a9f2a77c1c517282a0e3ac77f8a1252b6c50b9 (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.rs291
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,
+ );
+ }
+}