use rustc_hash::FxHashMap as HashMap;
use std::sync::atomic::{AtomicU64, AtomicUsize, Ordering};
use std::sync::Arc;
use parking_lot::{Mutex, MutexGuard, RwLock};
use salsa::Cancelled;
use crate::cache::AnalysisCache;
use crate::db::{FileSlab, MirDatabase, MirDbStorage, SourceFile};
use super::BatchReplayState;
pub(crate) struct IndexState {
ref_committed: FileSlab<RefCommit>,
defs_committed: FileSlab<Arc<str>>,
pub(crate) ref_queries: QueryMemo<RefQueryCacheKey, (Arc<str>, crate::Range)>,
pub(crate) subtype_queries: QueryMemo<SubtypeQueryCacheKey, super::SubtypeClassSite>,
dependency_graph: RwLock<DependencyGraphCache>,
transient_batch_replay: RwLock<Option<Arc<BatchReplayState>>>,
retirements: Mutex<Retirements>,
retire_seq: AtomicU64,
}
#[derive(Default)]
struct DependencyGraphCache {
epoch: u64,
graph: Option<(DependencyGraphStamp, crate::DependencyGraph)>,
}
#[derive(Clone, Copy, PartialEq, Eq, Debug)]
pub(crate) struct DependencyGraphStamp {
epoch: u64,
generation: u64,
on_demand_files: usize,
}
#[derive(Default)]
struct Retirements {
seq: u64,
retired_at: HashMap<Arc<str>, u64>,
}
#[derive(Clone, Copy, Debug)]
pub(crate) struct RetireEpoch(u64);
#[derive(Clone, Copy, Debug)]
pub(crate) struct ViewStamp {
pub(crate) generation: u64,
pub(crate) retire_epoch: RetireEpoch,
}
pub(crate) struct CommitGuard<'a>(MutexGuard<'a, Retirements>);
impl CommitGuard<'_> {
pub(crate) fn admit(&self, file: &str, epoch: RetireEpoch) -> Result<(), Cancelled> {
match self.0.retired_at.get(file) {
Some(&at) if at > epoch.0 => Err(Cancelled::PendingWrite),
_ => Ok(()),
}
}
}
impl Default for IndexState {
fn default() -> Self {
Self {
ref_committed: FileSlab::default(),
defs_committed: FileSlab::default(),
ref_queries: QueryMemo::new(REF_QUERY_CACHE_LOCATION_CAP),
subtype_queries: QueryMemo::new(SUBTYPE_QUERY_CACHE_SITE_CAP),
dependency_graph: RwLock::default(),
transient_batch_replay: RwLock::default(),
retirements: Mutex::default(),
retire_seq: AtomicU64::new(0),
}
}
}
struct RefCommit {
text: Arc<str>,
out: std::sync::Weak<crate::db::AnalyzeOutput>,
generation: u64,
resolved: bool,
live_analyzed: bool,
}
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
pub(crate) enum RefFreshness {
Fresh,
Stale,
StaleByGenerationOnly,
Uncommitted,
}
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
pub(crate) enum DefsFreshness {
Fresh,
Stale,
Uncommitted,
}
impl IndexState {
pub(crate) fn retire_epoch(&self) -> RetireEpoch {
RetireEpoch(self.retirements.lock().seq)
}
pub(crate) fn begin_commit(&self) -> CommitGuard<'_> {
CommitGuard(self.retirements.lock())
}
pub(crate) fn retire_references(&self, db: &MirDbStorage, file: &str) {
let mut retirements = self.retirements.lock();
self.record_retirement(&mut retirements, file);
let sf = db.lookup_source_file(file);
if let Some(sf) = sf {
self.forget_ref_committed(sf);
}
db.clear_file_references(file);
}
pub(crate) fn retire_file(&self, db: &MirDbStorage, file: &str) {
let mut retirements = self.retirements.lock();
self.record_retirement(&mut retirements, file);
if let Some(sf) = db.lookup_source_file(file) {
self.forget_ref_committed(sf);
self.forget_defs_committed(sf);
}
db.clear_file_references(file);
db.clear_file_class_edges(file);
}
fn record_retirement(&self, retirements: &mut Retirements, file: &str) {
retirements.seq += 1;
let seq = retirements.seq;
retirements.retired_at.insert(Arc::from(file), seq);
self.retire_seq.store(seq, Ordering::SeqCst);
}
pub(crate) fn retire_seq(&self) -> u64 {
self.retire_seq.load(Ordering::SeqCst)
}
pub(crate) fn ref_freshness(
&self,
file: SourceFile,
current_text: &Arc<str>,
current_gen: u64,
) -> RefFreshness {
self.ref_committed
.read(file, |c| {
let same_text = Arc::ptr_eq(&c.text, current_text);
if same_text && (c.resolved || c.generation == current_gen) {
RefFreshness::Fresh
} else if same_text && c.live_analyzed {
RefFreshness::StaleByGenerationOnly
} else {
RefFreshness::Stale
}
})
.unwrap_or(RefFreshness::Uncommitted)
}
pub(crate) fn ref_commit_is_current(
&self,
file: SourceFile,
current_text: &Arc<str>,
out: &Arc<crate::db::AnalyzeOutput>,
) -> bool {
self.ref_committed
.read(file, |c| {
Arc::ptr_eq(&c.text, current_text)
&& c.out.upgrade().is_some_and(|prev| Arc::ptr_eq(&prev, out))
})
.unwrap_or(false)
}
pub(crate) fn mark_ref_committed(
&self,
file: SourceFile,
text: &Arc<str>,
out: Option<&Arc<crate::db::AnalyzeOutput>>,
generation: u64,
resolved: bool,
) {
let commit = RefCommit {
text: text.clone(),
out: out.map(Arc::downgrade).unwrap_or_default(),
generation,
resolved,
live_analyzed: out.is_some(),
};
self.ref_committed.set(file, commit);
}
pub(crate) fn forget_ref_committed(&self, file: SourceFile) {
self.ref_committed.remove(file);
}
pub(crate) fn is_defs_committed(&self, file: SourceFile, current_text: &Arc<str>) -> bool {
self.defs_committed
.read(file, |t| Arc::ptr_eq(t, current_text))
.unwrap_or(false)
}
pub(crate) fn defs_freshness(
&self,
file: SourceFile,
current_text: &Arc<str>,
) -> DefsFreshness {
self.defs_committed
.read(file, |t| {
if Arc::ptr_eq(t, current_text) {
DefsFreshness::Fresh
} else {
DefsFreshness::Stale
}
})
.unwrap_or(DefsFreshness::Uncommitted)
}
pub(crate) fn mark_defs_committed(&self, file: SourceFile, text: &Arc<str>) {
self.defs_committed.set(file, text.clone());
}
pub(crate) fn forget_defs_committed(&self, file: SourceFile) {
self.defs_committed.remove(file);
}
pub(crate) fn dependency_graph_stamp(&self, db: &MirDbStorage) -> DependencyGraphStamp {
DependencyGraphStamp {
epoch: self.dependency_graph.read().epoch,
generation: db.workspace_revision_value(),
on_demand_files: db.on_demand_file_count(),
}
}
pub(crate) fn cached_dependency_graph(
&self,
stamp: DependencyGraphStamp,
) -> Option<crate::DependencyGraph> {
match &self.dependency_graph.read().graph {
Some((built_at, graph)) if *built_at == stamp => Some(graph.clone()),
_ => None,
}
}
pub(crate) fn store_dependency_graph(
&self,
stamp: DependencyGraphStamp,
graph: crate::DependencyGraph,
) {
let mut cache = self.dependency_graph.write();
if cache.epoch == stamp.epoch {
cache.graph = Some((stamp, graph));
}
}
pub(crate) fn clear_dependency_graph_cache(&self) {
let mut cache = self.dependency_graph.write();
cache.epoch += 1;
cache.graph = None;
}
pub(crate) fn transient_batch_replay(&self) -> Option<Arc<BatchReplayState>> {
self.transient_batch_replay.read().clone()
}
pub(crate) fn store_transient_batch_replay(&self, state: BatchReplayState) {
*self.transient_batch_replay.write() = Some(Arc::new(state));
}
pub(crate) fn clear_transient_batch_replay(&self) {
*self.transient_batch_replay.write() = None;
}
pub(crate) fn commit_file_refs(
&self,
db: &MirDbStorage,
file: &Arc<str>,
text: Option<Arc<str>>,
locs: Vec<crate::db::RefLoc>,
resolved: bool,
stamp: ViewStamp,
) -> Result<(), Cancelled> {
let commit = self.begin_commit();
commit.admit(file, stamp.retire_epoch)?;
self.clear_transient_batch_replay();
let file_no = db.intern_path(file);
db.set_file_reference_locations(file_no, locs);
self.clear_dependency_graph_cache();
if let (Some(text), Some(sf)) = (text, db.lookup_source_file(file)) {
self.mark_ref_committed(sf, &text, None, stamp.generation, resolved);
}
Ok(())
}
pub(crate) fn stage_analyzed(
&self,
db: &MirDbStorage,
cache: Option<&AnalysisCache>,
mention_scanner: Option<&crate::db::class_mention_index::MentionScanner>,
path: &Arc<str>,
) -> Option<AnalyzedFile> {
let sf = db.lookup_source_file(path.as_ref())?;
let text = sf.text(db as &dyn MirDatabase).clone();
let out = crate::db::analyze_file(db as &dyn MirDatabase, sf).clone();
let defs = crate::db::collect_file_definitions(db as &dyn MirDatabase, sf);
let entries = crate::db::subtype_index::entries_from_slice(&defs.slice);
let cache_put = if self.ref_commit_is_current(sf, &text, &out) {
None
} else {
cache.and_then(|cache| stage_ref_cache_put(cache, db, sf, path.as_ref(), &text, &out))
};
let mentions = mention_scanner.and_then(|s| {
(!db.class_mentions_current(path.as_ref(), &text, s.epoch())).then(|| s.scan(&text))
});
Some(AnalyzedFile {
file: path.clone(),
sf,
text,
out,
entries,
cache_put,
mentions,
})
}
pub(crate) fn commit_analyzed(
&self,
db: &MirDbStorage,
cache: Option<&AnalysisCache>,
mention_scanner: Option<&crate::db::class_mention_index::MentionScanner>,
analyzed: &mut [AnalyzedFile],
stamp: ViewStamp,
) -> Result<(), Cancelled> {
let mut dependency_graph_changed = false;
let mut refused = Ok(());
for a in analyzed.iter_mut() {
let commit = self.begin_commit();
if let Err(cancelled) = commit.admit(&a.file, stamp.retire_epoch) {
refused = Err(cancelled);
continue;
}
if !self.ref_commit_is_current(a.sf, &a.text, &a.out) {
let file_no = db.intern_path(&a.file);
db.set_file_reference_locations(file_no, a.out.ref_locs.to_vec());
dependency_graph_changed = true;
}
if let (Some(s), Some(m)) = (mention_scanner, a.mentions.take()) {
db.set_file_class_mentions(&a.file, &a.text, s.epoch(), m);
}
if let (Some(cache), Some(put)) = (cache, a.cache_put.take()) {
cache.put(
a.file.as_ref(),
put.content_hash,
put.surface_hash,
a.out.issues.clone(),
put.ref_locs,
);
}
self.mark_ref_committed(
a.sf,
&a.text,
Some(&a.out),
stamp.generation,
!a.out.has_unresolved_names(),
);
if !self.is_defs_committed(a.sf, &a.text) {
let file_no = db.intern_path(&a.file);
db.set_file_class_edges(file_no, a.entries.clone());
self.mark_defs_committed(a.sf, &a.text);
dependency_graph_changed = true;
}
drop(commit);
}
if dependency_graph_changed {
self.clear_dependency_graph_cache();
}
refused
}
pub(crate) fn commit_class_edges(
&self,
db: &MirDbStorage,
file: &Arc<str>,
text: &Arc<str>,
entries: Vec<crate::db::SubtypeEntry>,
epoch: RetireEpoch,
) -> Result<(), Cancelled> {
let commit = self.begin_commit();
commit.admit(file, epoch)?;
let file_no = db.intern_path(file);
db.set_file_class_edges(file_no, entries);
if let Some(sf) = db.lookup_source_file(file) {
self.mark_defs_committed(sf, text);
}
Ok(())
}
}
pub(crate) struct AnalyzedFile {
pub(crate) file: Arc<str>,
sf: SourceFile,
text: Arc<str>,
pub(crate) out: Arc<crate::db::AnalyzeOutput>,
entries: Vec<crate::db::SubtypeEntry>,
cache_put: Option<RefCachePut>,
mentions: Option<Box<[mir_types::Name]>>,
}
struct RefCachePut {
content_hash: String,
surface_hash: String,
ref_locs: Arc<[crate::cache::CachedRefLoc]>,
}
fn stage_ref_cache_put(
cache: &AnalysisCache,
db: &dyn MirDatabase,
sf: crate::db::SourceFile,
file: &str,
text: &Arc<str>,
out: &Arc<crate::db::AnalyzeOutput>,
) -> Option<RefCachePut> {
let content_hash = crate::cache::hash_content(text);
if cache.is_valid(file, &content_hash) {
return None;
}
let parsed = crate::db::parse_file(db, sf);
let surface_hash = crate::cache::surface_fingerprint(text, &parsed.0.program);
let ref_locs: Arc<[crate::cache::CachedRefLoc]> = out
.ref_locs
.iter()
.map(|r| (Arc::clone(&r.symbol_key), r.line, r.col_start, r.col_end))
.collect();
Some(RefCachePut {
content_hash,
surface_hash,
ref_locs,
})
}
pub(crate) type QueryGeneration = (salsa::Revision, u64, u64);
pub(crate) struct QueryMemo<K, T> {
map: RwLock<RevisionedMap<K, Arc<Vec<T>>>>,
items: AtomicUsize,
hits: AtomicU64,
cap: usize,
}
impl<K: std::hash::Hash + Eq, T: Clone> QueryMemo<K, T> {
fn new(cap: usize) -> Self {
Self {
map: RwLock::default(),
items: AtomicUsize::new(0),
hits: AtomicU64::new(0),
cap,
}
}
pub(crate) fn hits(&self) -> u64 {
self.hits.load(Ordering::Relaxed)
}
pub(crate) fn items(&self) -> usize {
self.items.load(Ordering::Relaxed)
}
pub(crate) fn get(&self, key: &K) -> Option<Vec<T>> {
let map = self.map.read();
let hit = map.get(key)?;
self.hits.fetch_add(1, Ordering::Relaxed);
Some((**hit).clone())
}
pub(crate) fn insert(&self, generation: QueryGeneration, key: K, result: &[T]) {
let mut map = self.map.write();
if !map.advance_to(generation, || self.items.store(0, Ordering::Relaxed)) {
return;
}
let new_len = result.len();
let prior = self.items.fetch_add(new_len, Ordering::Relaxed);
if prior + new_len > self.cap {
map.map.clear();
self.items.store(new_len, Ordering::Relaxed);
}
map.map.insert(key, Arc::new(result.to_vec()));
}
}
struct RevisionedMap<K, V> {
generation: Option<QueryGeneration>,
map: HashMap<K, V>,
}
impl<K, V> Default for RevisionedMap<K, V> {
fn default() -> Self {
Self {
generation: None,
map: HashMap::default(),
}
}
}
impl<K: std::hash::Hash + Eq, V> RevisionedMap<K, V> {
fn get(&self, key: &K) -> Option<&V> {
self.map.get(key)
}
fn advance_to(&mut self, generation: QueryGeneration, on_clear: impl FnOnce()) -> bool {
match self.generation {
Some(g) if g == generation => true,
Some(g) if g > generation => false,
_ => {
self.map.clear();
self.generation = Some(generation);
on_clear();
true
}
}
}
}
#[derive(PartialEq, Eq, Hash)]
pub(crate) struct RefQueryCacheKey {
pub(crate) symbol: String,
pub(crate) include_declaration: bool,
pub(crate) includes: super::ReferenceIncludes,
pub(crate) generation: QueryGeneration,
pub(crate) files_hash: u64,
}
const REF_QUERY_CACHE_LOCATION_CAP: usize = 200_000;
#[derive(PartialEq, Eq, Hash)]
pub(crate) struct SubtypeQueryCacheKey {
pub(crate) class_fqn: String,
pub(crate) include_trait_users: bool,
pub(crate) generation: QueryGeneration,
pub(crate) files_hash: u64,
}
const SUBTYPE_QUERY_CACHE_SITE_CAP: usize = 50_000;
pub(crate) fn hash_files(files: &[Arc<str>]) -> u64 {
use std::hash::{Hash, Hasher};
let mut hasher = rustc_hash::FxHasher::default();
files.len().hash(&mut hasher);
for f in files {
f.hash(&mut hasher);
}
hasher.finish()
}
#[cfg(test)]
mod tests {
use super::*;
fn empty_graph() -> crate::DependencyGraph {
crate::DependencyGraph::from_compact_parts(
Vec::new(),
HashMap::default(),
Vec::new(),
Vec::new(),
)
}
#[test]
fn a_dependency_graph_built_across_an_invalidation_is_not_cached() {
let state = IndexState::default();
let db = MirDbStorage::default();
let stamp = state.dependency_graph_stamp(&db);
state.store_dependency_graph(stamp, empty_graph());
assert!(state.cached_dependency_graph(stamp).is_some());
let stamp = state.dependency_graph_stamp(&db);
state.clear_dependency_graph_cache();
state.store_dependency_graph(stamp, empty_graph());
let current = state.dependency_graph_stamp(&db);
assert!(state.cached_dependency_graph(current).is_none());
}
}