Skip to main content

sqry_core/graph/unified/analysis/
cache.rs

1//! Analysis cache with memory-mapped file support
2
3use super::{CondensationDag, CsrAdjacency, SccData};
4use crate::graph::unified::edge::EdgeKind;
5use crate::graph::unified::persistence::{GraphStorage, load_from_path};
6use anyhow::Result;
7use std::collections::HashMap;
8use std::path::Path;
9use std::sync::Arc;
10
11/// Memory-mapped analysis cache for fast loading
12pub struct AnalysisCache {
13    /// Hash of the manifest file
14    pub manifest_hash: String,
15    /// Hash of node IDs for identity verification
16    pub node_id_hash: [u8; 32],
17    csr: Option<Arc<CsrAdjacency>>,
18    sccs: HashMap<EdgeKind, Arc<SccData>>,
19    condensations: HashMap<EdgeKind, Arc<CondensationDag>>,
20}
21
22impl AnalysisCache {
23    /// Load analysis cache from index directory
24    /// Returns an error if the operation fails.
25    ///
26    /// # Errors
27    ///
28    pub fn load(index_path: &Path) -> Result<Self> {
29        let storage = GraphStorage::new(index_path);
30
31        let manifest_hash = super::persistence::compute_manifest_hash(storage.manifest_path())?;
32        let graph = load_from_path(storage.snapshot_path(), None)?;
33        let node_id_hash = super::persistence::compute_node_id_hash(&graph.snapshot());
34        let identity =
35            super::persistence::AnalysisIdentity::new(manifest_hash.clone(), node_id_hash);
36
37        let csr = super::persistence::load_csr_checked(&storage.analysis_csr_path(), &identity)?;
38
39        let scc_calls =
40            super::persistence::load_scc_checked(&storage.analysis_scc_path("calls"), &identity)?;
41        let scc_imports =
42            super::persistence::load_scc_checked(&storage.analysis_scc_path("imports"), &identity)?;
43        let scc_references = super::persistence::load_scc_checked(
44            &storage.analysis_scc_path("references"),
45            &identity,
46        )?;
47        let scc_inherits = super::persistence::load_scc_checked(
48            &storage.analysis_scc_path("inherits"),
49            &identity,
50        )?;
51
52        let cond_calls = super::persistence::load_condensation_checked(
53            &storage.analysis_cond_path("calls"),
54            &identity,
55        )?;
56        let cond_imports = super::persistence::load_condensation_checked(
57            &storage.analysis_cond_path("imports"),
58            &identity,
59        )?;
60        let cond_references = super::persistence::load_condensation_checked(
61            &storage.analysis_cond_path("references"),
62            &identity,
63        )?;
64        let cond_inherits = super::persistence::load_condensation_checked(
65            &storage.analysis_cond_path("inherits"),
66            &identity,
67        )?;
68
69        let mut sccs = HashMap::new();
70        sccs.insert(scc_calls.edge_kind.clone(), Arc::new(scc_calls));
71        sccs.insert(scc_imports.edge_kind.clone(), Arc::new(scc_imports));
72        sccs.insert(scc_references.edge_kind.clone(), Arc::new(scc_references));
73        sccs.insert(scc_inherits.edge_kind.clone(), Arc::new(scc_inherits));
74
75        let mut condensations = HashMap::new();
76        condensations.insert(cond_calls.edge_kind.clone(), Arc::new(cond_calls));
77        condensations.insert(cond_imports.edge_kind.clone(), Arc::new(cond_imports));
78        condensations.insert(cond_references.edge_kind.clone(), Arc::new(cond_references));
79        condensations.insert(cond_inherits.edge_kind.clone(), Arc::new(cond_inherits));
80
81        Ok(Self {
82            manifest_hash,
83            node_id_hash,
84            csr: Some(Arc::new(csr)),
85            sccs,
86            condensations,
87        })
88    }
89
90    /// Get CSR adjacency
91    /// Returns an error if the operation fails.
92    ///
93    /// # Errors
94    ///
95    pub fn get_csr(&self) -> Result<Arc<CsrAdjacency>> {
96        self.csr
97            .clone()
98            .ok_or_else(|| anyhow::anyhow!("CSR not loaded"))
99    }
100
101    /// Get SCC data for an edge kind (matches by discriminant, ignores metadata)
102    /// Returns an error if the operation fails.
103    ///
104    /// # Errors
105    ///
106    pub fn get_scc(&self, kind: &EdgeKind) -> Result<Arc<SccData>> {
107        let kind_discriminant = std::mem::discriminant(kind);
108        self.sccs
109            .iter()
110            .find(|(k, _)| std::mem::discriminant(*k) == kind_discriminant)
111            .map(|(_, v)| v.clone())
112            .ok_or_else(|| anyhow::anyhow!("SCC for {kind:?} not loaded"))
113    }
114
115    /// Get condensation DAG for an edge kind (matches by discriminant, ignores metadata)
116    /// Returns an error if the operation fails.
117    ///
118    /// # Errors
119    ///
120    pub fn get_condensation(&self, kind: &EdgeKind) -> Result<Arc<CondensationDag>> {
121        let kind_discriminant = std::mem::discriminant(kind);
122        self.condensations
123            .iter()
124            .find(|(k, _)| std::mem::discriminant(*k) == kind_discriminant)
125            .map(|(_, v)| v.clone())
126            .ok_or_else(|| anyhow::anyhow!("Condensation for {kind:?} not loaded"))
127    }
128}
129
130#[cfg(test)]
131mod tests {
132    use super::*;
133    use crate::graph::unified::analysis::{AnalysisIdentity, GraphAnalyses};
134    use crate::graph::unified::compaction::snapshot_edges;
135    use crate::graph::unified::concurrent::CodeGraph;
136    use crate::graph::unified::edge::EdgeKind;
137    use crate::graph::unified::node::NodeKind;
138    use crate::graph::unified::persistence::{BuildProvenance, Manifest, save_to_path};
139    use crate::graph::unified::storage::NodeEntry;
140    use sha2::{Digest, Sha256};
141    use std::path::Path;
142    use tempfile::TempDir;
143
144    #[test]
145    fn test_analysis_cache_load_roundtrip() {
146        let temp_dir = TempDir::new().unwrap();
147        let root = temp_dir.path();
148        let storage = GraphStorage::new(root);
149        std::fs::create_dir_all(storage.graph_dir()).unwrap();
150
151        let mut graph = CodeGraph::new();
152        let file_id = graph.files_mut().register(Path::new("test.rs")).unwrap();
153        let name = graph.strings_mut().intern("A").unwrap();
154        let node = graph
155            .nodes_mut()
156            .alloc(NodeEntry::new(NodeKind::Function, name, file_id))
157            .unwrap();
158        let kind = EdgeKind::Calls {
159            argument_count: 0,
160            is_async: false,
161        };
162        graph
163            .edges_mut()
164            .add_edge(node, node, kind.clone(), file_id);
165
166        save_to_path(&graph, storage.snapshot_path()).unwrap();
167
168        let snapshot_bytes = std::fs::read(storage.snapshot_path()).unwrap();
169        let mut hasher = Sha256::new();
170        hasher.update(&snapshot_bytes);
171        let snapshot_hash = hex::encode(hasher.finalize());
172
173        let node_count = graph.nodes().len();
174        let edge_stats = graph.edges().stats();
175        let edge_count = edge_stats.forward.csr_edge_count + edge_stats.forward.delta_edge_count;
176
177        let provenance = BuildProvenance::default();
178        let manifest = Manifest::new(
179            root.to_string_lossy().to_string(),
180            node_count,
181            edge_count,
182            snapshot_hash,
183            provenance,
184        );
185        manifest.save(storage.manifest_path()).unwrap();
186
187        let snapshot = graph.snapshot();
188        let edges = snapshot.edges();
189        let forward_store = edges.forward();
190        let compaction_snapshot = snapshot_edges(&forward_store, snapshot.nodes().len());
191        let analyses = GraphAnalyses::build_all(&compaction_snapshot).unwrap();
192
193        let manifest_hash =
194            crate::graph::unified::analysis::compute_manifest_hash(storage.manifest_path())
195                .unwrap();
196        let node_id_hash = crate::graph::unified::analysis::compute_node_id_hash(&snapshot);
197        let identity = AnalysisIdentity::new(manifest_hash, node_id_hash);
198        analyses.persist_all(&storage, &identity).unwrap();
199
200        let cache = AnalysisCache::load(root).unwrap();
201        assert!(cache.get_csr().is_ok());
202        assert!(cache.get_scc(&kind).is_ok());
203        assert!(cache.get_condensation(&kind).is_ok());
204    }
205}