use std::sync::Arc;
use axum::extract::ws::Utf8Bytes;
use kernel::{AxCode, AxError, EventRecord};
#[derive(Debug, Clone)]
pub struct Committed {
record: Arc<EventRecord>,
frame: Utf8Bytes,
}
impl Committed {
pub fn new(record: EventRecord) -> Result<Self, AxError> {
let unframed = |detail: String| {
AxError::failure(AxCode::WireMismatch, "frame a committed record", detail)
.with_recovery("ask for the record's range; the ledger holds it")
};
let mut frame = b"{\"event\":".to_vec();
serde_json::to_writer(&mut frame, &record)
.map_err(|source| unframed(source.to_string()))?;
frame.push(b'}');
let text = String::from_utf8(frame).map_err(|source| unframed(source.to_string()))?;
Ok(Self {
record: Arc::new(record),
frame: Utf8Bytes::from(text),
})
}
#[must_use]
pub fn record(&self) -> &EventRecord {
&self.record
}
pub(crate) fn frame(&self) -> Utf8Bytes {
self.frame.clone()
}
}
#[cfg(test)]
#[allow(
clippy::unwrap_used,
clippy::expect_used,
clippy::panic,
clippy::disallowed_methods,
clippy::arithmetic_side_effects
)]
mod tests {
use std::time::Instant;
use kernel::{B3Hash, EventRecord, Seq, TimeMs};
use tokio::sync::broadcast;
use super::Committed;
use crate::frames::ServerFrame;
fn record(seq: u64) -> EventRecord {
EventRecord::from_draft(
kernel::EventDraft {
run: kernel::RunId::from_bytes([4u8; 16]),
t: TimeMs::new(seq),
who: "resident".to_owned(),
addr: None,
kind: kernel::EventKind::RunStarted,
data: kernel::Payload::empty(),
ig: false,
},
Seq::new(seq),
B3Hash::digest(b"prev"),
)
}
#[test]
fn the_spliced_frame_is_the_serialised_event_frame() {
let committed = Committed::new(record(7)).unwrap();
let whole = serde_json::to_string(&ServerFrame::Event(Box::new(record(7)))).unwrap();
assert_eq!(committed.frame().as_str(), whole);
}
fn fanout_ns_per_event(sockets: usize) -> u128 {
const EVENTS: u64 = 4096;
let (events, _) = broadcast::channel::<Committed>(64);
let mut subscribers: Vec<_> = (0..sockets).map(|_| events.subscribe()).collect();
let prepared: Vec<EventRecord> = (1..=EVENTS).map(record).collect();
let mut written = 0usize;
let started = Instant::now();
for event in prepared {
events.send(Committed::new(event).unwrap()).unwrap();
for subscriber in &mut subscribers {
let text = subscriber.try_recv().unwrap().frame();
written = written.wrapping_add(text.len());
}
}
let spent = started.elapsed().as_nanos();
assert!(written > 0);
spent / u128::from(EVENTS)
}
#[test]
#[ignore = "an instrument: run with --release --run-ignored only"]
fn instrument_fanout_cpu_per_event() {
let readings: Vec<(usize, u128)> = [1, 4, 16]
.into_iter()
.map(|sockets| (sockets, fanout_ns_per_event(sockets)))
.collect();
for (sockets, ns) in &readings {
println!("fanout: {sockets} sockets, {ns} ns per event");
}
let at_one = readings.first().map(|(_, ns)| *ns).unwrap();
let at_sixteen = readings.last().map(|(_, ns)| *ns).unwrap();
assert!(
at_sixteen <= at_one.saturating_mul(2),
"16 sockets cost {at_sixteen} ns per event against {at_one} ns at one: the record is serialised per socket again"
);
}
}