Skip to main content

zenkey_fleet/
tree.rs

1//! The live key tree (issue #15): an immutable snapshot of everything the
2//! monitor has seen, grouped by key chunks, with per-node statistics.
3//!
4//! Snapshots are rebuilt on the monitor's stats tick and published through
5//! an `ArcSwap` — render loops *pull* the latest snapshot at their own pace
6//! and never contend with the per-sample hot path (a hot bus cannot melt a
7//! zengui redraw).
8
9use std::collections::BTreeMap;
10
11use crate::stats::StatsTable;
12
13/// One node of the snapshot: a key chunk, its subtree, and — when a sample
14/// has landed exactly here — its stats.
15#[derive(Debug, Clone, Default)]
16pub struct TreeNode {
17    pub children: BTreeMap<String, TreeNode>,
18    /// Samples observed at exactly this key (leaf traffic).
19    pub count: u64,
20    pub bytes: u64,
21    pub rate_hz: f64,
22    /// Aggregate over the whole subtree (this node included).
23    pub subtree_count: u64,
24}
25
26/// An immutable point-in-time view of the observed keyspace.
27#[derive(Debug, Clone, Default)]
28pub struct KeyTreeSnapshot {
29    pub root: TreeNode,
30    pub keys: usize,
31}
32
33impl KeyTreeSnapshot {
34    /// Build from the stats table (called on the stats tick, off the
35    /// per-sample path).
36    pub fn build(stats: &StatsTable) -> KeyTreeSnapshot {
37        let mut root = TreeNode::default();
38        for (key, s) in stats.iter() {
39            let mut node = &mut root;
40            node.subtree_count += s.count;
41            for chunk in key.split('/') {
42                node = node.children.entry(chunk.to_string()).or_default();
43                node.subtree_count += s.count;
44            }
45            node.count = s.count;
46            node.bytes = s.bytes;
47            node.rate_hz = s.rate_hz;
48        }
49        KeyTreeSnapshot {
50            root,
51            keys: stats.len(),
52        }
53    }
54
55    /// Walk to a node by its chunk path.
56    pub fn node(&self, path: &[&str]) -> Option<&TreeNode> {
57        let mut node = &self.root;
58        for chunk in path {
59            node = node.children.get(*chunk)?;
60        }
61        Some(node)
62    }
63}
64
65#[cfg(test)]
66mod tests {
67    use super::*;
68    use std::time::Instant;
69
70    #[test]
71    fn builds_grouped_counts() {
72        let mut stats = StatsTable::new();
73        let now = Instant::now();
74        stats.record("zs/v1/h-a/telemetry/x/m1", 4, None, now);
75        stats.record("zs/v1/h-a/telemetry/x/m1", 4, None, now);
76        stats.record("zs/v1/h-a/telemetry/x/m2", 4, None, now);
77        stats.record("zs/v1/h-b/state/x/health", 4, None, now);
78
79        let snap = KeyTreeSnapshot::build(&stats);
80        assert_eq!(snap.keys, 3);
81        assert_eq!(snap.root.subtree_count, 4);
82        let telemetry = snap.node(&["zs", "v1", "h-a", "telemetry", "x"]).unwrap();
83        assert_eq!(telemetry.subtree_count, 3);
84        let m1 = snap
85            .node(&["zs", "v1", "h-a", "telemetry", "x", "m1"])
86            .unwrap();
87        assert_eq!(m1.count, 2);
88        assert_eq!(m1.bytes, 8);
89        assert!(snap.node(&["zs", "v1", "h-c"]).is_none());
90    }
91}