use crate::bencher::SampleSink;
use fluxbench_ipc::{FrameError, FrameWriter, Sample, SampleRingBuffer, WorkerMessage};
use std::io::Write;
use std::sync::{Arc, Mutex, MutexGuard};
pub(super) type IpcWriter = FrameWriter<Box<dyn Write + Send>>;
struct Stream {
writer: IpcWriter,
ring: Option<SampleRingBuffer>,
error: Option<FrameError>,
}
impl Stream {
fn write(&mut self, message: &WorkerMessage) {
if self.error.is_some() {
return;
}
if let Err(e) = self.writer.write(message) {
self.error = Some(e);
}
}
}
#[derive(Clone)]
pub(super) struct SharedStream(Arc<Mutex<Stream>>);
impl SharedStream {
pub(super) fn new(writer: IpcWriter) -> Self {
Self(Arc::new(Mutex::new(Stream {
writer,
ring: None,
error: None,
})))
}
fn lock(&self) -> MutexGuard<'_, Stream> {
match self.0.lock() {
Ok(guard) => guard,
Err(poisoned) => poisoned.into_inner(),
}
}
pub(super) fn send(&self, message: &WorkerMessage) -> Result<(), FrameError> {
let mut stream = self.lock();
if let Some(e) = stream.error.take() {
return Err(e);
}
stream.writer.write(message)
}
pub(super) fn begin(&self, bench_id: &str) {
let mut stream = self.lock();
stream.ring = Some(SampleRingBuffer::new(bench_id));
stream.error = None;
}
pub(super) fn push(&self, sample: Sample) {
let mut stream = self.lock();
let Some(ring) = stream.ring.as_mut() else {
return;
};
let batch = ring.push(sample).or_else(|| ring.check_timeout());
if let Some(batch) = batch {
stream.write(&WorkerMessage::SampleBatch(batch));
}
}
pub(super) fn flush_due(&self, stop_requested: bool) {
let mut stream = self.lock();
let Some(ring) = stream.ring.as_mut() else {
return;
};
let batch = if stop_requested {
ring.flush_shutdown()
} else {
ring.check_timeout()
};
if let Some(batch) = batch {
stream.write(&WorkerMessage::SampleBatch(batch));
}
}
pub(super) fn end(&self, last: &WorkerMessage) -> Result<(), FrameError> {
let mut stream = self.lock();
if let Some(batch) = stream.ring.take().and_then(|mut ring| ring.flush_final()) {
stream.write(&WorkerMessage::SampleBatch(batch));
}
stream.write(last);
match stream.error.take() {
Some(e) => Err(e),
None => Ok(()),
}
}
}
pub(super) struct StreamSink(pub(super) SharedStream);
impl SampleSink for StreamSink {
fn on_sample(&mut self, sample: &Sample) {
self.0.push(*sample);
}
}
#[cfg(test)]
mod tests {
use super::*;
use fluxbench_ipc::{FlushReason, FrameReader};
use std::io::Cursor;
#[derive(Clone, Default)]
struct Shared(Arc<Mutex<Vec<u8>>>);
impl Write for Shared {
fn write(&mut self, buf: &[u8]) -> std::io::Result<usize> {
self.0.lock().expect("lock").extend_from_slice(buf);
Ok(buf.len())
}
fn flush(&mut self) -> std::io::Result<()> {
Ok(())
}
}
fn stream() -> (SharedStream, Shared) {
let buffer = Shared::default();
let writer = FrameWriter::new(Box::new(buffer.clone()) as Box<dyn Write + Send>);
(SharedStream::new(writer), buffer)
}
fn messages(buffer: &Shared) -> Vec<WorkerMessage> {
let bytes = buffer.0.lock().expect("lock").clone();
let mut reader = FrameReader::new(Cursor::new(bytes));
let mut out = Vec::new();
while let Ok(msg) = reader.read::<WorkerMessage>() {
out.push(msg);
}
out
}
#[test]
fn end_sends_pending_samples_then_last_message() {
let (stream, buffer) = stream();
stream.begin("b");
for i in 0..5 {
stream.push(Sample::timing_only(i as f64));
}
stream
.end(&WorkerMessage::Complete {
total_iterations: 5,
total_duration_nanos: 10,
throughput: None,
})
.expect("end succeeds");
let msgs = messages(&buffer);
let sampled: usize = msgs
.iter()
.map(|m| match m {
WorkerMessage::SampleBatch(b) => b.samples.len(),
_ => 0,
})
.sum();
assert_eq!(sampled, 5);
assert!(matches!(msgs.last(), Some(WorkerMessage::Complete { .. })));
}
#[test]
fn stop_request_flushes_pending_samples() {
let (stream, buffer) = stream();
stream.begin("b");
stream.push(Sample::timing_only(1.0));
stream.push(Sample::timing_only(2.0));
stream.flush_due(true);
let msgs = messages(&buffer);
match msgs.as_slice() {
[WorkerMessage::SampleBatch(batch)] => {
assert_eq!(batch.samples.len(), 2);
assert_eq!(batch.flush_reason, FlushReason::Shutdown);
}
other => panic!("expected one batch, got {other:?}"),
}
}
#[test]
fn push_outside_a_run_sends_nothing() {
let (stream, buffer) = stream();
stream.push(Sample::timing_only(1.0));
stream.flush_due(true);
assert!(messages(&buffer).is_empty());
}
}