#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum VarianceSource {
FrequencyScaling,
ThermalThrottling,
CacheState,
SystemNoise,
Unknown,
}
impl VarianceSource {
pub fn name(&self) -> &'static str {
match self {
VarianceSource::FrequencyScaling => "CPU frequency scaling",
VarianceSource::ThermalThrottling => "thermal throttling",
VarianceSource::CacheState => "cache state variance",
VarianceSource::SystemNoise => "system noise",
VarianceSource::Unknown => "unknown",
}
}
pub fn mitigation(&self) -> &'static str {
match self {
VarianceSource::FrequencyScaling => {
"Pin CPU frequency with: cpupower frequency-set -g performance"
}
VarianceSource::ThermalThrottling => {
"Add cooldown periods between runs or improve cooling"
}
VarianceSource::CacheState => "Increase warmup iterations before measurement",
VarianceSource::SystemNoise => {
"Run with CPU isolation (isolcpus) or reduce background tasks"
}
VarianceSource::Unknown => "Profile with renacer for deeper analysis",
}
}
}
#[derive(Debug, Clone)]
pub struct VarianceAnalysis {
pub total_cv_percent: f64,
pub frequency_contribution: f64,
pub thermal_contribution: f64,
pub cache_contribution: f64,
pub residual_noise: f64,
pub dominant_source: VarianceSource,
pub recommendations: Vec<String>,
pub budget_met: bool,
pub sample_count: usize,
pub warmup_effect: f64,
pub trend_coefficient: f64,
}
#[derive(Debug, Clone)]
pub struct VarianceInput {
pub latencies: Vec<f64>,
pub frequencies: Option<Vec<f64>>,
pub temperatures: Option<Vec<f64>>,
pub warmup_count: usize,
}
impl VarianceAnalysis {
pub fn analyze(input: &VarianceInput) -> Option<Self> {
if input.latencies.is_empty() {
return None;
}
let n = input.latencies.len();
let mean = input.latencies.iter().sum::<f64>() / n as f64;
let variance = if n > 1 {
input
.latencies
.iter()
.map(|x| (x - mean).powi(2))
.sum::<f64>()
/ (n - 1) as f64
} else {
0.0
};
let std_dev = variance.sqrt();
let total_cv_percent = if mean > 0.0 {
(std_dev / mean) * 100.0
} else {
0.0
};
let frequency_contribution = if let Some(ref freqs) = input.frequencies {
estimate_frequency_contribution(freqs, &input.latencies)
} else {
0.0
};
let thermal_contribution = if let Some(ref temps) = input.temperatures {
estimate_thermal_contribution(temps, &input.latencies)
} else {
0.0
};
let (cache_contribution, warmup_effect) =
estimate_cache_contribution(&input.latencies, input.warmup_count);
let attributed = frequency_contribution + thermal_contribution + cache_contribution;
let residual_noise = (total_cv_percent - attributed).max(0.0);
let dominant_source = identify_dominant_source(
frequency_contribution,
thermal_contribution,
cache_contribution,
residual_noise,
);
let recommendations = generate_recommendations(
total_cv_percent,
frequency_contribution,
thermal_contribution,
cache_contribution,
residual_noise,
);
let trend_coefficient = calculate_trend(&input.latencies);
let budget_met = total_cv_percent < 5.0;
Some(Self {
total_cv_percent,
frequency_contribution,
thermal_contribution,
cache_contribution,
residual_noise,
dominant_source,
recommendations,
budget_met,
sample_count: n,
warmup_effect,
trend_coefficient,
})
}
pub fn summary(&self) -> String {
format!(
"CV={:.1}% (freq={:.1}% therm={:.1}% cache={:.1}% noise={:.1}%) dominant={}",
self.total_cv_percent,
self.frequency_contribution,
self.thermal_contribution,
self.cache_contribution,
self.residual_noise,
self.dominant_source.name()
)
}
pub fn has_dominant_source(&self) -> bool {
let max = self
.frequency_contribution
.max(self.thermal_contribution)
.max(self.cache_contribution)
.max(self.residual_noise);
max > self.total_cv_percent * 0.5
}
}
fn estimate_frequency_contribution(frequencies: &[f64], latencies: &[f64]) -> f64 {
if frequencies.len() < 2 || latencies.len() < 2 {
return 0.0;
}
let correlation = calculate_correlation(frequencies, latencies);
let freq_mean = frequencies.iter().sum::<f64>() / frequencies.len() as f64;
let freq_variance = frequencies
.iter()
.map(|f| (f - freq_mean).powi(2))
.sum::<f64>()
/ (frequencies.len() - 1) as f64;
let freq_cv = if freq_mean > 0.0 {
freq_variance.sqrt() / freq_mean * 100.0
} else {
0.0
};
correlation.abs() * freq_cv
}
fn estimate_thermal_contribution(temperatures: &[f64], latencies: &[f64]) -> f64 {
if temperatures.len() < 2 || latencies.len() < 2 {
return 0.0;
}
let correlation = calculate_correlation(temperatures, latencies);
let temp_mean = temperatures.iter().sum::<f64>() / temperatures.len() as f64;
let temp_variance = temperatures
.iter()
.map(|t| (t - temp_mean).powi(2))
.sum::<f64>()
/ (temperatures.len() - 1) as f64;
let temp_cv = if temp_mean > 0.0 {
temp_variance.sqrt() / temp_mean * 100.0
} else {
0.0
};
if correlation > 0.3 {
correlation * temp_cv
} else {
0.0
}
}
fn estimate_cache_contribution(latencies: &[f64], warmup_count: usize) -> (f64, f64) {
if latencies.len() <= warmup_count || warmup_count == 0 {
return (0.0, 1.0);
}
let cold_samples: Vec<f64> = latencies.iter().take(warmup_count).cloned().collect();
let warm_samples: Vec<f64> = latencies.iter().skip(warmup_count).cloned().collect();
if cold_samples.is_empty() || warm_samples.is_empty() {
return (0.0, 1.0);
}
let cold_mean = cold_samples.iter().sum::<f64>() / cold_samples.len() as f64;
let warm_mean = warm_samples.iter().sum::<f64>() / warm_samples.len() as f64;
let warmup_effect = if warm_mean > 0.0 {
cold_mean / warm_mean
} else {
1.0
};
let cold_cv = calculate_cv(&cold_samples);
let warm_cv = calculate_cv(&warm_samples);
let cache_contribution = (cold_cv - warm_cv).max(0.0);
(cache_contribution, warmup_effect)
}
fn calculate_cv(samples: &[f64]) -> f64 {
if samples.len() < 2 {
return 0.0;
}
let mean = samples.iter().sum::<f64>() / samples.len() as f64;
if mean == 0.0 {
return 0.0;
}
let variance =
samples.iter().map(|x| (x - mean).powi(2)).sum::<f64>() / (samples.len() - 1) as f64;
(variance.sqrt() / mean) * 100.0
}
fn calculate_correlation(x: &[f64], y: &[f64]) -> f64 {
let n = x.len().min(y.len());
if n < 2 {
return 0.0;
}
let x_mean = x.iter().take(n).sum::<f64>() / n as f64;
let y_mean = y.iter().take(n).sum::<f64>() / n as f64;
let mut numerator = 0.0;
let mut x_var = 0.0;
let mut y_var = 0.0;
for i in 0..n {
let dx = x[i] - x_mean;
let dy = y[i] - y_mean;
numerator += dx * dy;
x_var += dx * dx;
y_var += dy * dy;
}
let denominator = (x_var * y_var).sqrt();
if denominator > 0.0 {
numerator / denominator
} else {
0.0
}
}
fn calculate_trend(samples: &[f64]) -> f64 {
if samples.len() < 2 {
return 0.0;
}
let n = samples.len() as f64;
let x_mean = (n - 1.0) / 2.0; let y_mean = samples.iter().sum::<f64>() / n;
let mut numerator = 0.0;
let mut denominator = 0.0;
for (i, &y) in samples.iter().enumerate() {
let x = i as f64;
numerator += (x - x_mean) * (y - y_mean);
denominator += (x - x_mean).powi(2);
}
if denominator > 0.0 {
numerator / denominator
} else {
0.0
}
}
fn identify_dominant_source(freq: f64, thermal: f64, cache: f64, residual: f64) -> VarianceSource {
let max = freq.max(thermal).max(cache).max(residual);
if max < 0.5 {
VarianceSource::Unknown
} else if max == freq {
VarianceSource::FrequencyScaling
} else if max == thermal {
VarianceSource::ThermalThrottling
} else if max == cache {
VarianceSource::CacheState
} else {
VarianceSource::SystemNoise
}
}
fn generate_recommendations(
total_cv: f64,
freq: f64,
thermal: f64,
cache: f64,
residual: f64,
) -> Vec<String> {
let mut recs = Vec::new();
if total_cv >= 5.0 {
recs.push(format!(
"CV {:.1}% exceeds 5% target. Mitigation needed.",
total_cv
));
}
if freq > 1.0 {
recs.push(format!(
"Frequency variance ({:.1}%): {}",
freq,
VarianceSource::FrequencyScaling.mitigation()
));
}
if thermal > 1.0 {
recs.push(format!(
"Thermal variance ({:.1}%): {}",
thermal,
VarianceSource::ThermalThrottling.mitigation()
));
}
if cache > 1.0 {
recs.push(format!(
"Cache variance ({:.1}%): {}",
cache,
VarianceSource::CacheState.mitigation()
));
}
if residual > 2.0 {
recs.push(format!(
"Residual noise ({:.1}%): {}",
residual,
VarianceSource::SystemNoise.mitigation()
));
}
if recs.is_empty() {
recs.push("Variance within acceptable limits.".to_string());
}
recs
}
#[cfg(test)]
mod tests;