1use std::collections::BTreeSet;
12
13use serde::Serialize;
14
15#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize)]
18#[serde(rename_all = "snake_case")]
19pub enum OrderLabel {
20 Arrival,
23 Hlc,
26}
27
28#[derive(Debug, Clone, PartialEq, Eq, Serialize)]
39#[serde(tag = "claim", rename_all = "snake_case")]
40pub enum HlcClaim {
41 HappensBefore { stamper: String },
44 SkewedWallClock { stampers: BTreeSet<String> },
47 NoStampedSamples,
50}
51
52#[derive(Debug, Clone, PartialEq, Serialize)]
54#[serde(tag = "axis", rename_all = "snake_case")]
55pub enum AxisLabel {
56 Arrival {
57 clock: &'static str,
59 },
60 Hlc {
61 #[serde(flatten)]
62 claim: HlcClaim,
63 },
64}
65
66pub const ARRIVAL_CLOCK: &str = "observer monotonic, µs since window start";
68
69#[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord, Serialize)]
76#[serde(tag = "kind", rename_all = "snake_case")]
77pub enum LaneId {
78 Origin {
81 origin: String,
82 #[serde(skip_serializing_if = "Option::is_none")]
83 producer: Option<String>,
84 },
85 Foreign,
88 Unstamped,
92}
93
94impl LaneId {
95 pub fn label(&self) -> String {
97 match self {
98 LaneId::Origin {
99 origin,
100 producer: Some(p),
101 } => format!("{origin}/{p}"),
102 LaneId::Origin {
103 origin,
104 producer: None,
105 } => origin.clone(),
106 LaneId::Foreign => "foreign (not a v1 key under this base)".into(),
107 LaneId::Unstamped => "unstamped (arrival axis only)".into(),
108 }
109 }
110}
111
112#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Serialize)]
115#[serde(rename_all = "snake_case")]
116pub enum Provenance {
117 SelfStamped,
118 Foreign,
119 Unattributable,
120}
121
122#[derive(Debug, Clone, Copy, Default, PartialEq, Eq, Serialize)]
125pub struct ProvenanceCounts {
126 pub self_stamped: usize,
127 pub foreign: usize,
128 pub unattributable: usize,
129}
130
131#[derive(Debug, Clone, PartialEq, Eq, Serialize)]
133pub struct LaneSummary {
134 pub lane: LaneId,
135 pub samples: usize,
137 pub first_t_us: u64,
139 pub last_t_us: u64,
140 pub stampers: BTreeSet<String>,
143 pub provenance: ProvenanceCounts,
144}
145
146#[derive(Debug, Clone, PartialEq, Eq, Serialize)]
153#[serde(tag = "state", rename_all = "snake_case")]
154pub enum SnLaneReport {
155 Unavailable { reason: &'static str },
156 Present { sources: usize, samples: usize },
157}
158
159pub const SN_UNAVAILABLE_REASON: &str = "zenoh 1.9/1.10 deliver no SourceInfo to subscribers \
161 (eclipse-zenoh/zenoh#2563); `tests/stamper.rs` pins it";
162
163#[derive(Debug, Clone, PartialEq, Eq, Serialize)]
165#[serde(tag = "kind", rename_all = "snake_case")]
166pub enum TimelineSource {
167 Live,
168 Zrec { path: String },
169}
170
171#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize)]
173#[serde(rename_all = "snake_case")]
174pub enum RowKind {
175 Put,
176 Delete,
177}
178
179#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize)]
181#[serde(rename_all = "snake_case")]
182pub enum BreakKind {
183 Dropped,
185 Coalesced,
187}
188
189#[derive(Debug, Clone, PartialEq, Serialize)]
191#[serde(tag = "row", rename_all = "snake_case")]
192pub enum TimelineEntry {
193 Sample {
194 order_by: OrderLabel,
195 pos: usize,
197 lane: LaneId,
198 key: String,
199 t_us: u64,
202 #[serde(skip_serializing_if = "Option::is_none")]
205 hlc: Option<String>,
206 #[serde(skip_serializing_if = "Option::is_none")]
207 stamped_by: Option<String>,
208 #[serde(skip_serializing_if = "Option::is_none")]
209 provenance: Option<Provenance>,
210 kind: RowKind,
211 },
212 Break {
213 order_by: OrderLabel,
214 pos: usize,
215 #[serde(skip_serializing_if = "Option::is_none")]
216 lane: Option<LaneId>,
217 kind: BreakKind,
218 n: u64,
219 },
220}
221
222#[derive(Debug, Clone, PartialEq, Serialize)]
224pub struct TimelineReport {
225 pub order_by: OrderLabel,
226 #[serde(flatten)]
227 pub axis: AxisLabel,
228 pub scopes: Vec<String>,
230 #[serde(skip_serializing_if = "Option::is_none")]
233 pub window_s: Option<f64>,
234 pub source: TimelineSource,
235 pub lanes: Vec<LaneSummary>,
236 pub sn_lane: SnLaneReport,
237 #[serde(skip_serializing_if = "is_zero_usize")]
240 pub unstamped_excluded: usize,
241 pub dropped: u64,
243 #[serde(skip_serializing_if = "is_zero_u64")]
244 pub coalesced: u64,
245 pub keys_evicted: u64,
247 pub rows: Vec<TimelineEntry>,
248}
249
250fn is_zero_usize(n: &usize) -> bool {
251 *n == 0
252}
253
254fn is_zero_u64(n: &u64) -> bool {
255 *n == 0
256}
257
258#[cfg(test)]
259mod tests {
260 use super::*;
261 use serde_json::json;
262
263 fn lane() -> LaneId {
264 LaneId::Origin {
265 origin: "h-3fa9c2d41b7e".into(),
266 producer: Some("sysinfo".into()),
267 }
268 }
269
270 #[test]
273 fn a_happens_before_report_is_pinned() {
274 let report = TimelineReport {
275 order_by: OrderLabel::Hlc,
276 axis: AxisLabel::Hlc {
277 claim: HlcClaim::HappensBefore {
278 stamper: "33".into(),
279 },
280 },
281 scopes: vec!["v1/**".into()],
282 window_s: Some(10.0),
283 source: TimelineSource::Live,
284 lanes: vec![LaneSummary {
285 lane: lane(),
286 samples: 1,
287 first_t_us: 5,
288 last_t_us: 5,
289 stampers: ["33".to_string()].into_iter().collect(),
290 provenance: ProvenanceCounts {
291 unattributable: 1,
292 ..Default::default()
293 },
294 }],
295 sn_lane: SnLaneReport::Unavailable {
296 reason: SN_UNAVAILABLE_REASON,
297 },
298 unstamped_excluded: 2,
299 dropped: 0,
300 coalesced: 0,
301 keys_evicted: 0,
302 rows: vec![TimelineEntry::Sample {
303 order_by: OrderLabel::Hlc,
304 pos: 0,
305 lane: lane(),
306 key: "v1/h-3fa9c2d41b7e/telemetry/sysinfo/cpu".into(),
307 t_us: 5,
308 hlc: Some("100/33".into()),
309 stamped_by: Some("33".into()),
310 provenance: Some(Provenance::Unattributable),
311 kind: RowKind::Put,
312 }],
313 };
314 assert_eq!(
315 serde_json::to_value(&report).unwrap(),
316 json!({
317 "order_by": "hlc",
318 "axis": "hlc",
319 "claim": "happens_before",
320 "stamper": "33",
321 "scopes": ["v1/**"],
322 "window_s": 10.0,
323 "source": {"kind": "live"},
324 "lanes": [{
325 "lane": {"kind": "origin", "origin": "h-3fa9c2d41b7e", "producer": "sysinfo"},
326 "samples": 1,
327 "first_t_us": 5,
328 "last_t_us": 5,
329 "stampers": ["33"],
330 "provenance": {"self_stamped": 0, "foreign": 0, "unattributable": 1}
331 }],
332 "sn_lane": {
333 "state": "unavailable",
334 "reason": "zenoh 1.9/1.10 deliver no SourceInfo to subscribers (eclipse-zenoh/zenoh#2563); `tests/stamper.rs` pins it"
335 },
336 "unstamped_excluded": 2,
337 "dropped": 0,
338 "keys_evicted": 0,
339 "rows": [{
340 "row": "sample",
341 "order_by": "hlc",
342 "pos": 0,
343 "lane": {"kind": "origin", "origin": "h-3fa9c2d41b7e", "producer": "sysinfo"},
344 "key": "v1/h-3fa9c2d41b7e/telemetry/sysinfo/cpu",
345 "t_us": 5,
346 "hlc": "100/33",
347 "stamped_by": "33",
348 "provenance": "unattributable",
349 "kind": "put"
350 }]
351 })
352 );
353 }
354
355 #[test]
358 fn an_arrival_report_with_a_break_is_pinned_and_the_skew_claim_spells_its_stampers() {
359 let report = TimelineReport {
360 order_by: OrderLabel::Arrival,
361 axis: AxisLabel::Arrival {
362 clock: ARRIVAL_CLOCK,
363 },
364 scopes: vec!["v1/**".into()],
365 window_s: None,
366 source: TimelineSource::Zrec {
367 path: "bus.zrec".into(),
368 },
369 lanes: vec![],
370 sn_lane: SnLaneReport::Present {
371 sources: 1,
372 samples: 3,
373 },
374 unstamped_excluded: 0,
375 dropped: 7,
376 coalesced: 0,
377 keys_evicted: 0,
378 rows: vec![
379 TimelineEntry::Sample {
380 order_by: OrderLabel::Arrival,
381 pos: 0,
382 lane: LaneId::Unstamped,
383 key: "plain/key".into(),
384 t_us: 1,
385 hlc: None,
386 stamped_by: None,
387 provenance: None,
388 kind: RowKind::Delete,
389 },
390 TimelineEntry::Break {
391 order_by: OrderLabel::Arrival,
392 pos: 1,
393 lane: None,
394 kind: BreakKind::Dropped,
395 n: 7,
396 },
397 ],
398 };
399 assert_eq!(
400 serde_json::to_value(&report).unwrap(),
401 json!({
402 "order_by": "arrival",
403 "axis": "arrival",
404 "clock": "observer monotonic, µs since window start",
405 "scopes": ["v1/**"],
406 "source": {"kind": "zrec", "path": "bus.zrec"},
407 "lanes": [],
408 "sn_lane": {"state": "present", "sources": 1, "samples": 3},
409 "dropped": 7,
410 "keys_evicted": 0,
411 "rows": [
412 {
413 "row": "sample",
414 "order_by": "arrival",
415 "pos": 0,
416 "lane": {"kind": "unstamped"},
417 "key": "plain/key",
418 "t_us": 1,
419 "kind": "delete"
420 },
421 {"row": "break", "order_by": "arrival", "pos": 1, "kind": "dropped", "n": 7}
422 ]
423 })
424 );
425 let skew = AxisLabel::Hlc {
426 claim: HlcClaim::SkewedWallClock {
427 stampers: ["33".to_string(), "44".to_string()].into_iter().collect(),
428 },
429 };
430 assert_eq!(
431 serde_json::to_value(&skew).unwrap(),
432 json!({"axis": "hlc", "claim": "skewed_wall_clock", "stampers": ["33", "44"]})
433 );
434 let empty = AxisLabel::Hlc {
435 claim: HlcClaim::NoStampedSamples,
436 };
437 assert_eq!(
438 serde_json::to_value(&empty).unwrap(),
439 json!({"axis": "hlc", "claim": "no_stamped_samples"})
440 );
441 }
442}