zenkey_fleet/judge/
budget.rs1use std::collections::{BTreeMap, BTreeSet};
25
26use crate::SliceSet;
27use crate::judge::common::EXPANSION_CAP;
28use crate::model::examples::Examples;
29use crate::model::facts::{KeyFacts, KeyShape, OriginKind};
30use crate::report::{BudgetCell, BudgetWindow, TopicList};
31
32#[derive(Debug, Clone, Default)]
37pub struct BudgetObservation {
38 families: BTreeMap<(String, String), BTreeMap<String, BTreeSet<String>>>,
40}
41
42impl BudgetObservation {
43 pub fn observe<'a>(
50 base: &str,
51 slices: &SliceSet,
52 keys: impl IntoIterator<Item = &'a str>,
53 ) -> BudgetObservation {
54 let mut families: BTreeMap<(String, String), BTreeMap<String, BTreeSet<String>>> =
55 BTreeMap::new();
56 for key in keys {
57 let facts = KeyFacts::project(base, key);
58 let KeyShape::V1(v) = &facts.shape else {
59 continue;
60 };
61 if !v.class_kind.is_data_class() {
62 continue;
63 }
64 let producer = match v.origin_kind {
67 OriginKind::Host => v.producer.clone(),
68 OriginKind::Service => slices.by_service_origin(&v.origin).map(|s| s.name.clone()),
69 };
70 let Some(producer) = producer else {
71 continue;
72 };
73 let tail: Vec<&str> = v.subject.iter().map(String::as_str).collect();
74 let Some((decl, _)) = slices.refine(&producer, &v.class, &tail) else {
75 continue;
76 };
77 if !decl.path.contains('{') {
78 continue; }
80 families
81 .entry((producer, decl.path.clone()))
82 .or_default()
83 .entry(v.origin.clone())
84 .or_default()
85 .insert(key.to_string());
86 }
87 BudgetObservation { families }
88 }
89
90 pub fn family(
92 &self,
93 producer: &str,
94 path: &str,
95 ) -> Option<&BTreeMap<String, BTreeSet<String>>> {
96 self.families.get(&(producer.to_string(), path.to_string()))
97 }
98}
99
100pub fn join_budget(list: &mut TopicList, obs: &BudgetObservation, window: BudgetWindow) {
106 for row in &mut list.subjects {
107 if row.deprecated || !row.path.contains('{') {
108 continue;
109 }
110 let empty = BTreeMap::new();
111 let origins = obs.family(&row.producer, &row.path).unwrap_or(&empty);
112 let observed: usize = origins.values().map(BTreeSet::len).sum();
113 let (worst_origin, worst_observed) = origins
114 .iter()
115 .max_by_key(|(_, keys)| keys.len())
116 .map(|(o, keys)| (Some(o.clone()), keys.len()))
117 .unwrap_or((None, 0));
118 let examples = worst_origin
119 .as_ref()
120 .and_then(|o| origins.get(o))
121 .map(|keys| {
122 let mut ex = Examples::new(EXPANSION_CAP);
123 for key in keys {
124 ex.push_with(|| key.clone());
125 }
126 ex.into_vec()
127 })
128 .unwrap_or_default();
129 let exempt = row
130 .path
131 .contains("...")
132 .then(|| "rest-variable".to_string());
133 let over = exempt.is_none()
134 && row
135 .cardinality
136 .is_some_and(|declared| worst_observed as i64 > declared);
137 row.budget = Some(BudgetCell {
138 declared: row.cardinality,
139 observed,
140 origins: origins.len(),
141 worst_origin,
142 worst_observed,
143 exempt,
144 over,
145 examples,
146 });
147 }
148 list.budget = Some(window);
149}
150
151#[cfg(test)]
152mod tests {
153 use super::*;
154
155 const SLICE: &str = r#"
156 [registry]
157 version = "1.0"
158 app = "t"
159 convention = 1
160 [producer]
161 name = "sysinfo"
162 [[subject]]
163 path = "disk/{mount}/used"
164 class = "telemetry"
165 type = "Point"
166 cardinality = 16
167 [[subject]]
168 path = "health"
169 class = "state"
170 type = "Health"
171 "#;
172
173 #[test]
174 fn observation_groups_per_origin_and_skips_literals() {
175 let slices = SliceSet::from_toml_for_tests(SLICE);
176 let keys = [
177 "v1/h-aaaaaaaaaaaa/telemetry/sysinfo/disk/root/used",
178 "v1/h-aaaaaaaaaaaa/telemetry/sysinfo/disk/var/used",
179 "v1/h-bbbbbbbbbbbb/telemetry/sysinfo/disk/root/used",
180 "v1/h-aaaaaaaaaaaa/state/sysinfo/health",
182 "v1/h-aaaaaaaaaaaa/telemetry/sysinfo/not/registered",
183 ];
184 let obs = BudgetObservation::observe("", &slices, keys);
185 let fam = obs.family("sysinfo", "disk/{mount}/used").unwrap();
186 assert_eq!(fam.len(), 2, "two origins expanded the family");
187 assert_eq!(fam["h-aaaaaaaaaaaa"].len(), 2);
188 assert_eq!(fam["h-bbbbbbbbbbbb"].len(), 1);
189 assert!(
190 obs.family("sysinfo", "health").is_none(),
191 "literals excluded"
192 );
193 }
194}