1#![allow(clippy::cast_precision_loss)]
37#![allow(clippy::cast_possible_truncation)]
38
39use std::collections::VecDeque;
40
41#[derive(Debug, Clone, Default)]
55pub struct RunningStats {
56 count: u64,
58 mean: f64,
60 m2: f64,
62 min: f64,
64 max: f64,
66}
67
68impl RunningStats {
69 #[must_use]
71 pub fn new() -> Self {
72 Self {
73 count: 0,
74 mean: 0.0,
75 m2: 0.0,
76 min: f64::INFINITY,
77 max: f64::NEG_INFINITY,
78 }
79 }
80
81 pub fn push(&mut self, value: f64) {
85 if value.is_nan() {
86 return;
87 }
88 self.count += 1;
89 let delta = value - self.mean;
90 self.mean += delta / self.count as f64;
91 let delta2 = value - self.mean;
92 self.m2 += delta * delta2;
93
94 if value < self.min {
95 self.min = value;
96 }
97 if value > self.max {
98 self.max = value;
99 }
100 }
101
102 #[must_use]
104 pub fn count(&self) -> u64 {
105 self.count
106 }
107
108 #[must_use]
110 pub fn is_empty(&self) -> bool {
111 self.count == 0
112 }
113
114 #[must_use]
116 pub fn mean(&self) -> f64 {
117 if self.count == 0 {
118 0.0
119 } else {
120 self.mean
121 }
122 }
123
124 #[must_use]
126 pub fn variance(&self) -> f64 {
127 if self.count < 2 {
128 0.0
129 } else {
130 self.m2 / (self.count - 1) as f64
131 }
132 }
133
134 #[must_use]
136 pub fn population_variance(&self) -> f64 {
137 if self.count == 0 {
138 0.0
139 } else {
140 self.m2 / self.count as f64
141 }
142 }
143
144 #[must_use]
146 pub fn stddev(&self) -> f64 {
147 self.variance().sqrt()
148 }
149
150 #[must_use]
152 pub fn population_stddev(&self) -> f64 {
153 self.population_variance().sqrt()
154 }
155
156 #[must_use]
158 pub fn min(&self) -> f64 {
159 self.min
160 }
161
162 #[must_use]
164 pub fn max(&self) -> f64 {
165 self.max
166 }
167
168 #[must_use]
170 pub fn range(&self) -> f64 {
171 if self.count < 2 {
172 0.0
173 } else {
174 self.max - self.min
175 }
176 }
177
178 #[must_use]
182 pub fn cv(&self) -> f64 {
183 let m = self.mean();
184 if m == 0.0 {
185 f64::NAN
186 } else {
187 self.stddev() / m.abs()
188 }
189 }
190
191 pub fn merge(&mut self, other: &Self) {
201 if other.count == 0 {
202 return;
203 }
204 if self.count == 0 {
205 *self = other.clone();
206 return;
207 }
208 let combined = self.count + other.count;
209 let delta = other.mean - self.mean;
210 let new_mean =
211 (self.mean * self.count as f64 + other.mean * other.count as f64) / combined as f64;
212 let new_m2 = self.m2
213 + other.m2
214 + delta * delta * (self.count as f64 * other.count as f64) / combined as f64;
215
216 self.count = combined;
217 self.mean = new_mean;
218 self.m2 = new_m2;
219 if other.min < self.min {
220 self.min = other.min;
221 }
222 if other.max > self.max {
223 self.max = other.max;
224 }
225 }
226
227 pub fn reset(&mut self) {
229 *self = Self::new();
230 }
231}
232
233#[derive(Debug, Clone)]
254pub struct Ewma {
255 alpha: f64,
256 value: Option<f64>,
257 count: u64,
258 variance: f64,
260}
261
262impl Ewma {
263 #[must_use]
269 pub fn new(alpha: f64) -> Self {
270 debug_assert!(alpha > 0.0 && alpha <= 1.0, "alpha must be in (0, 1]");
271 let alpha = alpha.clamp(1e-9, 1.0);
272 Self {
273 alpha,
274 value: None,
275 count: 0,
276 variance: 0.0,
277 }
278 }
279
280 pub fn update(&mut self, sample: f64) {
284 if sample.is_nan() {
285 return;
286 }
287 self.count += 1;
288 match self.value {
289 None => {
290 self.value = Some(sample);
291 self.variance = 0.0;
292 }
293 Some(prev) => {
294 let new_val = self.alpha * sample + (1.0 - self.alpha) * prev;
295 let diff = sample - prev;
296 self.variance = (1.0 - self.alpha) * (self.variance + self.alpha * diff * diff);
297 self.value = Some(new_val);
298 }
299 }
300 }
301
302 #[must_use]
304 pub fn value_opt(&self) -> Option<f64> {
305 self.value
306 }
307
308 #[must_use]
310 pub fn value(&self) -> f64 {
311 self.value.unwrap_or(0.0)
312 }
313
314 #[must_use]
316 pub fn variance(&self) -> f64 {
317 self.variance
318 }
319
320 #[must_use]
322 pub fn stddev(&self) -> f64 {
323 self.variance.sqrt()
324 }
325
326 #[must_use]
328 pub fn count(&self) -> u64 {
329 self.count
330 }
331
332 #[must_use]
334 pub fn alpha(&self) -> f64 {
335 self.alpha
336 }
337
338 pub fn reset(&mut self) {
340 self.value = None;
341 self.count = 0;
342 self.variance = 0.0;
343 }
344}
345
346#[derive(Debug, Clone)]
354pub struct RollingWindow {
355 capacity: usize,
356 buffer: VecDeque<f64>,
357 sum: f64,
359 sum_sq: f64,
360}
361
362impl RollingWindow {
363 #[must_use]
369 pub fn new(capacity: usize) -> Self {
370 assert!(capacity >= 1, "RollingWindow capacity must be ≥ 1");
371 Self {
372 capacity,
373 buffer: VecDeque::with_capacity(capacity),
374 sum: 0.0,
375 sum_sq: 0.0,
376 }
377 }
378
379 pub fn push(&mut self, value: f64) {
381 if self.buffer.len() == self.capacity {
382 if let Some(old) = self.buffer.pop_front() {
384 self.sum -= old;
385 self.sum_sq -= old * old;
386 }
387 }
388 self.buffer.push_back(value);
389 self.sum += value;
390 self.sum_sq += value * value;
391 }
392
393 #[must_use]
395 pub fn len(&self) -> usize {
396 self.buffer.len()
397 }
398
399 #[must_use]
401 pub fn is_empty(&self) -> bool {
402 self.buffer.is_empty()
403 }
404
405 #[must_use]
407 pub fn is_full(&self) -> bool {
408 self.buffer.len() == self.capacity
409 }
410
411 #[must_use]
413 pub fn capacity(&self) -> usize {
414 self.capacity
415 }
416
417 #[must_use]
419 pub fn mean(&self) -> f64 {
420 if self.buffer.is_empty() {
421 0.0
422 } else {
423 self.sum / self.buffer.len() as f64
424 }
425 }
426
427 #[must_use]
431 pub fn variance(&self) -> f64 {
432 let n = self.buffer.len() as f64;
433 if n < 1.0 {
434 return 0.0;
435 }
436 let mean = self.sum / n;
437 let mean_sq = self.sum_sq / n;
438 (mean_sq - mean * mean).max(0.0)
440 }
441
442 #[must_use]
444 pub fn stddev(&self) -> f64 {
445 self.variance().sqrt()
446 }
447
448 #[must_use]
450 pub fn min(&self) -> f64 {
451 self.buffer.iter().copied().fold(f64::INFINITY, f64::min)
452 }
453
454 #[must_use]
456 pub fn max(&self) -> f64 {
457 self.buffer
458 .iter()
459 .copied()
460 .fold(f64::NEG_INFINITY, f64::max)
461 }
462
463 #[must_use]
465 pub fn samples(&self) -> Vec<f64> {
466 self.buffer.iter().copied().collect()
467 }
468}
469
470#[derive(Debug, Clone)]
487pub struct PercentileEstimator {
488 p: f64,
490 q: [f64; 5],
492 dn: [f64; 5],
494 n: [i64; 5],
496 count: u64,
498 bootstrap: Vec<f64>,
500}
501
502impl PercentileEstimator {
503 #[must_use]
509 pub fn new(p: f64) -> Self {
510 debug_assert!(p > 0.0 && p < 1.0, "p must be in (0, 1)");
511 let p = p.clamp(1e-9, 1.0 - 1e-9);
512 Self {
513 p,
514 q: [0.0; 5],
515 dn: [0.0, p / 2.0, p, (1.0 + p) / 2.0, 1.0],
516 n: [1, 2, 3, 4, 5],
517 count: 0,
518 bootstrap: Vec::with_capacity(5),
519 }
520 }
521
522 pub fn update(&mut self, x: f64) {
524 if x.is_nan() {
525 return;
526 }
527 self.count += 1;
528
529 if self.bootstrap.len() < 5 {
531 self.bootstrap.push(x);
532 if self.bootstrap.len() == 5 {
533 self.bootstrap
535 .sort_by(|a, b| a.partial_cmp(b).unwrap_or(std::cmp::Ordering::Equal));
536 for i in 0..5 {
537 self.q[i] = self.bootstrap[i];
538 }
539 self.n = [1, 2, 3, 4, 5];
540 }
541 return;
542 }
543
544 let k = if x < self.q[0] {
547 self.q[0] = x;
548 0
549 } else if x < self.q[1] {
550 0
551 } else if x < self.q[2] {
552 1
553 } else if x < self.q[3] {
554 2
555 } else if x < self.q[4] {
556 3
557 } else {
558 self.q[4] = x;
559 3
560 };
561
562 for i in (k + 1)..5 {
564 self.n[i] += 1;
565 }
566
567 let obs_count = (self.count - 5) as f64 + 1.0; self.dn = [
570 0.0,
571 self.p / 2.0 * obs_count,
572 self.p * obs_count,
573 (1.0 + self.p) / 2.0 * obs_count,
574 obs_count,
575 ];
576
577 for i in 1..=3 {
579 let d = self.dn[i] - self.n[i] as f64;
580 if (d >= 1.0 && (self.n[i + 1] - self.n[i]) > 1)
581 || (d <= -1.0 && (self.n[i - 1] - self.n[i]) < -1)
582 {
583 let sign = if d > 0.0 { 1 } else { -1 };
584 let q_new = self.parabolic(i, sign as f64);
585 if q_new > self.q[i - 1] && q_new < self.q[i + 1] {
586 self.q[i] = q_new;
587 } else {
588 self.q[i] = self.linear(i, sign as f64);
589 }
590 self.n[i] += sign;
591 }
592 }
593 }
594
595 fn parabolic(&self, i: usize, d: f64) -> f64 {
597 let qi = self.q[i];
598 let qm = self.q[i - 1];
599 let qp = self.q[i + 1];
600 let ni = self.n[i] as f64;
601 let nm = self.n[i - 1] as f64;
602 let np = self.n[i + 1] as f64;
603 qi + d / (np - nm)
604 * ((ni - nm + d) * (qp - qi) / (np - ni) + (np - ni - d) * (qi - qm) / (ni - nm))
605 }
606
607 fn linear(&self, i: usize, d: f64) -> f64 {
609 let qi = self.q[i];
610 let idx = if d > 0.0 { i + 1 } else { i - 1 };
611 let qother = self.q[idx];
612 let ni = self.n[i] as f64;
613 let nother = self.n[idx] as f64;
614 qi + d * (qother - qi) / (nother - ni)
615 }
616
617 #[must_use]
619 pub fn estimate(&self) -> Option<f64> {
620 if self.count < 5 {
621 None
622 } else {
623 Some(self.q[2])
624 }
625 }
626
627 #[must_use]
629 pub fn count(&self) -> u64 {
630 self.count
631 }
632
633 #[must_use]
635 pub fn quantile(&self) -> f64 {
636 self.p
637 }
638}
639
640#[derive(Debug)]
663pub struct BitrateRunningAnalyzer {
664 fps: f64,
666 global: RunningStats,
668 trend: Ewma,
670 window: RollingWindow,
672 p95: PercentileEstimator,
674 total_bits: u64,
676 frame_count: u64,
678}
679
680impl BitrateRunningAnalyzer {
681 #[must_use]
686 pub fn new(fps: f64, window_frames: usize) -> Self {
687 let window_frames = window_frames.max(1);
688 Self {
689 fps,
690 global: RunningStats::new(),
691 trend: Ewma::new(0.1),
692 window: RollingWindow::new(window_frames),
693 p95: PercentileEstimator::new(0.95),
694 total_bits: 0,
695 frame_count: 0,
696 }
697 }
698
699 pub fn push_frame(&mut self, bits_per_frame: u64) {
701 let bits_f = bits_per_frame as f64;
702 self.global.push(bits_f);
703 self.trend.update(bits_f);
704 self.window.push(bits_f);
705 self.p95.update(bits_f);
706 self.total_bits += bits_per_frame;
707 self.frame_count += 1;
708 }
709
710 #[must_use]
712 pub fn summary(&self) -> BitrateSummary {
713 let scale = self.fps; BitrateSummary {
715 frame_count: self.frame_count,
716 total_bits: self.total_bits,
717 mean_bps: self.global.mean() * scale,
718 stddev_bps: self.global.stddev() * scale,
719 peak_bps: self.global.max() * scale,
720 min_bps: if self.global.is_empty() {
721 0.0
722 } else {
723 self.global.min() * scale
724 },
725 trend_bps: self.trend.value() * scale,
726 window_mean_bps: self.window.mean() * scale,
727 window_stddev_bps: self.window.stddev() * scale,
728 p95_bps: self.p95.estimate().map(|v| v * scale),
729 cv: self.global.cv(),
730 }
731 }
732
733 pub fn reset(&mut self) {
735 self.global.reset();
736 self.trend.reset();
737 self.window = RollingWindow::new(self.window.capacity());
738 self.p95 = PercentileEstimator::new(0.95);
739 self.total_bits = 0;
740 self.frame_count = 0;
741 }
742}
743
744#[derive(Debug, Clone)]
746pub struct BitrateSummary {
747 pub frame_count: u64,
749 pub total_bits: u64,
751 pub mean_bps: f64,
753 pub stddev_bps: f64,
755 pub peak_bps: f64,
757 pub min_bps: f64,
759 pub trend_bps: f64,
761 pub window_mean_bps: f64,
763 pub window_stddev_bps: f64,
765 pub p95_bps: Option<f64>,
767 pub cv: f64,
769}
770
771#[cfg(test)]
774mod tests {
775 use super::*;
776
777 #[test]
780 fn test_running_stats_empty() {
781 let stats = RunningStats::new();
782 assert!(stats.is_empty());
783 assert_eq!(stats.count(), 0);
784 assert_eq!(stats.mean(), 0.0);
785 assert_eq!(stats.variance(), 0.0);
786 assert_eq!(stats.stddev(), 0.0);
787 }
788
789 #[test]
790 fn test_running_stats_single_sample() {
791 let mut stats = RunningStats::new();
792 stats.push(42.0);
793 assert_eq!(stats.count(), 1);
794 assert!((stats.mean() - 42.0).abs() < 1e-10);
795 assert_eq!(stats.variance(), 0.0);
797 assert_eq!(stats.min(), 42.0);
798 assert_eq!(stats.max(), 42.0);
799 }
800
801 #[test]
802 fn test_running_stats_known_values() {
803 let mut stats = RunningStats::new();
804 for v in [2.0_f64, 4.0, 4.0, 4.0, 5.0, 5.0, 7.0, 9.0] {
805 stats.push(v);
806 }
807 assert!((stats.mean() - 5.0).abs() < 1e-10, "mean {}", stats.mean());
809 assert!((stats.population_stddev() - 2.0).abs() < 1e-10);
810 assert_eq!(stats.min(), 2.0);
811 assert_eq!(stats.max(), 9.0);
812 assert_eq!(stats.range(), 7.0);
813 }
814
815 #[test]
816 fn test_running_stats_nan_ignored() {
817 let mut stats = RunningStats::new();
818 stats.push(10.0);
819 stats.push(f64::NAN);
820 stats.push(20.0);
821 assert_eq!(stats.count(), 2);
822 assert!((stats.mean() - 15.0).abs() < 1e-10);
823 }
824
825 #[test]
826 fn test_running_stats_merge() {
827 let mut a = RunningStats::new();
828 let mut b = RunningStats::new();
829 for v in [1.0_f64, 2.0, 3.0] {
830 a.push(v);
831 }
832 for v in [4.0_f64, 5.0, 6.0] {
833 b.push(v);
834 }
835 a.merge(&b);
836 assert_eq!(a.count(), 6);
837 assert!((a.mean() - 3.5).abs() < 1e-10, "merged mean {}", a.mean());
838 assert_eq!(a.min(), 1.0);
839 assert_eq!(a.max(), 6.0);
840 }
841
842 #[test]
843 fn test_running_stats_merge_empty_rhs() {
844 let mut a = RunningStats::new();
845 a.push(5.0);
846 let empty = RunningStats::new();
847 a.merge(&empty);
848 assert_eq!(a.count(), 1);
849 assert!((a.mean() - 5.0).abs() < 1e-10);
850 }
851
852 #[test]
853 fn test_running_stats_reset() {
854 let mut stats = RunningStats::new();
855 stats.push(100.0);
856 stats.reset();
857 assert!(stats.is_empty());
858 assert_eq!(stats.mean(), 0.0);
859 }
860
861 #[test]
862 fn test_running_stats_cv() {
863 let mut stats = RunningStats::new();
864 for _ in 0..5 {
866 stats.push(10.0);
867 }
868 let cv = stats.cv();
869 assert!(cv.is_finite() && cv < 1e-10, "cv {cv}");
870 }
871
872 #[test]
875 fn test_ewma_initial_value() {
876 let mut ewma = Ewma::new(0.5);
877 assert!(ewma.value_opt().is_none());
878 ewma.update(100.0);
879 assert!((ewma.value() - 100.0).abs() < 1e-10);
880 }
881
882 #[test]
883 fn test_ewma_convergence() {
884 let mut ewma = Ewma::new(0.3);
885 for _ in 0..200 {
886 ewma.update(50.0);
887 }
888 assert!((ewma.value() - 50.0).abs() < 0.1, "value {}", ewma.value());
890 }
891
892 #[test]
893 fn test_ewma_nan_ignored() {
894 let mut ewma = Ewma::new(0.5);
895 ewma.update(10.0);
896 let before = ewma.value();
897 ewma.update(f64::NAN);
898 assert!((ewma.value() - before).abs() < 1e-12);
899 assert_eq!(ewma.count(), 1);
900 }
901
902 #[test]
903 fn test_ewma_reset() {
904 let mut ewma = Ewma::new(0.2);
905 ewma.update(42.0);
906 ewma.reset();
907 assert!(ewma.value_opt().is_none());
908 assert_eq!(ewma.count(), 0);
909 }
910
911 #[test]
914 fn test_rolling_window_basic() {
915 let mut w = RollingWindow::new(3);
916 assert!(w.is_empty());
917 w.push(1.0);
918 w.push(2.0);
919 w.push(3.0);
920 assert!(w.is_full());
921 assert!((w.mean() - 2.0).abs() < 1e-10);
922 }
923
924 #[test]
925 fn test_rolling_window_eviction() {
926 let mut w = RollingWindow::new(3);
927 w.push(10.0);
928 w.push(20.0);
929 w.push(30.0);
930 w.push(40.0);
932 assert_eq!(w.len(), 3);
933 assert!((w.mean() - 30.0).abs() < 1e-10, "mean {}", w.mean());
934 }
935
936 #[test]
937 fn test_rolling_window_variance() {
938 let mut w = RollingWindow::new(4);
939 for v in [2.0_f64, 4.0, 4.0, 4.0] {
940 w.push(v);
941 }
942 assert!((w.variance() - 0.75).abs() < 1e-10, "var {}", w.variance());
944 }
945
946 #[test]
947 fn test_rolling_window_min_max() {
948 let mut w = RollingWindow::new(5);
949 for v in [5.0_f64, 3.0, 8.0, 1.0, 6.0] {
950 w.push(v);
951 }
952 assert_eq!(w.min(), 1.0);
953 assert_eq!(w.max(), 8.0);
954 }
955
956 #[test]
959 fn test_percentile_estimator_bootstrap() {
960 let mut est = PercentileEstimator::new(0.5);
961 for v in 1..=4 {
962 est.update(v as f64);
963 assert!(est.estimate().is_none());
965 }
966 est.update(5.0);
967 assert!(est.estimate().is_some());
968 }
969
970 #[test]
971 fn test_percentile_estimator_median_uniform() {
972 let mut est = PercentileEstimator::new(0.5);
973 for v in 1..=1000 {
974 est.update(v as f64);
975 }
976 let estimated = est.estimate().expect("should have estimate");
978 assert!(
979 (estimated - 500.5).abs() < 30.0,
980 "median estimate {estimated}"
981 );
982 }
983
984 #[test]
985 fn test_percentile_estimator_p95() {
986 let mut est = PercentileEstimator::new(0.95);
987 for v in 0..=99 {
989 est.update(v as f64);
990 }
991 let estimated = est.estimate().expect("should have estimate");
992 assert!((estimated - 94.05).abs() < 15.0, "p95 estimate {estimated}");
994 }
995
996 #[test]
999 fn test_bitrate_analyzer_basic() {
1000 let mut analyzer = BitrateRunningAnalyzer::new(30.0, 30);
1001 for bits in [40_000u64, 50_000, 60_000, 45_000, 55_000] {
1002 analyzer.push_frame(bits);
1003 }
1004 let s = analyzer.summary();
1005 assert_eq!(s.frame_count, 5);
1006 assert!(s.mean_bps > 0.0);
1007 assert!(s.peak_bps >= s.mean_bps);
1008 assert!(s.min_bps <= s.mean_bps);
1009 assert_eq!(s.total_bits, 250_000);
1010 }
1011
1012 #[test]
1013 fn test_bitrate_analyzer_reset() {
1014 let mut analyzer = BitrateRunningAnalyzer::new(25.0, 10);
1015 analyzer.push_frame(100_000);
1016 analyzer.reset();
1017 let s = analyzer.summary();
1018 assert_eq!(s.frame_count, 0);
1019 assert_eq!(s.total_bits, 0);
1020 }
1021
1022 #[test]
1023 fn test_bitrate_analyzer_trend_smoother_than_peak() {
1024 let mut analyzer = BitrateRunningAnalyzer::new(30.0, 10);
1025 for i in 0..100 {
1027 analyzer.push_frame(if i % 2 == 0 { 10_000 } else { 200_000 });
1028 }
1029 let s = analyzer.summary();
1030 assert!(s.peak_bps > s.trend_bps);
1032 }
1033}