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