1use std::sync::Arc;
13
14use axum::extract::ws::Utf8Bytes;
15use kernel::{AxCode, AxError, EventRecord};
16
17#[derive(Debug, Clone)]
20pub struct Committed {
21 record: Arc<EventRecord>,
22 frame: Utf8Bytes,
23}
24
25impl Committed {
26 pub fn new(record: EventRecord) -> Result<Self, AxError> {
34 let unframed = |detail: String| {
35 AxError::failure(AxCode::WireMismatch, "frame a committed record", detail)
36 .with_recovery("ask for the record's range; the ledger holds it")
37 };
38 let mut frame = b"{\"event\":".to_vec();
39 serde_json::to_writer(&mut frame, &record)
40 .map_err(|source| unframed(source.to_string()))?;
41 frame.push(b'}');
42 let text = String::from_utf8(frame).map_err(|source| unframed(source.to_string()))?;
43 Ok(Self {
44 record: Arc::new(record),
45 frame: Utf8Bytes::from(text),
46 })
47 }
48
49 #[must_use]
51 pub fn record(&self) -> &EventRecord {
52 &self.record
53 }
54
55 pub(crate) fn frame(&self) -> Utf8Bytes {
57 self.frame.clone()
58 }
59}
60
61#[cfg(test)]
62#[allow(
63 clippy::unwrap_used,
64 clippy::expect_used,
65 clippy::panic,
66 clippy::disallowed_methods,
67 clippy::arithmetic_side_effects
68)]
69mod tests {
70 use std::time::Instant;
71
72 use kernel::{B3Hash, EventRecord, Seq, TimeMs};
73 use tokio::sync::broadcast;
74
75 use super::Committed;
76 use crate::frames::ServerFrame;
77
78 fn record(seq: u64) -> EventRecord {
79 EventRecord::from_draft(
80 kernel::EventDraft {
81 run: kernel::RunId::from_bytes([4u8; 16]),
82 t: TimeMs::new(seq),
83 who: "resident".to_owned(),
84 addr: None,
85 kind: kernel::EventKind::RunStarted,
86 data: kernel::Payload::empty(),
87 ig: false,
88 },
89 Seq::new(seq),
90 B3Hash::digest(b"prev"),
91 )
92 }
93
94 #[test]
95 fn the_spliced_frame_is_the_serialised_event_frame() {
96 let committed = Committed::new(record(7)).unwrap();
97 let whole = serde_json::to_string(&ServerFrame::Event(Box::new(record(7)))).unwrap();
98 assert_eq!(committed.frame().as_str(), whole);
99 }
100
101 fn fanout_ns_per_event(sockets: usize) -> u128 {
104 const EVENTS: u64 = 4096;
105 let (events, _) = broadcast::channel::<Committed>(64);
106 let mut subscribers: Vec<_> = (0..sockets).map(|_| events.subscribe()).collect();
107 let prepared: Vec<EventRecord> = (1..=EVENTS).map(record).collect();
108 let mut written = 0usize;
109 let started = Instant::now();
110 for event in prepared {
111 events.send(Committed::new(event).unwrap()).unwrap();
112 for subscriber in &mut subscribers {
113 let text = subscriber.try_recv().unwrap().frame();
114 written = written.wrapping_add(text.len());
115 }
116 }
117 let spent = started.elapsed().as_nanos();
118 assert!(written > 0);
119 spent / u128::from(EVENTS)
120 }
121
122 #[test]
123 #[ignore = "an instrument: run with --release --run-ignored only"]
124 fn instrument_fanout_cpu_per_event() {
125 let readings: Vec<(usize, u128)> = [1, 4, 16]
126 .into_iter()
127 .map(|sockets| (sockets, fanout_ns_per_event(sockets)))
128 .collect();
129 for (sockets, ns) in &readings {
130 println!("fanout: {sockets} sockets, {ns} ns per event");
131 }
132 let at_one = readings.first().map(|(_, ns)| *ns).unwrap();
133 let at_sixteen = readings.last().map(|(_, ns)| *ns).unwrap();
134 assert!(
135 at_sixteen <= at_one.saturating_mul(2),
136 "16 sockets cost {at_sixteen} ns per event against {at_one} ns at one: the record is serialised per socket again"
137 );
138 }
139}