Skip to main content

oxirs_tdb/compression/
adaptive_selection.rs

1//! Adaptive compression algorithm selection
2//!
3//! This module automatically selects the best compression algorithm based on
4//! runtime performance metrics, data characteristics, and compression goals.
5
6use super::AdvancedCompressionType;
7use std::sync::atomic::{AtomicU64, Ordering};
8use std::sync::Arc;
9use std::time::{Duration, Instant};
10
11/// Simplified compression type for adaptive selection
12#[derive(Debug, Clone, Copy, PartialEq, Eq)]
13pub enum CompressionType {
14    /// No compression
15    None,
16    /// LZ4 compression (very fast)
17    Lz4,
18    /// Zstandard compression (excellent ratio)
19    Zstd,
20    /// Snappy compression (fast with good ratio)
21    Snappy,
22    /// Brotli compression (high compression ratio)
23    Brotli,
24    /// Zlib/DEFLATE compression
25    Zlib,
26}
27
28impl From<CompressionType> for AdvancedCompressionType {
29    fn from(ct: CompressionType) -> Self {
30        match ct {
31            CompressionType::None => AdvancedCompressionType::RunLength, // Fallback
32            CompressionType::Lz4 => AdvancedCompressionType::Lz4,
33            CompressionType::Zstd => AdvancedCompressionType::Zstd,
34            CompressionType::Snappy => AdvancedCompressionType::Snappy,
35            CompressionType::Brotli => AdvancedCompressionType::Brotli,
36            CompressionType::Zlib => AdvancedCompressionType::RunLength, // Fallback
37        }
38    }
39}
40
41/// Compression selection strategy
42#[derive(Debug, Clone, Copy, PartialEq, Eq)]
43pub enum SelectionStrategy {
44    /// Maximize compression ratio (smallest size)
45    MaximizeRatio,
46    /// Minimize compression time (fastest)
47    MinimizeTime,
48    /// Balance between ratio and time
49    Balanced,
50    /// Adaptive based on data size
51    Adaptive,
52}
53
54/// Compression performance metrics for an algorithm
55#[derive(Debug)]
56pub struct AlgorithmMetrics {
57    /// Total bytes compressed
58    pub bytes_compressed: AtomicU64,
59    /// Total compression time (microseconds)
60    pub total_time_us: AtomicU64,
61    /// Total compressed size
62    pub total_compressed_size: AtomicU64,
63    /// Number of compression operations
64    pub operations: AtomicU64,
65}
66
67impl Default for AlgorithmMetrics {
68    fn default() -> Self {
69        Self {
70            bytes_compressed: AtomicU64::new(0),
71            total_time_us: AtomicU64::new(0),
72            total_compressed_size: AtomicU64::new(0),
73            operations: AtomicU64::new(0),
74        }
75    }
76}
77
78impl AlgorithmMetrics {
79    /// Record a compression operation
80    pub fn record(&self, input_size: usize, compressed_size: usize, duration: Duration) {
81        self.bytes_compressed
82            .fetch_add(input_size as u64, Ordering::Relaxed);
83        self.total_compressed_size
84            .fetch_add(compressed_size as u64, Ordering::Relaxed);
85        self.total_time_us
86            .fetch_add(duration.as_micros() as u64, Ordering::Relaxed);
87        self.operations.fetch_add(1, Ordering::Relaxed);
88    }
89
90    /// Get average compression ratio (original_size / compressed_size)
91    pub fn avg_ratio(&self) -> f64 {
92        let total_in = self.bytes_compressed.load(Ordering::Relaxed) as f64;
93        let total_out = self.total_compressed_size.load(Ordering::Relaxed) as f64;
94        if total_out == 0.0 {
95            1.0
96        } else {
97            total_in / total_out
98        }
99    }
100
101    /// Get average compression throughput (MB/s)
102    pub fn avg_throughput_mbps(&self) -> f64 {
103        let bytes = self.bytes_compressed.load(Ordering::Relaxed) as f64;
104        let time_s = self.total_time_us.load(Ordering::Relaxed) as f64 / 1_000_000.0;
105        if time_s == 0.0 {
106            0.0
107        } else {
108            (bytes / (1024.0 * 1024.0)) / time_s
109        }
110    }
111
112    /// Get average time per operation (microseconds)
113    pub fn avg_time_us(&self) -> f64 {
114        let ops = self.operations.load(Ordering::Relaxed);
115        if ops == 0 {
116            0.0
117        } else {
118            self.total_time_us.load(Ordering::Relaxed) as f64 / ops as f64
119        }
120    }
121}
122
123/// Adaptive compression selector
124pub struct AdaptiveCompressionSelector {
125    /// Selection strategy
126    strategy: SelectionStrategy,
127    /// Metrics for each compression algorithm
128    metrics: Vec<(AdvancedCompressionType, Arc<AlgorithmMetrics>)>,
129    /// Minimum data size to enable compression
130    min_compression_size: usize,
131}
132
133impl AdaptiveCompressionSelector {
134    /// Create a new adaptive selector
135    pub fn new(strategy: SelectionStrategy) -> Self {
136        let compression_types = vec![
137            AdvancedCompressionType::Lz4,
138            AdvancedCompressionType::Zstd,
139            AdvancedCompressionType::Snappy,
140            AdvancedCompressionType::Brotli,
141            AdvancedCompressionType::RunLength, // Using RunLength as Zlib alternative
142        ];
143
144        let metrics = compression_types
145            .into_iter()
146            .map(|ctype| (ctype, Arc::new(AlgorithmMetrics::default())))
147            .collect();
148
149        Self {
150            strategy,
151            metrics,
152            min_compression_size: 128, // Don't compress data < 128 bytes
153        }
154    }
155
156    /// Select the best compression algorithm for the given data
157    pub fn select(&self, data_size: usize) -> AdvancedCompressionType {
158        // Don't compress small data - use RunLength as "no compression" fallback
159        if data_size < self.min_compression_size {
160            return AdvancedCompressionType::RunLength;
161        }
162
163        match self.strategy {
164            SelectionStrategy::MaximizeRatio => self.select_max_ratio(),
165            SelectionStrategy::MinimizeTime => self.select_min_time(),
166            SelectionStrategy::Balanced => self.select_balanced(),
167            SelectionStrategy::Adaptive => self.select_adaptive(data_size),
168        }
169    }
170
171    /// Select algorithm with best compression ratio
172    fn select_max_ratio(&self) -> AdvancedCompressionType {
173        self.metrics
174            .iter()
175            .max_by(|(_, a), (_, b)| {
176                a.avg_ratio()
177                    .partial_cmp(&b.avg_ratio())
178                    .unwrap_or(std::cmp::Ordering::Equal)
179            })
180            .map(|(ctype, _)| *ctype)
181            .unwrap_or(AdvancedCompressionType::Zstd)
182    }
183
184    /// Select algorithm with best throughput
185    fn select_min_time(&self) -> AdvancedCompressionType {
186        self.metrics
187            .iter()
188            .max_by(|(_, a), (_, b)| {
189                a.avg_throughput_mbps()
190                    .partial_cmp(&b.avg_throughput_mbps())
191                    .unwrap_or(std::cmp::Ordering::Equal)
192            })
193            .map(|(ctype, _)| *ctype)
194            .unwrap_or(AdvancedCompressionType::Lz4)
195    }
196
197    /// Select algorithm with balanced ratio and speed
198    fn select_balanced(&self) -> AdvancedCompressionType {
199        self.metrics
200            .iter()
201            .max_by(|(_, a), (_, b)| {
202                let score_a = a.avg_ratio() * a.avg_throughput_mbps().sqrt();
203                let score_b = b.avg_ratio() * b.avg_throughput_mbps().sqrt();
204                score_a
205                    .partial_cmp(&score_b)
206                    .unwrap_or(std::cmp::Ordering::Equal)
207            })
208            .map(|(ctype, _)| *ctype)
209            .unwrap_or(AdvancedCompressionType::Snappy)
210    }
211
212    /// Select algorithm based on data size
213    fn select_adaptive(&self, data_size: usize) -> AdvancedCompressionType {
214        match data_size {
215            0..=1024 => AdvancedCompressionType::Lz4, // Small: fast compression
216            1025..=10240 => AdvancedCompressionType::Snappy, // Medium: balanced
217            10241..=102400 => AdvancedCompressionType::Zstd, // Large: good ratio
218            _ => AdvancedCompressionType::Zstd,       // Very large: best ratio
219        }
220    }
221
222    /// Record compression metrics
223    pub fn record_compression(
224        &self,
225        compression_type: AdvancedCompressionType,
226        input_size: usize,
227        compressed_size: usize,
228        duration: Duration,
229    ) {
230        if let Some((_, metrics)) = self.metrics.iter().find(|(ct, _)| *ct == compression_type) {
231            metrics.record(input_size, compressed_size, duration);
232        }
233    }
234
235    /// Get metrics for an algorithm
236    pub fn get_metrics(
237        &self,
238        compression_type: AdvancedCompressionType,
239    ) -> Option<Arc<AlgorithmMetrics>> {
240        self.metrics
241            .iter()
242            .find(|(ct, _)| *ct == compression_type)
243            .map(|(_, m)| Arc::clone(m))
244    }
245
246    /// Get comprehensive statistics
247    pub fn stats(&self) -> AdaptiveCompressionStats {
248        let algorithm_stats = self
249            .metrics
250            .iter()
251            .map(|(ctype, metrics)| AlgorithmStats {
252                compression_type: *ctype,
253                avg_ratio: metrics.avg_ratio(),
254                avg_throughput_mbps: metrics.avg_throughput_mbps(),
255                operations: metrics.operations.load(Ordering::Relaxed),
256                total_bytes: metrics.bytes_compressed.load(Ordering::Relaxed),
257            })
258            .collect();
259
260        AdaptiveCompressionStats {
261            strategy: self.strategy,
262            algorithm_stats,
263        }
264    }
265}
266
267/// Statistics for adaptive compression
268#[derive(Debug)]
269pub struct AdaptiveCompressionStats {
270    /// Current selection strategy
271    pub strategy: SelectionStrategy,
272    /// Per-algorithm statistics
273    pub algorithm_stats: Vec<AlgorithmStats>,
274}
275
276/// Statistics for a single algorithm
277#[derive(Debug)]
278pub struct AlgorithmStats {
279    /// Compression type
280    pub compression_type: AdvancedCompressionType,
281    /// Average compression ratio
282    pub avg_ratio: f64,
283    /// Average throughput (MB/s)
284    pub avg_throughput_mbps: f64,
285    /// Number of operations
286    pub operations: u64,
287    /// Total bytes compressed
288    pub total_bytes: u64,
289}
290
291#[cfg(test)]
292mod tests {
293    use super::*;
294
295    #[test]
296    fn test_algorithm_metrics() {
297        let metrics = AlgorithmMetrics::default();
298
299        metrics.record(1000, 500, Duration::from_micros(100));
300        metrics.record(2000, 800, Duration::from_micros(200));
301
302        assert_eq!(metrics.operations.load(Ordering::Relaxed), 2);
303        assert_eq!(metrics.bytes_compressed.load(Ordering::Relaxed), 3000);
304        assert_eq!(metrics.total_compressed_size.load(Ordering::Relaxed), 1300);
305
306        // Ratio = 3000 / 1300 ≈ 2.31
307        assert!((metrics.avg_ratio() - 2.31).abs() < 0.01);
308
309        // Avg time = 300 / 2 = 150 us
310        assert_eq!(metrics.avg_time_us(), 150.0);
311    }
312
313    #[test]
314    fn test_adaptive_selector_creation() {
315        let selector = AdaptiveCompressionSelector::new(SelectionStrategy::Balanced);
316
317        assert_eq!(selector.strategy, SelectionStrategy::Balanced);
318        assert_eq!(selector.metrics.len(), 5); // 5 compression types
319    }
320
321    #[test]
322    fn test_small_data_not_compressed() {
323        let selector = AdaptiveCompressionSelector::new(SelectionStrategy::MaximizeRatio);
324
325        let selected = selector.select(50); // Small data
326        assert_eq!(selected, AdvancedCompressionType::RunLength); // Fallback for small data
327    }
328
329    #[test]
330    fn test_adaptive_selection_by_size() {
331        let selector = AdaptiveCompressionSelector::new(SelectionStrategy::Adaptive);
332
333        // Small data: Lz4
334        assert_eq!(selector.select(500), AdvancedCompressionType::Lz4);
335
336        // Medium data: Snappy
337        assert_eq!(selector.select(5000), AdvancedCompressionType::Snappy);
338
339        // Large data: Zstd
340        assert_eq!(selector.select(50000), AdvancedCompressionType::Zstd);
341
342        // Very large data: Zstd
343        assert_eq!(selector.select(500000), AdvancedCompressionType::Zstd);
344    }
345
346    #[test]
347    fn test_record_and_select() {
348        let selector = AdaptiveCompressionSelector::new(SelectionStrategy::MaximizeRatio);
349
350        // Record some operations with different ratios
351        selector.record_compression(
352            AdvancedCompressionType::Lz4,
353            1000,
354            800,
355            Duration::from_micros(50),
356        );
357        selector.record_compression(
358            AdvancedCompressionType::Zstd,
359            1000,
360            400,
361            Duration::from_micros(200),
362        );
363
364        // Zstd has better ratio (1000/400 = 2.5 vs 1000/800 = 1.25)
365        let selected = selector.select_max_ratio();
366        assert_eq!(selected, AdvancedCompressionType::Zstd);
367    }
368
369    #[test]
370    fn test_min_time_selection() {
371        let selector = AdaptiveCompressionSelector::new(SelectionStrategy::MinimizeTime);
372
373        // Record operations with different speeds
374        selector.record_compression(
375            AdvancedCompressionType::Lz4,
376            10000,
377            5000,
378            Duration::from_micros(100), // Fast
379        );
380        selector.record_compression(
381            AdvancedCompressionType::Zstd,
382            10000,
383            3000,
384            Duration::from_micros(500), // Slow but better ratio
385        );
386
387        // Should select Lz4 (faster)
388        let selected = selector.select_min_time();
389        assert_eq!(selected, AdvancedCompressionType::Lz4);
390    }
391
392    #[test]
393    fn test_get_metrics() {
394        let selector = AdaptiveCompressionSelector::new(SelectionStrategy::Balanced);
395
396        selector.record_compression(
397            AdvancedCompressionType::Lz4,
398            1000,
399            500,
400            Duration::from_micros(100),
401        );
402
403        let metrics = selector.get_metrics(AdvancedCompressionType::Lz4).unwrap();
404        assert_eq!(metrics.operations.load(Ordering::Relaxed), 1);
405    }
406
407    #[test]
408    fn test_stats() {
409        let selector = AdaptiveCompressionSelector::new(SelectionStrategy::Balanced);
410
411        selector.record_compression(
412            AdvancedCompressionType::Lz4,
413            1000,
414            500,
415            Duration::from_micros(100),
416        );
417        selector.record_compression(
418            AdvancedCompressionType::Zstd,
419            2000,
420            600,
421            Duration::from_micros(300),
422        );
423
424        let stats = selector.stats();
425        assert_eq!(stats.strategy, SelectionStrategy::Balanced);
426        assert!(stats.algorithm_stats.len() >= 2);
427
428        // Find Lz4 stats
429        let lz4_stats = stats
430            .algorithm_stats
431            .iter()
432            .find(|s| s.compression_type == AdvancedCompressionType::Lz4)
433            .unwrap();
434
435        assert_eq!(lz4_stats.operations, 1);
436        assert_eq!(lz4_stats.total_bytes, 1000);
437    }
438}