1use crate::storage::{BitPackedInts, DeltaBitPacked};
13use grafeo_common::types::{EdgeId, NodeId};
14use grafeo_common::utils::hash::{FxHashMap, FxHashSet};
15use parking_lot::RwLock;
16use smallvec::SmallVec;
17use std::sync::atomic::{AtomicUsize, Ordering};
18
19const DEFAULT_CHUNK_CAPACITY: usize = 64;
21
22const DELTA_COMPACTION_THRESHOLD: usize = 64;
27
28const COLD_COMPRESSION_THRESHOLD: usize = 4;
34
35#[derive(Debug, Clone)]
37struct AdjacencyChunk {
38 destinations: Vec<NodeId>,
40 edge_ids: Vec<EdgeId>,
42 capacity: usize,
44}
45
46impl AdjacencyChunk {
47 fn new(capacity: usize) -> Self {
48 Self {
49 destinations: Vec::with_capacity(capacity),
50 edge_ids: Vec::with_capacity(capacity),
51 capacity,
52 }
53 }
54
55 fn len(&self) -> usize {
56 self.destinations.len()
57 }
58
59 fn is_full(&self) -> bool {
60 self.destinations.len() >= self.capacity
61 }
62
63 fn push(&mut self, dst: NodeId, edge_id: EdgeId) -> bool {
64 if self.is_full() {
65 return false;
66 }
67 self.destinations.push(dst);
68 self.edge_ids.push(edge_id);
69 true
70 }
71
72 fn iter(&self) -> impl Iterator<Item = (NodeId, EdgeId)> + '_ {
73 self.destinations
74 .iter()
75 .copied()
76 .zip(self.edge_ids.iter().copied())
77 }
78
79 fn compress(&self) -> CompressedAdjacencyChunk {
84 let mut entries: Vec<_> = self
86 .destinations
87 .iter()
88 .copied()
89 .zip(self.edge_ids.iter().copied())
90 .collect();
91 entries.sort_by_key(|(dst, _)| dst.as_u64());
92
93 let sorted_dsts: Vec<u64> = entries.iter().map(|(d, _)| d.as_u64()).collect();
95 let sorted_edges: Vec<u64> = entries.iter().map(|(_, e)| e.as_u64()).collect();
96
97 let max_destination = sorted_dsts.last().copied().unwrap_or(0);
98
99 CompressedAdjacencyChunk {
100 destinations: DeltaBitPacked::encode(&sorted_dsts),
101 edge_ids: BitPackedInts::pack(&sorted_edges),
102 count: entries.len(),
103 max_destination,
104 }
105 }
106}
107
108#[derive(Debug, Clone)]
114struct CompressedAdjacencyChunk {
115 destinations: DeltaBitPacked,
117 edge_ids: BitPackedInts,
119 count: usize,
121 max_destination: u64,
123}
124
125impl CompressedAdjacencyChunk {
126 fn len(&self) -> usize {
128 self.count
129 }
130
131 #[cfg(test)]
133 fn is_empty(&self) -> bool {
134 self.count == 0
135 }
136
137 fn iter(&self) -> impl Iterator<Item = (NodeId, EdgeId)> + '_ {
139 let dsts = self.destinations.decode();
140 let edges = self.edge_ids.unpack();
141
142 dsts.into_iter()
143 .zip(edges)
144 .map(|(d, e)| (NodeId::new(d), EdgeId::new(e)))
145 }
146
147 fn memory_size(&self) -> usize {
149 let dest_size = 8 + self.destinations.to_bytes().len();
152 let edge_size = self.edge_ids.data().len() * 8;
153 dest_size + edge_size
154 }
155
156 #[must_use]
158 fn min_destination(&self) -> u64 {
159 self.destinations.base()
160 }
161
162 fn destinations_in_range(&self, min: u64, max: u64) -> Vec<(NodeId, EdgeId)> {
167 if min > self.max_destination || max < self.destinations.base() {
168 return Vec::new();
169 }
170 let destinations = self.destinations.decode();
171 let edges = self.edge_ids.unpack();
172 let start = destinations.partition_point(|&d| d < min);
173 let end = destinations.partition_point(|&d| d <= max);
174 destinations[start..end]
175 .iter()
176 .zip(&edges[start..end])
177 .map(|(&d, &e)| (NodeId::new(d), EdgeId::new(e)))
178 .collect()
179 }
180
181 #[cfg(test)]
183 fn compression_ratio(&self) -> f64 {
184 if self.count == 0 {
185 return 1.0;
186 }
187 let uncompressed = self.count * 16; let compressed = self.memory_size();
189 if compressed == 0 {
190 return f64::INFINITY;
191 }
192 uncompressed as f64 / compressed as f64
193 }
194}
195
196#[derive(Debug, Clone, Copy)]
202struct SkipIndexEntry {
203 min_destination: u64,
205 max_destination: u64,
207 chunk_index: usize,
209}
210
211#[derive(Debug)]
219struct AdjacencyList {
220 hot_chunks: Vec<AdjacencyChunk>,
222 cold_chunks: Vec<CompressedAdjacencyChunk>,
224 delta_inserts: SmallVec<[(NodeId, EdgeId); 16]>,
229 deleted: FxHashSet<EdgeId>,
231 skip_index: Vec<SkipIndexEntry>,
235}
236
237impl AdjacencyList {
238 fn new() -> Self {
239 Self {
240 hot_chunks: Vec::new(),
241 cold_chunks: Vec::new(),
242 delta_inserts: SmallVec::new(),
243 deleted: FxHashSet::default(),
244 skip_index: Vec::new(),
245 }
246 }
247
248 fn add_edge(&mut self, dst: NodeId, edge_id: EdgeId) {
249 if let Some(last) = self.hot_chunks.last_mut()
251 && last.push(dst, edge_id)
252 {
253 return;
254 }
255
256 self.delta_inserts.push((dst, edge_id));
258 }
259
260 fn mark_deleted(&mut self, edge_id: EdgeId) {
261 self.deleted.insert(edge_id);
262 }
263
264 fn compact(&mut self, chunk_capacity: usize) {
265 if self.delta_inserts.is_empty() {
266 return;
267 }
268
269 let last_has_room = self.hot_chunks.last().is_some_and(|c| !c.is_full());
272 let mut current_chunk = if last_has_room {
273 self.hot_chunks
275 .pop()
276 .expect("hot_chunks is non-empty: is_some_and() succeeded on previous line")
277 } else {
278 AdjacencyChunk::new(chunk_capacity)
279 };
280
281 for (dst, edge_id) in self.delta_inserts.drain(..) {
282 if !current_chunk.push(dst, edge_id) {
283 self.hot_chunks.push(current_chunk);
284 current_chunk = AdjacencyChunk::new(chunk_capacity);
285 current_chunk.push(dst, edge_id);
286 }
287 }
288
289 if current_chunk.len() > 0 {
290 self.hot_chunks.push(current_chunk);
291 }
292
293 self.maybe_compress_to_cold();
295 }
296
297 fn maybe_compress_to_cold(&mut self) {
302 while self.hot_chunks.len() > COLD_COMPRESSION_THRESHOLD {
304 let oldest = self.hot_chunks.remove(0);
306
307 if oldest.len() == 0 {
309 continue;
310 }
311
312 let compressed = oldest.compress();
314 let chunk_index = self.cold_chunks.len();
315 self.skip_index.push(SkipIndexEntry {
316 min_destination: compressed.min_destination(),
317 max_destination: compressed.max_destination,
318 chunk_index,
319 });
320 self.cold_chunks.push(compressed);
321 }
322 self.skip_index.sort_unstable_by_key(|e| e.min_destination);
324 }
325
326 fn freeze_all(&mut self) {
331 for chunk in self.hot_chunks.drain(..) {
332 if chunk.len() > 0 {
333 let compressed = chunk.compress();
334 let chunk_index = self.cold_chunks.len();
335 self.skip_index.push(SkipIndexEntry {
336 min_destination: compressed.min_destination(),
337 max_destination: compressed.max_destination,
338 chunk_index,
339 });
340 self.cold_chunks.push(compressed);
341 }
342 }
343 self.skip_index.sort_unstable_by_key(|e| e.min_destination);
344 }
345
346 fn iter(&self) -> impl Iterator<Item = (NodeId, EdgeId)> + '_ {
347 let deleted = &self.deleted;
348
349 let cold_iter = self.cold_chunks.iter().flat_map(|c| c.iter());
351
352 let hot_iter = self.hot_chunks.iter().flat_map(|c| c.iter());
354
355 let delta_iter = self.delta_inserts.iter().copied();
357
358 cold_iter
359 .chain(hot_iter)
360 .chain(delta_iter)
361 .filter(move |(_, edge_id)| !deleted.contains(edge_id))
362 }
363
364 fn contains(&self, destination: NodeId) -> bool {
370 let dst_raw = destination.as_u64();
371 let deleted = &self.deleted;
372
373 for entry in &self.skip_index {
375 if dst_raw < entry.min_destination || dst_raw > entry.max_destination {
376 continue;
377 }
378 let chunk = &self.cold_chunks[entry.chunk_index];
379 let decoded_dsts = chunk.destinations.decode();
380 let decoded_edges = chunk.edge_ids.unpack();
381 if let Ok(pos) = decoded_dsts.binary_search(&dst_raw) {
382 let mut i = pos;
384 while i > 0 && decoded_dsts[i - 1] == dst_raw {
385 i -= 1;
386 }
387 for j in i..decoded_dsts.len() {
388 if decoded_dsts[j] != dst_raw {
389 break;
390 }
391 if !deleted.contains(&EdgeId::new(decoded_edges[j])) {
392 return true;
393 }
394 }
395 }
396 }
397
398 for chunk in &self.hot_chunks {
400 for (dst, edge_id) in chunk.iter() {
401 if dst == destination && !deleted.contains(&edge_id) {
402 return true;
403 }
404 }
405 }
406
407 for &(dst, edge_id) in &self.delta_inserts {
409 if dst == destination && !deleted.contains(&edge_id) {
410 return true;
411 }
412 }
413
414 false
415 }
416
417 fn destinations_in_range(&self, min: NodeId, max: NodeId) -> Vec<(NodeId, EdgeId)> {
422 let min_raw = min.as_u64();
423 let max_raw = max.as_u64();
424 let deleted = &self.deleted;
425 let mut results = Vec::new();
426
427 for entry in &self.skip_index {
429 if entry.max_destination < min_raw || entry.min_destination > max_raw {
430 continue;
431 }
432 let chunk = &self.cold_chunks[entry.chunk_index];
433 results.extend(
434 chunk
435 .destinations_in_range(min_raw, max_raw)
436 .into_iter()
437 .filter(|(_, eid)| !deleted.contains(eid)),
438 );
439 }
440
441 for chunk in &self.hot_chunks {
443 for (dst, edge_id) in chunk.iter() {
444 if dst.as_u64() >= min_raw && dst.as_u64() <= max_raw && !deleted.contains(&edge_id)
445 {
446 results.push((dst, edge_id));
447 }
448 }
449 }
450
451 for &(dst, edge_id) in &self.delta_inserts {
453 if dst.as_u64() >= min_raw && dst.as_u64() <= max_raw && !deleted.contains(&edge_id) {
454 results.push((dst, edge_id));
455 }
456 }
457
458 results
459 }
460
461 fn neighbors(&self) -> impl Iterator<Item = NodeId> + '_ {
462 self.iter().map(|(dst, _)| dst)
463 }
464
465 fn degree(&self) -> usize {
466 self.iter().count()
467 }
468
469 fn hot_count(&self) -> usize {
471 self.hot_chunks.iter().map(|c| c.len()).sum::<usize>() + self.delta_inserts.len()
472 }
473
474 fn cold_count(&self) -> usize {
476 self.cold_chunks.iter().map(|c| c.len()).sum()
477 }
478
479 #[cfg(test)]
481 fn memory_size(&self) -> usize {
482 let hot_size = self.hot_chunks.iter().map(|c| c.len() * 16).sum::<usize>();
484
485 let cold_size = self
487 .cold_chunks
488 .iter()
489 .map(|c| c.memory_size())
490 .sum::<usize>();
491
492 let delta_size = self.delta_inserts.len() * 16;
494
495 let deleted_size = self.deleted.len() * 16;
497
498 hot_size + cold_size + delta_size + deleted_size
499 }
500}
501
502pub struct ChunkedAdjacency {
526 lists: RwLock<FxHashMap<NodeId, AdjacencyList>>,
529 chunk_capacity: usize,
531 edge_count: AtomicUsize,
533 deleted_count: AtomicUsize,
535}
536
537impl ChunkedAdjacency {
538 #[must_use]
540 pub fn new() -> Self {
541 Self::with_chunk_capacity(DEFAULT_CHUNK_CAPACITY)
542 }
543
544 #[must_use]
546 pub fn with_chunk_capacity(capacity: usize) -> Self {
547 Self {
548 lists: RwLock::new(FxHashMap::default()),
549 chunk_capacity: capacity,
550 edge_count: AtomicUsize::new(0),
551 deleted_count: AtomicUsize::new(0),
552 }
553 }
554
555 pub fn add_edge(&self, src: NodeId, dst: NodeId, edge_id: EdgeId) {
557 let mut lists = self.lists.write();
558 lists
559 .entry(src)
560 .or_insert_with(AdjacencyList::new)
561 .add_edge(dst, edge_id);
562 self.edge_count.fetch_add(1, Ordering::Relaxed);
563 }
564
565 pub fn batch_add_edges(&self, edges: &[(NodeId, NodeId, EdgeId)]) {
572 if edges.is_empty() {
573 return;
574 }
575 let mut lists = self.lists.write();
576 for &(src, dst, edge_id) in edges {
577 lists
578 .entry(src)
579 .or_insert_with(AdjacencyList::new)
580 .add_edge(dst, edge_id);
581 }
582 self.edge_count.fetch_add(edges.len(), Ordering::Relaxed);
583
584 for list in lists.values_mut() {
586 if list.delta_inserts.len() >= DELTA_COMPACTION_THRESHOLD {
587 list.compact(self.chunk_capacity);
588 }
589 }
590 }
591
592 pub fn mark_deleted(&self, src: NodeId, edge_id: EdgeId) {
594 let mut lists = self.lists.write();
595 if let Some(list) = lists.get_mut(&src) {
596 list.mark_deleted(edge_id);
597 self.deleted_count.fetch_add(1, Ordering::Relaxed);
598 }
599 }
600
601 pub fn unmark_deleted(&self, src: NodeId, edge_id: EdgeId) {
605 let mut lists = self.lists.write();
606 if let Some(list) = lists.get_mut(&src)
607 && list.deleted.remove(&edge_id)
608 {
609 self.deleted_count.fetch_sub(1, Ordering::Relaxed);
610 }
611 }
612
613 #[must_use]
619 pub fn neighbors(&self, src: NodeId) -> Vec<NodeId> {
620 let lists = self.lists.read();
621 lists
622 .get(&src)
623 .map(|list| list.neighbors().collect())
624 .unwrap_or_default()
625 }
626
627 #[must_use]
633 pub fn edges_from(&self, src: NodeId) -> Vec<(NodeId, EdgeId)> {
634 let lists = self.lists.read();
635 lists
636 .get(&src)
637 .map(|list| list.iter().collect())
638 .unwrap_or_default()
639 }
640
641 pub fn out_degree(&self, src: NodeId) -> usize {
645 let lists = self.lists.read();
646 lists.get(&src).map_or(0, |list| list.degree())
647 }
648
649 pub fn in_degree(&self, node: NodeId) -> usize {
656 let lists = self.lists.read();
657 lists.get(&node).map_or(0, |list| list.degree())
658 }
659
660 pub fn compact(&self) {
662 let mut lists = self.lists.write();
663 for list in lists.values_mut() {
664 list.compact(self.chunk_capacity);
665 }
666 }
667
668 pub fn compact_if_needed(&self) {
670 let mut lists = self.lists.write();
671 for list in lists.values_mut() {
672 if list.delta_inserts.len() >= DELTA_COMPACTION_THRESHOLD {
673 list.compact(self.chunk_capacity);
674 }
675 }
676 }
677
678 pub fn total_edge_count(&self) -> usize {
680 self.edge_count.load(Ordering::Relaxed)
681 }
682
683 pub fn active_edge_count(&self) -> usize {
685 self.edge_count.load(Ordering::Relaxed) - self.deleted_count.load(Ordering::Relaxed)
686 }
687
688 pub fn node_count(&self) -> usize {
690 self.lists.read().len()
691 }
692
693 #[must_use]
699 pub fn contains_edge(&self, src: NodeId, dst: NodeId) -> bool {
700 let lists = self.lists.read();
701 lists.get(&src).is_some_and(|list| list.contains(dst))
702 }
703
704 #[must_use]
708 pub fn edges_in_range(
709 &self,
710 src: NodeId,
711 min_dst: NodeId,
712 max_dst: NodeId,
713 ) -> Vec<(NodeId, EdgeId)> {
714 let lists = self.lists.read();
715 lists
716 .get(&src)
717 .map(|list| list.destinations_in_range(min_dst, max_dst))
718 .unwrap_or_default()
719 }
720
721 pub fn clear(&self) {
723 let mut lists = self.lists.write();
724 lists.clear();
725 self.edge_count.store(0, Ordering::Relaxed);
726 self.deleted_count.store(0, Ordering::Relaxed);
727 }
728
729 #[must_use]
731 pub fn memory_stats(&self) -> AdjacencyMemoryStats {
732 let lists = self.lists.read();
733
734 let mut hot_entries = 0usize;
735 let mut cold_entries = 0usize;
736 let mut hot_bytes = 0usize;
737 let mut cold_bytes = 0usize;
738
739 for list in lists.values() {
740 hot_entries += list.hot_count();
741 cold_entries += list.cold_count();
742
743 hot_bytes += list.hot_count() * 16;
745
746 for cold_chunk in &list.cold_chunks {
748 cold_bytes += cold_chunk.memory_size();
749 }
750 }
751
752 AdjacencyMemoryStats {
753 hot_entries,
754 cold_entries,
755 hot_bytes,
756 cold_bytes,
757 node_count: lists.len(),
758 }
759 }
760
761 #[must_use]
763 pub fn heap_memory_bytes(&self) -> usize {
764 let lists = self.lists.read();
765 let map_overhead = lists.capacity()
767 * (std::mem::size_of::<NodeId>() + std::mem::size_of::<AdjacencyList>() + 1);
768 let mut list_bytes = 0usize;
770 for list in lists.values() {
771 list_bytes += list.hot_chunks.capacity() * std::mem::size_of::<AdjacencyChunk>();
773 for chunk in &list.hot_chunks {
774 list_bytes += chunk.destinations.capacity() * std::mem::size_of::<NodeId>();
775 list_bytes += chunk.edge_ids.capacity() * std::mem::size_of::<EdgeId>();
776 }
777 list_bytes +=
779 list.cold_chunks.capacity() * std::mem::size_of::<CompressedAdjacencyChunk>();
780 for cold in &list.cold_chunks {
781 list_bytes += cold.memory_size();
782 }
783 if list.delta_inserts.spilled() {
785 list_bytes += list.delta_inserts.capacity() * 16;
786 }
787 list_bytes += list.deleted.capacity() * (std::mem::size_of::<EdgeId>() + 1);
789 list_bytes += list.skip_index.capacity() * std::mem::size_of::<SkipIndexEntry>();
791 }
792 map_overhead + list_bytes
793 }
794
795 pub fn freeze_all(&self) {
799 let mut lists = self.lists.write();
800 for list in lists.values_mut() {
801 list.freeze_all();
802 }
803 }
804}
805
806#[derive(Debug, Clone)]
808pub struct AdjacencyMemoryStats {
809 pub hot_entries: usize,
811 pub cold_entries: usize,
813 pub hot_bytes: usize,
815 pub cold_bytes: usize,
817 pub node_count: usize,
819}
820
821impl AdjacencyMemoryStats {
822 #[must_use]
824 pub fn total_entries(&self) -> usize {
825 self.hot_entries + self.cold_entries
826 }
827
828 #[must_use]
830 pub fn total_bytes(&self) -> usize {
831 self.hot_bytes + self.cold_bytes
832 }
833
834 #[must_use]
838 pub fn cold_compression_ratio(&self) -> f64 {
839 if self.cold_entries == 0 || self.cold_bytes == 0 {
840 return 1.0;
841 }
842 let uncompressed = self.cold_entries * 16;
843 uncompressed as f64 / self.cold_bytes as f64
844 }
845
846 #[must_use]
848 pub fn overall_compression_ratio(&self) -> f64 {
849 let total_entries = self.total_entries();
850 if total_entries == 0 || self.total_bytes() == 0 {
851 return 1.0;
852 }
853 let uncompressed = total_entries * 16;
854 uncompressed as f64 / self.total_bytes() as f64
855 }
856}
857
858impl Default for ChunkedAdjacency {
859 fn default() -> Self {
860 Self::new()
861 }
862}
863
864#[cfg(test)]
865mod tests {
866 use super::*;
867
868 #[test]
869 fn test_basic_adjacency() {
870 let adj = ChunkedAdjacency::new();
871
872 adj.add_edge(NodeId::new(0), NodeId::new(1), EdgeId::new(0));
873 adj.add_edge(NodeId::new(0), NodeId::new(2), EdgeId::new(1));
874 adj.add_edge(NodeId::new(0), NodeId::new(3), EdgeId::new(2));
875
876 let neighbors = adj.neighbors(NodeId::new(0));
877 assert_eq!(neighbors.len(), 3);
878 assert!(neighbors.contains(&NodeId::new(1)));
879 assert!(neighbors.contains(&NodeId::new(2)));
880 assert!(neighbors.contains(&NodeId::new(3)));
881 }
882
883 #[test]
884 fn test_out_degree() {
885 let adj = ChunkedAdjacency::new();
886
887 adj.add_edge(NodeId::new(0), NodeId::new(1), EdgeId::new(0));
888 adj.add_edge(NodeId::new(0), NodeId::new(2), EdgeId::new(1));
889
890 assert_eq!(adj.out_degree(NodeId::new(0)), 2);
891 assert_eq!(adj.out_degree(NodeId::new(1)), 0);
892 }
893
894 #[test]
895 fn test_mark_deleted() {
896 let adj = ChunkedAdjacency::new();
897
898 adj.add_edge(NodeId::new(0), NodeId::new(1), EdgeId::new(0));
899 adj.add_edge(NodeId::new(0), NodeId::new(2), EdgeId::new(1));
900
901 adj.mark_deleted(NodeId::new(0), EdgeId::new(0));
902
903 let neighbors = adj.neighbors(NodeId::new(0));
904 assert_eq!(neighbors.len(), 1);
905 assert!(neighbors.contains(&NodeId::new(2)));
906 }
907
908 #[test]
909 fn test_edges_from() {
910 let adj = ChunkedAdjacency::new();
911
912 adj.add_edge(NodeId::new(0), NodeId::new(1), EdgeId::new(10));
913 adj.add_edge(NodeId::new(0), NodeId::new(2), EdgeId::new(20));
914
915 let edges = adj.edges_from(NodeId::new(0));
916 assert_eq!(edges.len(), 2);
917 assert!(edges.contains(&(NodeId::new(1), EdgeId::new(10))));
918 assert!(edges.contains(&(NodeId::new(2), EdgeId::new(20))));
919 }
920
921 #[test]
922 fn test_compaction() {
923 let adj = ChunkedAdjacency::with_chunk_capacity(4);
924
925 for i in 0..10 {
927 adj.add_edge(NodeId::new(0), NodeId::new(i + 1), EdgeId::new(i));
928 }
929
930 adj.compact();
931
932 let neighbors = adj.neighbors(NodeId::new(0));
934 assert_eq!(neighbors.len(), 10);
935 }
936
937 #[test]
938 fn test_edge_counts() {
939 let adj = ChunkedAdjacency::new();
940
941 adj.add_edge(NodeId::new(0), NodeId::new(1), EdgeId::new(0));
942 adj.add_edge(NodeId::new(0), NodeId::new(2), EdgeId::new(1));
943 adj.add_edge(NodeId::new(1), NodeId::new(2), EdgeId::new(2));
944
945 assert_eq!(adj.total_edge_count(), 3);
946 assert_eq!(adj.active_edge_count(), 3);
947
948 adj.mark_deleted(NodeId::new(0), EdgeId::new(0));
949
950 assert_eq!(adj.total_edge_count(), 3);
951 assert_eq!(adj.active_edge_count(), 2);
952 }
953
954 #[test]
955 fn test_clear() {
956 let adj = ChunkedAdjacency::new();
957
958 adj.add_edge(NodeId::new(0), NodeId::new(1), EdgeId::new(0));
959 adj.add_edge(NodeId::new(0), NodeId::new(2), EdgeId::new(1));
960
961 adj.clear();
962
963 assert_eq!(adj.total_edge_count(), 0);
964 assert_eq!(adj.node_count(), 0);
965 }
966
967 #[test]
968 fn test_chunk_compression() {
969 let mut chunk = AdjacencyChunk::new(64);
971
972 for i in 0..20 {
974 chunk.push(NodeId::new(100 + i * 5), EdgeId::new(1000 + i));
975 }
976
977 let compressed = chunk.compress();
979
980 assert_eq!(compressed.len(), 20);
982
983 let entries: Vec<_> = compressed.iter().collect();
985 assert_eq!(entries.len(), 20);
986
987 for window in entries.windows(2) {
990 assert!(window[0].0.as_u64() <= window[1].0.as_u64());
991 }
992
993 let original_dsts: std::collections::HashSet<_> =
995 (0..20).map(|i| NodeId::new(100 + i * 5)).collect();
996 let compressed_dsts: std::collections::HashSet<_> =
997 entries.iter().map(|(d, _)| *d).collect();
998 assert_eq!(original_dsts, compressed_dsts);
999
1000 let ratio = compressed.compression_ratio();
1002 assert!(
1003 ratio > 1.0,
1004 "Expected compression ratio > 1.0, got {}",
1005 ratio
1006 );
1007 }
1008
1009 #[test]
1010 fn test_empty_chunk_compression() {
1011 let chunk = AdjacencyChunk::new(64);
1012 let compressed = chunk.compress();
1013
1014 assert_eq!(compressed.len(), 0);
1015 assert!(compressed.is_empty());
1016 assert_eq!(compressed.iter().count(), 0);
1017 }
1018
1019 #[test]
1020 fn test_hot_to_cold_migration() {
1021 let adj = ChunkedAdjacency::with_chunk_capacity(8);
1023
1024 for i in 0..100 {
1028 adj.add_edge(NodeId::new(0), NodeId::new(i + 1), EdgeId::new(i));
1029 }
1030
1031 adj.compact();
1033
1034 let neighbors = adj.neighbors(NodeId::new(0));
1036 assert_eq!(neighbors.len(), 100);
1037
1038 let stats = adj.memory_stats();
1040 assert_eq!(stats.total_entries(), 100);
1041
1042 assert!(
1045 stats.cold_entries > 0,
1046 "Expected some cold entries, got {}",
1047 stats.cold_entries
1048 );
1049 }
1050
1051 #[test]
1052 fn test_memory_stats() {
1053 let adj = ChunkedAdjacency::with_chunk_capacity(8);
1054
1055 for i in 0..20 {
1057 adj.add_edge(NodeId::new(0), NodeId::new(i + 1), EdgeId::new(i));
1058 }
1059
1060 adj.compact();
1061
1062 let stats = adj.memory_stats();
1063 assert_eq!(stats.total_entries(), 20);
1064 assert_eq!(stats.node_count, 1);
1065 assert!(stats.total_bytes() > 0);
1066 }
1067
1068 #[test]
1069 fn test_freeze_all() {
1070 let adj = ChunkedAdjacency::with_chunk_capacity(8);
1071
1072 for i in 0..30 {
1074 adj.add_edge(NodeId::new(0), NodeId::new(i + 1), EdgeId::new(i));
1075 }
1076
1077 adj.compact();
1078
1079 let before = adj.memory_stats();
1081
1082 adj.freeze_all();
1084
1085 let after = adj.memory_stats();
1087
1088 assert_eq!(after.hot_entries, 0);
1090 assert_eq!(after.cold_entries, before.total_entries());
1091
1092 let neighbors = adj.neighbors(NodeId::new(0));
1094 assert_eq!(neighbors.len(), 30);
1095 }
1096
1097 #[test]
1098 fn test_cold_compression_ratio() {
1099 let adj = ChunkedAdjacency::with_chunk_capacity(8);
1100
1101 for i in 0..200 {
1103 adj.add_edge(NodeId::new(0), NodeId::new(100 + i), EdgeId::new(i));
1104 }
1105
1106 adj.compact();
1107 adj.freeze_all();
1108
1109 let stats = adj.memory_stats();
1110
1111 let ratio = stats.cold_compression_ratio();
1113 assert!(
1114 ratio > 1.5,
1115 "Expected cold compression ratio > 1.5, got {}",
1116 ratio
1117 );
1118 }
1119
1120 #[test]
1121 fn test_deleted_edges_with_cold_storage() {
1122 let adj = ChunkedAdjacency::with_chunk_capacity(8);
1123
1124 for i in 0..50 {
1126 adj.add_edge(NodeId::new(0), NodeId::new(i + 1), EdgeId::new(i));
1127 }
1128
1129 adj.compact();
1130
1131 for i in (0..50).step_by(2) {
1133 adj.mark_deleted(NodeId::new(0), EdgeId::new(i));
1134 }
1135
1136 let neighbors = adj.neighbors(NodeId::new(0));
1138 assert_eq!(neighbors.len(), 25);
1139
1140 for neighbor in neighbors {
1142 assert!(neighbor.as_u64() % 2 == 0); }
1144 }
1145
1146 #[test]
1147 fn test_adjacency_list_memory_size() {
1148 let mut list = AdjacencyList::new();
1149
1150 for i in 0..50 {
1152 list.add_edge(NodeId::new(i + 1), EdgeId::new(i));
1153 }
1154
1155 list.compact(8);
1157
1158 let size = list.memory_size();
1159 assert!(size > 0);
1160
1161 assert!(size <= 50 * 16 + 200); }
1165
1166 #[test]
1167 fn test_cold_iteration_order() {
1168 let adj = ChunkedAdjacency::with_chunk_capacity(8);
1169
1170 for i in 0..50 {
1172 adj.add_edge(NodeId::new(0), NodeId::new(i + 1), EdgeId::new(i));
1173 }
1174
1175 adj.compact();
1176
1177 let edges = adj.edges_from(NodeId::new(0));
1179
1180 assert_eq!(edges.len(), 50);
1182
1183 let edge_ids: std::collections::HashSet<_> = edges.iter().map(|(_, e)| *e).collect();
1185 for i in 0..50 {
1186 assert!(edge_ids.contains(&EdgeId::new(i)));
1187 }
1188 }
1189
1190 #[test]
1191 fn test_in_degree() {
1192 let adj = ChunkedAdjacency::new();
1193
1194 adj.add_edge(NodeId::new(2), NodeId::new(1), EdgeId::new(0)); adj.add_edge(NodeId::new(2), NodeId::new(3), EdgeId::new(1)); assert_eq!(adj.in_degree(NodeId::new(2)), 2);
1202
1203 assert_eq!(adj.in_degree(NodeId::new(1)), 0);
1205 }
1206
1207 #[test]
1208 fn test_bidirectional_edges() {
1209 let forward = ChunkedAdjacency::new();
1210 let backward = ChunkedAdjacency::new();
1211
1212 let edge_id = EdgeId::new(100);
1214 forward.add_edge(NodeId::new(1), NodeId::new(2), edge_id);
1215 backward.add_edge(NodeId::new(2), NodeId::new(1), edge_id); let forward_edges = forward.edges_from(NodeId::new(1));
1219 assert_eq!(forward_edges.len(), 1);
1220 assert_eq!(forward_edges[0], (NodeId::new(2), edge_id));
1221
1222 assert_eq!(forward.edges_from(NodeId::new(2)).len(), 0);
1224
1225 let backward_edges = backward.edges_from(NodeId::new(2));
1227 assert_eq!(backward_edges.len(), 1);
1228 assert_eq!(backward_edges[0], (NodeId::new(1), edge_id));
1229
1230 assert_eq!(backward.edges_from(NodeId::new(1)).len(), 0);
1232 }
1233
1234 #[test]
1235 fn test_bidirectional_chain() {
1236 let forward = ChunkedAdjacency::new();
1238 let backward = ChunkedAdjacency::new();
1239
1240 let a = NodeId::new(1);
1241 let b = NodeId::new(2);
1242 let c = NodeId::new(3);
1243
1244 let edge_ab = EdgeId::new(10);
1246 forward.add_edge(a, b, edge_ab);
1247 backward.add_edge(b, a, edge_ab);
1248
1249 let edge_bc = EdgeId::new(20);
1251 forward.add_edge(b, c, edge_bc);
1252 backward.add_edge(c, b, edge_bc);
1253
1254 let from_a = forward.edges_from(a);
1256 assert_eq!(from_a.len(), 1);
1257 assert_eq!(from_a[0].0, b);
1258
1259 let from_b = forward.edges_from(b);
1261 assert_eq!(from_b.len(), 1);
1262 assert_eq!(from_b[0].0, c);
1263
1264 let to_c = backward.edges_from(c);
1266 assert_eq!(to_c.len(), 1);
1267 assert_eq!(to_c[0].0, b);
1268
1269 let to_b = backward.edges_from(b);
1271 assert_eq!(to_b.len(), 1);
1272 assert_eq!(to_b[0].0, a);
1273
1274 assert_eq!(backward.edges_from(a).len(), 0);
1276
1277 assert_eq!(forward.edges_from(c).len(), 0);
1279 }
1280
1281 #[test]
1284 fn test_contains_edge_basic() {
1285 let adj = ChunkedAdjacency::new();
1286
1287 adj.add_edge(NodeId::new(0), NodeId::new(1), EdgeId::new(0));
1288 adj.add_edge(NodeId::new(0), NodeId::new(2), EdgeId::new(1));
1289 adj.add_edge(NodeId::new(0), NodeId::new(3), EdgeId::new(2));
1290
1291 assert!(adj.contains_edge(NodeId::new(0), NodeId::new(1)));
1292 assert!(adj.contains_edge(NodeId::new(0), NodeId::new(2)));
1293 assert!(adj.contains_edge(NodeId::new(0), NodeId::new(3)));
1294 assert!(!adj.contains_edge(NodeId::new(0), NodeId::new(4)));
1295 assert!(!adj.contains_edge(NodeId::new(1), NodeId::new(0)));
1296 }
1297
1298 #[test]
1299 fn test_contains_edge_after_delete() {
1300 let adj = ChunkedAdjacency::new();
1301
1302 adj.add_edge(NodeId::new(0), NodeId::new(1), EdgeId::new(10));
1303 adj.add_edge(NodeId::new(0), NodeId::new(2), EdgeId::new(20));
1304
1305 assert!(adj.contains_edge(NodeId::new(0), NodeId::new(1)));
1306
1307 adj.mark_deleted(NodeId::new(0), EdgeId::new(10));
1308
1309 assert!(!adj.contains_edge(NodeId::new(0), NodeId::new(1)));
1310 assert!(adj.contains_edge(NodeId::new(0), NodeId::new(2)));
1311 }
1312
1313 #[test]
1314 fn test_contains_edge_in_cold_storage() {
1315 let adj = ChunkedAdjacency::with_chunk_capacity(8);
1316
1317 for i in 0..100 {
1319 adj.add_edge(NodeId::new(0), NodeId::new(100 + i), EdgeId::new(i));
1320 }
1321
1322 adj.compact();
1323 adj.freeze_all();
1324
1325 for i in 0..100 {
1327 assert!(
1328 adj.contains_edge(NodeId::new(0), NodeId::new(100 + i)),
1329 "Should find destination {} in cold storage",
1330 100 + i
1331 );
1332 }
1333
1334 assert!(!adj.contains_edge(NodeId::new(0), NodeId::new(0)));
1336 assert!(!adj.contains_edge(NodeId::new(0), NodeId::new(99)));
1337 assert!(!adj.contains_edge(NodeId::new(0), NodeId::new(200)));
1338 }
1339
1340 #[test]
1341 fn test_contains_edge_in_delta_only() {
1342 let adj = ChunkedAdjacency::new();
1343
1344 adj.add_edge(NodeId::new(0), NodeId::new(5), EdgeId::new(0));
1346 adj.add_edge(NodeId::new(0), NodeId::new(10), EdgeId::new(1));
1347
1348 assert!(adj.contains_edge(NodeId::new(0), NodeId::new(5)));
1349 assert!(adj.contains_edge(NodeId::new(0), NodeId::new(10)));
1350 assert!(!adj.contains_edge(NodeId::new(0), NodeId::new(7)));
1351 }
1352
1353 #[test]
1354 fn test_edges_in_range() {
1355 let adj = ChunkedAdjacency::with_chunk_capacity(8);
1356
1357 for i in 0..100 {
1359 adj.add_edge(NodeId::new(0), NodeId::new(100 + i), EdgeId::new(i));
1360 }
1361
1362 adj.compact();
1363
1364 let results = adj.edges_in_range(NodeId::new(0), NodeId::new(130), NodeId::new(140));
1366 assert_eq!(
1367 results.len(),
1368 11,
1369 "Expected 11 edges in range [130, 140], got {}",
1370 results.len()
1371 );
1372
1373 for (dst, _) in &results {
1375 assert!(dst.as_u64() >= 130 && dst.as_u64() <= 140);
1376 }
1377
1378 let empty = adj.edges_in_range(NodeId::new(0), NodeId::new(200), NodeId::new(300));
1380 assert!(empty.is_empty());
1381 }
1382
1383 #[test]
1384 fn test_skip_index_prunes_chunks() {
1385 let adj = ChunkedAdjacency::with_chunk_capacity(8);
1386
1387 for i in 0..8 {
1390 adj.add_edge(NodeId::new(0), NodeId::new(100 + i), EdgeId::new(i));
1391 }
1392 adj.compact();
1393
1394 for i in 0..8 {
1395 adj.add_edge(NodeId::new(0), NodeId::new(200 + i), EdgeId::new(100 + i));
1396 }
1397 adj.compact();
1398
1399 for i in 0..8 {
1400 adj.add_edge(NodeId::new(0), NodeId::new(300 + i), EdgeId::new(200 + i));
1401 }
1402 adj.compact();
1403 adj.freeze_all();
1404
1405 assert!(adj.contains_edge(NodeId::new(0), NodeId::new(103)));
1407 assert!(adj.contains_edge(NodeId::new(0), NodeId::new(205)));
1408 assert!(adj.contains_edge(NodeId::new(0), NodeId::new(307)));
1409
1410 assert!(!adj.contains_edge(NodeId::new(0), NodeId::new(150)));
1412 assert!(!adj.contains_edge(NodeId::new(0), NodeId::new(250)));
1413
1414 let range_b = adj.edges_in_range(NodeId::new(0), NodeId::new(200), NodeId::new(207));
1416 assert_eq!(range_b.len(), 8);
1417 }
1418
1419 #[test]
1420 fn test_contains_after_freeze_all() {
1421 let adj = ChunkedAdjacency::with_chunk_capacity(8);
1422
1423 for i in 0..50 {
1424 adj.add_edge(NodeId::new(0), NodeId::new(i + 1), EdgeId::new(i));
1425 }
1426
1427 adj.compact();
1428 adj.freeze_all();
1429
1430 let stats = adj.memory_stats();
1432 assert_eq!(stats.hot_entries, 0);
1433 assert_eq!(stats.cold_entries, 50);
1434
1435 for i in 0..50 {
1437 assert!(
1438 adj.contains_edge(NodeId::new(0), NodeId::new(i + 1)),
1439 "Should find destination {} after freeze_all",
1440 i + 1
1441 );
1442 }
1443 }
1444}