deno/cli/cache/module_info.rs
David Sherret 94f040ac28
fix: bump cache sqlite dbs to v2 for WAL journal mode change (#24030)
In https://github.com/denoland/deno/pull/23955 we changed the sqlite db
journal mode to WAL. This causes issues when someone is running an old
version of Deno using TRUNCATE and a new version because the two fight
against each other.
2024-05-29 18:38:18 +00:00

332 lines
7.6 KiB
Rust

// Copyright 2018-2024 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::ModuleInfo;
use deno_graph::ParserModuleAnalyzer;
use deno_runtime::deno_webstorage::rusqlite::params;
use super::cache_db::CacheDB;
use super::cache_db::CacheDBConfiguration;
use super::cache_db::CacheDBHash;
use super::cache_db::CacheFailure;
use super::ParsedSourceCache;
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: concat!(
"CREATE TABLE IF NOT EXISTS moduleinfocache (",
"specifier TEXT PRIMARY KEY,",
"media_type INTEGER NOT NULL,",
"source_hash INTEGER 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: CacheDBHash,
) -> 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: CacheDBHash,
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,
parsed_source_cache: &'a Arc<ParsedSourceCache>,
) -> ModuleInfoCacheModuleAnalyzer<'a> {
ModuleInfoCacheModuleAnalyzer {
module_info_cache: self,
parsed_source_cache,
}
}
}
pub struct ModuleInfoCacheModuleAnalyzer<'a> {
module_info_cache: &'a ModuleInfoCache,
parsed_source_cache: &'a Arc<ParsedSourceCache>,
}
#[async_trait::async_trait(?Send)]
impl<'a> deno_graph::ModuleAnalyzer for ModuleInfoCacheModuleAnalyzer<'a> {
async fn analyze(
&self,
specifier: &ModuleSpecifier,
source: Arc<str>,
media_type: MediaType,
) -> Result<ModuleInfo, deno_ast::ParseDiagnostic> {
// attempt to load from the cache
let source_hash = CacheDBHash::from_source(&source);
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 module_info = deno_core::unsync::spawn_blocking({
let cache = self.parsed_source_cache.clone();
let specifier = specifier.clone();
move || {
let parser = cache.as_capturing_parser();
let analyzer = ParserModuleAnalyzer::new(&parser);
analyzer.analyze_sync(&specifier, source, media_type)
}
})
.await
.unwrap()?;
// 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)
}
}
fn serialize_media_type(media_type: MediaType) -> i64 {
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,
CacheDBHash::new(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,
CacheDBHash::new(1),
&module_info,
)
.unwrap();
assert_eq!(
cache
.get_module_info(
&specifier1,
MediaType::JavaScript,
CacheDBHash::new(1)
)
.unwrap(),
Some(module_info.clone())
);
assert_eq!(
cache
.get_module_info(
&specifier2,
MediaType::JavaScript,
CacheDBHash::new(1)
)
.unwrap(),
None,
);
// different media type
assert_eq!(
cache
.get_module_info(
&specifier1,
MediaType::TypeScript,
CacheDBHash::new(1)
)
.unwrap(),
None,
);
// different source hash
assert_eq!(
cache
.get_module_info(
&specifier1,
MediaType::JavaScript,
CacheDBHash::new(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,
CacheDBHash::new(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,
CacheDBHash::new(1)
)
.unwrap(),
None,
);
}
}