Skip to main content

dsfb_gray/
regime.rs

1//! Workload phase classification for regime-conditioned envelopes.
2//!
3//! Different workload phases (warmup, steady-state, burst, cooldown)
4//! require different admissibility envelopes. This module classifies
5//! the current phase from observable signals.
6
7/// Workload phase for envelope conditioning.
8#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
9pub enum WorkloadPhase {
10    /// System is warming up (caches cold, connections establishing).
11    Warmup,
12    /// Normal operating conditions.
13    SteadyState,
14    /// High-load burst (e.g., traffic spike, batch job).
15    Burst,
16    /// System is draining or shutting down.
17    Cooldown,
18    /// Custom phase with a static label.
19    Custom(&'static str),
20}
21
22/// Classifier that determines the current workload phase from
23/// throughput and latency signals.
24pub struct RegimeClassifier {
25    current_phase: WorkloadPhase,
26    warmup_samples_remaining: u32,
27    throughput_ema: f64,
28    throughput_ema_alpha: f64,
29    burst_threshold_multiplier: f64,
30}
31
32impl RegimeClassifier {
33    /// Create a new classifier.
34    ///
35    /// - `warmup_samples`: number of samples to stay in Warmup phase
36    /// - `burst_threshold`: multiplier above steady-state EMA to trigger Burst
37    pub fn new(warmup_samples: u32, burst_threshold: f64) -> Self {
38        Self {
39            current_phase: WorkloadPhase::Warmup,
40            warmup_samples_remaining: warmup_samples,
41            throughput_ema: 0.0,
42            throughput_ema_alpha: 0.1,
43            burst_threshold_multiplier: burst_threshold.max(1.1),
44        }
45    }
46
47    /// Update the classifier with a new throughput observation.
48    pub fn observe_throughput(&mut self, throughput: f64) -> WorkloadPhase {
49        if self.warmup_samples_remaining > 0 {
50            self.warmup_samples_remaining -= 1;
51            self.throughput_ema = throughput; // Initialize EMA
52            if self.warmup_samples_remaining == 0 {
53                self.current_phase = WorkloadPhase::SteadyState;
54            }
55            return self.current_phase;
56        }
57
58        // Update EMA
59        self.throughput_ema = self.throughput_ema_alpha * throughput
60            + (1.0 - self.throughput_ema_alpha) * self.throughput_ema;
61
62        // Classify based on throughput relative to EMA
63        if throughput > self.throughput_ema * self.burst_threshold_multiplier {
64            self.current_phase = WorkloadPhase::Burst;
65        } else if throughput < self.throughput_ema * 0.1 {
66            self.current_phase = WorkloadPhase::Cooldown;
67        } else {
68            self.current_phase = WorkloadPhase::SteadyState;
69        }
70
71        self.current_phase
72    }
73
74    /// Force a specific phase (e.g., on system restart).
75    pub fn set_phase(&mut self, phase: WorkloadPhase) {
76        self.current_phase = phase;
77    }
78
79    /// Current phase.
80    pub fn phase(&self) -> WorkloadPhase {
81        self.current_phase
82    }
83
84    /// Reset to warmup phase.
85    pub fn reset(&mut self, warmup_samples: u32) {
86        self.current_phase = WorkloadPhase::Warmup;
87        self.warmup_samples_remaining = warmup_samples;
88        self.throughput_ema = 0.0;
89    }
90}
91
92#[cfg(test)]
93mod tests {
94    use super::*;
95
96    #[test]
97    fn test_starts_in_warmup() {
98        let classifier = RegimeClassifier::new(10, 2.0);
99        assert_eq!(classifier.phase(), WorkloadPhase::Warmup);
100    }
101
102    #[test]
103    fn test_transitions_to_steady_state() {
104        let mut classifier = RegimeClassifier::new(3, 2.0);
105        classifier.observe_throughput(100.0);
106        classifier.observe_throughput(100.0);
107        assert_eq!(classifier.phase(), WorkloadPhase::Warmup);
108        classifier.observe_throughput(100.0);
109        assert_eq!(classifier.phase(), WorkloadPhase::SteadyState);
110    }
111}