use crate::error::{Error, Result};
use chrono::{DateTime, Utc};
use serde::{Deserialize, Serialize};
use std::collections::{HashMap, VecDeque};
use std::sync::Arc;
use std::time::{Duration, Instant};
use tokio::sync::RwLock;
use tracing::{info, warn};
const MAX_VIOLATION_HISTORY: usize = 1000;
const MAX_COMPLIANCE_SAMPLES: usize = 10000;
const VIOLATION_DETECTION_LATENCY_MS: u64 = 100;
const PLATFORM_DETECTION_INTERVAL: Duration = Duration::from_secs(60);
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct BudgetConfig {
pub frame_time_ms: Option<f32>,
pub memory_mb: Option<f32>,
pub system_budgets: HashMap<String, f32>,
pub cpu_percent: Option<f32>,
pub gpu_time_ms: Option<f32>,
pub entity_count: Option<usize>,
pub draw_calls: Option<usize>,
pub network_bandwidth_kbps: Option<f32>,
pub platform_overrides: HashMap<Platform, PlatformBudgetOverride>,
pub auto_adjust: bool,
pub auto_adjust_percentile: f32,
pub violation_threshold: usize,
pub violation_window_seconds: u64,
}
impl Default for BudgetConfig {
fn default() -> Self {
Self {
frame_time_ms: Some(16.67), memory_mb: Some(500.0),
system_budgets: HashMap::new(),
cpu_percent: Some(80.0),
gpu_time_ms: Some(16.0),
entity_count: Some(10000),
draw_calls: Some(1000),
network_bandwidth_kbps: Some(1000.0),
platform_overrides: HashMap::new(),
auto_adjust: false,
auto_adjust_percentile: 95.0,
violation_threshold: 3,
violation_window_seconds: 10,
}
}
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct PlatformBudgetOverride {
pub frame_time_ms: Option<f32>,
pub memory_mb: Option<f32>,
pub cpu_percent: Option<f32>,
pub gpu_time_ms: Option<f32>,
}
#[derive(Debug, Clone, Copy, Hash, Eq, PartialEq, Serialize, Deserialize)]
pub enum Platform {
Windows,
MacOS,
Linux,
Web,
Mobile,
Console,
Unknown,
}
impl Platform {
pub fn detect() -> Self {
#[cfg(target_os = "windows")]
return Platform::Windows;
#[cfg(target_os = "macos")]
return Platform::MacOS;
#[cfg(target_os = "linux")]
return Platform::Linux;
#[cfg(target_arch = "wasm32")]
return Platform::Web;
#[cfg(any(target_os = "ios", target_os = "android"))]
return Platform::Mobile;
#[cfg(not(any(
target_os = "windows",
target_os = "macos",
target_os = "linux",
target_arch = "wasm32",
target_os = "ios",
target_os = "android"
)))]
return Platform::Unknown;
}
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct PerformanceMetrics {
pub frame_time_ms: f32,
pub memory_mb: f32,
pub system_times: HashMap<String, f32>,
pub cpu_percent: f32,
pub gpu_time_ms: f32,
pub entity_count: usize,
pub draw_calls: usize,
pub network_bandwidth_kbps: f32,
pub timestamp: DateTime<Utc>,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct BudgetViolation {
pub id: String,
pub metric: ViolatedMetric,
pub actual_value: f32,
pub budget_value: f32,
pub violation_percent: f32,
pub timestamp: DateTime<Utc>,
pub duration_ms: u64,
pub severity: ViolationSeverity,
pub context: HashMap<String, String>,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub enum ViolatedMetric {
FrameTime,
Memory,
SystemExecution(String),
CpuUsage,
GpuTime,
EntityCount,
DrawCalls,
NetworkBandwidth,
}
#[derive(Debug, Clone, Copy, Serialize, Deserialize)]
pub enum ViolationSeverity {
Warning, Minor, Major, Critical, }
impl ViolationSeverity {
fn from_violation_percent(percent: f32) -> Self {
match percent {
p if p <= 25.0 => ViolationSeverity::Warning,
p if p <= 50.0 => ViolationSeverity::Minor,
p if p <= 100.0 => ViolationSeverity::Major,
_ => ViolationSeverity::Critical,
}
}
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct ComplianceReport {
pub period_start: DateTime<Utc>,
pub period_end: DateTime<Utc>,
pub total_samples: usize,
pub metric_compliance: HashMap<String, MetricCompliance>,
pub overall_compliance_percent: f32,
pub top_violations: Vec<BudgetViolation>,
pub recommendations: Vec<BudgetRecommendation>,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct MetricCompliance {
pub metric_name: String,
pub compliant_samples: usize,
pub violation_count: usize,
pub compliance_percent: f32,
pub avg_value: f32,
pub p50_value: f32,
pub p95_value: f32,
pub p99_value: f32,
pub max_value: f32,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct BudgetRecommendation {
pub metric: String,
pub current_budget: f32,
pub recommended_budget: f32,
pub reason: String,
pub confidence: f32,
}
pub struct PerformanceBudgetMonitor {
inner: Arc<RwLock<PerformanceBudgetMonitorInner>>,
}
struct PerformanceBudgetMonitorInner {
config: BudgetConfig,
platform: Platform,
violation_history: VecDeque<BudgetViolation>,
compliance_samples: VecDeque<PerformanceMetrics>,
active_violations: HashMap<String, Instant>,
monitoring_active: bool,
last_detection: Instant,
stats: BudgetStatistics,
}
#[derive(Debug, Default, Serialize, Deserialize)]
struct BudgetStatistics {
total_violations: u64,
violations_by_metric: HashMap<String, u64>,
total_samples: u64,
avg_detection_latency_ms: f32,
last_platform_detection: Option<DateTime<Utc>>,
}
impl PerformanceBudgetMonitor {
pub fn new(config: BudgetConfig) -> Self {
Self {
inner: Arc::new(RwLock::new(PerformanceBudgetMonitorInner {
config,
platform: Platform::detect(),
violation_history: VecDeque::with_capacity(MAX_VIOLATION_HISTORY),
compliance_samples: VecDeque::with_capacity(MAX_COMPLIANCE_SAMPLES),
active_violations: HashMap::new(),
monitoring_active: false,
last_detection: Instant::now(),
stats: BudgetStatistics::default(),
})),
}
}
pub async fn start_monitoring(&self) -> Result<()> {
let mut inner = self.inner.write().await;
if inner.monitoring_active {
return Err(Error::Validation("Monitoring already active".to_string()));
}
inner.monitoring_active = true;
info!("Performance budget monitoring started");
Ok(())
}
pub async fn stop_monitoring(&self) -> Result<()> {
let mut inner = self.inner.write().await;
if !inner.monitoring_active {
return Err(Error::Validation("Monitoring not active".to_string()));
}
inner.monitoring_active = false;
info!("Performance budget monitoring stopped");
Ok(())
}
pub async fn check_violations(&self, metrics: PerformanceMetrics) -> Result<Vec<BudgetViolation>> {
let start = Instant::now();
let mut inner = self.inner.write().await;
if !inner.monitoring_active {
return Ok(Vec::new());
}
let mut violations = Vec::new();
let effective_config = self.apply_platform_overrides(&inner.config, inner.platform);
if let Some(budget) = effective_config.frame_time_ms {
if metrics.frame_time_ms > budget {
violations.push(self.create_violation_inner(
&mut inner,
ViolatedMetric::FrameTime,
metrics.frame_time_ms,
budget,
&metrics,
));
}
}
if let Some(budget) = effective_config.memory_mb {
if metrics.memory_mb > budget {
violations.push(self.create_violation_inner(
&mut inner,
ViolatedMetric::Memory,
metrics.memory_mb,
budget,
&metrics,
));
}
}
if let Some(budget) = effective_config.cpu_percent {
if metrics.cpu_percent > budget {
violations.push(self.create_violation_inner(
&mut inner,
ViolatedMetric::CpuUsage,
metrics.cpu_percent,
budget,
&metrics,
));
}
}
if let Some(budget) = effective_config.gpu_time_ms {
if metrics.gpu_time_ms > budget {
violations.push(self.create_violation_inner(
&mut inner,
ViolatedMetric::GpuTime,
metrics.gpu_time_ms,
budget,
&metrics,
));
}
}
if let Some(budget) = effective_config.entity_count {
if metrics.entity_count > budget {
violations.push(self.create_violation_inner(
&mut inner,
ViolatedMetric::EntityCount,
metrics.entity_count as f32,
budget as f32,
&metrics,
));
}
}
if let Some(budget) = effective_config.draw_calls {
if metrics.draw_calls > budget {
violations.push(self.create_violation_inner(
&mut inner,
ViolatedMetric::DrawCalls,
metrics.draw_calls as f32,
budget as f32,
&metrics,
));
}
}
if let Some(budget) = effective_config.network_bandwidth_kbps {
if metrics.network_bandwidth_kbps > budget {
violations.push(self.create_violation_inner(
&mut inner,
ViolatedMetric::NetworkBandwidth,
metrics.network_bandwidth_kbps,
budget,
&metrics,
));
}
}
for (system_name, &budget) in &effective_config.system_budgets {
if let Some(&actual) = metrics.system_times.get(system_name) {
if actual > budget {
violations.push(self.create_violation_inner(
&mut inner,
ViolatedMetric::SystemExecution(system_name.clone()),
actual,
budget,
&metrics,
));
}
}
}
Self::record_sample_inner(&mut inner, metrics);
for violation in &violations {
Self::record_violation_inner(&mut inner, violation.clone());
}
let detection_latency = start.elapsed().as_millis() as f32;
Self::update_statistics_inner(&mut inner, violations.len(), detection_latency);
if detection_latency > VIOLATION_DETECTION_LATENCY_MS as f32 {
warn!("Violation detection exceeded latency budget: {}ms", detection_latency);
}
Ok(violations)
}
fn apply_platform_overrides(&self, config: &BudgetConfig, platform: Platform) -> BudgetConfig {
let mut effective = config.clone();
if let Some(overrides) = config.platform_overrides.get(&platform) {
if let Some(frame_time) = overrides.frame_time_ms {
effective.frame_time_ms = Some(frame_time);
}
if let Some(memory) = overrides.memory_mb {
effective.memory_mb = Some(memory);
}
if let Some(cpu) = overrides.cpu_percent {
effective.cpu_percent = Some(cpu);
}
if let Some(gpu) = overrides.gpu_time_ms {
effective.gpu_time_ms = Some(gpu);
}
}
effective
}
fn create_violation_inner(
&self,
inner: &mut PerformanceBudgetMonitorInner,
metric: ViolatedMetric,
actual: f32,
budget: f32,
metrics: &PerformanceMetrics,
) -> BudgetViolation {
let violation_percent = ((actual - budget) / budget) * 100.0;
let severity = ViolationSeverity::from_violation_percent(violation_percent);
let metric_key = format!("{:?}", std::mem::discriminant(&metric));
let duration_ms = if let Some(start_time) = inner.active_violations.get(&metric_key) {
start_time.elapsed().as_millis() as u64
} else {
inner.active_violations.insert(metric_key.clone(), Instant::now());
0
};
BudgetViolation {
id: uuid::Uuid::new_v4().to_string(),
metric,
actual_value: actual,
budget_value: budget,
violation_percent,
timestamp: metrics.timestamp,
duration_ms,
severity,
context: HashMap::new(),
}
}
fn record_violation_inner(
inner: &mut PerformanceBudgetMonitorInner,
violation: BudgetViolation,
) {
if inner.violation_history.len() >= MAX_VIOLATION_HISTORY {
inner.violation_history.pop_front();
}
inner.violation_history.push_back(violation);
}
fn record_sample_inner(
inner: &mut PerformanceBudgetMonitorInner,
metrics: PerformanceMetrics,
) {
if inner.compliance_samples.len() >= MAX_COMPLIANCE_SAMPLES {
inner.compliance_samples.pop_front();
}
inner.compliance_samples.push_back(metrics);
}
fn update_statistics_inner(
inner: &mut PerformanceBudgetMonitorInner,
violation_count: usize,
detection_latency_ms: f32,
) {
inner.stats.total_violations += violation_count as u64;
inner.stats.total_samples += 1;
let alpha = 0.1; inner.stats.avg_detection_latency_ms =
(1.0 - alpha) * inner.stats.avg_detection_latency_ms + alpha * detection_latency_ms;
}
pub async fn generate_compliance_report(&self, duration: Duration) -> Result<ComplianceReport> {
let inner = self.inner.read().await;
let now = Utc::now();
let period_start = now - chrono::Duration::from_std(duration).unwrap();
let period_samples: Vec<_> = inner.compliance_samples
.iter()
.filter(|s| s.timestamp >= period_start)
.cloned()
.collect();
if period_samples.is_empty() {
return Err(Error::Validation("No samples in specified period".to_string()));
}
let mut metric_compliance = HashMap::new();
if let Some(budget) = inner.config.frame_time_ms {
let compliance = self.calculate_metric_compliance(
&period_samples,
"frame_time",
|m| m.frame_time_ms,
budget,
);
metric_compliance.insert("frame_time".to_string(), compliance);
}
if let Some(budget) = inner.config.memory_mb {
let compliance = self.calculate_metric_compliance(
&period_samples,
"memory",
|m| m.memory_mb,
budget,
);
metric_compliance.insert("memory".to_string(), compliance);
}
if let Some(budget) = inner.config.cpu_percent {
let compliance = self.calculate_metric_compliance(
&period_samples,
"cpu",
|m| m.cpu_percent,
budget,
);
metric_compliance.insert("cpu".to_string(), compliance);
}
let total_compliant: usize = metric_compliance.values()
.map(|c| c.compliant_samples)
.sum();
let total_violations: usize = metric_compliance.values()
.map(|c| c.violation_count)
.sum();
let overall_compliance_percent =
(total_compliant as f32 / (total_compliant + total_violations) as f32) * 100.0;
let top_violations: Vec<_> = inner.violation_history
.iter()
.filter(|v| v.timestamp >= period_start)
.take(10)
.cloned()
.collect();
let recommendations = Self::generate_recommendations(&inner.config, &metric_compliance);
Ok(ComplianceReport {
period_start,
period_end: now,
total_samples: period_samples.len(),
metric_compliance,
overall_compliance_percent,
top_violations,
recommendations,
})
}
fn calculate_metric_compliance<F>(
&self,
samples: &[PerformanceMetrics],
metric_name: &str,
extractor: F,
budget: f32,
) -> MetricCompliance
where
F: Fn(&PerformanceMetrics) -> f32,
{
let values: Vec<f32> = samples.iter().map(&extractor).collect();
let mut sorted_values = values.clone();
sorted_values.sort_by(|a, b| a.partial_cmp(b).unwrap());
let compliant_count = values.iter().filter(|&&v| v <= budget).count();
let violation_count = values.len() - compliant_count;
let avg_value = values.iter().sum::<f32>() / values.len() as f32;
let p50_value = sorted_values[sorted_values.len() / 2];
let p95_value = sorted_values[(sorted_values.len() as f32 * 0.95) as usize];
let p99_value = sorted_values[(sorted_values.len() as f32 * 0.99) as usize];
let max_value = *sorted_values.last().unwrap();
MetricCompliance {
metric_name: metric_name.to_string(),
compliant_samples: compliant_count,
violation_count,
compliance_percent: (compliant_count as f32 / values.len() as f32) * 100.0,
avg_value,
p50_value,
p95_value,
p99_value,
max_value,
}
}
fn generate_recommendations(
config: &BudgetConfig,
compliance: &HashMap<String, MetricCompliance>,
) -> Vec<BudgetRecommendation> {
let mut recommendations = Vec::new();
for (metric_name, stats) in compliance {
if stats.compliance_percent < 90.0 {
let percentile = config.auto_adjust_percentile;
let recommended_value = if percentile <= 50.0 {
stats.p50_value
} else if percentile <= 95.0 {
stats.p95_value
} else {
stats.p99_value
};
let current_budget = match metric_name.as_str() {
"frame_time" => config.frame_time_ms.unwrap_or(16.67),
"memory" => config.memory_mb.unwrap_or(500.0),
"cpu" => config.cpu_percent.unwrap_or(80.0),
"gpu_time" => config.gpu_time_ms.unwrap_or(16.0),
_ => continue, };
if (recommended_value - current_budget).abs() > current_budget * 0.05 {
recommendations.push(BudgetRecommendation {
metric: metric_name.clone(),
current_budget,
recommended_budget: recommended_value * 1.1, reason: format!(
"Current compliance is {:.1}%. Adjusting to P{} would improve compliance.",
stats.compliance_percent, percentile
),
confidence: 0.8,
});
}
}
}
recommendations
}
pub async fn update_config(&self, config: BudgetConfig) -> Result<()> {
let mut inner = self.inner.write().await;
inner.config = config;
info!("Performance budget configuration updated");
Ok(())
}
pub async fn get_config(&self) -> BudgetConfig {
self.inner.read().await.config.clone()
}
pub async fn get_violation_history(&self, limit: Option<usize>) -> Vec<BudgetViolation> {
let inner = self.inner.read().await;
let limit = limit.unwrap_or(100).min(inner.violation_history.len());
inner.violation_history.iter()
.rev()
.take(limit)
.cloned()
.collect()
}
pub async fn clear_violation_history(&self) {
let mut inner = self.inner.write().await;
inner.violation_history.clear();
inner.active_violations.clear();
info!("Violation history cleared");
}
pub async fn update_platform(&self) -> Platform {
let detected = Platform::detect();
let mut inner = self.inner.write().await;
inner.platform = detected;
inner.stats.last_platform_detection = Some(Utc::now());
info!("Platform detected: {:?}", detected);
detected
}
pub async fn get_statistics(&self) -> HashMap<String, serde_json::Value> {
let inner = self.inner.read().await;
let mut result = HashMap::new();
result.insert("total_violations".to_string(),
serde_json::json!(inner.stats.total_violations));
result.insert("total_samples".to_string(),
serde_json::json!(inner.stats.total_samples));
result.insert("avg_detection_latency_ms".to_string(),
serde_json::json!(inner.stats.avg_detection_latency_ms));
result.insert("monitoring_active".to_string(),
serde_json::json!(inner.monitoring_active));
result.insert("current_platform".to_string(),
serde_json::json!(format!("{:?}", inner.platform)));
if let Some(last_detection) = inner.stats.last_platform_detection {
result.insert("last_platform_detection".to_string(),
serde_json::json!(last_detection.to_rfc3339()));
}
result
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_platform_detection() {
let platform = Platform::detect();
assert_ne!(platform, Platform::Unknown);
}
#[test]
fn test_violation_severity() {
assert!(matches!(ViolationSeverity::from_violation_percent(10.0), ViolationSeverity::Warning));
assert!(matches!(ViolationSeverity::from_violation_percent(30.0), ViolationSeverity::Minor));
assert!(matches!(ViolationSeverity::from_violation_percent(75.0), ViolationSeverity::Major));
assert!(matches!(ViolationSeverity::from_violation_percent(150.0), ViolationSeverity::Critical));
}
#[tokio::test]
async fn test_budget_monitor_creation() {
let config = BudgetConfig::default();
let monitor = PerformanceBudgetMonitor::new(config);
assert!(!monitor.inner.read().await.monitoring_active);
assert!(monitor.start_monitoring().await.is_ok());
assert!(monitor.inner.read().await.monitoring_active);
assert!(monitor.start_monitoring().await.is_err());
assert!(monitor.stop_monitoring().await.is_ok());
assert!(!monitor.inner.read().await.monitoring_active);
}
}