use crate::config::{GROW_RATIO, SHRINK_RATIO};
use crate::resizable::ResizableStats;
pub(crate) const MIN_CACHE_SHARE_FOR_SHRINK: f64 = 0.25;
#[derive(Clone, Debug)]
pub struct CacheView {
pub name: String,
pub max_bytes: u64,
pub preferred_max_bytes: u64,
pub current_bytes: u64,
pub stats: ResizableStats,
}
#[derive(Clone, Debug)]
pub struct PolicyInput {
pub effective_target_bytes: u64,
pub rss_bytes: u64,
pub caches: Vec<CacheView>,
pub previous_cache_bytes: u64,
pub previous_cache_count: u32,
pub previous_rss_bytes: u64,
}
#[derive(Clone, Debug, Default)]
pub struct PolicyOutput {
pub new_max_bytes: Vec<u64>,
pub verdict: Verdict,
}
#[derive(Clone, Debug, Default)]
pub enum Verdict {
#[default]
Hold,
Shrink {
factor: f64,
},
SkipShrinkLeanCaches {
cache_bytes: u64,
rss_bytes: u64,
cache_share: f64,
},
SkipShrinkFlatCaches {
cache_bytes: u64,
previous_cache_bytes: u64,
cache_count: u32,
previous_cache_count: u32,
rss_bytes: u64,
},
Grow {
headroom_bytes: u64,
},
}
impl Verdict {
#[must_use]
pub fn label(&self) -> &'static str {
match self {
Verdict::Hold => "hold",
Verdict::Shrink { .. } => "shrink",
Verdict::SkipShrinkLeanCaches { .. } => "skip-shrink-lean-caches",
Verdict::SkipShrinkFlatCaches { .. } => "skip-shrink-flat-caches",
Verdict::Grow { .. } => "grow",
}
}
}
pub(crate) const FLAT_CACHE_TOLERANCE: f64 = 0.05;
pub trait Policy: Send + Sync {
fn reapportion(&self, input: &PolicyInput) -> PolicyOutput;
}
#[derive(Clone, Copy, Debug, Default)]
pub struct ProportionalPolicy;
impl Policy for ProportionalPolicy {
fn reapportion(&self, input: &PolicyInput) -> PolicyOutput {
let target = input.effective_target_bytes as f64;
let rss = input.rss_bytes as f64;
let shrink_threshold = target * SHRINK_RATIO;
let grow_threshold = target * GROW_RATIO;
let mut new_max_bytes = Vec::with_capacity(input.caches.len());
if target == 0.0 || rss >= shrink_threshold {
let cache_bytes: u64 = input.caches.iter().map(|c| c.current_bytes).sum();
let cache_count = input.caches.len() as u32;
let have_history = input.previous_rss_bytes > 0;
let rss_grew = input.rss_bytes > input.previous_rss_bytes;
let count_flat = cache_count <= input.previous_cache_count;
let bytes_flat = cache_bytes as f64
<= (input.previous_cache_bytes as f64) * (1.0 + FLAT_CACHE_TOLERANCE);
if have_history && rss_grew && count_flat && bytes_flat {
for cache in &input.caches {
new_max_bytes.push(cache.max_bytes);
}
return PolicyOutput {
new_max_bytes,
verdict: Verdict::SkipShrinkFlatCaches {
cache_bytes,
previous_cache_bytes: input.previous_cache_bytes,
cache_count,
previous_cache_count: input.previous_cache_count,
rss_bytes: input.rss_bytes,
},
};
}
let cache_share = if rss > 0.0 {
cache_bytes as f64 / rss
} else {
1.0
};
if rss > 0.0 && cache_share < MIN_CACHE_SHARE_FOR_SHRINK {
for cache in &input.caches {
new_max_bytes.push(cache.max_bytes);
}
return PolicyOutput {
new_max_bytes,
verdict: Verdict::SkipShrinkLeanCaches {
cache_bytes,
rss_bytes: input.rss_bytes,
cache_share,
},
};
}
let factor = if rss > 0.0 { target / rss } else { 1.0 };
for cache in &input.caches {
let shrunk = ((cache.max_bytes as f64) * factor).max(0.0) as u64;
new_max_bytes.push(shrunk);
}
return PolicyOutput {
new_max_bytes,
verdict: Verdict::Shrink { factor },
};
}
if rss <= grow_threshold {
let headroom = ((target - rss).max(0.0)) as u64;
grow_under_pressure(input, &mut new_max_bytes);
return PolicyOutput {
new_max_bytes,
verdict: Verdict::Grow {
headroom_bytes: headroom,
},
};
}
for cache in &input.caches {
new_max_bytes.push(cache.max_bytes);
}
PolicyOutput {
new_max_bytes,
verdict: Verdict::Hold,
}
}
}
fn grow_under_pressure(input: &PolicyInput, out: &mut Vec<u64>) {
let target = input.effective_target_bytes as f64;
let rss = input.rss_bytes as f64;
let headroom = (target - rss).max(0.0);
let weights: Vec<f64> = input
.caches
.iter()
.map(|c| c.stats.hit_rate().unwrap_or(0.0))
.collect();
let weight_sum: f64 = weights.iter().sum();
for (cache, weight) in input.caches.iter().zip(weights.iter()) {
let share = if weight_sum > 0.0 {
headroom * (weight / weight_sum)
} else {
headroom / (input.caches.len().max(1) as f64)
};
let proposed = (cache.max_bytes as f64) + share;
let bounded = proposed.min(cache.preferred_max_bytes as f64);
out.push(bounded as u64);
}
}