ktstr/scenario/snapshot/entry.rs
1//! [`SnapshotEntry`] — one entry's view across a `FailureDumpMap`
2//! variant. Carries the rendered key (when BTF was present at
3//! capture) plus accessors for typed reads, per-CPU narrowing, and
4//! aggregation across per-CPU slots.
5
6use crate::monitor::btf_render::RenderedValue;
7use crate::monitor::dump::{FailureDumpEntry, FailureDumpPercpuEntry, FailureDumpPercpuHashEntry};
8
9use super::{SnapshotError, SnapshotField, SnapshotResult, walk_dotted_path};
10
11/// One entry's view — either a HASH (key, value) pair, a per-CPU
12/// array entry, a per-CPU hash entry, a single rendered value, or
13/// a missing-entry marker.
14#[derive(Debug)]
15#[must_use = "SnapshotEntry is a borrowed view; chain accessors"]
16#[non_exhaustive]
17pub enum SnapshotEntry<'a> {
18 /// HASH map entry — `(key, value)` pair.
19 Hash(&'a FailureDumpEntry),
20 /// PERCPU_ARRAY entry — outer u32 key, inner per-CPU vec.
21 Percpu(&'a FailureDumpPercpuEntry),
22 /// PERCPU_HASH entry — rendered key, inner per-CPU vec.
23 PercpuHash(&'a FailureDumpPercpuHashEntry),
24 /// Single rendered value (ARRAY map's `value` field, or a
25 /// per-CPU slot resolved via [`super::SnapshotMap::cpu`]).
26 Value(&'a RenderedValue),
27 /// No entry matched.
28 Missing(SnapshotError),
29}
30
31impl<'a> SnapshotEntry<'a> {
32 /// True when the lookup succeeded.
33 pub fn is_present(&self) -> bool {
34 !matches!(self, SnapshotEntry::Missing(_))
35 }
36
37 /// Walk into the entry's value side along a dotted path. Each
38 /// path component names a [`RenderedValue::Struct`] member;
39 /// pointer dereferences are followed transparently. Returns
40 /// [`SnapshotField::Missing`] with an actionable error
41 /// when the path cannot be resolved.
42 pub fn get(&self, path: &str) -> SnapshotField<'a> {
43 let value = match self {
44 SnapshotEntry::Hash(e) => e.value.as_ref(),
45 SnapshotEntry::Percpu(_) | SnapshotEntry::PercpuHash(_) => {
46 let map_name = match self {
47 SnapshotEntry::Percpu(_) => "<percpu-array>".to_string(),
48 SnapshotEntry::PercpuHash(_) => "<percpu-hash>".to_string(),
49 _ => String::new(),
50 };
51 return SnapshotField::Missing(SnapshotError::PerCpuNotNarrowed { map: map_name });
52 }
53 SnapshotEntry::Value(v) => Some(*v),
54 SnapshotEntry::Missing(err) => {
55 return SnapshotField::Missing(err.clone());
56 }
57 };
58 let Some(v) = value else {
59 return SnapshotField::Missing(SnapshotError::NoRendered {
60 map: "<entry>".to_string(),
61 side: "value".to_string(),
62 });
63 };
64 walk_dotted_path(v, path)
65 }
66
67 /// Look up the entry's KEY side along a dotted path. Mirror
68 /// of [`Self::get`] but operates on the key's rendered
69 /// structure. Supports the three key-bearing variants: `Hash`
70 /// and `PercpuHash` walk their rendered key; `Percpu` returns
71 /// its u32 key as [`SnapshotField::PercpuKey`] for an empty
72 /// path (and `TypeMismatch` for a non-empty path).
73 pub fn key(&self, path: &str) -> SnapshotField<'a> {
74 match self {
75 SnapshotEntry::Hash(e) => match e.key.as_ref() {
76 Some(v) => walk_dotted_path(v, path),
77 None => SnapshotField::Missing(SnapshotError::NoRendered {
78 map: "<entry>".to_string(),
79 side: "key".to_string(),
80 }),
81 },
82 SnapshotEntry::PercpuHash(e) => match e.key.as_ref() {
83 Some(v) => walk_dotted_path(v, path),
84 None => SnapshotField::Missing(SnapshotError::NoRendered {
85 map: "<entry>".to_string(),
86 side: "key".to_string(),
87 }),
88 },
89 SnapshotEntry::Percpu(e) => {
90 if path.is_empty() {
91 SnapshotField::PercpuKey { key: e.key }
92 } else {
93 SnapshotField::Missing(SnapshotError::TypeMismatch {
94 expected: "Struct".to_string(),
95 actual: "Uint(percpu key)".to_string(),
96 requested: path.to_string(),
97 })
98 }
99 }
100 SnapshotEntry::Value(_) => SnapshotField::Missing(SnapshotError::TypeMismatch {
101 expected: "key".to_string(),
102 actual: "single Value (no key)".to_string(),
103 requested: path.to_string(),
104 }),
105 SnapshotEntry::Missing(err) => SnapshotField::Missing(err.clone()),
106 }
107 }
108
109 // -----------------------------------------------------------------
110 // Per-CPU aggregators. Apply only to `Percpu` / `PercpuHash`
111 // entries; other variants return an `Err`: `Hash` / `Value`
112 // yield `TypeMismatch`, `Missing` propagates its captured error.
113 // Inside the
114 // per_cpu vec, slots whose value is `None` (CPU unmapped / out of
115 // range — see `read_percpu_array_value` semantics) skip the
116 // aggregation; slots whose rendered value can't decode to the
117 // requested scalar return `Err(TypeMismatch)` immediately.
118 //
119 // `cpu_sum_*`, `cpu_max_*`, and `cpu_min_*` all return
120 // `Err(NoMatch)` when no slot contributes (every slot `None`). A
121 // `None` slot is UNREADABLE (host-read failure), NOT a real zero,
122 // so an all-None sum of `0` would silently drop the missing data;
123 // a readable map whose slots are all `0` still sums to `Ok(0)`.
124 // -----------------------------------------------------------------
125
126 /// Sum the per-CPU values at `path` as `u64`. Returns
127 /// `Err(NoMatch)` when every slot is `None`: a `None` slot is
128 /// UNREADABLE (host-read failure), not a real zero, so an all-None
129 /// sum of `0` would silently drop the unreadable data. A readable
130 /// map whose slots are all `0` still sums to `Ok(0)`. A slot whose
131 /// rendered value cannot decode to `u64` propagates an Err
132 /// immediately and stops the aggregation.
133 pub fn cpu_sum_u64(&self, path: &str) -> SnapshotResult<u64> {
134 let mut acc: u64 = 0;
135 let mut contributed = false;
136 self.try_for_each_cpu_value(path, |v| {
137 acc = acc.saturating_add(SnapshotField::Value(v).as_u64()?);
138 contributed = true;
139 Ok(())
140 })?;
141 if contributed {
142 Ok(acc)
143 } else {
144 Err(self.empty_aggregate_error("cpu_sum_u64"))
145 }
146 }
147
148 /// Maximum of per-CPU values at `path` as `u64`. Returns
149 /// `Err(NoMatch)` when every slot is `None` (no slot contributed).
150 /// A slot whose rendered value cannot decode to `u64` propagates
151 /// an Err immediately.
152 pub fn cpu_max_u64(&self, path: &str) -> SnapshotResult<u64> {
153 let mut best: Option<u64> = None;
154 self.try_for_each_cpu_value(path, |v| {
155 let n = SnapshotField::Value(v).as_u64()?;
156 best = Some(best.map_or(n, |b| b.max(n)));
157 Ok(())
158 })?;
159 best.ok_or_else(|| self.empty_aggregate_error("cpu_max_u64"))
160 }
161
162 /// Minimum of per-CPU values at `path` as `u64`. Returns
163 /// `Err(NoMatch)` when every slot is `None`. A slot whose
164 /// rendered value cannot decode to `u64` propagates an Err
165 /// immediately.
166 pub fn cpu_min_u64(&self, path: &str) -> SnapshotResult<u64> {
167 let mut best: Option<u64> = None;
168 self.try_for_each_cpu_value(path, |v| {
169 let n = SnapshotField::Value(v).as_u64()?;
170 best = Some(best.map_or(n, |b| b.min(n)));
171 Ok(())
172 })?;
173 best.ok_or_else(|| self.empty_aggregate_error("cpu_min_u64"))
174 }
175
176 /// Sum the per-CPU values at `path` as `i64`. Returns
177 /// `Err(NoMatch)` when every slot is `None`, matching
178 /// [`Self::cpu_sum_u64`]: a `None` slot is UNREADABLE, not a real
179 /// zero, so an all-None sum of `0` would be a silent drop. A
180 /// readable all-zero map still sums to `Ok(0)`. The sum saturates
181 /// at `i64::MIN` / `i64::MAX`. A slot whose rendered value cannot
182 /// decode to `i64` propagates an Err immediately and stops the
183 /// aggregation.
184 pub fn cpu_sum_i64(&self, path: &str) -> SnapshotResult<i64> {
185 let mut acc: i64 = 0;
186 let mut contributed = false;
187 self.try_for_each_cpu_value(path, |v| {
188 acc = acc.saturating_add(SnapshotField::Value(v).as_i64()?);
189 contributed = true;
190 Ok(())
191 })?;
192 if contributed {
193 Ok(acc)
194 } else {
195 Err(self.empty_aggregate_error("cpu_sum_i64"))
196 }
197 }
198
199 /// Maximum of per-CPU values at `path` as `i64`. Returns
200 /// `Err(NoMatch)` when every slot is `None` (no slot contributed).
201 /// A slot whose rendered value cannot decode to `i64` propagates
202 /// an Err immediately.
203 pub fn cpu_max_i64(&self, path: &str) -> SnapshotResult<i64> {
204 let mut best: Option<i64> = None;
205 self.try_for_each_cpu_value(path, |v| {
206 let n = SnapshotField::Value(v).as_i64()?;
207 best = Some(best.map_or(n, |b| b.max(n)));
208 Ok(())
209 })?;
210 best.ok_or_else(|| self.empty_aggregate_error("cpu_max_i64"))
211 }
212
213 /// Minimum of per-CPU values at `path` as `i64`. Returns
214 /// `Err(NoMatch)` when every slot is `None`. A slot whose
215 /// rendered value cannot decode to `i64` propagates an Err
216 /// immediately.
217 pub fn cpu_min_i64(&self, path: &str) -> SnapshotResult<i64> {
218 let mut best: Option<i64> = None;
219 self.try_for_each_cpu_value(path, |v| {
220 let n = SnapshotField::Value(v).as_i64()?;
221 best = Some(best.map_or(n, |b| b.min(n)));
222 Ok(())
223 })?;
224 best.ok_or_else(|| self.empty_aggregate_error("cpu_min_i64"))
225 }
226
227 /// Sum the per-CPU values at `path` as `f64`. Returns
228 /// `Err(NoMatch)` when every slot is `None`: a `None` slot is
229 /// UNREADABLE, not a real zero, so an all-None sum of `0.0` would
230 /// be a silent drop. A readable all-zero map still sums to
231 /// `Ok(0.0)`. A slot whose rendered value cannot decode to `f64`
232 /// propagates an Err immediately. NaN slot values propagate through
233 /// `+=` per IEEE-754 — a single NaN slot makes the result NaN.
234 pub fn cpu_sum_f64(&self, path: &str) -> SnapshotResult<f64> {
235 let mut acc: f64 = 0.0;
236 let mut contributed = false;
237 self.try_for_each_cpu_value(path, |v| {
238 acc += SnapshotField::Value(v).as_f64()?;
239 contributed = true;
240 Ok(())
241 })?;
242 if contributed {
243 Ok(acc)
244 } else {
245 Err(self.empty_aggregate_error("cpu_sum_f64"))
246 }
247 }
248
249 /// Maximum of per-CPU values at `path` as `f64`. Returns
250 /// `Err(NoMatch)` when every slot is `None`. A slot whose
251 /// rendered value cannot decode to `f64` propagates an Err
252 /// immediately. NaN slot values are filtered out per
253 /// `f64::max` semantics — `f64::max(NaN, x)` returns `x`, so a
254 /// NaN slot never wins against a non-NaN slot. An all-NaN run
255 /// is an edge case: the first NaN slot sets `best=NaN`, then
256 /// subsequent `NaN.max(NaN)` returns NaN, so the final result
257 /// is `Ok(NaN)` rather than NoMatch.
258 pub fn cpu_max_f64(&self, path: &str) -> SnapshotResult<f64> {
259 let mut best: Option<f64> = None;
260 self.try_for_each_cpu_value(path, |v| {
261 let n = SnapshotField::Value(v).as_f64()?;
262 best = Some(best.map_or(n, |b| b.max(n)));
263 Ok(())
264 })?;
265 best.ok_or_else(|| self.empty_aggregate_error("cpu_max_f64"))
266 }
267
268 /// Minimum of per-CPU values at `path` as `f64`. Returns
269 /// `Err(NoMatch)` when every slot is `None`. A slot whose
270 /// rendered value cannot decode to `f64` propagates an Err
271 /// immediately. NaN slot values are filtered out per
272 /// `f64::min` semantics — `f64::min(NaN, x)` returns `x`, so a
273 /// NaN slot never wins against a non-NaN slot. An all-NaN run
274 /// yields `Ok(NaN)` rather than NoMatch — same edge case as
275 /// `cpu_max_f64`.
276 pub fn cpu_min_f64(&self, path: &str) -> SnapshotResult<f64> {
277 let mut best: Option<f64> = None;
278 self.try_for_each_cpu_value(path, |v| {
279 let n = SnapshotField::Value(v).as_f64()?;
280 best = Some(best.map_or(n, |b| b.min(n)));
281 Ok(())
282 })?;
283 best.ok_or_else(|| self.empty_aggregate_error("cpu_min_f64"))
284 }
285
286 /// Iterate non-None per-CPU rendered values at `path`. For each
287 /// successful slot, invokes `f(cpu_idx, &RenderedValue)`. Slots
288 /// whose value is `None` are skipped silently; the iteration
289 /// stops at the first slot whose value cannot be reached via
290 /// `path` (returning the path-walk error). Returns `Err` for
291 /// non-percpu variants.
292 pub fn cpu_each<F>(&self, path: &str, mut f: F) -> SnapshotResult<()>
293 where
294 F: FnMut(usize, &'a RenderedValue) -> SnapshotResult<()>,
295 {
296 let per_cpu: &[Option<RenderedValue>] = match self {
297 SnapshotEntry::Percpu(e) => &e.per_cpu,
298 SnapshotEntry::PercpuHash(e) => &e.per_cpu,
299 SnapshotEntry::Hash(_) | SnapshotEntry::Value(_) => {
300 return Err(SnapshotError::TypeMismatch {
301 expected: "Percpu / PercpuHash".to_string(),
302 actual: self.variant_name().to_string(),
303 requested: path.to_string(),
304 });
305 }
306 SnapshotEntry::Missing(err) => return Err(err.clone()),
307 };
308 for (cpu_idx, slot) in per_cpu.iter().enumerate() {
309 let Some(rendered) = slot.as_ref() else {
310 continue;
311 };
312 let walked = walk_dotted_path(rendered, path);
313 let value = match walked {
314 SnapshotField::Value(v) => v,
315 SnapshotField::PercpuKey { .. } => {
316 return Err(SnapshotError::TypeMismatch {
317 expected: "rendered value".to_string(),
318 actual: "PercpuKey".to_string(),
319 requested: path.to_string(),
320 });
321 }
322 SnapshotField::Missing(err) => return Err(err),
323 };
324 f(cpu_idx, value)?;
325 }
326 Ok(())
327 }
328
329 /// Shared walk helper for `cpu_sum_*` / `cpu_max_*` / `cpu_min_*`
330 /// — invokes `f` on every non-None slot's rendered value.
331 fn try_for_each_cpu_value<F>(&self, path: &str, mut f: F) -> SnapshotResult<()>
332 where
333 F: FnMut(&'a RenderedValue) -> SnapshotResult<()>,
334 {
335 self.cpu_each(path, |_, v| f(v))
336 }
337
338 /// Name for diagnostic messages. Used by the per-CPU aggregator
339 /// `TypeMismatch` paths so the error names the actual variant.
340 fn variant_name(&self) -> &'static str {
341 match self {
342 SnapshotEntry::Hash(_) => "Hash",
343 SnapshotEntry::Percpu(_) => "Percpu",
344 SnapshotEntry::PercpuHash(_) => "PercpuHash",
345 SnapshotEntry::Value(_) => "Value",
346 SnapshotEntry::Missing(_) => "Missing",
347 }
348 }
349
350 /// Build the `NoMatch` error for an empty per-CPU aggregate
351 /// (max / min of all-None or all-decode-fail). `op` names the
352 /// caller so the error message points at the right method.
353 fn empty_aggregate_error(&self, op: &str) -> SnapshotError {
354 SnapshotError::NoMatch {
355 map: format!("<{}>", self.variant_name()),
356 op: op.to_string(),
357 len: 0,
358 available_keys: Vec::new(),
359 }
360 }
361}
362
363// ---------------------------------------------------------------------------
364// SnapshotField — terminal traversal value