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