use serde::{Deserialize, Serialize};
use std::collections::VecDeque;
use std::sync::{Arc, Mutex};
use std::time::{Duration, Instant};
fn default_instant() -> Instant {
Instant::now()
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct PerformanceMetrics {
pub chunk_size: usize,
pub duration: Duration,
pub throughput: f64,
pub cache_hit_rate: f64,
pub memory_usage: u64,
#[serde(skip, default = "default_instant")]
pub timestamp: Instant,
}
impl PerformanceMetrics {
pub fn new(
chunk_size: usize,
duration: Duration,
cache_hit_rate: f64,
memory_usage: u64,
) -> Self {
let throughput = if duration.as_secs_f64() > 0.0 {
chunk_size as f64 / duration.as_secs_f64()
} else {
0.0
};
Self {
chunk_size,
duration,
throughput,
cache_hit_rate,
memory_usage,
timestamp: Instant::now(),
}
}
pub fn score(&self) -> f64 {
let normalized_throughput = self.throughput.log10().max(0.0);
let cache_weight = 0.3;
let throughput_weight = 0.7;
(cache_weight * self.cache_hit_rate) + (throughput_weight * normalized_throughput)
}
}
#[derive(Debug, Clone)]
pub struct ChunkSizePredictor {
history: VecDeque<PerformanceMetrics>,
max_history: usize,
best_chunk_size: usize,
best_score: f64,
window_size: usize,
min_chunk_size: usize,
max_chunk_size: usize,
learning_rate: f64,
}
impl ChunkSizePredictor {
pub fn new(initial_chunk_size: usize, min_chunk_size: usize, max_chunk_size: usize) -> Self {
Self {
history: VecDeque::new(),
max_history: 100,
best_chunk_size: initial_chunk_size,
best_score: 0.0,
window_size: 10,
min_chunk_size,
max_chunk_size,
learning_rate: 0.1,
}
}
pub fn with_config(
initial_chunk_size: usize,
min_chunk_size: usize,
max_chunk_size: usize,
max_history: usize,
window_size: usize,
learning_rate: f64,
) -> Self {
Self {
history: VecDeque::new(),
max_history,
best_chunk_size: initial_chunk_size,
best_score: 0.0,
window_size,
min_chunk_size,
max_chunk_size,
learning_rate,
}
}
pub fn record(&mut self, metrics: PerformanceMetrics) {
let score = metrics.score();
if score > self.best_score {
self.best_score = score;
self.best_chunk_size = metrics.chunk_size;
}
self.history.push_back(metrics);
while self.history.len() > self.max_history {
self.history.pop_front();
}
}
pub fn predict(&self) -> usize {
if self.history.is_empty() {
return self.best_chunk_size;
}
let recent_metrics: Vec<&PerformanceMetrics> =
self.history.iter().rev().take(self.window_size).collect();
if recent_metrics.is_empty() {
return self.best_chunk_size;
}
let total_score: f64 = recent_metrics.iter().map(|m| m.score()).sum();
if total_score == 0.0 {
return self.best_chunk_size;
}
let weighted_chunk_size: f64 = recent_metrics
.iter()
.map(|m| m.chunk_size as f64 * m.score())
.sum::<f64>()
/ total_score;
let predicted_size = (self.best_chunk_size as f64 * (1.0 - self.learning_rate))
+ (weighted_chunk_size * self.learning_rate);
predicted_size
.round()
.max(self.min_chunk_size as f64)
.min(self.max_chunk_size as f64) as usize
}
pub fn best_chunk_size(&self) -> usize {
self.best_chunk_size
}
pub fn average_throughput(&self) -> f64 {
if self.history.is_empty() {
return 0.0;
}
let recent: Vec<&PerformanceMetrics> =
self.history.iter().rev().take(self.window_size).collect();
recent.iter().map(|m| m.throughput).sum::<f64>() / recent.len() as f64
}
pub fn average_cache_hit_rate(&self) -> f64 {
if self.history.is_empty() {
return 0.0;
}
let recent: Vec<&PerformanceMetrics> =
self.history.iter().rev().take(self.window_size).collect();
recent.iter().map(|m| m.cache_hit_rate).sum::<f64>() / recent.len() as f64
}
pub fn reset(&mut self) {
self.history.clear();
self.best_score = 0.0;
}
pub fn history_size(&self) -> usize {
self.history.len()
}
}
pub type SharedPredictor = Arc<Mutex<ChunkSizePredictor>>;
#[allow(dead_code)]
pub fn create_shared_predictor(
initial_chunk_size: usize,
min_chunk_size: usize,
max_chunk_size: usize,
) -> SharedPredictor {
Arc::new(Mutex::new(ChunkSizePredictor::new(
initial_chunk_size,
min_chunk_size,
max_chunk_size,
)))
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_performance_metrics_creation() {
let metrics = PerformanceMetrics::new(1000, Duration::from_secs(1), 0.8, 1024 * 1024);
assert_eq!(metrics.chunk_size, 1000);
assert_eq!(metrics.duration, Duration::from_secs(1));
assert!((metrics.throughput - 1000.0).abs() < 1e-6);
assert_eq!(metrics.cache_hit_rate, 0.8);
assert_eq!(metrics.memory_usage, 1024 * 1024);
}
#[test]
fn test_performance_score() {
let metrics1 = PerformanceMetrics::new(1000, Duration::from_millis(100), 0.9, 1024 * 1024);
let metrics2 = PerformanceMetrics::new(1000, Duration::from_millis(100), 0.5, 1024 * 1024);
assert!(metrics1.score() > metrics2.score());
}
#[test]
fn test_predictor_creation() {
let predictor = ChunkSizePredictor::new(1000, 100, 10000);
assert_eq!(predictor.best_chunk_size(), 1000);
assert_eq!(predictor.history_size(), 0);
}
#[test]
fn test_predictor_record_and_predict() {
let mut predictor = ChunkSizePredictor::new(1000, 100, 10000);
let metrics1 = PerformanceMetrics::new(1000, Duration::from_millis(100), 0.8, 1024 * 1024);
predictor.record(metrics1);
let metrics2 = PerformanceMetrics::new(1500, Duration::from_millis(80), 0.85, 1024 * 1024);
predictor.record(metrics2);
let metrics3 = PerformanceMetrics::new(2000, Duration::from_millis(90), 0.9, 1024 * 1024);
predictor.record(metrics3);
assert_eq!(predictor.history_size(), 3);
let predicted = predictor.predict();
assert!(predicted >= 100);
assert!(predicted <= 10000);
}
#[test]
fn test_predictor_best_tracking() {
let mut predictor = ChunkSizePredictor::new(1000, 100, 10000);
let good_metrics =
PerformanceMetrics::new(2000, Duration::from_millis(50), 0.95, 1024 * 1024);
predictor.record(good_metrics);
let bad_metrics =
PerformanceMetrics::new(1000, Duration::from_millis(200), 0.5, 1024 * 1024);
predictor.record(bad_metrics);
assert_eq!(predictor.best_chunk_size(), 2000);
}
#[test]
fn test_predictor_history_limit() {
let mut predictor = ChunkSizePredictor::with_config(
1000, 100, 10000, 5, 3, 0.1,
);
for i in 0..10 {
let metrics = PerformanceMetrics::new(
1000 + i * 100,
Duration::from_millis(100),
0.8,
1024 * 1024,
);
predictor.record(metrics);
}
assert_eq!(predictor.history_size(), 5);
}
#[test]
fn test_predictor_averages() {
let mut predictor = ChunkSizePredictor::new(1000, 100, 10000);
let metrics1 = PerformanceMetrics::new(1000, Duration::from_millis(100), 0.8, 1024 * 1024);
predictor.record(metrics1);
let metrics2 = PerformanceMetrics::new(1000, Duration::from_millis(100), 0.6, 1024 * 1024);
predictor.record(metrics2);
let avg_cache_hit = predictor.average_cache_hit_rate();
assert!((avg_cache_hit - 0.7).abs() < 1e-6);
assert!(predictor.average_throughput() > 0.0);
}
#[test]
fn test_predictor_reset() {
let mut predictor = ChunkSizePredictor::new(1000, 100, 10000);
let metrics = PerformanceMetrics::new(2000, Duration::from_millis(100), 0.8, 1024 * 1024);
predictor.record(metrics);
assert_eq!(predictor.history_size(), 1);
predictor.reset();
assert_eq!(predictor.history_size(), 0);
}
#[test]
fn test_shared_predictor() {
let predictor = create_shared_predictor(1000, 100, 10000);
{
let mut p = predictor.lock().expect("Lock failed");
let metrics =
PerformanceMetrics::new(1500, Duration::from_millis(100), 0.8, 1024 * 1024);
p.record(metrics);
}
{
let p = predictor.lock().expect("Lock failed");
assert_eq!(p.history_size(), 1);
}
}
}