use std::collections::BTreeSet;
use serde::Serialize;
#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize)]
#[serde(rename_all = "snake_case")]
pub enum OrderLabel {
Arrival,
Hlc,
}
#[derive(Debug, Clone, PartialEq, Eq, Serialize)]
#[serde(tag = "claim", rename_all = "snake_case")]
pub enum HlcClaim {
HappensBefore { stamper: String },
SkewedWallClock { stampers: BTreeSet<String> },
NoStampedSamples,
}
#[derive(Debug, Clone, PartialEq, Serialize)]
#[serde(tag = "axis", rename_all = "snake_case")]
pub enum AxisLabel {
Arrival {
clock: &'static str,
},
Hlc {
#[serde(flatten)]
claim: HlcClaim,
},
}
pub const ARRIVAL_CLOCK: &str = "observer monotonic, µs since window start";
#[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord, Serialize)]
#[serde(tag = "kind", rename_all = "snake_case")]
pub enum LaneId {
Origin {
origin: String,
#[serde(skip_serializing_if = "Option::is_none")]
producer: Option<String>,
},
Foreign,
Unstamped,
}
impl LaneId {
pub fn label(&self) -> String {
match self {
LaneId::Origin {
origin,
producer: Some(p),
} => format!("{origin}/{p}"),
LaneId::Origin {
origin,
producer: None,
} => origin.clone(),
LaneId::Foreign => "foreign (not a v1 key under this base)".into(),
LaneId::Unstamped => "unstamped (arrival axis only)".into(),
}
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Serialize)]
#[serde(rename_all = "snake_case")]
pub enum Provenance {
SelfStamped,
Foreign,
Unattributable,
}
#[derive(Debug, Clone, Copy, Default, PartialEq, Eq, Serialize)]
pub struct ProvenanceCounts {
pub self_stamped: usize,
pub foreign: usize,
pub unattributable: usize,
}
#[derive(Debug, Clone, PartialEq, Eq, Serialize)]
pub struct LaneSummary {
pub lane: LaneId,
pub samples: usize,
pub first_t_us: u64,
pub last_t_us: u64,
pub stampers: BTreeSet<String>,
pub provenance: ProvenanceCounts,
}
#[derive(Debug, Clone, PartialEq, Eq, Serialize)]
#[serde(tag = "state", rename_all = "snake_case")]
pub enum SnLaneReport {
Unavailable { reason: &'static str },
Present { sources: usize, samples: usize },
}
pub const SN_UNAVAILABLE_REASON: &str = "zenoh 1.9/1.10 deliver no SourceInfo to subscribers \
(eclipse-zenoh/zenoh#2563); `tests/stamper.rs` pins it";
#[derive(Debug, Clone, PartialEq, Eq, Serialize)]
#[serde(tag = "kind", rename_all = "snake_case")]
pub enum TimelineSource {
Live,
Zrec { path: String },
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize)]
#[serde(rename_all = "snake_case")]
pub enum RowKind {
Put,
Delete,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize)]
#[serde(rename_all = "snake_case")]
pub enum BreakKind {
Dropped,
Coalesced,
}
#[derive(Debug, Clone, PartialEq, Serialize)]
#[serde(tag = "row", rename_all = "snake_case")]
pub enum TimelineEntry {
Sample {
order_by: OrderLabel,
pos: usize,
lane: LaneId,
key: String,
t_us: u64,
#[serde(skip_serializing_if = "Option::is_none")]
hlc: Option<String>,
#[serde(skip_serializing_if = "Option::is_none")]
stamped_by: Option<String>,
#[serde(skip_serializing_if = "Option::is_none")]
provenance: Option<Provenance>,
kind: RowKind,
},
Break {
order_by: OrderLabel,
pos: usize,
#[serde(skip_serializing_if = "Option::is_none")]
lane: Option<LaneId>,
kind: BreakKind,
n: u64,
},
}
#[derive(Debug, Clone, PartialEq, Serialize)]
pub struct TimelineReport {
pub order_by: OrderLabel,
#[serde(flatten)]
pub axis: AxisLabel,
pub scopes: Vec<String>,
#[serde(skip_serializing_if = "Option::is_none")]
pub window_s: Option<f64>,
pub source: TimelineSource,
pub lanes: Vec<LaneSummary>,
pub sn_lane: SnLaneReport,
#[serde(skip_serializing_if = "is_zero_usize")]
pub unstamped_excluded: usize,
pub dropped: u64,
#[serde(skip_serializing_if = "is_zero_u64")]
pub coalesced: u64,
pub keys_evicted: u64,
pub rows: Vec<TimelineEntry>,
}
fn is_zero_usize(n: &usize) -> bool {
*n == 0
}
fn is_zero_u64(n: &u64) -> bool {
*n == 0
}
#[cfg(test)]
mod tests {
use super::*;
use serde_json::json;
fn lane() -> LaneId {
LaneId::Origin {
origin: "h-3fa9c2d41b7e".into(),
producer: Some("sysinfo".into()),
}
}
#[test]
fn a_happens_before_report_is_pinned() {
let report = TimelineReport {
order_by: OrderLabel::Hlc,
axis: AxisLabel::Hlc {
claim: HlcClaim::HappensBefore {
stamper: "33".into(),
},
},
scopes: vec!["v1/**".into()],
window_s: Some(10.0),
source: TimelineSource::Live,
lanes: vec![LaneSummary {
lane: lane(),
samples: 1,
first_t_us: 5,
last_t_us: 5,
stampers: ["33".to_string()].into_iter().collect(),
provenance: ProvenanceCounts {
unattributable: 1,
..Default::default()
},
}],
sn_lane: SnLaneReport::Unavailable {
reason: SN_UNAVAILABLE_REASON,
},
unstamped_excluded: 2,
dropped: 0,
coalesced: 0,
keys_evicted: 0,
rows: vec![TimelineEntry::Sample {
order_by: OrderLabel::Hlc,
pos: 0,
lane: lane(),
key: "v1/h-3fa9c2d41b7e/telemetry/sysinfo/cpu".into(),
t_us: 5,
hlc: Some("100/33".into()),
stamped_by: Some("33".into()),
provenance: Some(Provenance::Unattributable),
kind: RowKind::Put,
}],
};
assert_eq!(
serde_json::to_value(&report).unwrap(),
json!({
"order_by": "hlc",
"axis": "hlc",
"claim": "happens_before",
"stamper": "33",
"scopes": ["v1/**"],
"window_s": 10.0,
"source": {"kind": "live"},
"lanes": [{
"lane": {"kind": "origin", "origin": "h-3fa9c2d41b7e", "producer": "sysinfo"},
"samples": 1,
"first_t_us": 5,
"last_t_us": 5,
"stampers": ["33"],
"provenance": {"self_stamped": 0, "foreign": 0, "unattributable": 1}
}],
"sn_lane": {
"state": "unavailable",
"reason": "zenoh 1.9/1.10 deliver no SourceInfo to subscribers (eclipse-zenoh/zenoh#2563); `tests/stamper.rs` pins it"
},
"unstamped_excluded": 2,
"dropped": 0,
"keys_evicted": 0,
"rows": [{
"row": "sample",
"order_by": "hlc",
"pos": 0,
"lane": {"kind": "origin", "origin": "h-3fa9c2d41b7e", "producer": "sysinfo"},
"key": "v1/h-3fa9c2d41b7e/telemetry/sysinfo/cpu",
"t_us": 5,
"hlc": "100/33",
"stamped_by": "33",
"provenance": "unattributable",
"kind": "put"
}]
})
);
}
#[test]
fn an_arrival_report_with_a_break_is_pinned_and_the_skew_claim_spells_its_stampers() {
let report = TimelineReport {
order_by: OrderLabel::Arrival,
axis: AxisLabel::Arrival {
clock: ARRIVAL_CLOCK,
},
scopes: vec!["v1/**".into()],
window_s: None,
source: TimelineSource::Zrec {
path: "bus.zrec".into(),
},
lanes: vec![],
sn_lane: SnLaneReport::Present {
sources: 1,
samples: 3,
},
unstamped_excluded: 0,
dropped: 7,
coalesced: 0,
keys_evicted: 0,
rows: vec![
TimelineEntry::Sample {
order_by: OrderLabel::Arrival,
pos: 0,
lane: LaneId::Unstamped,
key: "plain/key".into(),
t_us: 1,
hlc: None,
stamped_by: None,
provenance: None,
kind: RowKind::Delete,
},
TimelineEntry::Break {
order_by: OrderLabel::Arrival,
pos: 1,
lane: None,
kind: BreakKind::Dropped,
n: 7,
},
],
};
assert_eq!(
serde_json::to_value(&report).unwrap(),
json!({
"order_by": "arrival",
"axis": "arrival",
"clock": "observer monotonic, µs since window start",
"scopes": ["v1/**"],
"source": {"kind": "zrec", "path": "bus.zrec"},
"lanes": [],
"sn_lane": {"state": "present", "sources": 1, "samples": 3},
"dropped": 7,
"keys_evicted": 0,
"rows": [
{
"row": "sample",
"order_by": "arrival",
"pos": 0,
"lane": {"kind": "unstamped"},
"key": "plain/key",
"t_us": 1,
"kind": "delete"
},
{"row": "break", "order_by": "arrival", "pos": 1, "kind": "dropped", "n": 7}
]
})
);
let skew = AxisLabel::Hlc {
claim: HlcClaim::SkewedWallClock {
stampers: ["33".to_string(), "44".to_string()].into_iter().collect(),
},
};
assert_eq!(
serde_json::to_value(&skew).unwrap(),
json!({"axis": "hlc", "claim": "skewed_wall_clock", "stampers": ["33", "44"]})
);
let empty = AxisLabel::Hlc {
claim: HlcClaim::NoStampedSamples,
};
assert_eq!(
serde_json::to_value(&empty).unwrap(),
json!({"axis": "hlc", "claim": "no_stamped_samples"})
);
}
}