use std::collections::BTreeMap;
use std::time::Instant;
use crate::stats::{KeyStats, StatsTable};
fn accumulate(node: &mut TreeNode, s: &KeyStats) {
node.subtree_count += s.count;
node.subtree_bytes += s.bytes;
node.subtree_rate_hz += s.rate_hz;
node.subtree_keys += 1;
node.subtree_last_seen = node.subtree_last_seen.max(Some(s.last_seen));
}
#[derive(Debug, Clone, Default)]
pub struct TreeNode {
pub children: BTreeMap<String, TreeNode>,
pub count: u64,
pub bytes: u64,
pub rate_hz: f64,
pub last_seen: Option<Instant>,
pub subtree_count: u64,
pub subtree_bytes: u64,
pub subtree_rate_hz: f64,
pub subtree_last_seen: Option<Instant>,
pub subtree_keys: usize,
}
#[derive(Debug, Clone, Default)]
pub struct KeyTreeSnapshot {
pub root: TreeNode,
pub keys: usize,
pub evicted: u64,
pub unwatched: u64,
}
impl KeyTreeSnapshot {
pub fn build(stats: &StatsTable) -> KeyTreeSnapshot {
let mut root = TreeNode::default();
for (key, s) in stats.iter() {
let mut node = &mut root;
accumulate(node, s);
for chunk in key.split('/') {
if !node.children.contains_key(chunk) {
node.children.insert(chunk.to_string(), TreeNode::default());
}
node = node
.children
.get_mut(chunk)
.expect("just inserted if it was missing");
accumulate(node, s);
}
node.count = s.count;
node.bytes = s.bytes;
node.rate_hz = s.rate_hz;
node.last_seen = Some(s.last_seen);
}
KeyTreeSnapshot {
root,
keys: stats.len(),
evicted: stats.evicted(),
unwatched: stats.unwatched(),
}
}
pub fn node(&self, path: &[&str]) -> Option<&TreeNode> {
let mut node = &self.root;
for chunk in path {
node = node.children.get(*chunk)?;
}
Some(node)
}
}
#[cfg(test)]
mod tests {
use super::*;
use std::time::Instant;
#[test]
fn builds_grouped_counts() {
let mut stats = StatsTable::new();
let now = Instant::now();
stats.record("zs/v1/h-a/telemetry/x/m1", 4, None, now, None);
stats.record("zs/v1/h-a/telemetry/x/m1", 4, None, now, None);
stats.record("zs/v1/h-a/telemetry/x/m2", 4, None, now, None);
stats.record("zs/v1/h-b/state/x/health", 4, None, now, None);
let snap = KeyTreeSnapshot::build(&stats);
assert_eq!(snap.keys, 3);
assert_eq!(snap.root.subtree_count, 4);
let telemetry = snap.node(&["zs", "v1", "h-a", "telemetry", "x"]).unwrap();
assert_eq!(telemetry.subtree_count, 3);
let m1 = snap
.node(&["zs", "v1", "h-a", "telemetry", "x", "m1"])
.unwrap();
assert_eq!(m1.count, 2);
assert_eq!(m1.bytes, 8);
assert!(snap.node(&["zs", "v1", "h-c"]).is_none());
}
#[test]
fn collapsed_nodes_aggregate_their_subtree() {
let mut stats = StatsTable::new();
let now = Instant::now();
stats.record("zs/v1/h-a/telemetry/x/m1", 4, None, now, None);
stats.record("zs/v1/h-a/telemetry/x/m1", 4, None, now, None);
stats.record("zs/v1/h-a/telemetry/x/m2", 10, None, now, None);
stats.record("zs/v1/h-b/state/x/health", 7, None, now, None);
let snap = KeyTreeSnapshot::build(&stats);
let root = &snap.root;
assert_eq!(root.subtree_count, 4);
assert_eq!(root.subtree_bytes, 4 + 4 + 10 + 7);
assert_eq!(root.subtree_keys, 3, "three distinct keys carried traffic");
assert!(root.subtree_last_seen.is_some());
assert_eq!(root.count, 0);
assert_eq!(root.last_seen, None);
let x = snap.node(&["zs", "v1", "h-a", "telemetry", "x"]).unwrap();
assert_eq!(x.subtree_count, 3);
assert_eq!(x.subtree_bytes, 18);
assert_eq!(x.subtree_keys, 2);
let m1 = snap
.node(&["zs", "v1", "h-a", "telemetry", "x", "m1"])
.unwrap();
assert_eq!(m1.last_seen, Some(now));
assert_eq!(m1.subtree_keys, 1, "a leaf counts only itself");
}
}