use crate::brick::{Brick, BrickAssertion, BrickBudget, BrickVerification};
use crate::ring_buffer::RingBuffer;
use std::any::Any;
use std::time::{Duration, Instant};
#[derive(Debug, Clone)]
pub struct IoUringMetrics {
pub timestamp: Instant,
pub sq_pending: u32,
pub cq_ready: u32,
pub submissions_per_sec: f64,
pub completions_per_sec: f64,
pub avg_latency_us: f64,
pub p99_latency_us: f64,
pub bytes_read: u64,
pub bytes_written: u64,
pub ublk_devices: u32,
}
impl Default for IoUringMetrics {
fn default() -> Self {
Self {
timestamp: Instant::now(),
sq_pending: 0,
cq_ready: 0,
submissions_per_sec: 0.0,
completions_per_sec: 0.0,
avg_latency_us: 0.0,
p99_latency_us: 0.0,
bytes_read: 0,
bytes_written: 0,
ublk_devices: 0,
}
}
}
pub struct PepitaCollectorBrick {
history: RingBuffer<IoUringMetrics>,
last_submissions: u64,
last_completions: u64,
last_collection: Instant,
available: bool,
}
impl PepitaCollectorBrick {
pub fn new() -> Self {
Self {
history: RingBuffer::new(120), last_submissions: 0,
last_completions: 0,
last_collection: Instant::now(),
available: Self::check_availability(),
}
}
fn check_availability() -> bool {
#[cfg(target_os = "linux")]
{
std::path::Path::new("/proc/sys/kernel/io_uring_disabled").exists()
|| std::path::Path::new("/sys/kernel/tracing/events/io_uring").exists()
}
#[cfg(not(target_os = "linux"))]
{
false
}
}
pub fn collect(&mut self) -> IoUringMetrics {
let now = Instant::now();
let elapsed = now.duration_since(self.last_collection).as_secs_f64();
let metrics = if self.available {
self.collect_real_metrics(elapsed)
} else {
self.collect_mock_metrics(elapsed)
};
self.last_collection = now;
self.history.push(metrics.clone());
metrics
}
fn collect_real_metrics(&mut self, elapsed: f64) -> IoUringMetrics {
self.collect_mock_metrics(elapsed)
}
fn collect_mock_metrics(&mut self, elapsed: f64) -> IoUringMetrics {
let submission_count = self.last_submissions + (elapsed * 50000.0) as u64;
let completion_count = self.last_completions + (elapsed * 49900.0) as u64;
let submissions_per_sec = if elapsed > 0.0 {
(submission_count - self.last_submissions) as f64 / elapsed
} else {
0.0
};
let completions_per_sec = if elapsed > 0.0 {
(completion_count - self.last_completions) as f64 / elapsed
} else {
0.0
};
self.last_submissions = submission_count;
self.last_completions = completion_count;
IoUringMetrics {
timestamp: Instant::now(),
sq_pending: 16,
cq_ready: 8,
submissions_per_sec,
completions_per_sec,
avg_latency_us: 12.5,
p99_latency_us: 45.0,
bytes_read: submission_count * 4096,
bytes_written: completion_count * 4096,
ublk_devices: 1,
}
}
pub fn history(&self) -> &RingBuffer<IoUringMetrics> {
&self.history
}
pub fn is_available(&self) -> bool {
self.available
}
pub fn interval_hint(&self) -> Duration {
Duration::from_millis(100) }
pub fn throughput_summary(&self) -> ThroughputSummary {
if let Some(last) = self.history.back() {
let elapsed = last.timestamp.elapsed().as_secs_f64().max(1.0);
ThroughputSummary {
read_mb_per_sec: (last.bytes_read as f64 / 1024.0 / 1024.0) / elapsed,
write_mb_per_sec: (last.bytes_written as f64 / 1024.0 / 1024.0) / elapsed,
iops: last.submissions_per_sec + last.completions_per_sec,
}
} else {
ThroughputSummary::default()
}
}
pub fn latency_breakdown(&self) -> LatencyBreakdown {
if let Some(last) = self.history.back() {
LatencyBreakdown {
avg_us: last.avg_latency_us,
p99_us: last.p99_latency_us,
queue_depth: last.sq_pending + last.cq_ready,
}
} else {
LatencyBreakdown::default()
}
}
}
#[derive(Debug, Clone, Default)]
pub struct ThroughputSummary {
pub read_mb_per_sec: f64,
pub write_mb_per_sec: f64,
pub iops: f64,
}
#[derive(Debug, Clone, Default)]
pub struct LatencyBreakdown {
pub avg_us: f64,
pub p99_us: f64,
pub queue_depth: u32,
}
impl Default for PepitaCollectorBrick {
fn default() -> Self {
Self::new()
}
}
impl Brick for PepitaCollectorBrick {
fn brick_name(&self) -> &'static str {
"pepita_collector"
}
fn assertions(&self) -> Vec<BrickAssertion> {
vec![
BrickAssertion::ValueInRange {
min: 0.0,
max: 1_000_000.0,
}, BrickAssertion::max_latency_ms(1), ]
}
fn budget(&self) -> BrickBudget {
BrickBudget {
collect_ms: 1,
layout_ms: 0,
render_ms: 0,
}
}
fn verify(&self) -> BrickVerification {
let mut v = BrickVerification::new();
for assertion in self.assertions() {
v.check(&assertion);
}
v
}
fn as_any(&self) -> &dyn Any {
self
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_pepita_collector_brick_name() {
let collector = PepitaCollectorBrick::new();
assert_eq!(collector.brick_name(), "pepita_collector");
}
#[test]
fn test_pepita_collector_has_assertions() {
let collector = PepitaCollectorBrick::new();
assert!(!collector.assertions().is_empty());
}
#[test]
fn test_pepita_collector_collect() {
let mut collector = PepitaCollectorBrick::new();
let metrics = collector.collect();
assert!(metrics.sq_pending > 0 || metrics.cq_ready > 0 || !collector.is_available());
}
#[test]
fn test_pepita_throughput_summary() {
let mut collector = PepitaCollectorBrick::new();
collector.collect();
let summary = collector.throughput_summary();
assert!(summary.iops >= 0.0);
}
#[test]
fn test_pepita_latency_breakdown() {
let mut collector = PepitaCollectorBrick::new();
collector.collect();
let breakdown = collector.latency_breakdown();
assert!(breakdown.avg_us >= 0.0);
assert!(breakdown.p99_us >= breakdown.avg_us);
}
#[test]
fn test_pepita_iops_rate() {
let mut collector = PepitaCollectorBrick::new();
collector.collect();
std::thread::sleep(Duration::from_millis(10));
let metrics = collector.collect();
assert!(metrics.submissions_per_sec >= 0.0);
assert!(metrics.completions_per_sec >= 0.0);
}
}