use std::sync::atomic::{AtomicU64, Ordering};
const MIN_SAMPLES: u64 = 10;
const ALPHA_NUMERATOR: u64 = 5;
const ALPHA_DENOMINATOR: u64 = 100;
const OUTLIER_RATIO: f64 = 10.0;
const MIN_MS_PER_UNIT: f64 = 0.001;
const MAX_MS_PER_UNIT: f64 = 50.0;
const SCALE: f64 = 1_000_000.0;
#[derive(Debug, Default)]
pub(crate) struct CboFeedbackLoop {
ema_scaled: AtomicU64,
sample_count: AtomicU64,
}
impl CboFeedbackLoop {
#[must_use]
pub(crate) fn new() -> Self {
Self::default()
}
pub(crate) fn record(&self, dataset_size: usize, ef_search: usize, actual_ms: f64) {
if actual_ms <= 0.0 || dataset_size == 0 {
return;
}
let estimated_cost = Self::estimate_cost(dataset_size, ef_search);
if estimated_cost <= 0.0 {
return;
}
let observed_ratio = actual_ms / estimated_cost;
let count = self.sample_count.load(Ordering::Relaxed);
if count >= MIN_SAMPLES {
let current_ema = self.current_ema();
if current_ema > 0.0 && observed_ratio / current_ema > OUTLIER_RATIO {
return;
}
}
self.sample_count.fetch_add(1, Ordering::Relaxed);
self.ema_update(observed_ratio);
}
#[must_use]
pub(crate) fn adjusted_ms_per_cost_unit(&self) -> Option<f64> {
if self.sample_count.load(Ordering::Relaxed) < MIN_SAMPLES {
return None;
}
let v = self.current_ema();
if v > 0.0 {
Some(v.clamp(MIN_MS_PER_UNIT, MAX_MS_PER_UNIT))
} else {
None
}
}
#[must_use]
pub(crate) fn sample_count(&self) -> u64 {
self.sample_count.load(Ordering::Relaxed)
}
#[must_use]
fn current_ema(&self) -> f64 {
#[allow(clippy::cast_precision_loss)]
let scaled = self.ema_scaled.load(Ordering::Relaxed) as f64;
scaled / SCALE
}
fn estimate_cost(dataset_size: usize, ef_search: usize) -> f64 {
#[allow(clippy::cast_precision_loss)]
let n_factor = (dataset_size as f64 + 1.0).log2();
#[allow(clippy::cast_precision_loss)]
let ef_factor = ef_search as f64 / 100.0;
n_factor * ef_factor
}
fn ema_update(&self, new_value: f64) {
let clamped = new_value.clamp(0.0, MAX_MS_PER_UNIT);
#[allow(clippy::cast_possible_truncation, clippy::cast_sign_loss)]
let new_scaled = (clamped * SCALE) as u64;
loop {
let old_scaled = self.ema_scaled.load(Ordering::Relaxed);
let new_ema_scaled = if old_scaled == 0 {
new_scaled
} else {
let num = u128::from(new_scaled) * u128::from(ALPHA_NUMERATOR)
+ u128::from(old_scaled) * u128::from(ALPHA_DENOMINATOR - ALPHA_NUMERATOR);
#[allow(clippy::cast_possible_truncation)]
let result = (num / u128::from(ALPHA_DENOMINATOR)) as u64;
result
};
if self
.ema_scaled
.compare_exchange_weak(
old_scaled,
new_ema_scaled,
Ordering::Relaxed,
Ordering::Relaxed,
)
.is_ok()
{
break;
}
}
}
}
#[cfg(test)]
#[path = "feedback_tests.rs"]
mod tests;