use crate::messages::{FlushReason, Sample, SampleBatch};
use crate::{HEARTBEAT_INTERVAL_NS, MAX_BATCH_BYTES, MAX_BATCH_SIZE};
pub struct SampleRingBuffer {
samples: Vec<Sample>,
bench_id_hash: u64,
batch_sequence: u32,
start_iteration: u64,
next_iteration: u64,
last_flush_ns: u64,
}
impl SampleRingBuffer {
pub fn new(bench_id: &str) -> Self {
Self {
samples: Vec::with_capacity(MAX_BATCH_SIZE),
bench_id_hash: hash_bench_id(bench_id),
batch_sequence: 0,
start_iteration: 0,
next_iteration: 0,
last_flush_ns: Self::now_ns(),
}
}
#[inline]
pub fn push(&mut self, sample: Sample) -> Option<SampleBatch> {
self.samples.push(sample);
self.next_iteration += 1;
if self.samples.len() >= MAX_BATCH_SIZE {
return Some(self.flush(FlushReason::BatchFull));
}
if self.estimated_bytes() >= MAX_BATCH_BYTES {
return Some(self.flush(FlushReason::ByteLimitReached));
}
None
}
pub fn check_timeout(&mut self) -> Option<SampleBatch> {
if self.samples.is_empty() {
return None;
}
let now = Self::now_ns();
if now.saturating_sub(self.last_flush_ns) >= HEARTBEAT_INTERVAL_NS {
return Some(self.flush(FlushReason::HeartbeatTimeout));
}
None
}
pub fn flush_final(&mut self) -> Option<SampleBatch> {
if self.samples.is_empty() {
return None;
}
Some(self.flush(FlushReason::BenchmarkComplete))
}
pub fn flush_shutdown(&mut self) -> Option<SampleBatch> {
if self.samples.is_empty() {
return None;
}
Some(self.flush(FlushReason::Shutdown))
}
#[inline]
pub fn has_pending(&self) -> bool {
!self.samples.is_empty()
}
#[inline]
pub fn pending_count(&self) -> usize {
self.samples.len()
}
#[inline]
pub fn current_sequence(&self) -> u32 {
self.batch_sequence
}
fn estimated_bytes(&self) -> usize {
self.samples.len() * std::mem::size_of::<Sample>()
}
fn flush(&mut self, reason: FlushReason) -> SampleBatch {
let batch = SampleBatch {
bench_id_hash: self.bench_id_hash,
batch_sequence: self.batch_sequence,
start_iteration: self.start_iteration,
samples: std::mem::take(&mut self.samples),
flush_reason: reason,
};
self.batch_sequence += 1;
self.start_iteration = self.next_iteration;
self.last_flush_ns = Self::now_ns();
self.samples.reserve(MAX_BATCH_SIZE);
batch
}
#[inline(always)]
fn now_ns() -> u64 {
use std::time::Instant;
static START: std::sync::OnceLock<Instant> = std::sync::OnceLock::new();
let start = START.get_or_init(Instant::now);
start.elapsed().as_nanos() as u64
}
}
fn hash_bench_id(id: &str) -> u64 {
use std::hash::{Hash, Hasher};
let mut hasher = std::collections::hash_map::DefaultHasher::new();
id.hash(&mut hasher);
hasher.finish()
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_new_buffer() {
let buffer = SampleRingBuffer::new("test_benchmark");
assert!(!buffer.has_pending());
assert_eq!(buffer.pending_count(), 0);
assert_eq!(buffer.current_sequence(), 0);
}
#[test]
fn test_push_single() {
let mut buffer = SampleRingBuffer::new("test");
let sample = Sample::timing_only(1000.0);
let result = buffer.push(sample);
assert!(result.is_none()); assert!(buffer.has_pending());
assert_eq!(buffer.pending_count(), 1);
}
#[test]
fn test_flush_on_byte_limit() {
let mut buffer = SampleRingBuffer::new("test");
let samples_before_flush = MAX_BATCH_BYTES.div_ceil(std::mem::size_of::<Sample>());
for i in 0..(samples_before_flush - 1) {
let sample = Sample::timing_only(i as f64);
let result = buffer.push(sample);
assert!(result.is_none(), "unexpected flush at sample {}", i);
}
let sample = Sample::timing_only(9999.0);
let batch = buffer.push(sample);
assert!(batch.is_some());
let batch = batch.unwrap();
assert_eq!(batch.samples.len(), samples_before_flush);
assert_eq!(batch.flush_reason, FlushReason::ByteLimitReached);
assert_eq!(batch.batch_sequence, 0);
assert!(!buffer.has_pending());
assert_eq!(buffer.current_sequence(), 1);
}
#[test]
fn test_flush_final() {
let mut buffer = SampleRingBuffer::new("test");
buffer.push(Sample::timing_only(100.0));
buffer.push(Sample::timing_only(200.0));
buffer.push(Sample::timing_only(300.0));
let batch = buffer.flush_final();
assert!(batch.is_some());
let batch = batch.unwrap();
assert_eq!(batch.samples.len(), 3);
assert_eq!(batch.flush_reason, FlushReason::BenchmarkComplete);
}
#[test]
fn test_flush_final_empty() {
let mut buffer = SampleRingBuffer::new("test");
assert!(buffer.flush_final().is_none());
}
#[test]
fn test_bench_id_hash_consistency() {
let hash1 = hash_bench_id("my_benchmark");
let hash2 = hash_bench_id("my_benchmark");
let hash3 = hash_bench_id("other_benchmark");
assert_eq!(hash1, hash2);
assert_ne!(hash1, hash3);
}
}