1use std::collections::BTreeMap;
10use std::time::Instant;
11
12use crate::stats::{KeyStats, StatsTable};
13
14fn accumulate(node: &mut TreeNode, s: &KeyStats) {
16 node.subtree_count += s.count;
17 node.subtree_bytes += s.bytes;
18 node.subtree_rate_hz += s.rate_hz;
19 node.subtree_keys += 1;
20 node.subtree_last_seen = node.subtree_last_seen.max(Some(s.last_seen));
21}
22
23#[derive(Debug, Clone, Default)]
30pub struct TreeNode {
31 pub children: BTreeMap<String, TreeNode>,
32 pub count: u64,
34 pub bytes: u64,
35 pub rate_hz: f64,
36 pub last_seen: Option<Instant>,
38 pub subtree_count: u64,
40 pub subtree_bytes: u64,
41 pub subtree_rate_hz: f64,
42 pub subtree_last_seen: Option<Instant>,
44 pub subtree_keys: usize,
46}
47
48#[derive(Debug, Clone, Default)]
57pub struct KeyTreeSnapshot {
58 pub root: TreeNode,
59 pub keys: usize,
60 pub evicted: u64,
62 pub unwatched: u64,
64}
65
66impl KeyTreeSnapshot {
67 pub fn build(stats: &StatsTable) -> KeyTreeSnapshot {
70 let mut root = TreeNode::default();
71 for (key, s) in stats.iter() {
72 let mut node = &mut root;
73 accumulate(node, s);
74 for chunk in key.split('/') {
75 if !node.children.contains_key(chunk) {
83 node.children.insert(chunk.to_string(), TreeNode::default());
84 }
85 node = node
86 .children
87 .get_mut(chunk)
88 .expect("just inserted if it was missing");
89 accumulate(node, s);
90 }
91 node.count = s.count;
92 node.bytes = s.bytes;
93 node.rate_hz = s.rate_hz;
94 node.last_seen = Some(s.last_seen);
95 }
96 KeyTreeSnapshot {
97 root,
98 keys: stats.len(),
99 evicted: stats.evicted(),
100 unwatched: stats.unwatched(),
101 }
102 }
103
104 pub fn node(&self, path: &[&str]) -> Option<&TreeNode> {
106 let mut node = &self.root;
107 for chunk in path {
108 node = node.children.get(*chunk)?;
109 }
110 Some(node)
111 }
112}
113
114#[cfg(test)]
115mod tests {
116 use super::*;
117 use std::time::Instant;
118
119 #[test]
120 fn builds_grouped_counts() {
121 let mut stats = StatsTable::new();
122 let now = Instant::now();
123 stats.record("zs/v1/h-a/telemetry/x/m1", 4, None, now, None);
124 stats.record("zs/v1/h-a/telemetry/x/m1", 4, None, now, None);
125 stats.record("zs/v1/h-a/telemetry/x/m2", 4, None, now, None);
126 stats.record("zs/v1/h-b/state/x/health", 4, None, now, None);
127
128 let snap = KeyTreeSnapshot::build(&stats);
129 assert_eq!(snap.keys, 3);
130 assert_eq!(snap.root.subtree_count, 4);
131 let telemetry = snap.node(&["zs", "v1", "h-a", "telemetry", "x"]).unwrap();
132 assert_eq!(telemetry.subtree_count, 3);
133 let m1 = snap
134 .node(&["zs", "v1", "h-a", "telemetry", "x", "m1"])
135 .unwrap();
136 assert_eq!(m1.count, 2);
137 assert_eq!(m1.bytes, 8);
138 assert!(snap.node(&["zs", "v1", "h-c"]).is_none());
139 }
140
141 #[test]
144 fn collapsed_nodes_aggregate_their_subtree() {
145 let mut stats = StatsTable::new();
146 let now = Instant::now();
147 stats.record("zs/v1/h-a/telemetry/x/m1", 4, None, now, None);
148 stats.record("zs/v1/h-a/telemetry/x/m1", 4, None, now, None);
149 stats.record("zs/v1/h-a/telemetry/x/m2", 10, None, now, None);
150 stats.record("zs/v1/h-b/state/x/health", 7, None, now, None);
151
152 let snap = KeyTreeSnapshot::build(&stats);
153 let root = &snap.root;
154 assert_eq!(root.subtree_count, 4);
155 assert_eq!(root.subtree_bytes, 4 + 4 + 10 + 7);
156 assert_eq!(root.subtree_keys, 3, "three distinct keys carried traffic");
157 assert!(root.subtree_last_seen.is_some());
158 assert_eq!(root.count, 0);
160 assert_eq!(root.last_seen, None);
161
162 let x = snap.node(&["zs", "v1", "h-a", "telemetry", "x"]).unwrap();
163 assert_eq!(x.subtree_count, 3);
164 assert_eq!(x.subtree_bytes, 18);
165 assert_eq!(x.subtree_keys, 2);
166
167 let m1 = snap
168 .node(&["zs", "v1", "h-a", "telemetry", "x", "m1"])
169 .unwrap();
170 assert_eq!(m1.last_seen, Some(now));
171 assert_eq!(m1.subtree_keys, 1, "a leaf counts only itself");
172 }
173}