use std::collections::HashMap;
use super::{
BaselineEntry, RegressionCheck, RegressionThreshold, SystemContext, Trend,
DEFAULT_COLD_START_MARGIN, DEFAULT_STALENESS_SEC, MIN_SAMPLES_FOR_CONTEXT,
};
#[derive(Debug)]
pub struct ContextRegressionPredictor {
baselines: HashMap<String, Vec<BaselineEntry>>,
max_history: usize,
cold_start_margin: f64,
min_margin: f64,
staleness_sec: u64,
temp_factor: f64,
memory_factor: f64,
freq_factor: f64,
cache_cold_penalty: f64,
}
impl Default for ContextRegressionPredictor {
fn default() -> Self {
Self {
baselines: HashMap::new(),
max_history: 100,
cold_start_margin: DEFAULT_COLD_START_MARGIN,
min_margin: 3.0,
staleness_sec: DEFAULT_STALENESS_SEC,
temp_factor: 2.0,
memory_factor: 1.0,
freq_factor: 5.0,
cache_cold_penalty: 10.0,
}
}
}
fn entries_mean(entries: &[BaselineEntry], field: impl Fn(&BaselineEntry) -> f64) -> f64 {
let sum: f64 = entries.iter().map(&field).sum();
sum / entries.len() as f64
}
fn context_adjustment(
current: f64,
historical_avg: f64,
scale: f64,
factor: f64,
clamp_positive: bool,
) -> f64 {
let diff = current - historical_avg;
let scaled = diff * scale;
if clamp_positive {
scaled.max(0.0) * factor
} else {
scaled * factor
}
}
struct LinearFit {
slope: f64,
r_squared: f64,
}
fn linear_regression(
entries: &[BaselineEntry],
x_fn: impl Fn(&BaselineEntry) -> f64,
y_fn: impl Fn(&BaselineEntry) -> f64,
) -> Option<LinearFit> {
let n = entries.len() as f64;
let mut sum_x = 0.0_f64;
let mut sum_y = 0.0_f64;
let mut sum_xy = 0.0_f64;
let mut sum_xx = 0.0_f64;
for entry in entries {
let x = x_fn(entry);
let y = y_fn(entry);
sum_x += x;
sum_y += y;
sum_xy += x * y;
sum_xx += x * x;
}
let denom = n * sum_xx - sum_x * sum_x;
if denom.abs() < 1e-10 {
return None;
}
let slope = (n * sum_xy - sum_x * sum_y) / denom;
let intercept = (sum_y - slope * sum_x) / n;
let mean_y = sum_y / n;
let mut ss_res = 0.0;
let mut ss_tot = 0.0;
for entry in entries {
let x = x_fn(entry);
let y_pred = slope * x + intercept;
let y = y_fn(entry);
ss_res += (y - y_pred).powi(2);
ss_tot += (y - mean_y).powi(2);
}
let r_squared = if ss_tot > 0.0 {
1.0 - ss_res / ss_tot
} else {
0.0
};
Some(LinearFit { slope, r_squared })
}
fn coefficient_of_variation(values: &[f64]) -> f64 {
let n = values.len();
if n < 2 {
return 5.0;
}
let mean = values.iter().sum::<f64>() / n as f64;
if mean.abs() <= 1e-10 {
return 5.0;
}
let variance = values.iter().map(|v| (v - mean).powi(2)).sum::<f64>() / (n - 1).max(1) as f64;
(variance.sqrt() / mean.abs()) * 100.0
}
impl ContextRegressionPredictor {
pub fn new() -> Self {
Self::default()
}
pub fn with_cold_start_margin(mut self, margin: f64) -> Self {
self.cold_start_margin = margin.max(5.0);
self
}
pub fn with_min_margin(mut self, margin: f64) -> Self {
self.min_margin = margin.max(1.0);
self
}
pub fn with_temp_factor(mut self, factor: f64) -> Self {
self.temp_factor = factor.max(0.0);
self
}
pub fn with_staleness(mut self, sec: u64) -> Self {
self.staleness_sec = sec;
self
}
pub fn add_baseline(&mut self, metric: &str, value: f64, context: SystemContext) {
let entry = BaselineEntry::new(metric, value, context);
self.baselines
.entry(metric.to_string())
.or_default()
.push(entry);
if let Some(entries) = self.baselines.get_mut(metric) {
while entries.len() > self.max_history {
entries.remove(0);
}
}
}
pub fn baseline_count(&self, metric: &str) -> usize {
self.baselines.get(metric).map(|e| e.len()).unwrap_or(0)
}
pub fn has_sufficient_history(&self, metric: &str) -> bool {
self.baseline_count(metric) >= MIN_SAMPLES_FOR_CONTEXT
}
pub fn compute_threshold(
&self,
metric: &str,
current_context: &SystemContext,
) -> RegressionThreshold {
let sample_count = self.baseline_count(metric);
if sample_count < MIN_SAMPLES_FOR_CONTEXT {
return RegressionThreshold {
base_percent: self.cold_start_margin,
temp_adjustment: 0.0,
memory_adjustment: 0.0,
freq_adjustment: 0.0,
cache_adjustment: 0.0,
final_percent: self.cold_start_margin,
confidence: 0.1,
sample_count,
};
}
let entries = self
.baselines
.get(metric)
.expect("metric should exist in baselines after sufficient history check");
let values: Vec<f64> = entries.iter().map(|e| e.value).collect();
let cv = coefficient_of_variation(&values);
let base_percent = (cv * 2.0).max(self.min_margin);
let avg_temp = entries_mean(entries, |e| e.context.cpu_temp_c);
let temp_adjustment = context_adjustment(
current_context.cpu_temp_c,
avg_temp,
1.0 / 10.0,
self.temp_factor,
false,
);
let avg_mem = entries_mean(entries, |e| e.context.memory_percent);
let memory_adjustment = context_adjustment(
current_context.memory_percent,
avg_mem,
1.0 / 10.0,
self.memory_factor,
true,
);
let avg_freq_util = entries_mean(entries, |e| e.context.freq_utilization());
let freq_adjustment = context_adjustment(
avg_freq_util,
current_context.freq_utilization(),
10.0,
self.freq_factor,
true,
);
let cache_adjustment = if !current_context.cache_warm {
self.cache_cold_penalty
} else {
0.0
};
let final_percent = (base_percent
+ temp_adjustment
+ memory_adjustment
+ freq_adjustment
+ cache_adjustment)
.max(self.min_margin);
let confidence = (sample_count as f64 / 50.0).min(1.0);
RegressionThreshold {
base_percent,
temp_adjustment,
memory_adjustment,
freq_adjustment,
cache_adjustment,
final_percent,
confidence,
sample_count,
}
}
pub fn detect_trend(&self, metric: &str) -> Option<Trend> {
let entries = self.baselines.get(metric)?;
if entries.len() < MIN_SAMPLES_FOR_CONTEXT {
return None;
}
let base_time = entries.first()?.context.timestamp;
let fit = linear_regression(
entries,
|e| (e.context.timestamp - base_time) as f64 / 86400.0,
|e| e.value,
)?;
let direction = if fit.slope > 0.1 {
"increasing"
} else if fit.slope < -0.1 {
"decreasing"
} else {
"stable"
};
Some(Trend {
slope_per_day: fit.slope,
r_squared: fit.r_squared,
direction,
})
}
pub fn check_regression(
&self,
metric: &str,
current_value: f64,
context: &SystemContext,
) -> RegressionCheck {
let threshold = self.compute_threshold(metric, context);
let entries = self.baselines.get(metric);
let baseline_mean = entries
.map(|e| entries_mean(e, |x| x.value))
.unwrap_or(current_value);
let percent_change = if baseline_mean.abs() > 1e-10 {
((current_value - baseline_mean) / baseline_mean) * 100.0
} else {
0.0
};
let is_regression = threshold.is_regression(percent_change);
let trend = self.detect_trend(metric);
RegressionCheck {
metric: metric.to_string(),
current_value,
baseline_mean,
percent_change,
threshold,
is_regression,
trend,
}
}
pub fn clear(&mut self, metric: &str) {
self.baselines.remove(metric);
}
pub fn clear_all(&mut self) {
self.baselines.clear();
}
pub fn export_json(&self, metric: &str) -> Option<String> {
let entries = self.baselines.get(metric)?;
let entries_json: Vec<String> = entries
.iter()
.map(|e| {
format!(
r#"{{"value":{},"context":{}}}"#,
e.value,
e.context.to_json()
)
})
.collect();
Some(format!(
r#"{{"metric":"{}","entries":[{}]}}"#,
metric,
entries_json.join(",")
))
}
}