1use std::collections::HashMap;
5
6#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash)]
15pub enum CompressionCodec {
16 Zstd,
18 Lz4,
20 Snappy,
22 Brotli,
24 None,
26}
27
28#[derive(Clone, Debug)]
34pub struct CodecProfile {
35 pub codec: CompressionCodec,
37 pub avg_ratio: f64,
39 pub avg_compress_micros: u64,
41 pub avg_decompress_micros: u64,
43 pub total_bytes_processed: u64,
45 pub uses: u64,
47}
48
49impl CodecProfile {
50 pub fn efficiency_score(&self) -> f64 {
54 (1.0 - self.avg_ratio) / (self.avg_compress_micros as f64 + 1.0)
55 }
56}
57
58#[derive(Clone, Debug)]
64pub struct DataCharacteristics {
65 pub size_bytes: u64,
67 pub is_text: bool,
69 pub is_already_compressed: bool,
71 pub latency_sensitive: bool,
73}
74
75#[derive(Clone, Debug)]
81pub struct CodecRecommendation {
82 pub codec: CompressionCodec,
84 pub reason: String,
86}
87
88#[derive(Clone, Debug)]
94pub struct CompressionRegistryStats {
95 pub total_codecs: usize,
97 pub total_bytes_processed: u64,
99 pub best_ratio_codec: Option<CompressionCodec>,
101 pub fastest_codec: Option<CompressionCodec>,
103}
104
105pub struct StorageCompressionRegistry {
131 pub profiles: HashMap<CompressionCodec, CodecProfile>,
133}
134
135impl StorageCompressionRegistry {
136 pub fn new() -> Self {
148 let defaults: &[(CompressionCodec, f64, u64, u64)] = &[
149 (CompressionCodec::Zstd, 0.40, 500, 100),
150 (CompressionCodec::Lz4, 0.60, 50, 30),
151 (CompressionCodec::Snappy, 0.70, 100, 50),
152 (CompressionCodec::Brotli, 0.35, 2000, 100),
153 (CompressionCodec::None, 1.00, 1, 1),
154 ];
155
156 let profiles = defaults
157 .iter()
158 .map(
159 |&(codec, avg_ratio, avg_compress_micros, avg_decompress_micros)| {
160 (
161 codec,
162 CodecProfile {
163 codec,
164 avg_ratio,
165 avg_compress_micros,
166 avg_decompress_micros,
167 total_bytes_processed: 0,
168 uses: 0,
169 },
170 )
171 },
172 )
173 .collect();
174
175 Self { profiles }
176 }
177
178 pub fn record_usage(
195 &mut self,
196 codec: CompressionCodec,
197 original_bytes: u64,
198 compressed_bytes: u64,
199 compress_micros: u64,
200 decompress_micros: u64,
201 ) {
202 let new_ratio = if original_bytes == 0 {
203 1.0
204 } else {
205 compressed_bytes as f64 / original_bytes as f64
206 };
207
208 let profile = self.profiles.entry(codec).or_insert_with(|| CodecProfile {
209 codec,
210 avg_ratio: new_ratio,
211 avg_compress_micros: compress_micros,
212 avg_decompress_micros: decompress_micros,
213 total_bytes_processed: 0,
214 uses: 0,
215 });
216
217 if profile.uses == 0 {
218 profile.avg_ratio = new_ratio;
220 profile.avg_compress_micros = compress_micros;
221 profile.avg_decompress_micros = decompress_micros;
222 } else {
223 profile.avg_ratio = 0.9 * profile.avg_ratio + 0.1 * new_ratio;
225 profile.avg_compress_micros = (0.9 * profile.avg_compress_micros as f64
226 + 0.1 * compress_micros as f64)
227 .round() as u64;
228 profile.avg_decompress_micros = (0.9 * profile.avg_decompress_micros as f64
229 + 0.1 * decompress_micros as f64)
230 .round() as u64;
231 }
232
233 profile.uses += 1;
234 profile.total_bytes_processed += original_bytes;
235 }
236
237 pub fn recommend(&self, data: &DataCharacteristics) -> CodecRecommendation {
247 if data.is_already_compressed {
248 return CodecRecommendation {
249 codec: CompressionCodec::None,
250 reason: "already compressed".to_string(),
251 };
252 }
253
254 if data.size_bytes < 256 {
255 return CodecRecommendation {
256 codec: CompressionCodec::None,
257 reason: "too small".to_string(),
258 };
259 }
260
261 if data.latency_sensitive {
262 return CodecRecommendation {
263 codec: CompressionCodec::Lz4,
264 reason: "latency optimized".to_string(),
265 };
266 }
267
268 if data.is_text {
269 return CodecRecommendation {
270 codec: CompressionCodec::Brotli,
271 reason: "text compression".to_string(),
272 };
273 }
274
275 let best = [
277 CompressionCodec::Zstd,
278 CompressionCodec::Lz4,
279 CompressionCodec::Snappy,
280 CompressionCodec::Brotli,
281 ]
282 .iter()
283 .filter_map(|c| self.profiles.get(c).map(|p| (c, p.efficiency_score())))
284 .max_by(|a, b| a.1.partial_cmp(&b.1).unwrap_or(std::cmp::Ordering::Equal));
285
286 match best {
287 Some((&codec, _)) => CodecRecommendation {
288 codec,
289 reason: "best efficiency".to_string(),
290 },
291 Option::None => CodecRecommendation {
292 codec: CompressionCodec::None,
293 reason: "best efficiency".to_string(),
294 },
295 }
296 }
297
298 pub fn get_profile(&self, codec: CompressionCodec) -> Option<&CodecProfile> {
300 self.profiles.get(&codec)
301 }
302
303 pub fn stats(&self) -> CompressionRegistryStats {
305 let total_codecs = self.profiles.len();
306 let total_bytes_processed = self
307 .profiles
308 .values()
309 .map(|p| p.total_bytes_processed)
310 .sum();
311
312 let used_profiles: Vec<&CodecProfile> =
314 self.profiles.values().filter(|p| p.uses > 0).collect();
315
316 let best_ratio_codec = used_profiles
317 .iter()
318 .min_by(|a, b| {
319 a.avg_ratio
320 .partial_cmp(&b.avg_ratio)
321 .unwrap_or(std::cmp::Ordering::Equal)
322 })
323 .map(|p| p.codec);
324
325 let fastest_codec = used_profiles
326 .iter()
327 .min_by_key(|p| p.avg_compress_micros)
328 .map(|p| p.codec);
329
330 CompressionRegistryStats {
331 total_codecs,
332 total_bytes_processed,
333 best_ratio_codec,
334 fastest_codec,
335 }
336 }
337}
338
339impl Default for StorageCompressionRegistry {
340 fn default() -> Self {
341 Self::new()
342 }
343}
344
345#[cfg(test)]
350mod tests {
351 use super::*;
352
353 #[test]
356 fn test_new_creates_five_profiles() {
357 let registry = StorageCompressionRegistry::new();
358 assert_eq!(registry.profiles.len(), 5);
359 }
360
361 #[test]
362 fn test_new_contains_all_codecs() {
363 let registry = StorageCompressionRegistry::new();
364 assert!(registry.profiles.contains_key(&CompressionCodec::Zstd));
365 assert!(registry.profiles.contains_key(&CompressionCodec::Lz4));
366 assert!(registry.profiles.contains_key(&CompressionCodec::Snappy));
367 assert!(registry.profiles.contains_key(&CompressionCodec::Brotli));
368 assert!(registry.profiles.contains_key(&CompressionCodec::None));
369 }
370
371 #[test]
372 fn test_new_default_ratios() {
373 let registry = StorageCompressionRegistry::new();
374 let zstd = registry.get_profile(CompressionCodec::Zstd).unwrap();
375 let lz4 = registry.get_profile(CompressionCodec::Lz4).unwrap();
376 let snappy = registry.get_profile(CompressionCodec::Snappy).unwrap();
377 let brotli = registry.get_profile(CompressionCodec::Brotli).unwrap();
378 let none = registry.get_profile(CompressionCodec::None).unwrap();
379
380 assert!((zstd.avg_ratio - 0.40).abs() < 1e-9);
381 assert!((lz4.avg_ratio - 0.60).abs() < 1e-9);
382 assert!((snappy.avg_ratio - 0.70).abs() < 1e-9);
383 assert!((brotli.avg_ratio - 0.35).abs() < 1e-9);
384 assert!((none.avg_ratio - 1.00).abs() < 1e-9);
385 }
386
387 #[test]
388 fn test_new_default_compress_micros() {
389 let registry = StorageCompressionRegistry::new();
390 assert_eq!(
391 registry
392 .get_profile(CompressionCodec::Zstd)
393 .unwrap()
394 .avg_compress_micros,
395 500
396 );
397 assert_eq!(
398 registry
399 .get_profile(CompressionCodec::Lz4)
400 .unwrap()
401 .avg_compress_micros,
402 50
403 );
404 assert_eq!(
405 registry
406 .get_profile(CompressionCodec::Snappy)
407 .unwrap()
408 .avg_compress_micros,
409 100
410 );
411 assert_eq!(
412 registry
413 .get_profile(CompressionCodec::Brotli)
414 .unwrap()
415 .avg_compress_micros,
416 2000
417 );
418 assert_eq!(
419 registry
420 .get_profile(CompressionCodec::None)
421 .unwrap()
422 .avg_compress_micros,
423 1
424 );
425 }
426
427 #[test]
428 fn test_new_default_decompress_micros() {
429 let registry = StorageCompressionRegistry::new();
430 assert_eq!(
431 registry
432 .get_profile(CompressionCodec::Zstd)
433 .unwrap()
434 .avg_decompress_micros,
435 100
436 );
437 assert_eq!(
438 registry
439 .get_profile(CompressionCodec::Lz4)
440 .unwrap()
441 .avg_decompress_micros,
442 30
443 );
444 assert_eq!(
445 registry
446 .get_profile(CompressionCodec::Snappy)
447 .unwrap()
448 .avg_decompress_micros,
449 50
450 );
451 assert_eq!(
452 registry
453 .get_profile(CompressionCodec::Brotli)
454 .unwrap()
455 .avg_decompress_micros,
456 100
457 );
458 assert_eq!(
459 registry
460 .get_profile(CompressionCodec::None)
461 .unwrap()
462 .avg_decompress_micros,
463 1
464 );
465 }
466
467 #[test]
468 fn test_new_uses_zero() {
469 let registry = StorageCompressionRegistry::new();
470 for profile in registry.profiles.values() {
471 assert_eq!(
472 profile.uses, 0,
473 "Codec {:?} should start with uses=0",
474 profile.codec
475 );
476 }
477 }
478
479 #[test]
480 fn test_new_total_bytes_zero() {
481 let registry = StorageCompressionRegistry::new();
482 for profile in registry.profiles.values() {
483 assert_eq!(profile.total_bytes_processed, 0);
484 }
485 }
486
487 #[test]
490 fn test_record_usage_first_use_direct_ratio() {
491 let mut registry = StorageCompressionRegistry::new();
492 registry.record_usage(CompressionCodec::Zstd, 1000, 300, 400, 80);
494 let p = registry.get_profile(CompressionCodec::Zstd).unwrap();
495 assert!((p.avg_ratio - 0.30).abs() < 1e-9);
496 assert_eq!(p.avg_compress_micros, 400);
497 assert_eq!(p.avg_decompress_micros, 80);
498 assert_eq!(p.uses, 1);
499 assert_eq!(p.total_bytes_processed, 1000);
500 }
501
502 #[test]
503 fn test_record_usage_first_use_updates_uses_and_bytes() {
504 let mut registry = StorageCompressionRegistry::new();
505 registry.record_usage(CompressionCodec::Lz4, 2048, 1200, 40, 25);
506 let p = registry.get_profile(CompressionCodec::Lz4).unwrap();
507 assert_eq!(p.uses, 1);
508 assert_eq!(p.total_bytes_processed, 2048);
509 }
510
511 #[test]
514 fn test_record_usage_ema_ratio() {
515 let mut registry = StorageCompressionRegistry::new();
516 registry.record_usage(CompressionCodec::Zstd, 1000, 300, 500, 100);
518 registry.record_usage(CompressionCodec::Zstd, 1000, 200, 500, 100);
520 let p = registry.get_profile(CompressionCodec::Zstd).unwrap();
521 assert!((p.avg_ratio - 0.29).abs() < 1e-9);
522 }
523
524 #[test]
525 fn test_record_usage_ema_compress_micros() {
526 let mut registry = StorageCompressionRegistry::new();
527 registry.record_usage(CompressionCodec::Zstd, 1000, 400, 500, 100);
528 registry.record_usage(CompressionCodec::Zstd, 1000, 400, 100, 100);
530 let p = registry.get_profile(CompressionCodec::Zstd).unwrap();
531 assert_eq!(p.avg_compress_micros, 460);
532 }
533
534 #[test]
535 fn test_record_usage_ema_decompress_micros() {
536 let mut registry = StorageCompressionRegistry::new();
537 registry.record_usage(CompressionCodec::Zstd, 1000, 400, 500, 200);
538 registry.record_usage(CompressionCodec::Zstd, 1000, 400, 500, 20);
540 let p = registry.get_profile(CompressionCodec::Zstd).unwrap();
541 assert_eq!(p.avg_decompress_micros, 182);
542 }
543
544 #[test]
545 fn test_record_usage_accumulates_uses() {
546 let mut registry = StorageCompressionRegistry::new();
547 for _ in 0..5 {
548 registry.record_usage(CompressionCodec::Snappy, 512, 350, 90, 40);
549 }
550 assert_eq!(
551 registry.get_profile(CompressionCodec::Snappy).unwrap().uses,
552 5
553 );
554 }
555
556 #[test]
557 fn test_record_usage_accumulates_total_bytes() {
558 let mut registry = StorageCompressionRegistry::new();
559 registry.record_usage(CompressionCodec::Snappy, 1000, 700, 90, 40);
560 registry.record_usage(CompressionCodec::Snappy, 2000, 1400, 90, 40);
561 assert_eq!(
562 registry
563 .get_profile(CompressionCodec::Snappy)
564 .unwrap()
565 .total_bytes_processed,
566 3000
567 );
568 }
569
570 #[test]
571 fn test_record_usage_zero_original_bytes_ratio_is_one() {
572 let mut registry = StorageCompressionRegistry::new();
573 registry.record_usage(CompressionCodec::Zstd, 0, 0, 1, 1);
574 let p = registry.get_profile(CompressionCodec::Zstd).unwrap();
575 assert!((p.avg_ratio - 1.0).abs() < 1e-9);
576 }
577
578 #[test]
581 fn test_recommend_already_compressed_returns_none() {
582 let registry = StorageCompressionRegistry::new();
583 let data = DataCharacteristics {
584 size_bytes: 8192,
585 is_text: false,
586 is_already_compressed: true,
587 latency_sensitive: false,
588 };
589 let rec = registry.recommend(&data);
590 assert_eq!(rec.codec, CompressionCodec::None);
591 assert_eq!(rec.reason, "already compressed");
592 }
593
594 #[test]
595 fn test_recommend_small_data_returns_none() {
596 let registry = StorageCompressionRegistry::new();
597 let data = DataCharacteristics {
598 size_bytes: 100,
599 is_text: false,
600 is_already_compressed: false,
601 latency_sensitive: false,
602 };
603 let rec = registry.recommend(&data);
604 assert_eq!(rec.codec, CompressionCodec::None);
605 assert_eq!(rec.reason, "too small");
606 }
607
608 #[test]
609 fn test_recommend_exactly_256_bytes_not_too_small() {
610 let registry = StorageCompressionRegistry::new();
611 let data = DataCharacteristics {
612 size_bytes: 256,
613 is_text: false,
614 is_already_compressed: false,
615 latency_sensitive: false,
616 };
617 let rec = registry.recommend(&data);
618 assert_ne!(rec.reason, "too small");
620 }
621
622 #[test]
623 fn test_recommend_latency_sensitive_returns_lz4() {
624 let registry = StorageCompressionRegistry::new();
625 let data = DataCharacteristics {
626 size_bytes: 4096,
627 is_text: false,
628 is_already_compressed: false,
629 latency_sensitive: true,
630 };
631 let rec = registry.recommend(&data);
632 assert_eq!(rec.codec, CompressionCodec::Lz4);
633 assert_eq!(rec.reason, "latency optimized");
634 }
635
636 #[test]
637 fn test_recommend_text_returns_brotli() {
638 let registry = StorageCompressionRegistry::new();
639 let data = DataCharacteristics {
640 size_bytes: 4096,
641 is_text: true,
642 is_already_compressed: false,
643 latency_sensitive: false,
644 };
645 let rec = registry.recommend(&data);
646 assert_eq!(rec.codec, CompressionCodec::Brotli);
647 assert_eq!(rec.reason, "text compression");
648 }
649
650 #[test]
651 fn test_recommend_already_compressed_takes_priority_over_small() {
652 let registry = StorageCompressionRegistry::new();
653 let data = DataCharacteristics {
654 size_bytes: 10,
655 is_text: false,
656 is_already_compressed: true,
657 latency_sensitive: false,
658 };
659 let rec = registry.recommend(&data);
660 assert_eq!(rec.reason, "already compressed");
661 }
662
663 #[test]
664 fn test_recommend_best_efficiency_excludes_none() {
665 let registry = StorageCompressionRegistry::new();
666 let data = DataCharacteristics {
667 size_bytes: 4096,
668 is_text: false,
669 is_already_compressed: false,
670 latency_sensitive: false,
671 };
672 let rec = registry.recommend(&data);
673 assert_ne!(rec.codec, CompressionCodec::None);
674 assert_eq!(rec.reason, "best efficiency");
675 }
676
677 #[test]
678 fn test_recommend_best_efficiency_selects_highest_score() {
679 let mut registry = StorageCompressionRegistry::new();
681 registry.record_usage(CompressionCodec::Lz4, 10000, 100, 1, 1);
683
684 let data = DataCharacteristics {
685 size_bytes: 8192,
686 is_text: false,
687 is_already_compressed: false,
688 latency_sensitive: false,
689 };
690 let rec = registry.recommend(&data);
691 assert_eq!(rec.codec, CompressionCodec::Lz4);
693 }
694
695 #[test]
698 fn test_efficiency_score_formula() {
699 let profile = CodecProfile {
700 codec: CompressionCodec::Zstd,
701 avg_ratio: 0.4,
702 avg_compress_micros: 499,
703 avg_decompress_micros: 100,
704 total_bytes_processed: 0,
705 uses: 0,
706 };
707 let expected = 0.6 / 500.0;
709 assert!((profile.efficiency_score() - expected).abs() < 1e-12);
710 }
711
712 #[test]
713 fn test_efficiency_score_none_codec_is_zero() {
714 let profile = CodecProfile {
715 codec: CompressionCodec::None,
716 avg_ratio: 1.0,
717 avg_compress_micros: 1,
718 avg_decompress_micros: 1,
719 total_bytes_processed: 0,
720 uses: 0,
721 };
722 assert!((profile.efficiency_score() - 0.0).abs() < 1e-12);
724 }
725
726 #[test]
727 fn test_efficiency_score_higher_for_better_ratio() {
728 let good = CodecProfile {
729 codec: CompressionCodec::Brotli,
730 avg_ratio: 0.2,
731 avg_compress_micros: 1000,
732 avg_decompress_micros: 100,
733 total_bytes_processed: 0,
734 uses: 0,
735 };
736 let bad = CodecProfile {
737 codec: CompressionCodec::Snappy,
738 avg_ratio: 0.8,
739 avg_compress_micros: 1000,
740 avg_decompress_micros: 100,
741 total_bytes_processed: 0,
742 uses: 0,
743 };
744 assert!(good.efficiency_score() > bad.efficiency_score());
745 }
746
747 #[test]
750 fn test_stats_total_codecs() {
751 let registry = StorageCompressionRegistry::new();
752 assert_eq!(registry.stats().total_codecs, 5);
753 }
754
755 #[test]
756 fn test_stats_total_bytes_processed() {
757 let mut registry = StorageCompressionRegistry::new();
758 registry.record_usage(CompressionCodec::Zstd, 1000, 400, 500, 100);
759 registry.record_usage(CompressionCodec::Lz4, 2000, 1200, 50, 30);
760 assert_eq!(registry.stats().total_bytes_processed, 3000);
761 }
762
763 #[test]
764 fn test_stats_best_ratio_codec_ignores_uses_zero() {
765 let registry = StorageCompressionRegistry::new();
767 let s = registry.stats();
768 assert!(s.best_ratio_codec.is_none(), "All uses=0, expected None");
769 }
770
771 #[test]
772 fn test_stats_best_ratio_codec_after_use() {
773 let mut registry = StorageCompressionRegistry::new();
774 registry.record_usage(CompressionCodec::Brotli, 1000, 100, 2000, 100);
776 let s = registry.stats();
777 assert_eq!(s.best_ratio_codec, Some(CompressionCodec::Brotli));
778 }
779
780 #[test]
781 fn test_stats_fastest_codec_ignores_uses_zero() {
782 let registry = StorageCompressionRegistry::new();
783 let s = registry.stats();
784 assert!(s.fastest_codec.is_none(), "All uses=0, expected None");
785 }
786
787 #[test]
788 fn test_stats_fastest_codec_after_use() {
789 let mut registry = StorageCompressionRegistry::new();
790 registry.record_usage(CompressionCodec::None, 1000, 1000, 1, 1);
792 let s = registry.stats();
793 assert_eq!(s.fastest_codec, Some(CompressionCodec::None));
794 }
795
796 #[test]
797 fn test_stats_best_ratio_selects_lowest_ratio_among_used() {
798 let mut registry = StorageCompressionRegistry::new();
799 registry.record_usage(CompressionCodec::Zstd, 1000, 500, 500, 100);
801 registry.record_usage(CompressionCodec::Lz4, 1000, 400, 50, 30);
802 let s = registry.stats();
803 assert_eq!(s.best_ratio_codec, Some(CompressionCodec::Lz4));
804 }
805
806 #[test]
807 fn test_get_profile_existing_codec() {
808 let registry = StorageCompressionRegistry::new();
809 assert!(registry.get_profile(CompressionCodec::Zstd).is_some());
810 }
811
812 #[test]
813 fn test_default_impl() {
814 let registry = StorageCompressionRegistry::default();
815 assert_eq!(registry.profiles.len(), 5);
816 }
817}