Skip to main content

ipfrs_storage/
compression_registry.rs

1//! `StorageCompressionRegistry` — tracks codec registrations, usage statistics,
2//! and provides codec selection recommendations based on data characteristics.
3
4use std::collections::HashMap;
5
6// ─────────────────────────────────────────────────────────────────────────────
7// CompressionCodec
8// ─────────────────────────────────────────────────────────────────────────────
9
10/// Compression codecs supported by the registry.
11///
12/// Re-exported as `RegistryCompressionCodec` from `lib.rs` to avoid name
13/// collision with `compression_advisor::CompressionCodec`.
14#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash)]
15pub enum CompressionCodec {
16    /// Zstandard — balanced ratio and speed.
17    Zstd,
18    /// LZ4 — extremely fast, moderate ratio.
19    Lz4,
20    /// Snappy — fast with decent ratio.
21    Snappy,
22    /// Brotli — best ratio, slower speed.
23    Brotli,
24    /// No compression applied.
25    None,
26}
27
28// ─────────────────────────────────────────────────────────────────────────────
29// CodecProfile
30// ─────────────────────────────────────────────────────────────────────────────
31
32/// Runtime profile for a single compression codec, updated via EMA on each use.
33#[derive(Clone, Debug)]
34pub struct CodecProfile {
35    /// Which codec this profile belongs to.
36    pub codec: CompressionCodec,
37    /// Average compression ratio: compressed / original (lower = better).
38    pub avg_ratio: f64,
39    /// Average compression latency in microseconds.
40    pub avg_compress_micros: u64,
41    /// Average decompression latency in microseconds.
42    pub avg_decompress_micros: u64,
43    /// Total bytes processed (original sizes) across all recorded uses.
44    pub total_bytes_processed: u64,
45    /// Number of times this codec has been recorded.
46    pub uses: u64,
47}
48
49impl CodecProfile {
50    /// Efficiency score: `(1.0 - avg_ratio) / (avg_compress_micros + 1)`.
51    ///
52    /// Higher values indicate better efficiency (better ratio **and** faster).
53    pub fn efficiency_score(&self) -> f64 {
54        (1.0 - self.avg_ratio) / (self.avg_compress_micros as f64 + 1.0)
55    }
56}
57
58// ─────────────────────────────────────────────────────────────────────────────
59// DataCharacteristics
60// ─────────────────────────────────────────────────────────────────────────────
61
62/// Describes characteristics of a data block to inform codec selection.
63#[derive(Clone, Debug)]
64pub struct DataCharacteristics {
65    /// Size of the data in bytes.
66    pub size_bytes: u64,
67    /// Whether the data is primarily text (benefits from Brotli).
68    pub is_text: bool,
69    /// Whether the data is already compressed (avoid re-compressing).
70    pub is_already_compressed: bool,
71    /// Whether low latency is preferred over compression ratio.
72    pub latency_sensitive: bool,
73}
74
75// ─────────────────────────────────────────────────────────────────────────────
76// CodecRecommendation
77// ─────────────────────────────────────────────────────────────────────────────
78
79/// A recommended codec together with a human-readable rationale.
80#[derive(Clone, Debug)]
81pub struct CodecRecommendation {
82    /// The recommended codec.
83    pub codec: CompressionCodec,
84    /// Human-readable reason for this recommendation.
85    pub reason: String,
86}
87
88// ─────────────────────────────────────────────────────────────────────────────
89// CompressionRegistryStats
90// ─────────────────────────────────────────────────────────────────────────────
91
92/// Aggregate statistics across all registered codecs.
93#[derive(Clone, Debug)]
94pub struct CompressionRegistryStats {
95    /// Number of codecs tracked by this registry.
96    pub total_codecs: usize,
97    /// Total bytes processed across all codecs.
98    pub total_bytes_processed: u64,
99    /// Codec with the lowest `avg_ratio` among those with at least one use.
100    pub best_ratio_codec: Option<CompressionCodec>,
101    /// Codec with the lowest `avg_compress_micros` among those with at least one use.
102    pub fastest_codec: Option<CompressionCodec>,
103}
104
105// ─────────────────────────────────────────────────────────────────────────────
106// StorageCompressionRegistry
107// ─────────────────────────────────────────────────────────────────────────────
108
109/// Registry that tracks compression codec profiles and recommends a codec based
110/// on observed performance data and data characteristics.
111///
112/// # Example
113/// ```
114/// use ipfrs_storage::compression_registry::{
115///     StorageCompressionRegistry, CompressionCodec, DataCharacteristics,
116/// };
117///
118/// let mut registry = StorageCompressionRegistry::new();
119/// registry.record_usage(CompressionCodec::Zstd, 1024, 400, 480, 90);
120///
121/// let chars = DataCharacteristics {
122///     size_bytes: 4096,
123///     is_text: false,
124///     is_already_compressed: false,
125///     latency_sensitive: false,
126/// };
127/// let rec = registry.recommend(&chars);
128/// println!("Recommended: {:?} — {}", rec.codec, rec.reason);
129/// ```
130pub struct StorageCompressionRegistry {
131    /// Per-codec runtime profiles.
132    pub profiles: HashMap<CompressionCodec, CodecProfile>,
133}
134
135impl StorageCompressionRegistry {
136    /// Create a new registry pre-populated with default profiles for all five codecs.
137    ///
138    /// Default values are based on typical real-world measurements:
139    ///
140    /// | Codec   | avg_ratio | compress µs | decompress µs |
141    /// |---------|-----------|-------------|---------------|
142    /// | Zstd    | 0.40      | 500         | 100           |
143    /// | Lz4     | 0.60      | 50          | 30            |
144    /// | Snappy  | 0.70      | 100         | 50            |
145    /// | Brotli  | 0.35      | 2000        | 100           |
146    /// | None    | 1.00      | 1           | 1             |
147    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    /// Record a compression event and update the codec profile.
179    ///
180    /// For the **first** use (`uses == 0` before the call) the measurements are
181    /// stored directly.  For subsequent uses an exponential moving average
182    /// (α = 0.1) is applied:
183    ///
184    /// ```text
185    /// new_value = 0.9 * old_value + 0.1 * sample
186    /// ```
187    ///
188    /// # Arguments
189    /// * `codec` — the codec that was used.
190    /// * `original_bytes` — uncompressed size.
191    /// * `compressed_bytes` — size after compression.
192    /// * `compress_micros` — time taken to compress.
193    /// * `decompress_micros` — time taken to decompress.
194    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            // First use: store directly without EMA.
219            profile.avg_ratio = new_ratio;
220            profile.avg_compress_micros = compress_micros;
221            profile.avg_decompress_micros = decompress_micros;
222        } else {
223            // Subsequent uses: apply EMA with α = 0.1.
224            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    /// Recommend a compression codec for the given data characteristics.
238    ///
239    /// Decision logic (in order of priority):
240    /// 1. Already compressed → `None` ("already compressed").
241    /// 2. Very small block (< 256 B) → `None` ("too small").
242    /// 3. Latency-sensitive → `Lz4` ("latency optimized").
243    /// 4. Text data → `Brotli` ("text compression").
244    /// 5. Otherwise → codec with the best `efficiency_score` (excluding `None`)
245    ///    ("best efficiency").
246    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        // Select the codec with the highest efficiency_score, excluding None.
276        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    /// Return a reference to the profile for the given codec, if present.
299    pub fn get_profile(&self, codec: CompressionCodec) -> Option<&CodecProfile> {
300        self.profiles.get(&codec)
301    }
302
303    /// Compute aggregate statistics across all registered profiles.
304    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        // Only consider codecs that have been used at least once.
313        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// ─────────────────────────────────────────────────────────────────────────────
346// Tests
347// ─────────────────────────────────────────────────────────────────────────────
348
349#[cfg(test)]
350mod tests {
351    use super::*;
352
353    // ── new() ─────────────────────────────────────────────────────────────────
354
355    #[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    // ── record_usage — first use (direct set) ─────────────────────────────────
488
489    #[test]
490    fn test_record_usage_first_use_direct_ratio() {
491        let mut registry = StorageCompressionRegistry::new();
492        // Override default; first use should set directly.
493        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    // ── record_usage — EMA update ─────────────────────────────────────────────
512
513    #[test]
514    fn test_record_usage_ema_ratio() {
515        let mut registry = StorageCompressionRegistry::new();
516        // First use: sets ratio to 0.30
517        registry.record_usage(CompressionCodec::Zstd, 1000, 300, 500, 100);
518        // Second use: ratio = 0.20, EMA -> 0.9*0.30 + 0.1*0.20 = 0.29
519        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        // Second: micros=100, EMA -> 0.9*500 + 0.1*100 = 460
529        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        // Second: decompress=20, EMA -> 0.9*200 + 0.1*20 = 182
539        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    // ── recommend ─────────────────────────────────────────────────────────────
579
580    #[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        // Should not be "too small" — falls through to efficiency selection.
619        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        // Manipulate profiles so Lz4 has an artificially great efficiency score.
680        let mut registry = StorageCompressionRegistry::new();
681        // First use sets directly — give Lz4 a very low ratio and very fast time.
682        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        // Lz4 should win with ratio=0.01, compress_micros=1 → score=(1-0.01)/(1+1)=0.495
692        assert_eq!(rec.codec, CompressionCodec::Lz4);
693    }
694
695    // ── efficiency_score ─────────────────────────────────────────────────────
696
697    #[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        // (1.0 - 0.4) / (499.0 + 1.0) = 0.6 / 500.0 = 0.0012
708        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        // (1.0 - 1.0) / 2.0 = 0.0
723        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    // ── stats ─────────────────────────────────────────────────────────────────
748
749    #[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        // No codec has been used yet → best_ratio_codec should be None.
766        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        // Give Brotli a use with a great ratio.
775        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        // Give None codec a use (compress_micros=1).
791        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        // Use Zstd with ratio 0.50 and Lz4 with ratio 0.40.
800        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}