1use std::collections::BTreeMap;
26use std::mem::size_of;
27
28use crate::{LoraBinary, LoraPoint, LoraVector, NodeRecord, PropertyValue, RelationshipRecord};
29
30use super::chunked_vec::ChunkedVec;
31use super::entity_index_store::{IndexBundle, ScopedPropertyKey};
32use super::fulltext_index::FulltextRegistry;
33use super::hnsw::HnswBackend;
34use super::id_set::IdSet;
35use super::index_catalog::{IndexCatalog, StoredIndexEntity};
36use super::point_index::PointRegistry;
37use super::property_index::{
38 PropertyIndex, PropertyIndexKey, PropertyIndexRegistry, PropertyIndexState,
39};
40use super::sorted_property_index::SortedPropertyIndex;
41use super::text_index::TrigramRegistry;
42use super::vector_index::{FlatBackend, VectorBackend, VectorIndexRegistry};
43use super::ConstraintCatalog;
44
45const BTREE_PER_ENTRY: usize = 24;
49
50const HASHMAP_PER_ENTRY: usize = 24;
53
54const ARC_HEADER: usize = 16;
57
58#[derive(Debug, Default, Clone, PartialEq, serde::Serialize, serde::Deserialize)]
65#[serde(rename_all = "camelCase")]
66pub struct MemoryReport {
67 pub live_node_count: usize,
68 pub live_relationship_count: usize,
69 pub node_tombstone_count: usize,
70 pub relationship_tombstone_count: usize,
71
72 pub nodes_bytes: usize,
76 pub relationships_bytes: usize,
78
79 pub outgoing_bytes: usize,
82 pub incoming_bytes: usize,
84
85 pub label_index_bytes: usize,
87 pub type_index_bytes: usize,
89
90 pub property_index_bytes: usize,
92 pub sorted_index_bytes: usize,
94 pub text_index_bytes: usize,
96 pub point_index_bytes: usize,
98 pub fulltext_index_bytes: usize,
100 pub vector_index_bytes: usize,
102
103 pub index_catalog_bytes: usize,
104 pub constraint_catalog_bytes: usize,
105}
106
107impl MemoryReport {
108 pub fn graph_core_bytes(&self) -> usize {
112 self.nodes_bytes
113 + self.relationships_bytes
114 + self.outgoing_bytes
115 + self.incoming_bytes
116 + self.label_index_bytes
117 + self.type_index_bytes
118 }
119
120 pub fn secondary_index_bytes(&self) -> usize {
123 self.property_index_bytes
124 + self.sorted_index_bytes
125 + self.text_index_bytes
126 + self.point_index_bytes
127 + self.fulltext_index_bytes
128 + self.vector_index_bytes
129 }
130
131 pub fn catalog_bytes(&self) -> usize {
132 self.index_catalog_bytes + self.constraint_catalog_bytes
133 }
134
135 pub fn total_bytes(&self) -> usize {
136 self.graph_core_bytes() + self.secondary_index_bytes() + self.catalog_bytes()
137 }
138
139 pub fn bytes_per_live_node(&self) -> f64 {
143 ratio(self.nodes_bytes, self.live_node_count)
144 }
145
146 pub fn bytes_per_live_relationship(&self) -> f64 {
147 ratio(self.relationships_bytes, self.live_relationship_count)
148 }
149
150 pub fn summary(&self) -> String {
153 format!(
154 "total={} graph={} (nodes={} rels={} out={} in={} labels={} types={}) idx={} cat={}",
155 self.total_bytes(),
156 self.graph_core_bytes(),
157 self.nodes_bytes,
158 self.relationships_bytes,
159 self.outgoing_bytes,
160 self.incoming_bytes,
161 self.label_index_bytes,
162 self.type_index_bytes,
163 self.secondary_index_bytes(),
164 self.catalog_bytes(),
165 )
166 }
167}
168
169fn ratio(num: usize, denom: usize) -> f64 {
170 if denom == 0 {
171 0.0
172 } else {
173 num as f64 / denom as f64
174 }
175}
176
177pub(super) fn estimate(graph: &super::InMemoryGraph) -> MemoryReport {
178 let mut report = MemoryReport {
179 live_node_count: graph.live_node_count,
180 live_relationship_count: graph.live_rel_count,
181 node_tombstone_count: graph.nodes.len().saturating_sub(graph.live_node_count),
182 relationship_tombstone_count: graph
183 .relationships
184 .len()
185 .saturating_sub(graph.live_rel_count),
186 ..MemoryReport::default()
187 };
188
189 report.nodes_bytes = node_slab_bytes(&graph.nodes);
190 report.relationships_bytes = rel_slab_bytes(&graph.relationships);
191 report.outgoing_bytes = adjacency_bytes(&graph.outgoing);
192 report.incoming_bytes = adjacency_bytes(&graph.incoming);
193 report.label_index_bytes = label_or_type_bytes(&graph.nodes_by_label);
194 report.type_index_bytes = label_or_type_bytes(&graph.relationships_by_type);
195
196 let bundle = &graph.indexes;
197 if let Ok(props) = bundle.properties.read() {
198 report.property_index_bytes = property_registry_bytes(&props);
199 }
200 report.sorted_index_bytes = sorted_registry_bytes(bundle);
201 report.text_index_bytes = text_registry_bytes(bundle);
202 report.point_index_bytes = point_registry_bytes(bundle);
203 report.fulltext_index_bytes = fulltext_registry_bytes(bundle);
204 report.vector_index_bytes = vector_registry_bytes(bundle);
205
206 if let Ok(catalog) = bundle.catalog.read() {
207 report.index_catalog_bytes = index_catalog_bytes(&catalog);
208 }
209 if let Ok(catalog) = graph.constraint_catalog.read() {
210 report.constraint_catalog_bytes = constraint_catalog_bytes(&catalog);
211 }
212
213 report
214}
215
216fn chunked_outer_bytes<T>(v: &ChunkedVec<T>) -> usize {
221 v.capacity() * size_of::<T>()
222 + v.chunk_count() * (ARC_HEADER + size_of::<Vec<T>>() + size_of::<usize>())
223}
224
225fn node_slab_bytes(slab: &ChunkedVec<Option<std::sync::Arc<NodeRecord>>>) -> usize {
226 let outer = chunked_outer_bytes(slab);
227 let mut payload = 0;
228 for arc in slab.iter().flatten() {
229 payload += ARC_HEADER + size_of::<NodeRecord>() + node_record_heap_bytes(arc);
230 }
231 outer + payload
232}
233
234fn rel_slab_bytes(slab: &ChunkedVec<Option<std::sync::Arc<RelationshipRecord>>>) -> usize {
235 let outer = chunked_outer_bytes(slab);
236 let mut payload = 0;
237 for arc in slab.iter().flatten() {
238 payload += ARC_HEADER + size_of::<RelationshipRecord>() + rel_record_heap_bytes(arc);
239 }
240 outer + payload
241}
242
243fn node_record_heap_bytes(record: &NodeRecord) -> usize {
244 let labels = record.labels.capacity() * size_of::<String>()
245 + record.labels.iter().map(|l| l.capacity()).sum::<usize>();
246 labels + properties_heap_bytes(&record.properties)
247}
248
249fn rel_record_heap_bytes(record: &RelationshipRecord) -> usize {
250 record.rel_type.capacity() + properties_heap_bytes(&record.properties)
251}
252
253fn adjacency_bytes(adj: &ChunkedVec<super::graph::AdjList>) -> usize {
254 let outer = chunked_outer_bytes(adj);
255 let inner: usize = adj
257 .iter()
258 .filter(|v| v.spilled())
259 .map(|v| v.capacity() * size_of::<u64>())
260 .sum();
261 outer + inner
262}
263
264fn label_or_type_bytes(map: &BTreeMap<String, ChunkedVec<u64>>) -> usize {
265 let mut total = 0;
266 for (key, ids) in map {
267 total += BTREE_PER_ENTRY
268 + size_of::<String>()
269 + key.capacity()
270 + size_of::<ChunkedVec<u64>>()
271 + chunked_outer_bytes(ids);
272 }
273 total
274}
275
276pub fn property_value_heap_bytes(value: &PropertyValue) -> usize {
282 match value {
283 PropertyValue::Null
284 | PropertyValue::Bool(_)
285 | PropertyValue::Int(_)
286 | PropertyValue::Float(_)
287 | PropertyValue::Date(_)
288 | PropertyValue::Time(_)
289 | PropertyValue::LocalTime(_)
290 | PropertyValue::LocalDateTime(_)
291 | PropertyValue::DateTime(_)
292 | PropertyValue::Duration(_) => 0,
293 PropertyValue::String(s) => s.capacity(),
294 PropertyValue::Binary(b) => binary_heap_bytes(b),
295 PropertyValue::Point(p) => point_heap_bytes(p),
296 PropertyValue::Vector(v) => vector_heap_bytes(v),
297 PropertyValue::List(items) => {
298 items.capacity() * size_of::<PropertyValue>()
299 + items.iter().map(property_value_heap_bytes).sum::<usize>()
300 }
301 PropertyValue::Map(map) => {
302 let mut total = 0;
303 for (key, value) in map {
304 total += BTREE_PER_ENTRY
305 + size_of::<String>()
306 + key.capacity()
307 + size_of::<PropertyValue>()
308 + property_value_heap_bytes(value);
309 }
310 total
311 }
312 }
313}
314
315fn binary_heap_bytes(b: &LoraBinary) -> usize {
316 let mut total = std::mem::size_of_val(b.segments());
317 for seg in b.segments() {
318 total += seg.capacity();
319 }
320 total
321}
322
323fn point_heap_bytes(_: &LoraPoint) -> usize {
324 0
326}
327
328fn vector_heap_bytes(v: &LoraVector) -> usize {
329 use crate::types::VectorValues;
330 match &v.values {
331 VectorValues::Float64(xs) => xs.capacity() * size_of::<f64>(),
332 VectorValues::Float32(xs) => xs.capacity() * size_of::<f32>(),
333 VectorValues::Integer64(xs) => xs.capacity() * size_of::<i64>(),
334 VectorValues::Integer32(xs) => xs.capacity() * size_of::<i32>(),
335 VectorValues::Integer16(xs) => xs.capacity() * size_of::<i16>(),
336 VectorValues::Integer8(xs) => xs.capacity() * size_of::<i8>(),
337 }
338}
339
340fn properties_heap_bytes(properties: &crate::Properties) -> usize {
341 let slots = properties.capacity() * size_of::<(std::sync::Arc<str>, PropertyValue)>();
345 slots
346 + properties
347 .values()
348 .map(property_value_heap_bytes)
349 .sum::<usize>()
350}
351
352fn property_registry_bytes(reg: &PropertyIndexRegistry) -> usize {
355 property_state_bytes(®.node_properties) + property_state_bytes(®.relationship_properties)
356}
357
358fn property_state_bytes(state: &PropertyIndexState) -> usize {
359 let mut total = 0;
360 for key in &state.active_keys {
361 total += BTREE_PER_ENTRY + size_of::<String>() + key.capacity();
362 }
363 total += property_index_map_bytes(&state.values);
364 for (scope, by_property) in &state.scoped_values {
365 total += HASHMAP_PER_ENTRY + size_of::<String>() + scope.capacity();
366 total += property_index_map_bytes(by_property);
367 }
368 total
369}
370
371fn property_index_map_bytes(values: &PropertyIndex) -> usize {
372 let mut total = 0;
373 for (key, buckets) in values {
374 total += HASHMAP_PER_ENTRY + size_of::<String>() + key.capacity();
375 for (indexed, ids) in buckets.iter() {
376 total += HASHMAP_PER_ENTRY
377 + property_index_key_bytes(indexed)
378 + size_of::<IdSet>()
379 + ids.heap_bytes();
380 }
381 }
382 total
383}
384
385fn property_index_key_bytes(key: &PropertyIndexKey) -> usize {
386 size_of::<PropertyIndexKey>()
387 + match key {
388 PropertyIndexKey::Null
389 | PropertyIndexKey::Bool(_)
390 | PropertyIndexKey::Int(_)
391 | PropertyIndexKey::Float(_)
392 | PropertyIndexKey::Temporal { .. } => 0,
393 PropertyIndexKey::String(s) => 16 + s.len(),
394 PropertyIndexKey::Binary(b) => binary_heap_bytes(b),
395 PropertyIndexKey::List(items) => {
396 items.capacity() * size_of::<PropertyIndexKey>()
397 + items.iter().map(property_index_key_bytes).sum::<usize>()
398 }
399 PropertyIndexKey::Map(map) => map
400 .iter()
401 .map(|(k, v)| {
402 BTREE_PER_ENTRY
403 + size_of::<String>()
404 + k.capacity()
405 + property_index_key_bytes(v)
406 })
407 .sum::<usize>(),
408 }
409}
410
411fn scoped_key_bytes(scope: &ScopedPropertyKey) -> usize {
412 size_of::<ScopedPropertyKey>() + scope.label.capacity() + scope.property.capacity()
413}
414
415fn sorted_registry_bytes(bundle: &IndexBundle) -> usize {
416 let node = bundle.sorted.read(StoredIndexEntity::Node);
417 let rel = bundle.sorted.read(StoredIndexEntity::Relationship);
418 sorted_one(&node) + sorted_one(&rel)
419}
420
421fn sorted_one(index: &SortedPropertyIndex) -> usize {
422 let mut total = 0;
423 for (scope, sorted_scope) in &index.by_scope {
424 total += BTREE_PER_ENTRY + scoped_key_bytes(scope);
425 for (indexed, ids) in sorted_scope.by_value.iter() {
426 total += BTREE_PER_ENTRY
427 + property_index_key_bytes(indexed)
428 + size_of::<IdSet>()
429 + ids.heap_bytes();
430 }
431 }
432 total
433}
434
435fn text_registry_bytes(bundle: &IndexBundle) -> usize {
436 let node = bundle.text.read(StoredIndexEntity::Node);
437 let rel = bundle.text.read(StoredIndexEntity::Relationship);
438 text_one(&node) + text_one(&rel)
439}
440
441fn text_one(registry: &TrigramRegistry) -> usize {
442 let mut total = 0;
443 for (scope, trigram_scope) in ®istry.by_scope {
444 total += HASHMAP_PER_ENTRY + scoped_key_bytes(scope);
445 for ids in trigram_scope.grams.values() {
446 total += BTREE_PER_ENTRY + 3 + ids.heap_bytes();
447 }
448 }
449 total
450}
451
452fn point_registry_bytes(bundle: &IndexBundle) -> usize {
453 let node = bundle.point.read(StoredIndexEntity::Node);
454 let rel = bundle.point.read(StoredIndexEntity::Relationship);
455 point_one(&node) + point_one(&rel)
456}
457
458fn point_one(registry: &PointRegistry) -> usize {
459 let mut total = 0;
460 for (scope, scope_data) in ®istry.by_scope {
461 total += HASHMAP_PER_ENTRY + scoped_key_bytes(scope);
462 for cell in scope_data.grid.cells.values() {
463 total += HASHMAP_PER_ENTRY + cell.heap_bytes();
464 }
465 }
466 total
467}
468
469fn fulltext_registry_bytes(bundle: &IndexBundle) -> usize {
470 let node = bundle.fulltext.read(StoredIndexEntity::Node);
471 let rel = bundle.fulltext.read(StoredIndexEntity::Relationship);
472 fulltext_one(&node) + fulltext_one(&rel)
473}
474
475fn fulltext_one(registry: &FulltextRegistry) -> usize {
476 let mut total = 0;
477 for (name, index) in registry.iter() {
478 total += HASHMAP_PER_ENTRY + size_of::<String>() + name.capacity();
479 total += index.labels.capacity() * size_of::<String>();
480 for label in &index.labels {
481 total += label.capacity();
482 }
483 total += index.properties.capacity() * size_of::<String>();
484 for property in &index.properties {
485 total += property.capacity();
486 }
487 for (term, postings) in index.postings.iter() {
490 total += BTREE_PER_ENTRY + size_of::<std::sync::Arc<str>>() + ARC_HEADER + term.len();
491 total += postings.heap_bytes();
492 }
493 for terms in index.entity_terms.values() {
494 total += BTREE_PER_ENTRY
495 + size_of::<u64>()
496 + ARC_HEADER
497 + terms.len() * size_of::<std::sync::Arc<str>>();
498 }
499 }
500 total
501}
502
503fn vector_registry_bytes(bundle: &IndexBundle) -> usize {
504 let node = bundle.vector.read(StoredIndexEntity::Node);
505 let rel = bundle.vector.read(StoredIndexEntity::Relationship);
506 vector_one(&node) + vector_one(&rel)
507}
508
509fn vector_one(registry: &VectorIndexRegistry) -> usize {
510 let mut total = 0;
511 for (name, entry) in ®istry.by_name {
512 total += BTREE_PER_ENTRY
513 + size_of::<String>()
514 + name.capacity()
515 + entry.label.capacity()
516 + entry.property.capacity();
517 total += match &entry.backend {
518 VectorBackend::Flat(b) => flat_backend_bytes(b),
519 VectorBackend::Hnsw(b) => hnsw_backend_bytes(b),
520 };
521 }
522 total
523}
524
525fn flat_backend_bytes(b: &FlatBackend) -> usize {
526 let mut total = 0;
527 for v in b.items.values() {
528 total +=
529 BTREE_PER_ENTRY + size_of::<u64>() + size_of::<LoraVector>() + vector_heap_bytes(v);
530 }
531 total
532}
533
534fn hnsw_backend_bytes(b: &HnswBackend) -> usize {
535 let mut total = 0;
536 for node in b.nodes.values() {
537 total += BTREE_PER_ENTRY
538 + size_of::<u64>()
539 + size_of::<LoraVector>()
540 + vector_heap_bytes(&node.vector)
541 + size_of::<usize>() + node.neighbors.capacity() * size_of::<Vec<u64>>();
543 for layer in &node.neighbors {
544 total += layer.capacity() * size_of::<u64>();
545 }
546 }
547 total
548}
549
550fn index_catalog_bytes(catalog: &IndexCatalog) -> usize {
553 let mut total = 0;
554 for def in catalog.list() {
555 total += BTREE_PER_ENTRY + size_of::<String>() + def.name.capacity();
556 if let Some(label) = &def.label {
557 total += label.capacity();
558 }
559 for label in &def.additional_labels {
560 total += size_of::<String>() + label.capacity();
561 }
562 for property in &def.properties {
563 total += size_of::<String>() + property.capacity();
564 }
565 for key in def.options.keys() {
566 total += BTREE_PER_ENTRY + size_of::<String>() + key.capacity();
567 }
568 }
569 total
570}
571
572fn constraint_catalog_bytes(catalog: &ConstraintCatalog) -> usize {
573 let mut total = 0;
574 for def in catalog.list() {
575 total += BTREE_PER_ENTRY + size_of::<String>() + def.name.capacity() + def.label.capacity();
576 for property in &def.properties {
577 total += size_of::<String>() + property.capacity();
578 }
579 if let Some(idx) = &def.owned_index {
580 total += idx.capacity();
581 }
582 }
583 total
584}