Skip to main content

grafeo_engine/
memory_usage.rs

1//! Hierarchical memory usage breakdown for the database.
2//!
3//! Store-level types (`StoreMemory`, `IndexMemory`, etc.) live in grafeo-common.
4//! This module defines the top-level `MemoryUsage` aggregate and engine-specific
5//! types (`CacheMemory`, `BufferManagerMemory`, `RdfMemory`, `CdcMemory`).
6
7pub use grafeo_common::memory::usage::{
8    IndexMemory, MvccMemory, NamedMemory, StoreMemory, StringPoolMemory,
9};
10use serde::{Deserialize, Serialize};
11
12/// Hierarchical memory usage breakdown for the entire database.
13#[derive(Debug, Clone, Default, Serialize, Deserialize)]
14pub struct MemoryUsage {
15    /// Total estimated memory usage in bytes.
16    pub total_bytes: usize,
17    /// Graph storage (nodes, edges, properties).
18    pub store: StoreMemory,
19    /// Index structures.
20    pub indexes: IndexMemory,
21    /// MVCC versioning overhead.
22    pub mvcc: MvccMemory,
23    /// Caches (query plans, etc.).
24    pub caches: CacheMemory,
25    /// String interning (ArcStr label/type registries).
26    pub string_pool: StringPoolMemory,
27    /// Buffer manager tracked allocations.
28    pub buffer_manager: BufferManagerMemory,
29    /// RDF triple store (only populated when the `triple-store` feature is enabled).
30    #[serde(default, skip_serializing_if = "RdfMemory::is_empty")]
31    pub rdf: RdfMemory,
32    /// Change data capture log (only populated when the `cdc` feature is enabled).
33    #[serde(default, skip_serializing_if = "CdcMemory::is_empty")]
34    pub cdc: CdcMemory,
35}
36
37impl MemoryUsage {
38    /// Recomputes `total_bytes` from child totals.
39    pub fn compute_total(&mut self) {
40        self.total_bytes = self.store.total_bytes
41            + self.indexes.total_bytes
42            + self.mvcc.total_bytes
43            + self.caches.total_bytes
44            + self.string_pool.total_bytes
45            + self.buffer_manager.allocated_bytes
46            + self.rdf.total_bytes
47            + self.cdc.total_bytes;
48    }
49}
50
51/// Cache memory usage.
52#[derive(Debug, Clone, Default, Serialize, Deserialize)]
53pub struct CacheMemory {
54    /// Total cache memory.
55    pub total_bytes: usize,
56    /// Parsed plan cache.
57    pub parsed_plan_cache_bytes: usize,
58    /// Optimized plan cache.
59    pub optimized_plan_cache_bytes: usize,
60    /// Number of cached plans (parsed + optimized).
61    pub cached_plan_count: usize,
62}
63
64impl CacheMemory {
65    /// Recomputes `total_bytes` from child values.
66    pub fn compute_total(&mut self) {
67        self.total_bytes = self.parsed_plan_cache_bytes + self.optimized_plan_cache_bytes;
68    }
69}
70
71/// RDF triple store memory breakdown.
72///
73/// Default is empty (all zeros) when the `triple-store` feature is disabled,
74/// so users on LPG-only builds see no RDF line in the hierarchical report.
75#[derive(Debug, Clone, Default, Serialize, Deserialize)]
76pub struct RdfMemory {
77    /// Total estimated RDF memory in bytes.
78    pub total_bytes: usize,
79    /// Number of triples across the default graph and any named graphs.
80    pub triple_count: usize,
81    /// Primary triple set and all six index maps (subject, predicate, object, SP, PO, OS).
82    pub triples_and_indexes_bytes: usize,
83    /// Cached term dictionary bytes (None when no cache is warm).
84    pub term_dictionary_bytes: usize,
85    /// Cached Ring index bytes (only populated when `ring-index` is enabled).
86    pub ring_index_bytes: usize,
87    /// Named graphs in the default store (does not include nested graph memory,
88    /// which is summed into `triples_and_indexes_bytes`).
89    pub named_graph_count: usize,
90}
91
92impl RdfMemory {
93    /// True when no RDF memory is reported. Used by `skip_serializing_if` so
94    /// LPG-only builds don't emit an empty `rdf` block in JSON.
95    #[must_use]
96    pub fn is_empty(&self) -> bool {
97        self.total_bytes == 0 && self.triple_count == 0
98    }
99
100    /// Recomputes `total_bytes` from child values.
101    pub fn compute_total(&mut self) {
102        self.total_bytes =
103            self.triples_and_indexes_bytes + self.term_dictionary_bytes + self.ring_index_bytes;
104    }
105}
106
107/// CDC log memory breakdown.
108///
109/// Default is empty when the `cdc` feature is disabled.
110#[derive(Debug, Clone, Default, Serialize, Deserialize)]
111pub struct CdcMemory {
112    /// Total estimated CDC memory in bytes.
113    pub total_bytes: usize,
114    /// Number of entities with at least one recorded event.
115    pub entity_count: usize,
116    /// Total number of recorded change events across all entities.
117    pub event_count: usize,
118}
119
120impl CdcMemory {
121    /// True when no CDC memory is reported.
122    #[must_use]
123    pub fn is_empty(&self) -> bool {
124        self.event_count == 0 && self.total_bytes == 0
125    }
126}
127
128/// Buffer manager tracked allocations.
129#[derive(Debug, Clone, Default, Serialize, Deserialize)]
130pub struct BufferManagerMemory {
131    /// Budget configured for the buffer manager.
132    pub budget_bytes: usize,
133    /// Currently allocated via grants.
134    pub allocated_bytes: usize,
135    /// Graph storage region.
136    pub graph_storage_bytes: usize,
137    /// Index buffers region.
138    pub index_buffers_bytes: usize,
139    /// Execution buffers region.
140    pub execution_buffers_bytes: usize,
141    /// Spill staging region.
142    pub spill_staging_bytes: usize,
143}
144
145#[cfg(test)]
146mod tests {
147    use super::*;
148
149    #[test]
150    fn default_memory_usage_is_zero() {
151        let usage = MemoryUsage::default();
152        assert_eq!(usage.total_bytes, 0);
153        assert_eq!(usage.store.total_bytes, 0);
154        assert_eq!(usage.indexes.total_bytes, 0);
155        assert_eq!(usage.mvcc.total_bytes, 0);
156        assert_eq!(usage.caches.total_bytes, 0);
157        assert_eq!(usage.string_pool.total_bytes, 0);
158        assert_eq!(usage.buffer_manager.allocated_bytes, 0);
159    }
160
161    #[test]
162    fn compute_total_sums_children() {
163        let mut usage = MemoryUsage {
164            store: StoreMemory {
165                total_bytes: 100,
166                ..Default::default()
167            },
168            indexes: IndexMemory {
169                total_bytes: 200,
170                ..Default::default()
171            },
172            mvcc: MvccMemory {
173                total_bytes: 50,
174                ..Default::default()
175            },
176            caches: CacheMemory {
177                total_bytes: 30,
178                ..Default::default()
179            },
180            string_pool: StringPoolMemory {
181                total_bytes: 10,
182                ..Default::default()
183            },
184            buffer_manager: BufferManagerMemory {
185                allocated_bytes: 20,
186                ..Default::default()
187            },
188            rdf: RdfMemory {
189                total_bytes: 500,
190                triple_count: 10,
191                triples_and_indexes_bytes: 500,
192                ..Default::default()
193            },
194            cdc: CdcMemory {
195                total_bytes: 40,
196                event_count: 3,
197                entity_count: 2,
198            },
199            ..Default::default()
200        };
201        usage.compute_total();
202        assert_eq!(usage.total_bytes, 950);
203    }
204
205    #[test]
206    fn rdf_and_cdc_default_is_empty() {
207        let rdf = RdfMemory::default();
208        assert!(rdf.is_empty());
209        let cdc = CdcMemory::default();
210        assert!(cdc.is_empty());
211    }
212
213    #[test]
214    fn rdf_compute_total_sums_children() {
215        let mut rdf = RdfMemory {
216            triples_and_indexes_bytes: 100,
217            term_dictionary_bytes: 50,
218            ring_index_bytes: 25,
219            ..Default::default()
220        };
221        rdf.compute_total();
222        assert_eq!(rdf.total_bytes, 175);
223    }
224
225    #[test]
226    fn serde_roundtrip() {
227        let mut usage = MemoryUsage::default();
228        usage.store.nodes_bytes = 1024;
229        usage.indexes.vector_indexes.push(NamedMemory {
230            name: "vec_idx".to_string(),
231            bytes: 512,
232            item_count: 100,
233        });
234        usage.mvcc.average_chain_depth = 1.5;
235
236        let json = serde_json::to_string(&usage).unwrap();
237        let deserialized: MemoryUsage = serde_json::from_str(&json).unwrap();
238
239        assert_eq!(deserialized.store.nodes_bytes, 1024);
240        assert_eq!(deserialized.indexes.vector_indexes.len(), 1);
241        assert_eq!(deserialized.indexes.vector_indexes[0].name, "vec_idx");
242        assert!((deserialized.mvcc.average_chain_depth - 1.5).abs() < f64::EPSILON);
243    }
244}