use crate::types::SystemPressure;
use std::collections::BTreeMap;
use std::sync::Arc;
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum BudgetDirection {
UpperBound,
LowerBound,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord)]
pub enum BudgetSeverity {
Healthy,
Warning,
Critical,
}
#[derive(Debug, Clone, PartialEq)]
pub struct PerformanceBudget {
pub id: String,
pub component: String,
pub metric: String,
pub direction: BudgetDirection,
pub threshold: f64,
pub warning_margin: f64,
pub recommendation: String,
}
impl PerformanceBudget {
#[must_use]
pub fn new(
id: &str,
component: &str,
metric: &str,
direction: BudgetDirection,
threshold: f64,
) -> Self {
assert!(!id.trim().is_empty(), "budget id must not be empty");
assert!(
!component.trim().is_empty(),
"budget component must not be empty"
);
assert!(!metric.trim().is_empty(), "budget metric must not be empty");
assert!(
threshold.is_finite() && threshold > 0.0,
"budget threshold must be positive and finite"
);
Self {
id: id.trim().to_string(),
component: component.trim().to_string(),
metric: metric.trim().to_string(),
direction,
threshold,
warning_margin: 0.10,
recommendation: String::new(),
}
}
#[must_use]
pub fn with_warning_margin(mut self, warning_margin: f64) -> Self {
assert!(
warning_margin.is_finite() && (0.0..1.0).contains(&warning_margin),
"warning_margin must be finite and in [0.0, 1.0)"
);
self.warning_margin = warning_margin;
self
}
#[must_use]
pub fn with_recommendation(mut self, recommendation: &str) -> Self {
self.recommendation = recommendation.trim().to_string();
self
}
}
#[derive(Debug, Clone, PartialEq)]
pub struct BudgetSample {
pub budget_id: String,
pub observed: f64,
}
impl BudgetSample {
#[must_use]
pub fn new(budget_id: &str, observed: f64) -> Self {
assert!(
!budget_id.trim().is_empty(),
"sample budget id must not be empty"
);
assert!(
observed.is_finite() && observed >= 0.0,
"sample value must be finite and non-negative"
);
Self {
budget_id: budget_id.trim().to_string(),
observed,
}
}
}
#[derive(Debug, Clone, PartialEq)]
pub struct BudgetEvaluation {
pub budget_id: String,
pub component: String,
pub metric: String,
pub observed: f64,
pub threshold: f64,
pub severity: BudgetSeverity,
pub headroom: f32,
}
#[derive(Debug, Clone, PartialEq)]
pub struct BudgetAlert {
pub sequence: u64,
pub budget_id: String,
pub component: String,
pub metric: String,
pub severity: BudgetSeverity,
pub observed: f64,
pub threshold: f64,
pub recommendation: String,
}
#[derive(Debug, Clone, PartialEq)]
pub struct PerformanceBudgetSnapshot {
pub sequence: u64,
pub evaluations: Vec<BudgetEvaluation>,
pub alerts: Vec<BudgetAlert>,
pub worst_severity: BudgetSeverity,
pub worst_headroom: f32,
}
#[derive(Debug, Default)]
pub struct PerformanceBudgetMonitor {
budgets: BTreeMap<String, PerformanceBudget>,
pressure: Option<Arc<SystemPressure>>,
}
impl PerformanceBudgetMonitor {
#[must_use]
pub fn new() -> Self {
Self::default()
}
#[must_use]
pub fn with_pressure(mut self, pressure: Arc<SystemPressure>) -> Self {
self.pressure = Some(pressure);
self
}
pub fn register_budget(&mut self, budget: PerformanceBudget) {
self.budgets.insert(budget.id.clone(), budget);
}
#[must_use]
pub fn evaluate(&self, sequence: u64, samples: &[BudgetSample]) -> PerformanceBudgetSnapshot {
let mut sample_map = BTreeMap::new();
for sample in samples {
sample_map.insert(sample.budget_id.as_str(), sample);
}
let mut evaluations = Vec::new();
let mut alerts = Vec::new();
let mut worst_severity = BudgetSeverity::Healthy;
let mut worst_headroom = 1.0_f32;
for (budget_id, budget) in &self.budgets {
let Some(sample) = sample_map.get(budget_id.as_str()) else {
continue;
};
let (severity, headroom) = classify_budget(budget, sample.observed);
let evaluation = BudgetEvaluation {
budget_id: budget.id.clone(),
component: budget.component.clone(),
metric: budget.metric.clone(),
observed: sample.observed,
threshold: budget.threshold,
severity,
headroom,
};
worst_severity = worst_severity.max(severity);
worst_headroom = worst_headroom.min(headroom);
if severity != BudgetSeverity::Healthy {
alerts.push(BudgetAlert {
sequence,
budget_id: budget.id.clone(),
component: budget.component.clone(),
metric: budget.metric.clone(),
severity,
observed: sample.observed,
threshold: budget.threshold,
recommendation: budget.recommendation.clone(),
});
}
evaluations.push(evaluation);
}
alerts.sort_by(|left, right| {
right
.severity
.cmp(&left.severity)
.then_with(|| left.budget_id.cmp(&right.budget_id))
});
if let Some(pressure) = &self.pressure {
pressure.set_headroom(worst_headroom);
}
PerformanceBudgetSnapshot {
sequence,
evaluations,
alerts,
worst_severity,
worst_headroom,
}
}
}
fn classify_budget(budget: &PerformanceBudget, observed: f64) -> (BudgetSeverity, f32) {
match budget.direction {
BudgetDirection::UpperBound => classify_upper_bound(budget, observed),
BudgetDirection::LowerBound => classify_lower_bound(budget, observed),
}
}
fn classify_upper_bound(budget: &PerformanceBudget, observed: f64) -> (BudgetSeverity, f32) {
let warning_level = budget.threshold * (1.0 - budget.warning_margin);
if observed > budget.threshold {
return (BudgetSeverity::Critical, 0.0);
}
if observed <= warning_level {
return (BudgetSeverity::Healthy, 1.0);
}
let span = budget.threshold - warning_level;
let headroom = ((budget.threshold - observed) / span).clamp(0.0, 1.0) as f32;
(BudgetSeverity::Warning, headroom)
}
fn classify_lower_bound(budget: &PerformanceBudget, observed: f64) -> (BudgetSeverity, f32) {
let warning_level = budget.threshold * (1.0 + budget.warning_margin);
if observed < budget.threshold {
return (BudgetSeverity::Critical, 0.0);
}
if observed >= warning_level {
return (BudgetSeverity::Healthy, 1.0);
}
let span = warning_level - budget.threshold;
let headroom = ((observed - budget.threshold) / span).clamp(0.0, 1.0) as f32;
(BudgetSeverity::Warning, headroom)
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn upper_bound_budget_transitions_from_healthy_to_critical() {
let pressure = Arc::new(SystemPressure::new());
let mut monitor = PerformanceBudgetMonitor::new().with_pressure(pressure.clone());
monitor.register_budget(
PerformanceBudget::new(
"sched.p99_latency_ms",
"scheduler",
"p99_latency_ms",
BudgetDirection::UpperBound,
10.0,
)
.with_warning_margin(0.20)
.with_recommendation("inspect queue contention"),
);
let healthy = monitor.evaluate(1, &[BudgetSample::new("sched.p99_latency_ms", 7.0)]);
assert_eq!(healthy.worst_severity, BudgetSeverity::Healthy);
assert!(healthy.alerts.is_empty());
assert!((pressure.headroom() - 1.0).abs() < f32::EPSILON);
let warning = monitor.evaluate(2, &[BudgetSample::new("sched.p99_latency_ms", 8.5)]);
assert_eq!(warning.worst_severity, BudgetSeverity::Warning);
assert_eq!(warning.alerts.len(), 1);
assert_eq!(warning.alerts[0].severity, BudgetSeverity::Warning);
assert!(pressure.headroom() < 1.0);
assert!(pressure.headroom() > 0.0);
let critical = monitor.evaluate(3, &[BudgetSample::new("sched.p99_latency_ms", 12.0)]);
assert_eq!(critical.worst_severity, BudgetSeverity::Critical);
assert_eq!(critical.alerts.len(), 1);
assert_eq!(critical.alerts[0].severity, BudgetSeverity::Critical);
assert!(pressure.headroom().abs() < f32::EPSILON);
}
#[test]
fn lower_bound_budget_warns_before_falling_below_threshold() {
let mut monitor = PerformanceBudgetMonitor::new();
monitor.register_budget(
PerformanceBudget::new(
"codec.throughput_ops_per_sec",
"codec",
"throughput_ops_per_sec",
BudgetDirection::LowerBound,
1_000.0,
)
.with_warning_margin(0.10),
);
let warning = monitor.evaluate(
7,
&[BudgetSample::new("codec.throughput_ops_per_sec", 1_050.0)],
);
assert_eq!(warning.worst_severity, BudgetSeverity::Warning);
assert_eq!(warning.alerts.len(), 1);
assert_eq!(warning.alerts[0].severity, BudgetSeverity::Warning);
let critical = monitor.evaluate(
8,
&[BudgetSample::new("codec.throughput_ops_per_sec", 980.0)],
);
assert_eq!(critical.worst_severity, BudgetSeverity::Critical);
assert_eq!(critical.alerts.len(), 1);
assert_eq!(critical.alerts[0].severity, BudgetSeverity::Critical);
assert!(critical.worst_headroom.abs() < f32::EPSILON);
}
#[test]
fn alerts_are_sorted_by_severity_then_budget_id() {
let mut monitor = PerformanceBudgetMonitor::new();
monitor.register_budget(PerformanceBudget::new(
"b.latency",
"network",
"latency_ms",
BudgetDirection::UpperBound,
5.0,
));
monitor.register_budget(
PerformanceBudget::new(
"a.throughput",
"network",
"throughput_ops_per_sec",
BudgetDirection::LowerBound,
100.0,
)
.with_warning_margin(0.25),
);
let snapshot = monitor.evaluate(
11,
&[
BudgetSample::new("b.latency", 7.0),
BudgetSample::new("a.throughput", 110.0),
],
);
assert_eq!(snapshot.alerts.len(), 2);
assert_eq!(snapshot.alerts[0].budget_id, "b.latency");
assert_eq!(snapshot.alerts[0].severity, BudgetSeverity::Critical);
assert_eq!(snapshot.alerts[1].budget_id, "a.throughput");
assert_eq!(snapshot.alerts[1].severity, BudgetSeverity::Warning);
}
#[test]
fn unknown_budget_samples_are_ignored() {
let mut monitor = PerformanceBudgetMonitor::new();
monitor.register_budget(PerformanceBudget::new(
"runtime.queue_depth",
"runtime",
"queue_depth",
BudgetDirection::UpperBound,
1_000.0,
));
let snapshot = monitor.evaluate(13, &[BudgetSample::new("other.metric", 123.0)]);
assert!(snapshot.evaluations.is_empty());
assert!(snapshot.alerts.is_empty());
assert_eq!(snapshot.worst_severity, BudgetSeverity::Healthy);
assert!((snapshot.worst_headroom - 1.0).abs() < f32::EPSILON);
}
}