use std::collections::VecDeque;
use std::time::{Duration, Instant};
#[derive(Copy, Clone, Debug)]
pub struct Occupancy {
percentage: usize,
}
pub struct AdaptiveChunking {
current_size: usize,
pub min_size: usize,
pub max_size: usize,
pub adjustment_window: usize,
pub measurements: VecDeque<Occupancy>,
pub last_adjustment_time: Option<Instant>,
min_adjustment_interval: Duration,
}
impl AdaptiveChunking {
#[must_use]
pub fn new() -> Self {
Self {
current_size: 256,
min_size: 16,
max_size: 1024,
adjustment_window: 50,
measurements: VecDeque::with_capacity(50),
last_adjustment_time: None,
min_adjustment_interval: Duration::from_secs(1),
}
}
pub fn observe(&mut self, items_buffered: usize, capacity: usize) -> Option<usize> {
let pct = if capacity == 0 {
0
} else {
(items_buffered * 100)
.checked_div(capacity)
.unwrap_or(100)
.min(100)
};
self.measurements.push_back(Occupancy { percentage: pct });
while self.measurements.len() > self.adjustment_window {
self.measurements.pop_front();
}
if self.measurements.len() == self.adjustment_window && self.should_adjust() {
return self.calculate_adjustment();
}
None
}
#[must_use]
pub const fn current_size(&self) -> usize {
self.current_size
}
pub fn with_bounds(mut self, min_size: usize, max_size: usize) -> Self {
if min_size == 0 || max_size < min_size {
tracing::warn!(
"invalid chunk bounds: min={}, max={}, keeping defaults",
min_size,
max_size
);
return self;
}
self.min_size = min_size;
self.max_size = max_size;
if self.current_size < min_size {
self.current_size = min_size;
} else if self.current_size > max_size {
self.current_size = max_size;
}
tracing::debug!(
"adaptive chunking bounds set: min={}, max={}, current={}",
self.min_size,
self.max_size,
self.current_size
);
self
}
#[must_use]
pub fn average_occupancy(&self) -> usize {
if self.measurements.is_empty() {
return 0;
}
let sum: usize = self.measurements.iter().map(|m| m.percentage).sum();
sum / self.measurements.len()
}
fn should_adjust(&self) -> bool {
if let Some(last_adj) = self.last_adjustment_time {
if last_adj.elapsed() < self.min_adjustment_interval {
return false;
}
}
let avg = self.average_occupancy();
!(20..=80).contains(&avg)
}
fn calculate_adjustment(&mut self) -> Option<usize> {
let avg = self.average_occupancy();
let old_size = self.current_size;
let new_size = if avg > 80 {
((self.current_size as f64 / 1.5).floor() as usize).max(self.min_size)
} else if avg < 20 {
((self.current_size as f64 * 1.5).ceil() as usize).min(self.max_size)
} else {
old_size
};
if new_size != old_size {
self.current_size = new_size;
self.last_adjustment_time = Some(Instant::now());
self.measurements.clear(); Some(new_size)
} else {
None
}
}
}
impl Default for AdaptiveChunking {
fn default() -> Self {
Self::new()
}
}
#[cfg(test)]
mod tests;