1#![forbid(unsafe_code)]
2
3use std::cell::RefCell;
46use std::sync::{Arc, Mutex};
47use web_time::Instant;
48
49use crate::conformal_alert::{AlertConfig, AlertDecision, AlertStats, ConformalAlert};
50use crate::resize_coalescer::{DecisionLog, TelemetryHooks};
51use crate::voi_sampling::{VoiConfig, VoiSampler, VoiSummary};
52
53#[derive(Debug, Clone)]
55pub struct SlaConfig {
56 pub alpha: f64,
59
60 pub min_calibration: usize,
63
64 pub max_calibration: usize,
67
68 pub target_latency_ms: f64,
72
73 pub enable_logging: bool,
76
77 pub alert_cooldown: u64,
80
81 pub hysteresis: f64,
84
85 pub voi_sampling: Option<VoiConfig>,
88}
89
90impl Default for SlaConfig {
91 fn default() -> Self {
92 Self {
93 alpha: 0.05,
94 min_calibration: 20,
95 max_calibration: 200,
96 target_latency_ms: 100.0,
97 enable_logging: true,
98 alert_cooldown: 10,
99 hysteresis: 1.1,
100 voi_sampling: None,
101 }
102 }
103}
104
105#[derive(Debug, Clone)]
107pub struct ResizeEvidence {
108 pub timestamp: Instant,
110 pub latency_ms: f64,
112 pub applied_size: (u16, u16),
114 pub forced: bool,
116 pub regime: &'static str,
118 pub coalesce_ms: Option<f64>,
120}
121
122#[derive(Debug, Clone)]
124pub struct SlaLogEntry {
125 pub event_idx: u64,
127 pub event_type: &'static str,
129 pub latency_ms: f64,
131 pub target_latency_ms: f64,
133 pub threshold_ms: f64,
135 pub e_value: f64,
137 pub is_alert: bool,
139 pub alert_reason: Option<String>,
141 pub applied_size: (u16, u16),
143 pub forced: bool,
145}
146
147#[derive(Debug, Clone)]
149pub struct SlaSummary {
150 pub total_events: u64,
152 pub calibration_events: usize,
154 pub total_alerts: u64,
156 pub current_threshold_ms: f64,
158 pub mean_latency_ms: f64,
160 pub std_latency_ms: f64,
162 pub current_e_value: f64,
164 pub empirical_fpr: f64,
166 pub target_latency_ms: f64,
168}
169
170pub struct ResizeSlaMonitor {
175 config: SlaConfig,
176 alerter: RefCell<ConformalAlert>,
177 event_count: RefCell<u64>,
178 total_alerts: RefCell<u64>,
179 last_alert: RefCell<Option<AlertDecision>>,
180 logs: RefCell<Vec<SlaLogEntry>>,
181 sampler: RefCell<Option<VoiSampler>>,
182}
183
184impl ResizeSlaMonitor {
185 pub fn new(config: SlaConfig) -> Self {
187 let alert_config = AlertConfig {
188 alpha: config.alpha,
189 min_calibration: config.min_calibration,
190 max_calibration: config.max_calibration,
191 enable_logging: config.enable_logging,
192 hysteresis: config.hysteresis,
193 alert_cooldown: config.alert_cooldown,
194 ..AlertConfig::default()
195 };
196 let sampler = config.voi_sampling.clone().map(VoiSampler::new);
197
198 Self {
199 config,
200 alerter: RefCell::new(ConformalAlert::new(alert_config)),
201 event_count: RefCell::new(0),
202 total_alerts: RefCell::new(0),
203 last_alert: RefCell::new(None),
204 logs: RefCell::new(Vec::new()),
205 sampler: RefCell::new(sampler),
206 }
207 }
208
209 pub fn on_decision(&self, entry: &DecisionLog) -> Option<AlertDecision> {
211 let latency_ms = entry.coalesce_ms.unwrap_or(entry.time_since_render_ms);
213 let applied_size = entry.applied_size?;
214 if let Some(ref mut sampler) = *self.sampler.borrow_mut() {
215 let decision = sampler.decide(entry.timestamp);
216 if !decision.should_sample {
217 return None;
218 }
219 let result = self.process_latency(latency_ms, applied_size, entry.forced);
220 let violated = latency_ms > self.config.target_latency_ms;
221 sampler.observe_at(violated, entry.timestamp);
222 return result;
223 }
224
225 self.process_latency(latency_ms, applied_size, entry.forced)
226 }
227
228 fn process_latency(
230 &self,
231 latency_ms: f64,
232 applied_size: (u16, u16),
233 forced: bool,
234 ) -> Option<AlertDecision> {
235 *self.event_count.borrow_mut() += 1;
236 let event_idx = *self.event_count.borrow();
237
238 let mut alerter = self.alerter.borrow_mut();
239
240 if alerter.calibration_count() < self.config.min_calibration {
242 alerter.calibrate(latency_ms);
243
244 if self.config.enable_logging {
245 self.logs.borrow_mut().push(SlaLogEntry {
246 event_idx,
247 event_type: "calibrate",
248 latency_ms,
249 target_latency_ms: self.config.target_latency_ms,
250 threshold_ms: alerter.threshold(),
251 e_value: alerter.e_value(),
252 is_alert: false,
253 alert_reason: None,
254 applied_size,
255 forced,
256 });
257 }
258
259 return None;
260 }
261
262 let decision = alerter.observe(latency_ms);
264
265 if self.config.enable_logging {
266 self.logs.borrow_mut().push(SlaLogEntry {
267 event_idx,
268 event_type: if decision.is_alert {
269 "alert"
270 } else {
271 "observe"
272 },
273 latency_ms,
274 target_latency_ms: self.config.target_latency_ms,
275 threshold_ms: decision.evidence.conformal_threshold,
276 e_value: decision.evidence.e_value,
277 is_alert: decision.is_alert,
278 alert_reason: if decision.is_alert {
279 Some(format!("{:?}", decision.evidence.reason))
280 } else {
281 None
282 },
283 applied_size,
284 forced,
285 });
286 }
287
288 if decision.is_alert {
289 *self.total_alerts.borrow_mut() += 1;
290 *self.last_alert.borrow_mut() = Some(decision.clone());
291 }
292
293 Some(decision)
294 }
295
296 pub fn last_alert(&self) -> Option<AlertDecision> {
298 self.last_alert.borrow().clone()
299 }
300
301 pub fn summary(&self) -> SlaSummary {
303 let alerter = self.alerter.borrow();
304 let stats = alerter.stats();
305
306 SlaSummary {
307 total_events: *self.event_count.borrow(),
308 calibration_events: stats.calibration_samples,
309 total_alerts: *self.total_alerts.borrow(),
310 current_threshold_ms: stats.current_threshold,
311 mean_latency_ms: stats.calibration_mean,
312 std_latency_ms: stats.calibration_std,
313 current_e_value: stats.current_e_value,
314 empirical_fpr: stats.empirical_fpr,
315 target_latency_ms: self.config.target_latency_ms,
316 }
317 }
318
319 pub fn alerter_stats(&self) -> AlertStats {
321 self.alerter.borrow().stats()
322 }
323
324 pub fn logs(&self) -> Vec<SlaLogEntry> {
326 self.logs.borrow().clone()
327 }
328
329 pub fn logs_to_jsonl(&self) -> String {
331 let logs = self.logs.borrow();
332 let mut output = String::new();
333
334 for entry in logs.iter() {
335 let line = format!(
336 r#"{{"event":"sla","idx":{},"type":"{}","latency_ms":{:.3},"target_ms":{:.1},"threshold_ms":{:.3},"e_value":{:.6},"alert":{},"reason":{},"size":[{},{}],"forced":{}}}"#,
337 entry.event_idx,
338 entry.event_type,
339 entry.latency_ms,
340 entry.target_latency_ms,
341 entry.threshold_ms,
342 entry.e_value,
343 entry.is_alert,
344 entry
345 .alert_reason
346 .as_ref()
347 .map(|r| format!("\"{}\"", r))
348 .unwrap_or_else(|| "null".to_string()),
349 entry.applied_size.0,
350 entry.applied_size.1,
351 entry.forced
352 );
353 output.push_str(&line);
354 output.push('\n');
355 }
356
357 output
358 }
359
360 pub fn clear_logs(&self) {
362 self.logs.borrow_mut().clear();
363 }
364
365 pub fn reset(&self) {
367 let alert_config = AlertConfig {
368 alpha: self.config.alpha,
369 min_calibration: self.config.min_calibration,
370 max_calibration: self.config.max_calibration,
371 enable_logging: self.config.enable_logging,
372 hysteresis: self.config.hysteresis,
373 alert_cooldown: self.config.alert_cooldown,
374 ..AlertConfig::default()
375 };
376
377 *self.alerter.borrow_mut() = ConformalAlert::new(alert_config);
378 *self.event_count.borrow_mut() = 0;
379 *self.total_alerts.borrow_mut() = 0;
380 *self.last_alert.borrow_mut() = None;
381 self.logs.borrow_mut().clear();
382 *self.sampler.borrow_mut() = self.config.voi_sampling.clone().map(VoiSampler::new);
383 }
384
385 pub fn threshold_ms(&self) -> f64 {
387 self.alerter.borrow().threshold()
388 }
389
390 pub fn is_active(&self) -> bool {
392 self.alerter.borrow().calibration_count() >= self.config.min_calibration
393 }
394
395 pub fn calibration_count(&self) -> usize {
397 self.alerter.borrow().calibration_count()
398 }
399
400 pub fn sampling_summary(&self) -> Option<VoiSummary> {
402 self.sampler.borrow().as_ref().map(VoiSampler::summary)
403 }
404
405 pub fn sampling_logs_to_jsonl(&self) -> Option<String> {
407 self.sampler
408 .borrow()
409 .as_ref()
410 .map(|sampler| sampler.logs_to_jsonl())
411 }
412}
413
414pub fn make_sla_hooks(config: SlaConfig) -> (TelemetryHooks, Arc<Mutex<ResizeSlaMonitor>>) {
422 let monitor = Arc::new(Mutex::new(ResizeSlaMonitor::new(config)));
423 let monitor_clone = Arc::clone(&monitor);
424
425 let hooks = TelemetryHooks::new().on_resize_applied(move |entry: &DecisionLog| {
427 if (entry.action == "apply" || entry.action == "apply_forced")
429 && let Ok(monitor) = monitor_clone.lock()
430 {
431 monitor.on_decision(entry);
432 }
433 });
434
435 (hooks, monitor)
436}
437
438#[cfg(test)]
443mod tests {
444 use super::*;
445 use crate::resize_coalescer::Regime;
446
447 fn test_config() -> SlaConfig {
448 SlaConfig {
449 alpha: 0.05,
450 min_calibration: 5,
451 max_calibration: 50,
452 target_latency_ms: 50.0,
453 enable_logging: true,
454 alert_cooldown: 0,
455 hysteresis: 1.0,
456 voi_sampling: None,
457 }
458 }
459
460 fn sample_decision_log(now: Instant, latency_ms: f64) -> DecisionLog {
461 DecisionLog {
462 timestamp: now,
463 elapsed_ms: 0.0,
464 event_idx: 1,
465 dt_ms: 0.0,
466 event_rate: 0.0,
467 regime: Regime::Steady,
468 action: "apply",
469 pending_size: None,
470 applied_size: Some((80, 24)),
471 time_since_render_ms: latency_ms,
472 coalesce_ms: Some(latency_ms),
473 forced: false,
474 transition_reason_code: None,
475 transition_confidence: None,
476 }
477 }
478
479 #[test]
484 fn initial_state() {
485 let monitor = ResizeSlaMonitor::new(test_config());
486
487 assert!(!monitor.is_active());
488 assert_eq!(monitor.calibration_count(), 0);
489 assert!(monitor.last_alert().is_none());
490 assert!(monitor.logs().is_empty());
491 }
492
493 #[test]
494 fn calibration_phase() {
495 let monitor = ResizeSlaMonitor::new(test_config());
496
497 for i in 0..5 {
499 let result = monitor.process_latency(10.0 + i as f64, (80, 24), false);
500 assert!(result.is_none(), "Should be in calibration phase");
501 }
502
503 assert!(monitor.is_active());
504 assert_eq!(monitor.calibration_count(), 5);
505 }
506
507 #[test]
508 fn detection_phase_normal() {
509 let monitor = ResizeSlaMonitor::new(test_config());
510
511 for i in 0..5 {
513 monitor.process_latency(10.0 + i as f64, (80, 24), false);
514 }
515
516 let result = monitor.process_latency(12.0, (80, 24), false);
518 assert!(result.is_some());
519 assert!(!result.unwrap().is_alert);
520 }
521
522 #[test]
523 fn detection_phase_alert() {
524 let mut config = test_config();
525 config.hysteresis = 0.1; let monitor = ResizeSlaMonitor::new(config);
527
528 for _ in 0..5 {
530 monitor.process_latency(10.0, (80, 24), false);
531 }
532
533 let result = monitor.process_latency(1000.0, (80, 24), false);
535 assert!(result.is_some());
536
537 let decision = result.unwrap();
538 assert!(
539 decision.evidence.conformal_alert || decision.evidence.eprocess_alert,
540 "Extreme latency should trigger alert"
541 );
542 }
543
544 #[test]
549 fn logging_captures_events() {
550 let monitor = ResizeSlaMonitor::new(test_config());
551
552 for i in 0..5 {
554 monitor.process_latency(10.0 + i as f64, (80, 24), false);
555 }
556
557 monitor.process_latency(12.0, (80, 24), false);
559 monitor.process_latency(15.0, (100, 40), true);
560
561 let logs = monitor.logs();
562 assert_eq!(logs.len(), 7);
563
564 assert_eq!(logs[0].event_type, "calibrate");
566 assert_eq!(logs[4].event_type, "calibrate");
567
568 assert_eq!(logs[5].event_type, "observe");
570 assert_eq!(logs[6].applied_size, (100, 40));
571 assert!(logs[6].forced);
572 }
573
574 #[test]
575 fn jsonl_format() {
576 let monitor = ResizeSlaMonitor::new(test_config());
577
578 for i in 0..5 {
583 monitor.process_latency(10.0 + i as f64, (80, 24), false);
584 }
585 monitor.process_latency(12.0, (80, 24), false);
586
587 let jsonl = monitor.logs_to_jsonl();
588 assert!(jsonl.contains(r#""event":"sla""#));
589 assert!(jsonl.contains(r#""type":"calibrate""#));
590 assert!(jsonl.contains(r#""type":"observe""#));
591 assert!(jsonl.contains(r#""latency_ms":"#));
592 assert!(jsonl.contains(r#""threshold_ms":"#));
593 }
594
595 #[test]
600 fn summary_reflects_state() {
601 let monitor = ResizeSlaMonitor::new(test_config());
602
603 for i in 0..10 {
604 monitor.process_latency(10.0 + (i as f64) * 2.0, (80, 24), false);
605 }
606
607 let summary = monitor.summary();
608 assert_eq!(summary.total_events, 10);
609 assert!(summary.mean_latency_ms > 0.0);
610 assert!(summary.current_threshold_ms > 0.0);
611 assert_eq!(summary.target_latency_ms, 50.0);
612 }
613
614 #[test]
619 fn reset_clears_state() {
620 let monitor = ResizeSlaMonitor::new(test_config());
621
622 for i in 0..10 {
623 monitor.process_latency(10.0 + i as f64, (80, 24), false);
624 }
625
626 assert!(monitor.is_active());
627 assert!(!monitor.logs().is_empty());
628
629 monitor.reset();
630
631 assert!(!monitor.is_active());
632 assert!(monitor.logs().is_empty());
633 assert_eq!(monitor.calibration_count(), 0);
634 }
635
636 #[test]
641 fn on_decision_processes_entry() {
642 use crate::resize_coalescer::Regime;
643
644 let monitor = ResizeSlaMonitor::new(test_config());
645
646 let entry = DecisionLog {
648 timestamp: Instant::now(),
649 elapsed_ms: 0.0,
650 event_idx: 1,
651 dt_ms: 0.0,
652 event_rate: 0.0,
653 regime: Regime::Steady,
654 action: "apply",
655 pending_size: None,
656 applied_size: Some((100, 40)),
657 time_since_render_ms: 15.0,
658 coalesce_ms: Some(15.0),
659 forced: false,
660 transition_reason_code: None,
661 transition_confidence: None,
662 };
663
664 let result = monitor.on_decision(&entry);
665 assert!(result.is_none()); for i in 0..5 {
669 let entry = DecisionLog {
670 timestamp: Instant::now(),
671 elapsed_ms: 0.0,
672 event_idx: 2 + i,
673 dt_ms: 0.0,
674 event_rate: 0.0,
675 regime: Regime::Steady,
676 action: "apply",
677 pending_size: None,
678 applied_size: Some((100, 40)),
679 time_since_render_ms: 15.0 + i as f64,
680 coalesce_ms: Some(15.0 + i as f64),
681 forced: false,
682 transition_reason_code: None,
683 transition_confidence: None,
684 };
685 monitor.on_decision(&entry);
686 }
687
688 assert!(monitor.is_active());
689 }
690
691 #[test]
696 fn make_sla_hooks_creates_valid_hooks() {
697 let (_hooks, monitor) = make_sla_hooks(test_config());
698
699 let monitor = monitor.lock().expect("sla monitor lock");
701 assert!(!monitor.is_active());
702 assert_eq!(monitor.calibration_count(), 0);
703 }
704
705 #[test]
710 fn property_calibration_mean_accurate() {
711 let monitor = ResizeSlaMonitor::new(test_config());
712
713 let samples: Vec<f64> = vec![10.0, 20.0, 30.0, 40.0, 50.0];
714 let expected_mean: f64 = samples.iter().sum::<f64>() / samples.len() as f64;
715
716 for &s in &samples {
717 monitor.process_latency(s, (80, 24), false);
718 }
719
720 let summary = monitor.summary();
721 assert!(
722 (summary.mean_latency_ms - expected_mean).abs() < 0.01,
723 "Mean should be accurate: {} vs {}",
724 summary.mean_latency_ms,
725 expected_mean
726 );
727 }
728
729 #[test]
730 fn property_alert_count_nondecreasing() {
731 let mut config = test_config();
732 config.hysteresis = 0.1;
733 config.alert_cooldown = 0;
734 let monitor = ResizeSlaMonitor::new(config);
735
736 for _ in 0..5 {
738 monitor.process_latency(10.0, (80, 24), false);
739 }
740
741 let mut prev_alerts = 0u64;
742 for i in 0..20 {
743 let latency = if i % 3 == 0 { 1000.0 } else { 10.0 };
744 monitor.process_latency(latency, (80, 24), false);
745
746 let current_alerts = *monitor.total_alerts.borrow();
747 assert!(
748 current_alerts >= prev_alerts,
749 "Alert count should be non-decreasing"
750 );
751 prev_alerts = current_alerts;
752 }
753 }
754
755 #[test]
756 fn deterministic_behavior() {
757 let config = test_config();
758
759 let run = || {
760 let monitor = ResizeSlaMonitor::new(config.clone());
761 for i in 0..10 {
762 monitor.process_latency(10.0 + i as f64, (80, 24), false);
763 }
764 (
765 monitor.summary().mean_latency_ms,
766 monitor.threshold_ms(),
767 *monitor.total_alerts.borrow(),
768 )
769 };
770
771 let (m1, t1, a1) = run();
772 let (m2, t2, a2) = run();
773
774 assert!((m1 - m2).abs() < 1e-10, "Mean must be deterministic");
775 assert!((t1 - t2).abs() < 1e-10, "Threshold must be deterministic");
776 assert_eq!(a1, a2, "Alert count must be deterministic");
777 }
778
779 #[test]
780 fn voi_sampling_skips_when_policy_says_no() {
781 let mut config = test_config();
782 config.voi_sampling = Some(VoiConfig {
783 sample_cost: 10.0,
784 max_interval_events: 0,
785 max_interval_ms: 0,
786 ..VoiConfig::default()
787 });
788 let monitor = ResizeSlaMonitor::new(config);
789
790 let entry = sample_decision_log(Instant::now(), 12.0);
791 let result = monitor.on_decision(&entry);
792 assert!(result.is_none(), "Sampling should skip under high cost");
793
794 let summary = monitor.summary();
795 assert_eq!(summary.total_events, 0);
796 let sampling = monitor.sampling_summary().expect("sampling summary");
797 assert_eq!(sampling.total_events, 1);
798 }
799
800 #[test]
801 fn voi_sampling_forced_sample_records_event() {
802 let mut config = test_config();
803 config.min_calibration = 0;
806 config.voi_sampling = Some(VoiConfig {
807 sample_cost: 10.0,
808 max_interval_events: 1,
809 ..VoiConfig::default()
810 });
811 let monitor = ResizeSlaMonitor::new(config);
812
813 let entry = sample_decision_log(Instant::now(), 12.0);
814 let result = monitor.on_decision(&entry);
815 assert!(result.is_some());
816
817 let summary = monitor.summary();
818 assert_eq!(summary.total_events, 1);
819 let sampling = monitor.sampling_summary().expect("sampling summary");
820 assert_eq!(sampling.total_samples, 1);
821 }
822
823 #[test]
824 fn sla_config_default_values() {
825 let config = SlaConfig::default();
826 assert!((config.alpha - 0.05).abs() < 1e-10);
827 assert_eq!(config.min_calibration, 20);
828 assert_eq!(config.max_calibration, 200);
829 assert!((config.target_latency_ms - 100.0).abs() < 1e-10);
830 assert!(config.enable_logging);
831 assert_eq!(config.alert_cooldown, 10);
832 assert!((config.hysteresis - 1.1).abs() < 1e-10);
833 assert!(config.voi_sampling.is_none());
834 }
835
836 #[test]
837 fn last_alert_initially_none() {
838 let monitor = ResizeSlaMonitor::new(test_config());
839 assert!(monitor.last_alert().is_none());
840 }
841
842 #[test]
843 fn clear_logs_empties_log_vec() {
844 let monitor = ResizeSlaMonitor::new(test_config());
845 let now = Instant::now();
846 for i in 0..3 {
847 monitor.on_decision(&sample_decision_log(now, 10.0 + i as f64));
848 }
849 assert!(!monitor.logs().is_empty());
850 monitor.clear_logs();
851 assert!(monitor.logs().is_empty());
852 }
853
854 #[test]
855 fn threshold_ms_returns_value() {
856 let monitor = ResizeSlaMonitor::new(test_config());
857 let threshold = monitor.threshold_ms();
858 assert!(threshold.is_finite());
860 }
861
862 #[test]
863 fn is_active_after_calibration() {
864 let monitor = ResizeSlaMonitor::new(test_config());
865 assert!(!monitor.is_active());
866 let now = Instant::now();
867 for i in 0..5 {
868 monitor.on_decision(&sample_decision_log(now, 10.0 + i as f64));
869 }
870 assert!(monitor.is_active());
871 }
872
873 #[test]
874 fn calibration_count_tracks_samples() {
875 let monitor = ResizeSlaMonitor::new(test_config());
876 assert_eq!(monitor.calibration_count(), 0);
877 let now = Instant::now();
878 monitor.on_decision(&sample_decision_log(now, 10.0));
879 assert_eq!(monitor.calibration_count(), 1);
880 }
881
882 #[test]
883 fn alerter_stats_returns_valid() {
884 let monitor = ResizeSlaMonitor::new(test_config());
885 let stats = monitor.alerter_stats();
886 assert_eq!(stats.calibration_samples, 0);
887 }
888
889 #[test]
890 fn sampling_summary_none_without_voi() {
891 let monitor = ResizeSlaMonitor::new(test_config());
892 assert!(monitor.sampling_summary().is_none());
893 }
894
895 #[test]
896 fn sampling_logs_to_jsonl_none_without_voi() {
897 let monitor = ResizeSlaMonitor::new(test_config());
898 assert!(monitor.sampling_logs_to_jsonl().is_none());
899 }
900
901 #[test]
906 fn edge_on_decision_none_applied_size() {
907 let monitor = ResizeSlaMonitor::new(test_config());
908 let entry = DecisionLog {
909 timestamp: Instant::now(),
910 elapsed_ms: 0.0,
911 event_idx: 1,
912 dt_ms: 0.0,
913 event_rate: 0.0,
914 regime: Regime::Steady,
915 action: "apply",
916 pending_size: None,
917 applied_size: None, time_since_render_ms: 10.0,
919 coalesce_ms: Some(10.0),
920 forced: false,
921 transition_reason_code: None,
922 transition_confidence: None,
923 };
924 let result = monitor.on_decision(&entry);
925 assert!(
926 result.is_none(),
927 "Should return None when applied_size is None"
928 );
929 assert_eq!(*monitor.event_count.borrow(), 0);
931 }
932
933 #[test]
934 fn edge_on_decision_coalesce_ms_none_falls_back() {
935 let monitor = ResizeSlaMonitor::new(test_config());
936 let entry = DecisionLog {
937 timestamp: Instant::now(),
938 elapsed_ms: 0.0,
939 event_idx: 1,
940 dt_ms: 0.0,
941 event_rate: 0.0,
942 regime: Regime::Steady,
943 action: "apply",
944 pending_size: None,
945 applied_size: Some((80, 24)),
946 time_since_render_ms: 42.0,
947 coalesce_ms: None, forced: false,
949 transition_reason_code: None,
950 transition_confidence: None,
951 };
952 let result = monitor.on_decision(&entry);
953 assert!(result.is_none()); assert_eq!(monitor.calibration_count(), 1);
956 }
957
958 #[test]
959 fn edge_zero_latency() {
960 let monitor = ResizeSlaMonitor::new(test_config());
961 for _ in 0..5 {
962 monitor.process_latency(0.0, (80, 24), false);
963 }
964 let result = monitor.process_latency(0.0, (80, 24), false);
966 assert!(result.is_some());
967 assert!(!result.unwrap().is_alert);
968 }
969
970 #[test]
971 fn edge_negative_latency() {
972 let monitor = ResizeSlaMonitor::new(test_config());
973 for _ in 0..5 {
975 monitor.process_latency(-10.0, (80, 24), false);
976 }
977 let result = monitor.process_latency(-5.0, (80, 24), false);
978 assert!(result.is_some());
979 }
980
981 #[test]
982 fn edge_nan_latency() {
983 let monitor = ResizeSlaMonitor::new(test_config());
984 for _ in 0..5 {
985 monitor.process_latency(10.0, (80, 24), false);
986 }
987 let result = monitor.process_latency(f64::NAN, (80, 24), false);
989 assert!(result.is_some());
990 }
991
992 #[test]
993 fn edge_infinity_latency() {
994 let mut config = test_config();
995 config.hysteresis = 0.1;
996 let monitor = ResizeSlaMonitor::new(config);
997 for _ in 0..5 {
998 monitor.process_latency(10.0, (80, 24), false);
999 }
1000 let result = monitor.process_latency(f64::INFINITY, (80, 24), false);
1001 assert!(result.is_some());
1002 assert!(result.unwrap().evidence.conformal_alert);
1004 }
1005
1006 #[test]
1007 fn edge_logging_disabled() {
1008 let mut config = test_config();
1009 config.enable_logging = false;
1010 let monitor = ResizeSlaMonitor::new(config);
1011
1012 for i in 0..10 {
1013 monitor.process_latency(10.0 + i as f64, (80, 24), false);
1014 }
1015
1016 assert!(
1017 monitor.logs().is_empty(),
1018 "Logs should be empty when disabled"
1019 );
1020 assert!(monitor.logs_to_jsonl().is_empty());
1021 }
1022
1023 #[test]
1024 fn edge_reset_then_reuse() {
1025 let monitor = ResizeSlaMonitor::new(test_config());
1026
1027 for i in 0..10 {
1029 monitor.process_latency(10.0 + i as f64, (80, 24), false);
1030 }
1031 assert!(monitor.is_active());
1032 let summary1 = monitor.summary();
1033 assert_eq!(summary1.total_events, 10);
1034
1035 monitor.reset();
1037 assert!(!monitor.is_active());
1038 assert_eq!(monitor.calibration_count(), 0);
1039 assert!(monitor.last_alert().is_none());
1040
1041 for i in 0..10 {
1043 monitor.process_latency(50.0 + i as f64, (120, 40), false);
1044 }
1045 assert!(monitor.is_active());
1046 let summary2 = monitor.summary();
1047 assert_eq!(summary2.total_events, 10);
1048 assert!(summary2.mean_latency_ms > 40.0);
1050 }
1051
1052 #[test]
1053 fn edge_multiple_resets() {
1054 let monitor = ResizeSlaMonitor::new(test_config());
1055
1056 for _ in 0..3 {
1057 for i in 0..5 {
1058 monitor.process_latency(10.0 + i as f64, (80, 24), false);
1059 }
1060 monitor.reset();
1061 }
1062
1063 assert!(!monitor.is_active());
1064 assert_eq!(*monitor.event_count.borrow(), 0);
1065 }
1066
1067 #[test]
1068 fn edge_min_calibration_zero() {
1069 let mut config = test_config();
1070 config.min_calibration = 0;
1071 let monitor = ResizeSlaMonitor::new(config);
1072
1073 assert!(monitor.is_active());
1075
1076 let result = monitor.process_latency(10.0, (80, 24), false);
1078 assert!(result.is_some());
1079 }
1080
1081 #[test]
1082 fn edge_last_alert_updates() {
1083 let mut config = test_config();
1084 config.hysteresis = 0.1;
1085 config.alert_cooldown = 0;
1086 let monitor = ResizeSlaMonitor::new(config);
1087
1088 for _ in 0..5 {
1090 monitor.process_latency(10.0, (80, 24), false);
1091 }
1092
1093 let mut got_alert = false;
1095 for _ in 0..10 {
1096 let result = monitor.process_latency(1000.0, (80, 24), false);
1097 if let Some(decision) = result
1098 && decision.is_alert
1099 {
1100 got_alert = true;
1101 }
1102 }
1103
1104 if got_alert {
1105 let last = monitor.last_alert();
1106 assert!(last.is_some());
1107 assert!(last.unwrap().is_alert);
1108 }
1109 }
1110
1111 #[test]
1112 fn edge_forced_flag_propagates_to_log() {
1113 let monitor = ResizeSlaMonitor::new(test_config());
1114
1115 monitor.process_latency(10.0, (80, 24), true);
1116
1117 let logs = monitor.logs();
1118 assert_eq!(logs.len(), 1);
1119 assert!(logs[0].forced);
1120 }
1121
1122 #[test]
1123 fn edge_applied_size_propagates_to_log() {
1124 let monitor = ResizeSlaMonitor::new(test_config());
1125
1126 monitor.process_latency(10.0, (200, 60), false);
1127
1128 let logs = monitor.logs();
1129 assert_eq!(logs.len(), 1);
1130 assert_eq!(logs[0].applied_size, (200, 60));
1131 }
1132
1133 #[test]
1134 fn edge_event_count_accuracy() {
1135 let monitor = ResizeSlaMonitor::new(test_config());
1136
1137 for i in 0..15 {
1138 monitor.process_latency(10.0 + i as f64, (80, 24), false);
1139 }
1140
1141 assert_eq!(*monitor.event_count.borrow(), 15);
1142 assert_eq!(monitor.summary().total_events, 15);
1143 }
1144
1145 #[test]
1146 fn edge_jsonl_with_alert() {
1147 let mut config = test_config();
1148 config.hysteresis = 0.1;
1149 config.alert_cooldown = 0;
1150 let monitor = ResizeSlaMonitor::new(config);
1151
1152 for _ in 0..5 {
1154 monitor.process_latency(10.0, (80, 24), false);
1155 }
1156
1157 monitor.process_latency(10000.0, (80, 24), false);
1159
1160 let jsonl = monitor.logs_to_jsonl();
1161 assert!(jsonl.contains(r#""type":"calibrate""#));
1163 let has_alert_or_observe =
1164 jsonl.contains(r#""type":"alert""#) || jsonl.contains(r#""type":"observe""#);
1165 assert!(has_alert_or_observe);
1166 }
1167
1168 #[test]
1169 fn edge_summary_after_reset() {
1170 let monitor = ResizeSlaMonitor::new(test_config());
1171
1172 for i in 0..10 {
1173 monitor.process_latency(10.0 + i as f64, (80, 24), false);
1174 }
1175
1176 monitor.reset();
1177
1178 let summary = monitor.summary();
1179 assert_eq!(summary.total_events, 0);
1180 assert_eq!(summary.total_alerts, 0);
1181 assert_eq!(summary.calibration_events, 0);
1182 }
1183
1184 #[test]
1185 fn edge_sla_config_clone_debug() {
1186 let config = SlaConfig::default();
1187 let cloned = config.clone();
1188 assert_eq!(cloned.alpha, config.alpha);
1189 assert_eq!(cloned.min_calibration, config.min_calibration);
1190 let debug = format!("{:?}", config);
1191 assert!(debug.contains("SlaConfig"));
1192 }
1193
1194 #[test]
1195 fn edge_resize_evidence_clone_debug() {
1196 let ev = ResizeEvidence {
1197 timestamp: Instant::now(),
1198 latency_ms: 42.0,
1199 applied_size: (80, 24),
1200 forced: false,
1201 regime: "steady",
1202 coalesce_ms: Some(10.0),
1203 };
1204 let cloned = ev.clone();
1205 assert_eq!(cloned.latency_ms, 42.0);
1206 assert_eq!(cloned.applied_size, (80, 24));
1207 let debug = format!("{:?}", ev);
1208 assert!(debug.contains("ResizeEvidence"));
1209 }
1210
1211 #[test]
1212 fn edge_sla_log_entry_clone_debug() {
1213 let entry = SlaLogEntry {
1214 event_idx: 1,
1215 event_type: "calibrate",
1216 latency_ms: 10.0,
1217 target_latency_ms: 50.0,
1218 threshold_ms: 20.0,
1219 e_value: 1.0,
1220 is_alert: false,
1221 alert_reason: None,
1222 applied_size: (80, 24),
1223 forced: false,
1224 };
1225 let cloned = entry.clone();
1226 assert_eq!(cloned.event_idx, 1);
1227 assert_eq!(cloned.event_type, "calibrate");
1228 let debug = format!("{:?}", entry);
1229 assert!(debug.contains("SlaLogEntry"));
1230 }
1231
1232 #[test]
1233 fn edge_sla_summary_clone_debug() {
1234 let monitor = ResizeSlaMonitor::new(test_config());
1235 for i in 0..5 {
1236 monitor.process_latency(10.0 + i as f64, (80, 24), false);
1237 }
1238 let summary = monitor.summary();
1239 let cloned = summary.clone();
1240 assert_eq!(cloned.total_events, summary.total_events);
1241 let debug = format!("{:?}", summary);
1242 assert!(debug.contains("SlaSummary"));
1243 }
1244
1245 #[test]
1246 fn edge_max_calibration_small() {
1247 let mut config = test_config();
1248 config.max_calibration = 3;
1249 config.min_calibration = 3;
1250 let monitor = ResizeSlaMonitor::new(config);
1251
1252 for i in 0..10 {
1254 monitor.process_latency(10.0 + i as f64, (80, 24), false);
1255 }
1256
1257 assert!(monitor.calibration_count() <= 3);
1259 }
1260
1261 #[test]
1262 fn edge_large_latency_values() {
1263 let monitor = ResizeSlaMonitor::new(test_config());
1264 for _ in 0..5 {
1265 monitor.process_latency(1e15, (80, 24), false);
1266 }
1267 let result = monitor.process_latency(1e15, (80, 24), false);
1269 assert!(result.is_some());
1270 let summary = monitor.summary();
1271 assert!(summary.mean_latency_ms.is_finite());
1272 }
1273}