Skip to main content

linkrs_storage/edge/node_group/
stats.rs

1//! Observability views: per-group stats and whole-set memory and
2//! fragmentation metrics summed across existing groups.
3
4use linkrs_core::types::EdgeStrategy;
5
6use super::super::{CsrBase, FragmentationStats, MutableCsrTrait};
7use super::{group_base, CsrShardSet};
8
9/// Observability view of one group.
10#[derive(Debug, Clone, Copy, PartialEq)]
11pub struct NodeGroupStats {
12    pub group: usize,
13    pub base: u32,
14    pub rows: usize,
15    pub live_edges: u64,
16    pub capacity: usize,
17    pub density: f32,
18    pub dirty: super::GroupDirty,
19}
20
21impl CsrShardSet {
22    /// Observability view of one group.
23    pub fn group_stats(&self, gid: usize) -> Option<NodeGroupStats> {
24        let shard = self.shards.get(&gid)?;
25        let live = shard.variant.edge_count();
26        let capacity = shard
27            .variant
28            .fragmentation_stats()
29            .map(|stats| stats.total_capacity)
30            .unwrap_or_else(|| shard.variant.vertex_capacity());
31        let density = if capacity == 0 {
32            1.0
33        } else {
34            live as f32 / capacity as f32
35        };
36        Some(NodeGroupStats {
37            group: gid,
38            base: group_base(gid, self.group_bits),
39            rows: self.group_size(),
40            live_edges: live,
41            capacity,
42            density,
43            dirty: shard.dirty,
44        })
45    }
46
47    /// Live edge count of one group for segment statistics.
48    pub fn group_live_count(&self, gid: usize) -> u64 {
49        self.shards
50            .get(&gid)
51            .map(|shard| shard.variant.edge_count())
52            .unwrap_or(0)
53    }
54
55    /// Average bytes per edge based on actual memory usage.
56    /// Measured value only, with no fallback: empty tables report zero, and
57    /// a degenerate zero measurement on a non-empty table reports zero with
58    /// a debug line. Callers handle zero explicitly.
59    pub fn bytes_per_edge(&self) -> usize {
60        FragmentationStats::measured_bytes_per_edge(self.used_memory_size(), self.edge_count())
61    }
62
63    /// Whole-set fragmentation statistics, summed across groups.
64    /// Observation metric only; collection triggers use per-vertex counts.
65    /// Sums every group reporting detailed stats; groups without reserved
66    /// row capacity contribute nothing. Returns none when no group reports.
67    pub fn fragmentation_stats(&self) -> Option<FragmentationStats> {
68        let mut total_capacity = 0usize;
69        let mut reachable_edges = 0usize;
70        let mut dead_entries = 0usize;
71        let mut wasted_capacity = 0usize;
72        let mut reported = false;
73        for shard in self.shards.values() {
74            if let Some(stats) = shard.variant.fragmentation_stats() {
75                reported = true;
76                total_capacity += stats.total_capacity;
77                reachable_edges += stats.reachable_edges;
78                dead_entries += stats.dead_entries;
79                wasted_capacity += stats.wasted_capacity;
80            }
81        }
82        if !reported {
83            return None;
84        }
85        Some(FragmentationStats::with_dead_info(
86            total_capacity,
87            reachable_edges,
88            dead_entries,
89            wasted_capacity,
90        ))
91    }
92
93    /// Whole-set fragmentation ratio, summed across groups.
94    /// Observation metric only; collection triggers use per-vertex counts.
95    /// Shares the single wasted-share caliber through the summed snapshot
96    /// below instead of a second aggregation loop.
97    pub fn fragmentation_ratio(&self) -> f32 {
98        self.fragmentation_stats()
99            .map(|stats| stats.fragmentation_ratio())
100            .unwrap_or(0.0)
101    }
102
103    /// Estimate wasted bytes due to fragmentation, summed across groups.
104    pub fn wasted_bytes_estimate(&self) -> usize {
105        self.shards
106            .values()
107            .map(|shard| shard.variant.wasted_bytes_estimate())
108            .sum()
109    }
110
111    /// Approximate memory usage in bytes, summed across existing groups.
112    /// Missing groups consume nothing, so sparse tables stay proportional to
113    /// materialized groups rather than the endpoint span. Derived serving
114    /// caches count on top of their authoritative heap form.
115    pub fn used_memory_size(&self) -> usize {
116        if self.strategy == EdgeStrategy::None {
117            return std::mem::size_of::<Self>();
118        }
119        self.shards
120            .values()
121            .map(|shard| {
122                shard.variant.used_memory_size()
123                    + shard.mapped.as_ref().map_or(0, |m| m.used_memory_size())
124            })
125            .sum::<usize>()
126            + std::mem::size_of::<Self>()
127    }
128
129    /// Narrow vs wide row counts summed across groups.
130    ///
131    /// Thresholds stay fixed; this only reports so slow traversals can be
132    /// attributed to a row width layer. Only multi-edge groups report;
133    /// other forms contribute zero.
134    pub fn layer_counts(&self) -> (usize, usize) {
135        let mut narrow = 0usize;
136        let mut wide = 0usize;
137        for shard in self.shards.values() {
138            if let super::super::CsrVariant::Multiple(csr) = &shard.variant {
139                let (n, w) = csr.layer_counts();
140                narrow += n;
141                wide += w;
142            }
143        }
144        (narrow, wide)
145    }
146}