kevy_vector/hnsw_stats.rs
1//! Read-only counters for [`Hnsw`] (child module via `#[path]`, the
2//! `segment_stats.rs` house pattern), split from `hnsw.rs` for the
3//! 500-LOC ceiling. The running-counter `stats()` and its walking
4//! reference live side by side so they cannot drift apart unnoticed.
5
6use super::{Hnsw, VectorStats};
7
8impl Hnsw {
9 /// Live (non-tombstoned) vectors — already tracked, so `O(1)`.
10 /// [`Self::stats`] walks every node and every link to estimate bytes;
11 /// a caller that only wants the count should not trigger that walk.
12 pub fn vectors(&self) -> u64 {
13 self.live
14 }
15
16 /// Counters — O(1): `links_total` and `tombstones`
17 /// are maintained at the three mutation sites (link push, shrink,
18 /// tombstoning) instead of walking every node per call (this ran
19 /// on every tiering tick). [`Self::recompute_stats`] is the
20 /// walking reference the tests hold them to.
21 pub fn stats(&self) -> VectorStats {
22 self.stats_from(self.links_total, self.tombstones)
23 }
24
25 fn stats_from(&self, links: u64, tombstones: u64) -> VectorStats {
26 let bytes_vec = (self.dim * 4) as u64;
27 let approx_bytes: u64 = self.nodes.len() as u64 * (bytes_vec + 40)
28 + links * 8
29 + self.live * 32;
30 VectorStats {
31 vectors: self.live,
32 tombstones,
33 links,
34 approx_bytes,
35 rebuild_recommended: !self.nodes.is_empty() && tombstones * 10 > self.nodes.len() as u64 * 3,
36 }
37 }
38
39 /// The walking reference — recomputes both counters from the live
40 /// graph. Test-only: production reads the running counters.
41 #[cfg(test)]
42 pub fn recompute_stats(&self) -> VectorStats {
43 let links: u64 = self.nodes.iter().map(|n| n.links.iter().map(Vec::len).sum::<usize>() as u64).sum();
44 let tombstones = self.nodes.iter().filter(|n| n.dead).count() as u64;
45 self.stats_from(links, tombstones)
46 }
47
48}