krabka_bench_driver/ids.rs
1//! Newtypes over the bare integers that the driver and report layers thread
2//! around.
3//!
4//! A mix-up between two same-typed values becomes a compile error and not a
5//! silently wrong benchmark number. Two such pairs are a message count against
6//! a window length, and a wallclock epoch-ms against a ms-into-run offset.
7//!
8//! Each type is `#[serde(transparent)]`, so the on-disk `RunOutput` JSON is
9//! byte-identical to the bare primitive it wraps. The report aggregator and
10//! any external tool that reads the artifacts see no change.
11
12use {crate::numeric::saturating_u64_to_i64, krabka_units::prelude::*};
13
14/// A number of Kafka messages (produced, consumed, or dropped).
15#[krabka_macros::scalar_id]
16#[derive(Default)]
17pub struct MessageCount(pub u64);
18
19/// Milliseconds **into a run**.
20///
21/// This is a time-series sample offset relative to the start of the measurement
22/// window or the wallclock window. It is ordered, so it can key the per-offset
23/// averaging maps. Do not confuse it with [`WallclockMs`], which is an absolute
24/// unix-epoch timestamp.
25///
26/// This stays an integer and does not become a [`Time`], because it is a
27/// *coordinate* on the fixed sampling grid. The cross-run averaging in
28/// [`crate::aggregate`] keys `BTreeMap`s by it, which a `f64`-backed quantity
29/// cannot do. [`Self::as_time`] and [`Self::since`] are the seams that turn a
30/// pair of coordinates into the extent between them.
31#[krabka_macros::scalar_id]
32#[derive(Default)]
33pub struct TimeOffsetMs(pub u64);
34
35impl TimeOffsetMs {
36 /// This offset as the time extent from the window start.
37 #[must_use]
38 pub fn as_time(self) -> Time { Time::from_millis(saturating_u64_to_i64(self.0)) }
39
40 /// The extent from `earlier` to this offset. This is no time at all when
41 /// the two are the wrong way round.
42 #[must_use]
43 pub fn since(self, earlier: Self) -> Time {
44 Time::from_millis(saturating_u64_to_i64(self.0.saturating_sub(earlier.0)))
45 }
46}
47
48/// An **absolute** wallclock timestamp in unix-epoch milliseconds.
49///
50/// It is an `i64` to match `chrono::Utc::now().timestamp_millis()`. It is
51/// distinct from the run-relative [`TimeOffsetMs`], so no one can swap the
52/// paired start and end fields on `RunOutput`.
53#[krabka_macros::scalar_id]
54pub struct WallclockMs(pub i64);
55
56#[cfg(test)]
57mod tests {
58 use {super::*, assert2::check};
59
60 #[test]
61 fn scalar_ids_keep_their_json_and_primitive_interfaces() {
62 let count = MessageCount::from(17_u64);
63 check!(serde_json::to_string(&count).unwrap() == "17");
64 check!(serde_json::from_str::<MessageCount>("17").unwrap() == count);
65 check!(count > 16_u64);
66 check!(count.to_string() == "17");
67 check!(format!("{count:?}") == "MessageCount(17)");
68 check!(MessageCount::default() == 0_u64);
69
70 let offset = TimeOffsetMs::from(u64::MAX);
71 let encoded = "18446744073709551615";
72 check!(serde_json::to_string(&offset).unwrap() == encoded);
73 check!(serde_json::from_str::<TimeOffsetMs>(encoded).unwrap() == offset);
74 check!(TimeOffsetMs::default() == 0_u64);
75 let raw_offset: u64 = offset.into();
76 check!(raw_offset == u64::MAX);
77
78 let wallclock = WallclockMs::from(i64::MIN);
79 let encoded = "-9223372036854775808";
80 check!(serde_json::to_string(&wallclock).unwrap() == encoded);
81 check!(serde_json::from_str::<WallclockMs>(encoded).unwrap() == wallclock);
82 let raw_wallclock: i64 = wallclock.into();
83 check!(raw_wallclock == i64::MIN);
84 }
85
86 #[test]
87 fn offsets_convert_to_extents_and_differences() {
88 check!(TimeOffsetMs(2_500).as_time() == millis(2500));
89 check!(TimeOffsetMs(6_000).since(TimeOffsetMs(4_000)) == secs(2));
90 // Out of order: no negative extents.
91 check!(TimeOffsetMs(4_000).since(TimeOffsetMs(6_000)) == Time::ZERO);
92 }
93}