1use serde::{Deserialize, Serialize};
17use std::collections::HashMap;
18use std::sync::{Arc, Mutex};
19use std::time::{Duration, Instant};
20
21#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize)]
23#[repr(u8)]
24pub enum CompressionAlgorithm {
25 None = 0,
27 Zlib = 1,
29 Zstd = 2,
31 Brotli = 3,
33}
34
35#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize)]
37pub enum CompressionLevel {
38 Fast,
40 Default,
42 Best,
44}
45
46#[derive(Debug, Clone, Serialize, Deserialize)]
48pub struct CompressionMetrics {
49 pub original_size: usize,
52 pub compressed_size: usize,
54 pub compression_ratio: f64,
56 pub space_saved: usize,
58 pub space_saved_percent: f64,
60
61 pub compression_time: Duration,
64 pub decompression_time: Option<Duration>,
66 pub throughput_mbps: f64,
68
69 pub algorithm: CompressionAlgorithm,
72 pub level: CompressionLevel,
74
75 pub total_operations: u64,
78 pub total_bytes_processed: u64,
80 pub avg_compression_ratio: f64,
82}
83
84impl CompressionMetrics {
85 pub fn new() -> Self {
87 Self::default()
88 }
89
90 pub fn from_sizes(original_size: usize, compressed_size: usize) -> Self {
92 let compression_ratio = if original_size == 0 {
93 1.0
94 } else {
95 original_size as f64 / compressed_size as f64
96 };
97
98 let space_saved = original_size.saturating_sub(compressed_size);
99 let space_saved_percent = if original_size == 0 {
100 0.0
101 } else {
102 (space_saved as f64 / original_size as f64) * 100.0
103 };
104
105 CompressionMetrics {
106 original_size,
107 compressed_size,
108 compression_ratio,
109 space_saved,
110 space_saved_percent,
111 compression_time: Duration::from_secs(0),
112 decompression_time: None,
113 throughput_mbps: 0.0,
114 algorithm: CompressionAlgorithm::None,
115 level: CompressionLevel::Default,
116 total_operations: 1,
117 total_bytes_processed: original_size as u64,
118 avg_compression_ratio: compression_ratio,
119 }
120 }
121
122 pub fn record_compression(
124 &mut self,
125 original: &[u8],
126 compressed: &[u8],
127 duration: Duration,
128 algorithm: CompressionAlgorithm,
129 level: CompressionLevel,
130 ) {
131 self.original_size = original.len();
132 self.compressed_size = compressed.len();
133
134 self.compression_ratio = if compressed.is_empty() {
136 f64::INFINITY
137 } else {
138 original.len() as f64 / compressed.len() as f64
139 };
140
141 self.space_saved = original.len().saturating_sub(compressed.len());
142 self.space_saved_percent = if original.is_empty() {
143 0.0
144 } else {
145 (self.space_saved as f64 / original.len() as f64) * 100.0
146 };
147
148 self.compression_time = duration;
149 self.algorithm = algorithm;
150 self.level = level;
151
152 let mb = original.len() as f64 / 1_048_576.0;
154 let seconds = duration.as_secs_f64();
155 self.throughput_mbps = if seconds > 0.0 { mb / seconds } else { 0.0 };
156
157 self.total_operations += 1;
159 self.total_bytes_processed += original.len() as u64;
160 self.update_avg_ratio();
161 }
162
163 pub fn record_decompression(&mut self, duration: Duration) {
165 self.decompression_time = Some(duration);
166 }
167
168 fn update_avg_ratio(&mut self) {
170 if self.total_operations == 0 {
171 self.avg_compression_ratio = 1.0;
172 } else {
173 let weight = 1.0 / self.total_operations as f64;
175 self.avg_compression_ratio =
176 self.avg_compression_ratio * (1.0 - weight) + self.compression_ratio * weight;
177 }
178 }
179
180 pub fn to_prometheus_metrics(&self) -> String {
182 format!(
183 "# HELP mediagit_compression_ratio Compression ratio (original/compressed)\n\
184 # TYPE mediagit_compression_ratio gauge\n\
185 mediagit_compression_ratio{{algorithm=\"{:?}\",level=\"{:?}\"}} {}\n\
186 \n\
187 # HELP mediagit_compression_throughput_mbps Compression throughput in MB/s\n\
188 # TYPE mediagit_compression_throughput_mbps gauge\n\
189 mediagit_compression_throughput_mbps{{algorithm=\"{:?}\",level=\"{:?}\"}} {}\n\
190 \n\
191 # HELP mediagit_compression_space_saved_bytes Space saved in bytes\n\
192 # TYPE mediagit_compression_space_saved_bytes counter\n\
193 mediagit_compression_space_saved_bytes{{algorithm=\"{:?}\",level=\"{:?}\"}} {}\n\
194 \n\
195 # HELP mediagit_compression_total_operations Total compression operations\n\
196 # TYPE mediagit_compression_total_operations counter\n\
197 mediagit_compression_total_operations{{algorithm=\"{:?}\",level=\"{:?}\"}} {}\n\
198 \n\
199 # HELP mediagit_compression_total_bytes_processed Total bytes processed\n\
200 # TYPE mediagit_compression_total_bytes_processed counter\n\
201 mediagit_compression_total_bytes_processed{{algorithm=\"{:?}\",level=\"{:?}\"}} {}\n\
202 \n\
203 # HELP mediagit_compression_avg_ratio Average compression ratio\n\
204 # TYPE mediagit_compression_avg_ratio gauge\n\
205 mediagit_compression_avg_ratio{{algorithm=\"{:?}\",level=\"{:?}\"}} {}\n",
206 self.algorithm,
207 self.level,
208 self.compression_ratio,
209 self.algorithm,
210 self.level,
211 self.throughput_mbps,
212 self.algorithm,
213 self.level,
214 self.space_saved,
215 self.algorithm,
216 self.level,
217 self.total_operations,
218 self.algorithm,
219 self.level,
220 self.total_bytes_processed,
221 self.algorithm,
222 self.level,
223 self.avg_compression_ratio
224 )
225 }
226
227 pub fn to_json(&self) -> serde_json::Value {
229 serde_json::json!({
230 "size_metrics": {
231 "original_size": self.original_size,
232 "compressed_size": self.compressed_size,
233 "compression_ratio": self.compression_ratio,
234 "space_saved": self.space_saved,
235 "space_saved_percent": self.space_saved_percent
236 },
237 "performance_metrics": {
238 "compression_time_ms": self.compression_time.as_millis(),
239 "decompression_time_ms": self.decompression_time.map(|d| d.as_millis()),
240 "throughput_mbps": self.throughput_mbps
241 },
242 "algorithm": {
243 "name": format!("{:?}", self.algorithm),
244 "level": format!("{:?}", self.level)
245 },
246 "accumulated_metrics": {
247 "total_operations": self.total_operations,
248 "total_bytes_processed": self.total_bytes_processed,
249 "avg_compression_ratio": self.avg_compression_ratio
250 }
251 })
252 }
253
254 pub fn summary(&self) -> String {
256 format!(
257 "Compressed {} → {} bytes ({:.1}% reduction, {:.2}x ratio) in {:.2}ms ({:.1} MB/s)",
258 self.original_size,
259 self.compressed_size,
260 self.space_saved_percent,
261 self.compression_ratio,
262 self.compression_time.as_millis(),
263 self.throughput_mbps
264 )
265 }
266
267 pub fn compression_ratio_legacy(&self) -> f64 {
269 if self.original_size == 0 {
270 1.0
271 } else {
272 self.compressed_size as f64 / self.original_size as f64
273 }
274 }
275
276 pub fn bytes_saved(&self) -> i64 {
278 self.space_saved as i64
279 }
280
281 pub fn savings_percentage(&self) -> f64 {
283 self.space_saved_percent
284 }
285
286 pub fn compression_throughput(&self) -> Option<f64> {
288 let secs = self.compression_time.as_secs_f64();
289 if secs > 0.0 {
290 Some(self.original_size as f64 / secs)
291 } else {
292 None
293 }
294 }
295
296 pub fn decompression_throughput(&self) -> Option<f64> {
298 self.decompression_time.map(|duration| {
299 let secs = duration.as_secs_f64();
300 if secs > 0.0 {
301 self.compressed_size as f64 / secs
302 } else {
303 0.0
304 }
305 })
306 }
307
308 pub fn format_summary(&self) -> String {
310 self.summary()
311 }
312}
313
314impl Default for CompressionMetrics {
315 fn default() -> Self {
316 CompressionMetrics {
317 original_size: 0,
318 compressed_size: 0,
319 compression_ratio: 1.0,
320 space_saved: 0,
321 space_saved_percent: 0.0,
322 compression_time: Duration::from_secs(0),
323 decompression_time: None,
324 throughput_mbps: 0.0,
325 algorithm: CompressionAlgorithm::None,
326 level: CompressionLevel::Default,
327 total_operations: 0,
328 total_bytes_processed: 0,
329 avg_compression_ratio: 1.0,
330 }
331 }
332}
333
334pub struct CompressionTimer {
336 start: Instant,
337}
338
339impl CompressionTimer {
340 pub fn start() -> Self {
342 CompressionTimer {
343 start: Instant::now(),
344 }
345 }
346
347 pub fn stop(self) -> Duration {
349 self.start.elapsed()
350 }
351}
352
353#[cfg(test)]
354#[allow(clippy::unwrap_used)]
355mod tests {
356 use super::*;
357
358 #[test]
359 fn test_record_compression() {
360 let mut metrics = CompressionMetrics::new();
361 let original = vec![0u8; 1000];
362 let compressed = vec![0u8; 400];
363
364 metrics.record_compression(
365 &original,
366 &compressed,
367 Duration::from_millis(50),
368 CompressionAlgorithm::Zstd,
369 CompressionLevel::Default,
370 );
371
372 assert_eq!(metrics.original_size, 1000);
373 assert_eq!(metrics.compressed_size, 400);
374 assert_eq!(metrics.space_saved, 600);
375 assert_eq!(metrics.space_saved_percent, 60.0);
376 assert!((metrics.compression_ratio - 2.5).abs() < 0.01); assert_eq!(metrics.total_operations, 1);
378 assert_eq!(metrics.total_bytes_processed, 1000);
379 }
380
381 #[test]
382 fn test_prometheus_export() {
383 let mut metrics = CompressionMetrics::new();
384 let original = vec![0u8; 1000];
385 let compressed = vec![0u8; 500];
386
387 metrics.record_compression(
388 &original,
389 &compressed,
390 Duration::from_millis(10),
391 CompressionAlgorithm::Zstd,
392 CompressionLevel::Best,
393 );
394
395 let prometheus = metrics.to_prometheus_metrics();
396
397 assert!(prometheus.contains("mediagit_compression_ratio"));
398 assert!(prometheus.contains("mediagit_compression_throughput_mbps"));
399 assert!(prometheus.contains("mediagit_compression_space_saved_bytes"));
400 assert!(prometheus.contains("algorithm=\"Zstd\""));
401 assert!(prometheus.contains("level=\"Best\""));
402 }
403
404 #[test]
405 fn test_json_export() {
406 let mut metrics = CompressionMetrics::new();
407 let original = vec![0u8; 2000];
408 let compressed = vec![0u8; 800];
409
410 metrics.record_compression(
411 &original,
412 &compressed,
413 Duration::from_millis(20),
414 CompressionAlgorithm::Brotli,
415 CompressionLevel::Fast,
416 );
417
418 let json = metrics.to_json();
419
420 assert_eq!(json["size_metrics"]["original_size"], 2000);
421 assert_eq!(json["size_metrics"]["compressed_size"], 800);
422 assert_eq!(json["size_metrics"]["space_saved"], 1200);
423 assert_eq!(json["algorithm"]["name"], "Brotli");
424 assert_eq!(json["algorithm"]["level"], "Fast");
425 assert!(json["performance_metrics"]["throughput_mbps"].is_number());
426 }
427
428 #[test]
429 fn test_summary_output() {
430 let mut metrics = CompressionMetrics::new();
431 let original = vec![0u8; 1000];
432 let compressed = vec![0u8; 250];
433
434 metrics.record_compression(
435 &original,
436 &compressed,
437 Duration::from_millis(5),
438 CompressionAlgorithm::Zstd,
439 CompressionLevel::Default,
440 );
441
442 let summary = metrics.summary();
443
444 assert!(summary.contains("1000"));
445 assert!(summary.contains("250"));
446 assert!(summary.contains("75.0%")); assert!(summary.contains("4.00x")); }
449
450 #[test]
451 fn test_accumulated_metrics() {
452 let mut metrics = CompressionMetrics::new();
453
454 metrics.record_compression(
456 &vec![0u8; 1000],
457 &vec![0u8; 500],
458 Duration::from_millis(10),
459 CompressionAlgorithm::Zstd,
460 CompressionLevel::Default,
461 );
462
463 metrics.record_compression(
465 &vec![0u8; 2000],
466 &vec![0u8; 800],
467 Duration::from_millis(20),
468 CompressionAlgorithm::Zstd,
469 CompressionLevel::Default,
470 );
471
472 assert_eq!(metrics.total_operations, 2);
473 assert_eq!(metrics.total_bytes_processed, 3000);
474 assert!(metrics.avg_compression_ratio > 2.0);
475 }
476
477 #[test]
478 fn test_throughput_calculation() {
479 let mut metrics = CompressionMetrics::new();
480 let data = vec![0u8; 1_048_576]; let compressed = vec![0u8; 524_288]; metrics.record_compression(
484 &data,
485 &compressed,
486 Duration::from_secs(1),
487 CompressionAlgorithm::Zstd,
488 CompressionLevel::Fast,
489 );
490
491 assert!((metrics.throughput_mbps - 1.0).abs() < 0.01);
493 }
494
495 #[test]
496 fn test_decompression_recording() {
497 let mut metrics = CompressionMetrics::new();
498
499 metrics.record_compression(
500 &vec![0u8; 1000],
501 &vec![0u8; 500],
502 Duration::from_millis(10),
503 CompressionAlgorithm::Zstd,
504 CompressionLevel::Default,
505 );
506
507 metrics.record_decompression(Duration::from_millis(5));
508
509 assert!(metrics.decompression_time.is_some());
510 assert_eq!(
511 metrics.decompression_time.unwrap(),
512 Duration::from_millis(5)
513 );
514 }
515
516 #[test]
517 fn test_backward_compatibility() {
518 let metrics = CompressionMetrics::from_sizes(1000, 500);
519
520 assert_eq!(metrics.original_size, 1000);
521 assert_eq!(metrics.compressed_size, 500);
522 assert_eq!(metrics.bytes_saved(), 500);
523 assert_eq!(metrics.savings_percentage(), 50.0);
524 assert!((metrics.compression_ratio_legacy() - 0.5).abs() < 0.01);
525 }
526
527 #[test]
528 fn test_zero_size_handling() {
529 let metrics = CompressionMetrics::from_sizes(0, 0);
530
531 assert_eq!(metrics.compression_ratio, 1.0);
532 assert_eq!(metrics.space_saved_percent, 0.0);
533 }
534
535 #[test]
536 fn test_timer() {
537 let timer = CompressionTimer::start();
538 std::thread::sleep(Duration::from_millis(10));
539 let elapsed = timer.stop();
540
541 assert!(elapsed >= Duration::from_millis(10));
542 }
543
544 #[test]
545 fn test_metrics_overhead() {
546 let iterations = 1000;
548 let start = Instant::now();
549
550 for _ in 0..iterations {
551 let mut metrics = CompressionMetrics::new();
552 metrics.record_compression(
553 &[0u8; 100],
554 &[0u8; 50],
555 Duration::from_micros(1),
556 CompressionAlgorithm::Zstd,
557 CompressionLevel::Fast,
558 );
559 let _ = metrics.to_prometheus_metrics();
560 let _ = metrics.to_json();
561 }
562
563 let elapsed = start.elapsed();
564 let per_op = elapsed.as_micros() / iterations;
565
566 assert!(
568 per_op < 100,
569 "Metrics overhead too high: {}μs per operation",
570 per_op
571 );
572 }
573}
574
575#[derive(Debug, Clone)]
580pub struct MetricsAggregator {
581 per_algorithm: Arc<Mutex<HashMap<CompressionAlgorithm, AggregatedStats>>>,
583 per_level: Arc<Mutex<HashMap<CompressionLevel, AggregatedStats>>>,
585 global: Arc<Mutex<AggregatedStats>>,
587}
588
589#[derive(Debug, Clone, Default, Serialize, Deserialize)]
591pub struct AggregatedStats {
592 pub operations: usize,
594 pub total_bytes_processed: usize,
596 pub total_bytes_compressed: usize,
598 pub total_time_ms: u64,
600 pub min_ratio: f64,
602 pub max_ratio: f64,
604 pub avg_ratio: f64,
606}
607
608impl AggregatedStats {
609 pub fn record(&mut self, metrics: &CompressionMetrics) {
611 self.operations += 1;
612 self.total_bytes_processed += metrics.original_size;
613 self.total_bytes_compressed += metrics.compressed_size;
614 self.total_time_ms += metrics.compression_time.as_millis() as u64;
615
616 if self.operations == 1 {
618 self.min_ratio = metrics.compression_ratio;
619 self.max_ratio = metrics.compression_ratio;
620 } else {
621 self.min_ratio = self.min_ratio.min(metrics.compression_ratio);
622 self.max_ratio = self.max_ratio.max(metrics.compression_ratio);
623 }
624
625 let weight = 1.0 / self.operations as f64;
627 self.avg_ratio = self.avg_ratio * (1.0 - weight) + metrics.compression_ratio * weight;
628 }
629
630 pub fn compression_ratio(&self) -> f64 {
632 if self.total_bytes_processed == 0 {
633 1.0
634 } else {
635 self.total_bytes_compressed as f64 / self.total_bytes_processed as f64
636 }
637 }
638
639 pub fn avg_throughput_mbps(&self) -> f64 {
641 if self.total_time_ms == 0 {
642 0.0
643 } else {
644 let mb = self.total_bytes_processed as f64 / 1_048_576.0;
645 let seconds = self.total_time_ms as f64 / 1000.0;
646 mb / seconds
647 }
648 }
649
650 pub fn total_space_saved(&self) -> i64 {
652 self.total_bytes_processed as i64 - self.total_bytes_compressed as i64
653 }
654
655 pub fn space_saved_percent(&self) -> f64 {
657 if self.total_bytes_processed == 0 {
658 0.0
659 } else {
660 self.total_space_saved() as f64 / self.total_bytes_processed as f64 * 100.0
661 }
662 }
663}
664
665impl MetricsAggregator {
666 pub fn new() -> Self {
668 MetricsAggregator {
669 per_algorithm: Arc::new(Mutex::new(HashMap::new())),
670 per_level: Arc::new(Mutex::new(HashMap::new())),
671 global: Arc::new(Mutex::new(AggregatedStats::default())),
672 }
673 }
674
675 pub fn record(&self, metrics: &CompressionMetrics) {
677 if let Ok(mut global) = self.global.lock() {
679 global.record(metrics);
680 }
681
682 if let Ok(mut per_algo) = self.per_algorithm.lock() {
684 per_algo
685 .entry(metrics.algorithm)
686 .or_insert_with(AggregatedStats::default)
687 .record(metrics);
688 }
689
690 if let Ok(mut per_level) = self.per_level.lock() {
692 per_level
693 .entry(metrics.level)
694 .or_insert_with(AggregatedStats::default)
695 .record(metrics);
696 }
697 }
698
699 pub fn global_stats(&self) -> AggregatedStats {
701 self.global
702 .lock()
703 .ok()
704 .map(|s| s.clone())
705 .unwrap_or_default()
706 }
707
708 pub fn algorithm_stats(&self, algorithm: CompressionAlgorithm) -> Option<AggregatedStats> {
710 self.per_algorithm.lock().ok()?.get(&algorithm).cloned()
711 }
712
713 pub fn level_stats(&self, level: CompressionLevel) -> Option<AggregatedStats> {
715 self.per_level.lock().ok()?.get(&level).cloned()
716 }
717
718 pub fn all_algorithm_stats(&self) -> HashMap<CompressionAlgorithm, AggregatedStats> {
720 self.per_algorithm
721 .lock()
722 .ok()
723 .map(|s| s.clone())
724 .unwrap_or_default()
725 }
726
727 pub fn all_level_stats(&self) -> HashMap<CompressionLevel, AggregatedStats> {
729 self.per_level
730 .lock()
731 .ok()
732 .map(|s| s.clone())
733 .unwrap_or_default()
734 }
735
736 pub fn reset(&self) {
738 if let Ok(mut global) = self.global.lock() {
739 *global = AggregatedStats::default();
740 }
741 if let Ok(mut per_algo) = self.per_algorithm.lock() {
742 per_algo.clear();
743 }
744 if let Ok(mut per_level) = self.per_level.lock() {
745 per_level.clear();
746 }
747 }
748
749 pub fn to_prometheus_metrics(&self) -> String {
751 let mut output = String::new();
752
753 let global = self.global_stats();
755 output.push_str("# HELP mediagit_compression_global_ratio Global compression ratio\n");
756 output.push_str("# TYPE mediagit_compression_global_ratio gauge\n");
757 output.push_str(&format!(
758 "mediagit_compression_global_ratio {}\n\n",
759 global.avg_ratio
760 ));
761
762 output.push_str("# HELP mediagit_compression_global_throughput Global throughput (MB/s)\n");
763 output.push_str("# TYPE mediagit_compression_global_throughput gauge\n");
764 output.push_str(&format!(
765 "mediagit_compression_global_throughput {}\n\n",
766 global.avg_throughput_mbps()
767 ));
768
769 output.push_str("# HELP mediagit_compression_global_operations Total operations\n");
770 output.push_str("# TYPE mediagit_compression_global_operations counter\n");
771 output.push_str(&format!(
772 "mediagit_compression_global_operations {}\n\n",
773 global.operations
774 ));
775
776 output.push_str(
778 "# HELP mediagit_compression_algorithm_ratio Compression ratio by algorithm\n",
779 );
780 output.push_str("# TYPE mediagit_compression_algorithm_ratio gauge\n");
781 for (algo, stats) in self.all_algorithm_stats() {
782 output.push_str(&format!(
783 "mediagit_compression_algorithm_ratio{{algorithm=\"{:?}\"}} {}\n",
784 algo, stats.avg_ratio
785 ));
786 }
787 output.push('\n');
788
789 output.push_str("# HELP mediagit_compression_level_ratio Compression ratio by level\n");
791 output.push_str("# TYPE mediagit_compression_level_ratio gauge\n");
792 for (level, stats) in self.all_level_stats() {
793 output.push_str(&format!(
794 "mediagit_compression_level_ratio{{level=\"{:?}\"}} {}\n",
795 level, stats.avg_ratio
796 ));
797 }
798
799 output
800 }
801
802 pub fn to_json(&self) -> serde_json::Value {
804 serde_json::json!({
805 "global": self.global_stats(),
806 "by_algorithm": self.all_algorithm_stats(),
807 "by_level": self.all_level_stats(),
808 })
809 }
810}
811
812impl Default for MetricsAggregator {
813 fn default() -> Self {
814 Self::new()
815 }
816}
817
818#[cfg(test)]
819#[allow(clippy::unwrap_used)]
820mod aggregator_tests {
821 use super::*;
822
823 #[test]
824 fn test_aggregator_basic() {
825 let aggregator = MetricsAggregator::new();
826
827 let mut metrics1 = CompressionMetrics::default();
828 metrics1.record_compression(
829 &vec![0u8; 1000],
830 &vec![0u8; 500],
831 Duration::from_millis(10),
832 CompressionAlgorithm::Zstd,
833 CompressionLevel::Default,
834 );
835
836 aggregator.record(&metrics1);
837
838 let global = aggregator.global_stats();
839 assert_eq!(global.operations, 1);
840 assert_eq!(global.total_bytes_processed, 1000);
841 assert_eq!(global.total_bytes_compressed, 500);
842 }
843
844 #[test]
845 fn test_aggregator_multiple_operations() {
846 let aggregator = MetricsAggregator::new();
847
848 for _ in 0..10 {
849 let mut metrics = CompressionMetrics::default();
850 metrics.record_compression(
851 &vec![0u8; 1000],
852 &vec![0u8; 500],
853 Duration::from_millis(10),
854 CompressionAlgorithm::Zstd,
855 CompressionLevel::Default,
856 );
857 aggregator.record(&metrics);
858 }
859
860 let global = aggregator.global_stats();
861 assert_eq!(global.operations, 10);
862 assert_eq!(global.total_bytes_processed, 10000);
863 assert_eq!(global.total_bytes_compressed, 5000);
864 assert!((global.avg_ratio - 2.0).abs() < 0.01);
865 }
866
867 #[test]
868 fn test_aggregator_per_algorithm() {
869 let aggregator = MetricsAggregator::new();
870
871 let mut metrics_zstd = CompressionMetrics::default();
873 metrics_zstd.record_compression(
874 &vec![0u8; 1000],
875 &vec![0u8; 500],
876 Duration::from_millis(10),
877 CompressionAlgorithm::Zstd,
878 CompressionLevel::Default,
879 );
880 aggregator.record(&metrics_zstd);
881
882 let mut metrics_brotli = CompressionMetrics::default();
884 metrics_brotli.record_compression(
885 &vec![0u8; 1000],
886 &vec![0u8; 400],
887 Duration::from_millis(20),
888 CompressionAlgorithm::Brotli,
889 CompressionLevel::Best,
890 );
891 aggregator.record(&metrics_brotli);
892
893 let zstd_stats = aggregator
895 .algorithm_stats(CompressionAlgorithm::Zstd)
896 .unwrap();
897 assert_eq!(zstd_stats.operations, 1);
898 assert_eq!(zstd_stats.total_bytes_compressed, 500);
899
900 let brotli_stats = aggregator
901 .algorithm_stats(CompressionAlgorithm::Brotli)
902 .unwrap();
903 assert_eq!(brotli_stats.operations, 1);
904 assert_eq!(brotli_stats.total_bytes_compressed, 400);
905
906 let global = aggregator.global_stats();
908 assert_eq!(global.operations, 2);
909 }
910
911 #[test]
912 fn test_aggregator_prometheus_export() {
913 let aggregator = MetricsAggregator::new();
914
915 let mut metrics = CompressionMetrics::default();
916 metrics.record_compression(
917 &vec![0u8; 1000],
918 &vec![0u8; 500],
919 Duration::from_millis(10),
920 CompressionAlgorithm::Zstd,
921 CompressionLevel::Default,
922 );
923 aggregator.record(&metrics);
924
925 let prometheus = aggregator.to_prometheus_metrics();
926
927 assert!(prometheus.contains("mediagit_compression_global_ratio"));
928 assert!(prometheus.contains("mediagit_compression_algorithm_ratio"));
929 assert!(prometheus.contains("algorithm=\"Zstd\""));
930 }
931
932 #[test]
933 fn test_aggregator_reset() {
934 let aggregator = MetricsAggregator::new();
935
936 let mut metrics = CompressionMetrics::default();
937 metrics.record_compression(
938 &vec![0u8; 1000],
939 &vec![0u8; 500],
940 Duration::from_millis(10),
941 CompressionAlgorithm::Zstd,
942 CompressionLevel::Default,
943 );
944 aggregator.record(&metrics);
945
946 assert_eq!(aggregator.global_stats().operations, 1);
947
948 aggregator.reset();
949
950 assert_eq!(aggregator.global_stats().operations, 0);
951 }
952}