use std::time::Duration;
#[derive(Debug, Clone)]
pub struct MetricsCollector {
metrics: ChunkMetrics,
}
impl Default for MetricsCollector {
fn default() -> Self {
Self::new()
}
}
impl MetricsCollector {
pub fn new() -> Self {
Self {
metrics: ChunkMetrics::new(),
}
}
pub fn chunks_seen(&self) -> usize {
self.metrics.chunks_seen
}
pub fn chunk_sizes_avg(&self) -> f64 {
self.metrics.chunk_sizes.average()
}
pub fn processing_speed_mbps(&self) -> f64 {
self.metrics.processing_speed_mbps()
}
pub fn record_chunk(&mut self, chunk_bytes: usize, elapsed_ms: Duration, line_count: usize) {
self.metrics
.record_chunk_processing(chunk_bytes, elapsed_ms, line_count)
}
pub fn ready_to_adapt(&self, interval: usize) -> bool {
self.metrics.chunks_seen > 0 && self.metrics.chunks_seen % interval == 0 && self.metrics.lines_moving.count >= 3
}
pub fn snapshot(&self) -> &ChunkMetrics {
&self.metrics
}
}
#[derive(Debug, Clone)]
pub struct ChunkMetrics {
pub chunks_seen: usize,
pub parsed_per_ms: f64,
pub parsed_moving: MovingAverage<20>,
pub memory_bytes: usize,
pub memory_moving: MovingAverage<20>,
pub chunk_sizes: MovingAverage<20>,
pub lines_per_chunk: usize,
pub lines_moving: MovingAverage<20>,
total_bytes: usize,
total_duration: Duration,
}
impl Default for ChunkMetrics {
fn default() -> Self {
Self::new()
}
}
impl ChunkMetrics {
pub fn new() -> Self {
Self {
chunks_seen: 0,
chunk_sizes: MovingAverage::new(),
parsed_per_ms: 0.0,
parsed_moving: MovingAverage::new(),
memory_bytes: 0,
memory_moving: MovingAverage::new(),
lines_per_chunk: 0,
lines_moving: MovingAverage::new(),
total_bytes: 0,
total_duration: Duration::new(0, 0),
}
}
pub fn record_chunk_processing(&mut self, chunk_size: usize, duration: Duration, lines: usize) {
self.chunks_seen += 1;
self.lines_per_chunk = lines;
self.lines_moving.push(lines as f64);
let mb_processed = chunk_size as f64 / (1024.0 * 1024.0);
let seconds = duration.as_secs_f64();
let mbps = if seconds > 0.0 { mb_processed / seconds } else { 0.0 };
self.parsed_per_ms = mbps;
self.parsed_moving.push(mbps);
if let Some(stats) = memory_stats::memory_stats() {
self.memory_bytes = stats.physical_mem;
self.memory_moving.push(stats.physical_mem as f64);
}
self.chunk_sizes.push(chunk_size as f64);
self.total_bytes += chunk_size;
self.total_duration += duration;
}
pub fn overall_throughput_mbps(&self) -> f64 {
let total_mb = self.total_bytes as f64 / (1024.0 * 1024.0);
let total_secs = self.total_duration.as_secs_f64();
if total_secs > 0.0 { total_mb / total_secs } else { 0.0 }
}
pub fn processing_speed_mbps(&self) -> f64 {
self.parsed_moving.average()
}
pub fn speed_moving(&self) -> &MovingAverage<20> {
&self.parsed_moving
}
pub fn memory_moving(&self) -> &MovingAverage<20> {
&self.memory_moving
}
pub fn chunk_sizes(&self) -> &MovingAverage<20> {
&self.chunk_sizes
}
pub fn should_adapt(&self, interval: usize) -> bool {
self.chunks_seen > 0 && self.chunks_seen % interval == 0 && self.lines_moving.count >= 3
}
}
#[derive(Debug, Clone)]
pub struct MovingAverage<const N: usize> {
values: [f64; N],
index: usize,
count: usize,
}
impl<const N: usize> Default for MovingAverage<N> {
fn default() -> Self {
Self::new()
}
}
impl<const N: usize> MovingAverage<N> {
pub fn new() -> Self {
Self {
values: [0.0; N],
index: 0,
count: 0,
}
}
pub fn push(&mut self, value: f64) {
self.values[self.index] = value;
self.index = (self.index + 1) % N; if self.count < N {
self.count += 1;
}
}
pub fn average(&self) -> f64 {
if self.count == 0 {
return 0.0;
}
let sum: f64 = if self.count < N {
self.values[0..self.count].iter().sum()
} else {
self.values.iter().sum()
};
sum / self.count as f64
}
pub fn trend(&self, stable: f64) -> Trend {
if self.count < 2 {
return Trend::Unknown;
}
let mid = self.count / 2;
let oldest_idx = if self.count < N {
0 } else {
self.index };
let mut ptr = oldest_idx;
let mut sum = 0.0;
for _i in 0..mid {
sum += self.values[ptr];
ptr = (ptr + 1) % N
}
let older = sum / mid as f64;
ptr = (oldest_idx + mid) % N;
let mut sum = 0.0;
for _i in 0..mid {
sum += self.values[ptr];
ptr = (ptr + 1) % N;
}
let newer = sum / mid as f64;
let percent_diff = if older != 0.0 {
((newer - older) / older.abs()) * 100.0
} else {
0.0
};
if percent_diff.abs() < stable {
Trend::Stable
} else if percent_diff > 0.0 {
Trend::Improving
} else {
Trend::Degrading
}
}
}
#[derive(Debug, Clone, Default)]
pub enum Trend {
Improving,
Stable,
Degrading,
#[default]
Unknown,
}
#[cfg(test)]
mod test {
use super::*;
#[test]
fn metrics_collectors_work() {
let mut collector = MetricsCollector::new();
collector.record_chunk(8192, Duration::from_millis(10), 100);
collector.record_chunk(8192, Duration::from_millis(12), 95);
collector.record_chunk(8192, Duration::from_millis(8), 105);
assert!(!collector.ready_to_adapt(5));
collector.record_chunk(8192, Duration::from_millis(11), 98);
collector.record_chunk(8192, Duration::from_millis(9), 102);
assert!(collector.ready_to_adapt(5));
let snapshot = collector.snapshot();
assert!(snapshot.processing_speed_mbps() > 0.0);
}
#[test]
fn moving_average_can_average() {
let mut avg = MovingAverage::<5>::new();
avg.push(1.0);
avg.push(2.0);
avg.push(3.0);
assert_eq!(avg.average(), 2.0);
avg.push(4.0);
avg.push(5.0);
assert_eq!(avg.average(), 3.0);
avg.push(6.0); assert_eq!(avg.average(), 4.0); }
#[test]
fn can_keep_up_with_depeche_mode() {
let mut avg = MovingAverage::<4>::new();
avg.push(1.0);
avg.push(2.0);
avg.push(3.0);
avg.push(4.0);
assert!(matches!(avg.trend(0.5), Trend::Improving));
let mut avg = MovingAverage::<4>::new();
avg.push(5.0);
avg.push(5.1);
avg.push(4.9);
avg.push(5.0);
assert!(matches!(avg.trend(4.0), Trend::Stable));
}
#[test]
fn can_pass_thresholds() {
let mut avg = MovingAverage::<4>::new();
avg.push(50.0);
avg.push(51.0);
avg.push(49.0);
avg.push(50.0);
assert!(matches!(avg.trend(5.0), Trend::Stable));
let mut avg = MovingAverage::<4>::new();
avg.push(50.0);
avg.push(51.0);
avg.push(49.0);
avg.push(50.0);
assert!(matches!(avg.trend(0.5), Trend::Degrading));
}
}