use std::sync::atomic::{AtomicUsize, Ordering};
const GROW_THRESHOLD: f64 = 0.8;
const SHRINK_THRESHOLD: f64 = 0.5;
const GROW_FACTOR: f64 = 1.1;
const SHRINK_FACTOR: f64 = 0.95;
#[derive(Debug)]
pub(crate) struct DynamicWriteBatchSizeEstimator {
current: AtomicUsize,
min: usize,
max: usize,
}
impl DynamicWriteBatchSizeEstimator {
pub fn new(min_size: usize, max_size: usize) -> Self {
Self {
current: AtomicUsize::new(max_size),
min: min_size.min(max_size),
max: max_size,
}
}
pub fn current(&self) -> usize {
self.current.load(Ordering::Relaxed)
}
pub fn update(&self, actual: usize) -> usize {
let prev = self.current.load(Ordering::Relaxed);
let cur = prev as f64;
let actual = actual as f64;
let next = if actual > cur * GROW_THRESHOLD {
cur * GROW_FACTOR
} else if actual < cur * SHRINK_THRESHOLD {
cur * SHRINK_FACTOR
} else {
cur
};
let clamped = (next as usize).clamp(self.min, self.max);
if clamped != prev {
self.current.store(clamped, Ordering::Relaxed);
}
clamped
}
}
#[cfg(test)]
mod tests {
use super::*;
const MIN: usize = 256 * 1024;
const MAX: usize = 2 * 1024 * 1024;
const CONVERGENCE_STEPS: usize = 50;
#[test]
fn starts_at_max() {
let est = DynamicWriteBatchSizeEstimator::new(MIN, MAX);
assert_eq!(est.current(), MAX);
}
#[test]
fn min_clamped_to_max_when_misconfigured() {
let est = DynamicWriteBatchSizeEstimator::new(MAX * 2, MAX);
assert_eq!(est.current(), MAX);
assert_eq!(est.update(0), MAX);
}
#[test]
fn grows_when_above_grow_threshold() {
let est = DynamicWriteBatchSizeEstimator::new(MIN, MAX);
for _ in 0..CONVERGENCE_STEPS {
est.update(0);
}
assert_eq!(est.current(), MIN);
let next = est.update((MIN as f64 * 0.9) as usize);
assert_eq!(next, ((MIN as f64) * GROW_FACTOR) as usize);
}
#[test]
fn shrinks_when_below_shrink_threshold() {
let est = DynamicWriteBatchSizeEstimator::new(MIN, MAX);
let next = est.update((MAX as f64 * 0.4) as usize);
assert_eq!(next, ((MAX as f64) * SHRINK_FACTOR) as usize);
}
#[test]
fn shrink_clamps_to_min() {
let est = DynamicWriteBatchSizeEstimator::new(MIN, MAX);
for _ in 0..CONVERGENCE_STEPS {
est.update(0);
}
assert_eq!(est.current(), MIN);
}
#[test]
fn grow_clamps_to_max() {
let est = DynamicWriteBatchSizeEstimator::new(MIN, MAX);
for _ in 0..CONVERGENCE_STEPS {
est.update(0);
}
for _ in 0..CONVERGENCE_STEPS {
est.update(est.current());
}
assert_eq!(est.current(), MAX);
}
#[test]
fn oversized_actual_clamps_at_max() {
let est = DynamicWriteBatchSizeEstimator::new(MIN, MAX);
assert_eq!(est.update(MAX * 4), MAX);
}
#[test]
fn dead_zone_is_a_fixed_point() {
let est = DynamicWriteBatchSizeEstimator::new(MIN, MAX);
let initial = est.current();
for _ in 0..20 {
est.update((est.current() as f64 * 0.65) as usize);
}
assert_eq!(est.current(), initial);
}
}