use rkyv::{Archive, Deserialize as RkyvDeserialize, Serialize as RkyvSerialize};
#[derive(Debug, Clone, Copy, Archive, RkyvSerialize, RkyvDeserialize)]
#[repr(C, align(8))]
pub struct Sample {
pub duration_nanos: u64,
pub alloc_bytes: u64,
pub alloc_count: u32,
pub cpu_cycles: u64,
}
impl Sample {
#[inline]
pub fn new(duration_nanos: u64, alloc_bytes: u64, alloc_count: u32, cpu_cycles: u64) -> Self {
Self {
duration_nanos,
alloc_bytes,
alloc_count,
cpu_cycles,
}
}
#[inline]
pub fn timing_only(duration_nanos: u64) -> Self {
Self {
duration_nanos,
alloc_bytes: 0,
alloc_count: 0,
cpu_cycles: 0,
}
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Archive, RkyvSerialize, RkyvDeserialize)]
pub enum FlushReason {
BatchFull,
ByteLimitReached,
HeartbeatTimeout,
BenchmarkComplete,
Shutdown,
}
#[derive(Debug, Clone, Archive, RkyvSerialize, RkyvDeserialize)]
pub struct SampleBatch {
pub bench_id_hash: u64,
pub batch_sequence: u32,
pub start_iteration: u64,
pub samples: Vec<Sample>,
pub flush_reason: FlushReason,
}
impl SampleBatch {
pub fn estimated_size(&self) -> usize {
std::mem::size_of::<u64>() * 2 + std::mem::size_of::<u32>() + std::mem::size_of::<FlushReason>()
+ self.samples.len() * std::mem::size_of::<Sample>()
}
}
#[derive(Debug, Clone, Archive, RkyvSerialize, RkyvDeserialize)]
pub struct WorkerCapabilities {
pub protocol_version: u32,
pub has_rdtsc: bool,
pub has_alloc_tracking: bool,
pub has_perf_counters: bool,
pub cpu_count: u32,
pub cpu_model: String,
}
impl Default for WorkerCapabilities {
fn default() -> Self {
Self {
protocol_version: crate::PROTOCOL_VERSION,
has_rdtsc: cfg!(target_arch = "x86_64"),
has_alloc_tracking: true,
has_perf_counters: false,
cpu_count: num_cpus(),
cpu_model: cpu_model_string(),
}
}
}
#[derive(Debug, Clone, Archive, RkyvSerialize, RkyvDeserialize)]
pub enum WorkerMessage {
Hello(WorkerCapabilities),
SampleBatch(SampleBatch),
WarmupComplete {
iterations: u64,
duration_nanos: u64,
},
Complete {
total_iterations: u64,
total_duration_nanos: u64,
},
Failure {
kind: FailureKind,
message: String,
backtrace: Option<String>,
},
Progress {
completed: u64,
estimated_total: u64,
},
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Archive, RkyvSerialize, RkyvDeserialize)]
pub enum FailureKind {
Panic,
Timeout,
Assertion,
AllocationLimit,
Signal,
Unknown,
}
#[derive(Debug, Clone, Archive, RkyvSerialize, RkyvDeserialize)]
pub enum SupervisorCommand {
Run {
bench_id: String,
config: BenchmarkConfig,
},
Abort,
Shutdown,
Ping,
}
#[derive(Debug, Clone, Archive, RkyvSerialize, RkyvDeserialize)]
pub struct BenchmarkConfig {
pub warmup_time_ns: u64,
pub measurement_time_ns: u64,
pub min_iterations: Option<u64>,
pub max_iterations: Option<u64>,
pub track_allocations: bool,
pub fail_on_allocation: bool,
pub timeout_ns: u64,
}
impl Default for BenchmarkConfig {
fn default() -> Self {
Self {
warmup_time_ns: 3_000_000_000, measurement_time_ns: 5_000_000_000, min_iterations: Some(100),
max_iterations: None,
track_allocations: true,
fail_on_allocation: false,
timeout_ns: 60_000_000_000, }
}
}
impl BenchmarkConfig {
pub fn validate(&self) -> Result<(), String> {
if self.warmup_time_ns == 0 {
return Err("warmup_time_ns must be > 0".to_string());
}
if self.measurement_time_ns == 0 {
return Err("measurement_time_ns must be > 0".to_string());
}
if self.timeout_ns > 0 && self.timeout_ns < self.measurement_time_ns {
return Err(format!(
"timeout_ns ({}) must be >= measurement_time_ns ({})",
self.timeout_ns, self.measurement_time_ns
));
}
if let (Some(min), Some(max)) = (self.min_iterations, self.max_iterations) {
if max < min {
return Err(format!(
"max_iterations ({}) must be >= min_iterations ({})",
max, min
));
}
}
Ok(())
}
}
fn num_cpus() -> u32 {
std::thread::available_parallelism()
.map(|p| p.get() as u32)
.unwrap_or(1)
}
fn cpu_model_string() -> String {
#[cfg(target_os = "linux")]
{
std::fs::read_to_string("/proc/cpuinfo")
.ok()
.and_then(|content| {
content
.lines()
.find(|line| line.starts_with("model name"))
.and_then(|line| line.split(':').nth(1))
.map(|s| s.trim().to_string())
})
.unwrap_or_else(|| "Unknown CPU".to_string())
}
#[cfg(not(target_os = "linux"))]
{
"Unknown CPU".to_string()
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_sample_creation() {
let sample = Sample::new(1000, 512, 5, 4000);
assert_eq!(sample.duration_nanos, 1000);
assert_eq!(sample.alloc_bytes, 512);
assert_eq!(sample.alloc_count, 5);
assert_eq!(sample.cpu_cycles, 4000);
}
#[test]
fn test_sample_timing_only() {
let sample = Sample::timing_only(5000);
assert_eq!(sample.duration_nanos, 5000);
assert_eq!(sample.alloc_bytes, 0);
assert_eq!(sample.alloc_count, 0);
assert_eq!(sample.cpu_cycles, 0);
}
#[test]
fn test_benchmark_config_validate_default() {
assert!(BenchmarkConfig::default().validate().is_ok());
}
#[test]
fn test_benchmark_config_validate_zero_warmup() {
let config = BenchmarkConfig {
warmup_time_ns: 0,
..Default::default()
};
assert!(config.validate().is_err());
}
#[test]
fn test_benchmark_config_validate_timeout_lt_measurement() {
let config = BenchmarkConfig {
timeout_ns: 1_000_000, measurement_time_ns: 5_000_000_000, ..Default::default()
};
assert!(config.validate().is_err());
}
#[test]
fn test_benchmark_config_validate_max_lt_min() {
let config = BenchmarkConfig {
min_iterations: Some(200),
max_iterations: Some(50),
..Default::default()
};
assert!(config.validate().is_err());
}
#[test]
fn test_worker_capabilities_default() {
let caps = WorkerCapabilities::default();
assert_eq!(caps.protocol_version, crate::PROTOCOL_VERSION);
assert!(caps.cpu_count >= 1);
}
}