use super::metrics::MacSystemMetrics;
use crate::MetricDetail;
use crate::PowerReserveMeterProvider;
use crate::error::PwrzvResult;
use crate::sigmoid::{SigmoidFn, get_sigmoid_config};
use std::collections::HashMap;
fn get_cpu_usage_config() -> SigmoidFn {
get_sigmoid_config("PWRZV_MACOS_CPU_USAGE", 0.60, 8.0)
}
fn get_cpu_load_config() -> SigmoidFn {
get_sigmoid_config("PWRZV_MACOS_CPU_LOAD", 1.2, 5.0)
}
fn get_memory_usage_config() -> SigmoidFn {
get_sigmoid_config("PWRZV_MACOS_MEMORY_USAGE", 0.85, 20.0)
}
fn get_memory_compressed_config() -> SigmoidFn {
get_sigmoid_config("PWRZV_MACOS_MEMORY_COMPRESSED", 0.60, 15.0)
}
fn get_network_dropped_config() -> SigmoidFn {
get_sigmoid_config("PWRZV_MACOS_NETWORK_DROPPED", 0.02, 100.0)
}
fn get_fd_config() -> SigmoidFn {
get_sigmoid_config("PWRZV_MACOS_FD", 0.90, 30.0)
}
fn get_process_config() -> SigmoidFn {
get_sigmoid_config("PWRZV_MACOS_PROCESS", 0.80, 12.0)
}
#[derive(Debug, Clone)]
pub(crate) struct MacProvider;
impl PowerReserveMeterProvider for MacProvider {
async fn get_power_reserve_level(&self) -> PwrzvResult<f32> {
let metrics = MacSystemMetrics::collect_system_metrics().await?;
let (level, _) = Self::calculate(&metrics)?;
Ok(level)
}
async fn get_power_reserve_level_with_details(
&self,
) -> PwrzvResult<(f32, HashMap<String, MetricDetail>)> {
let metrics = MacSystemMetrics::collect_system_metrics().await?;
let (level, details) = Self::calculate(&metrics)?;
Ok((level, details))
}
}
impl MacProvider {
fn calculate(metrics: &MacSystemMetrics) -> PwrzvResult<(f32, HashMap<String, MetricDetail>)> {
let mut details = HashMap::new();
let mut available_scores = Vec::new();
if let Some(value) = metrics.cpu_usage_ratio {
let score = get_cpu_usage_config().evaluate(value);
let n = Self::five_point_scale_with_decimal(score);
details.insert("cpu_usage".to_string(), MetricDetail { value, score: n });
available_scores.push(n);
}
if let Some(value) = metrics.cpu_load_ratio {
let score = get_cpu_load_config().evaluate(value);
let n = Self::five_point_scale_with_decimal(score);
details.insert("cpu_load".to_string(), MetricDetail { value, score: n });
available_scores.push(n);
}
if let Some(value) = metrics.memory_usage_ratio {
let score = get_memory_usage_config().evaluate(value);
let n = Self::five_point_scale_with_decimal(score);
details.insert("memory_usage".to_string(), MetricDetail { value, score: n });
available_scores.push(n);
}
if let Some(value) = metrics.memory_compressed_ratio {
let score = get_memory_compressed_config().evaluate(value);
let n = Self::five_point_scale_with_decimal(score);
details.insert(
"memory_compressed".to_string(),
MetricDetail { value, score: n },
);
available_scores.push(n);
}
if let Some(value) = metrics.network_dropped_packets_ratio {
let score = get_network_dropped_config().evaluate(value);
let n = Self::five_point_scale_with_decimal(score);
details.insert(
"network_dropped_packets".to_string(),
MetricDetail { value, score: n },
);
available_scores.push(n);
}
if let Some(value) = metrics.fd_usage_ratio {
let score = get_fd_config().evaluate(value);
let n = Self::five_point_scale_with_decimal(score);
details.insert(
"file_descriptors".to_string(),
MetricDetail { value, score: n },
);
available_scores.push(n);
}
if let Some(value) = metrics.process_count_ratio {
let score = get_process_config().evaluate(value);
let n = Self::five_point_scale_with_decimal(score);
details.insert(
"process_count".to_string(),
MetricDetail { value, score: n },
);
available_scores.push(n);
}
if let Some(&final_score) = available_scores
.iter()
.min_by(|a, b| a.partial_cmp(b).unwrap())
{
return Ok((final_score, details));
}
Ok((3.0, details))
}
fn five_point_scale_with_decimal(score: f32) -> f32 {
let score = 5.0 * (1.0 - score);
let factor = 10_000f32;
(score * factor).round() / factor
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_calculate_with_full_metrics() {
let metrics = MacSystemMetrics {
cpu_usage_ratio: Some(0.5),
cpu_load_ratio: Some(0.8),
memory_usage_ratio: Some(0.7),
memory_compressed_ratio: Some(0.3),
network_dropped_packets_ratio: Some(0.001),
fd_usage_ratio: Some(0.5),
process_count_ratio: Some(0.6),
};
let result = MacProvider::calculate(&metrics);
assert!(
result.is_ok(),
"Calculation should succeed with full metrics"
);
let (level, details) = result.unwrap();
assert_eq!(details.len(), 7, "Should have 7 metric details");
for detail in details.values() {
assert!(
detail.score >= 0.0 && detail.score <= 5.0,
"Score {} should be in range [0, 5]",
detail.score
);
}
assert!((0.0..=5.0).contains(&level));
}
#[test]
fn test_calculate_with_no_metrics() {
let metrics = MacSystemMetrics {
cpu_usage_ratio: None,
cpu_load_ratio: None,
memory_usage_ratio: None,
memory_compressed_ratio: None,
network_dropped_packets_ratio: None,
fd_usage_ratio: None,
process_count_ratio: None,
};
let result = MacProvider::calculate(&metrics);
assert!(
result.is_ok(),
"Calculation should succeed even with no metrics"
);
let (level, details) = result.unwrap();
assert_eq!(level, 3.0, "Should default to medium level");
assert!(details.is_empty(), "Should have no detailed scores");
}
#[test]
#[allow(unused_variables)]
fn test_calculate_with_partial_metrics() {
let metrics = MacSystemMetrics {
cpu_usage_ratio: Some(0.3),
cpu_load_ratio: None,
memory_usage_ratio: Some(0.6),
memory_compressed_ratio: Some(0.2),
network_dropped_packets_ratio: None,
fd_usage_ratio: None,
process_count_ratio: None,
};
let result = MacProvider::calculate(&metrics);
assert!(
result.is_ok(),
"Calculation should succeed with partial metrics"
);
let (level, details) = result.unwrap();
assert_eq!(details.len(), 3, "Should have 3 metric details");
let has_cpu = details.contains_key("cpu_usage");
let has_memory_usage = details.contains_key("memory_usage");
let has_memory_compressed = details.contains_key("memory_compressed");
let has_cpu_load = details.contains_key("cpu_load");
assert!(has_cpu, "Should have CPU usage metric");
assert!(has_memory_usage, "Should have memory usage metric");
assert!(
has_memory_compressed,
"Should have memory compressed metric"
);
assert!(!has_cpu_load, "Should not have CPU load metric");
}
#[test]
fn test_calculate_extreme_values() {
let metrics = MacSystemMetrics {
cpu_usage_ratio: Some(0.95), cpu_load_ratio: Some(3.0), memory_usage_ratio: Some(0.98), memory_compressed_ratio: Some(0.9), network_dropped_packets_ratio: Some(0.1), fd_usage_ratio: Some(0.95), process_count_ratio: Some(0.9), };
let result = MacProvider::calculate(&metrics);
assert!(result.is_ok());
let (level, details) = result.unwrap();
assert!(
level <= 2.5,
"High load should result in low power reserve (taking minimum score), got: {level}"
);
let low_scores = details.values().filter(|d| d.score <= 2.5).count();
assert!(
low_scores > 0,
"Should have some low scores with high system load"
);
}
#[test]
fn test_calculate_low_values() {
let low_load_metrics = MacSystemMetrics {
cpu_usage_ratio: Some(0.1),
cpu_load_ratio: Some(0.3),
memory_usage_ratio: Some(0.4),
memory_compressed_ratio: Some(0.1),
network_dropped_packets_ratio: Some(0.0),
fd_usage_ratio: Some(0.2),
process_count_ratio: Some(0.3),
};
let result = MacProvider::calculate(&low_load_metrics);
assert!(result.is_ok(), "Should handle low system load");
let (level, details) = result.unwrap();
assert!(
(4.0..=5.0).contains(&level),
"Low load should result in high power reserve"
);
let high_scores = details.values().filter(|d| d.score >= 4.0).count();
assert!(
high_scores > 0,
"Should have some high scores with low system load"
);
}
}