1use runmat_time::Instant;
7use std::collections::HashMap;
8#[cfg(all(feature = "compression", not(target_arch = "wasm32")))]
9use std::convert::TryFrom;
10#[cfg(all(feature = "compression", target_arch = "wasm32"))]
11use std::io::{Cursor, Read};
12
13use crate::format::{CompressionAlgorithm, CompressionInfo};
14use crate::{SnapshotError, SnapshotResult};
15#[cfg(all(feature = "compression", target_arch = "wasm32"))]
16use ruzstd::decoding::StreamingDecoder;
17
18pub struct CompressionEngine {
20 config: CompressionConfig,
22
23 stats: CompressionStats,
25}
26
27#[derive(Debug, Clone)]
29pub struct CompressionConfig {
30 pub default_level: u32,
32
33 pub adaptive_selection: bool,
35
36 pub size_threshold: usize,
38
39 pub target_ratio: f64,
41
42 pub max_compression_time: std::time::Duration,
44
45 pub prefer_speed: bool,
47}
48
49#[derive(Debug, Clone, Default)]
51pub struct CompressionStats {
52 pub attempts: HashMap<String, u64>,
54
55 pub ratios: HashMap<String, f64>,
57
58 pub times: HashMap<String, std::time::Duration>,
60
61 pub total_bytes: u64,
63
64 pub total_time: std::time::Duration,
66}
67
68#[derive(Debug)]
70pub struct CompressionResult {
71 pub data: Vec<u8>,
73
74 pub info: CompressionInfo,
76
77 pub metrics: CompressionMetrics,
79}
80
81#[derive(Debug)]
83pub struct CompressionMetrics {
84 pub compression_time: std::time::Duration,
86
87 pub compression_ratio: f64,
89
90 pub throughput: f64,
92
93 pub memory_usage: usize,
95}
96
97impl CompressionEngine {
98 pub fn new(config: CompressionConfig) -> Self {
100 Self {
101 config,
102 stats: CompressionStats::default(),
103 }
104 }
105
106 pub fn compress(&mut self, data: &[u8]) -> SnapshotResult<CompressionResult> {
108 if data.len() < self.config.size_threshold {
110 return Ok(CompressionResult {
111 data: data.to_vec(),
112 info: CompressionInfo {
113 algorithm: CompressionAlgorithm::None,
114 level: 0,
115 parameters: HashMap::new(),
116 },
117 metrics: CompressionMetrics {
118 compression_time: std::time::Duration::ZERO,
119 compression_ratio: 1.0,
120 throughput: 0.0,
121 memory_usage: data.len(),
122 },
123 });
124 }
125
126 let algorithm = if self.config.adaptive_selection {
127 self.select_optimal_algorithm(data)?
128 } else {
129 CompressionAlgorithm::Lz4 {
130 fast: self.config.prefer_speed,
131 }
132 };
133
134 self.compress_with_algorithm(data, algorithm)
135 }
136
137 pub fn decompress(&self, data: &[u8], info: &CompressionInfo) -> SnapshotResult<Vec<u8>> {
139 let start = Instant::now();
140
141 let result = match &info.algorithm {
142 CompressionAlgorithm::None => data.to_vec(),
143
144 #[cfg(feature = "compression")]
145 CompressionAlgorithm::Lz4 { .. } => {
146 let original_size = info
148 .parameters
149 .get("original_size")
150 .and_then(|s| s.parse::<usize>().ok())
151 .unwrap_or_else(|| {
152 data.len() * 4
154 });
155
156 decompress_lz4_block(data, original_size)?
157 }
158
159 #[cfg(feature = "compression")]
160 CompressionAlgorithm::Zstd { dictionary } => {
161 if dictionary.is_some() {
162 log::debug!(
164 "Dictionary decompression not available, using standard decompression"
165 );
166 }
167
168 decompress_zstd_block(data)?
169 }
170
171 #[cfg(not(feature = "compression"))]
172 _ => {
173 return Err(SnapshotError::Configuration {
174 message: "Compression feature not enabled".to_string(),
175 });
176 }
177 };
178
179 let duration = start.elapsed();
180 log::debug!(
181 "Decompressed {} bytes to {} bytes in {:?}",
182 data.len(),
183 result.len(),
184 duration
185 );
186
187 Ok(result)
188 }
189
190 pub fn select_optimal_algorithm(&self, data: &[u8]) -> SnapshotResult<CompressionAlgorithm> {
192 let characteristics = self.analyze_data(data);
194
195 if characteristics.entropy > 0.9 {
197 return Ok(CompressionAlgorithm::None);
199 }
200
201 if self.config.prefer_speed {
202 Ok(CompressionAlgorithm::Lz4 { fast: true })
203 } else if characteristics.repetition_ratio > 0.7 {
204 Ok(CompressionAlgorithm::Zstd { dictionary: None })
206 } else {
207 Ok(CompressionAlgorithm::Lz4 { fast: false })
209 }
210 }
211
212 pub fn compress_with_algorithm(
214 &mut self,
215 data: &[u8],
216 algorithm: CompressionAlgorithm,
217 ) -> SnapshotResult<CompressionResult> {
218 let start = Instant::now();
219 let start_memory = self.estimate_memory_usage();
220
221 let (compressed_data, algorithm_used) = match algorithm {
222 CompressionAlgorithm::None => (data.to_vec(), algorithm),
223
224 #[cfg(feature = "compression")]
225 CompressionAlgorithm::Lz4 { fast } => {
226 let compressed = compress_lz4_block(data, fast)?;
227 (compressed, CompressionAlgorithm::Lz4 { fast })
228 }
229
230 #[cfg(feature = "compression")]
231 CompressionAlgorithm::Zstd { dictionary } => {
232 let level = if self.config.prefer_speed {
233 1
234 } else {
235 self.config.default_level as i32
236 };
237
238 if dictionary.is_some() {
239 log::debug!("Dictionary compression not available, using standard compression");
241 }
242
243 let compressed = compress_zstd_block(data, level)?;
244
245 (compressed, CompressionAlgorithm::Zstd { dictionary })
246 }
247
248 #[cfg(not(feature = "compression"))]
249 _ => {
250 return Err(SnapshotError::Configuration {
251 message: "Compression feature not enabled".to_string(),
252 });
253 }
254 };
255
256 let compression_time = start.elapsed();
257 let end_memory = self.estimate_memory_usage();
258
259 let compression_ratio = compressed_data.len() as f64 / data.len() as f64;
261 if compression_ratio > 0.95 && !matches!(algorithm_used, CompressionAlgorithm::None) {
262 log::debug!(
264 "Compression ratio {compression_ratio:.3} not effective, using uncompressed data"
265 );
266 return Ok(CompressionResult {
267 data: data.to_vec(),
268 info: CompressionInfo {
269 algorithm: CompressionAlgorithm::None,
270 level: 0,
271 parameters: HashMap::new(),
272 },
273 metrics: CompressionMetrics {
274 compression_time,
275 compression_ratio: 1.0,
276 throughput: data.len() as f64 / compression_time.as_secs_f64(),
277 memory_usage: end_memory.saturating_sub(start_memory),
278 },
279 });
280 }
281
282 self.update_stats(
284 &algorithm_used,
285 data.len(),
286 compressed_data.len(),
287 compression_time,
288 );
289
290 let metrics = CompressionMetrics {
291 compression_time,
292 compression_ratio,
293 throughput: data.len() as f64 / compression_time.as_secs_f64(),
294 memory_usage: end_memory.saturating_sub(start_memory),
295 };
296
297 let mut parameters = HashMap::new();
298 parameters.insert("original_size".to_string(), data.len().to_string());
299 parameters.insert(
300 "compressed_size".to_string(),
301 compressed_data.len().to_string(),
302 );
303
304 Ok(CompressionResult {
305 data: compressed_data,
306 info: CompressionInfo {
307 algorithm: algorithm_used,
308 level: self.config.default_level,
309 parameters,
310 },
311 metrics,
312 })
313 }
314
315 pub fn analyze_data(&self, data: &[u8]) -> DataCharacteristics {
317 let mut characteristics = DataCharacteristics {
318 entropy: 0.0,
319 repetition_ratio: 0.0,
320 pattern_density: 0.0,
321 ascii_ratio: 0.0,
322 };
323
324 if data.is_empty() {
325 return characteristics;
326 }
327
328 let mut byte_counts = [0u32; 256];
330 let mut ascii_count = 0;
331
332 for &byte in data {
333 byte_counts[byte as usize] += 1;
334 if byte.is_ascii() {
335 ascii_count += 1;
336 }
337 }
338
339 let len = data.len() as f64;
340 let mut entropy = 0.0;
341 for &count in &byte_counts {
342 if count > 0 {
343 let p = count as f64 / len;
344 entropy -= p * p.log2();
345 }
346 }
347 characteristics.entropy = entropy / 8.0; characteristics.ascii_ratio = ascii_count as f64 / len;
349
350 let mut repetition_count = 0;
352 let window_size = 64.min(data.len() / 2);
353
354 if data.len() > window_size * 2 {
355 for i in 0..data.len() - window_size {
356 for j in (i + window_size)..data.len() - window_size {
357 if data[i..i + window_size] == data[j..j + window_size] {
358 repetition_count += 1;
359 break;
360 }
361 }
362 }
363 characteristics.repetition_ratio =
364 repetition_count as f64 / (data.len() - window_size) as f64;
365 }
366
367 characteristics
368 }
369
370 fn update_stats(
372 &mut self,
373 algorithm: &CompressionAlgorithm,
374 original_size: usize,
375 compressed_size: usize,
376 time: std::time::Duration,
377 ) {
378 let algo_name = match algorithm {
379 CompressionAlgorithm::None => "none",
380 CompressionAlgorithm::Lz4 { fast } => {
381 if *fast {
382 "lz4-fast"
383 } else {
384 "lz4"
385 }
386 }
387 CompressionAlgorithm::Zstd { .. } => "zstd",
388 };
389
390 let ratio = compressed_size as f64 / original_size as f64;
391
392 *self
393 .stats
394 .attempts
395 .entry(algo_name.to_string())
396 .or_insert(0) += 1;
397
398 let attempts = self.stats.attempts[algo_name];
400 if let Some(existing_ratio) = self.stats.ratios.get_mut(algo_name) {
401 *existing_ratio = (*existing_ratio * (attempts - 1) as f64 + ratio) / attempts as f64;
402 } else {
403 self.stats.ratios.insert(algo_name.to_string(), ratio);
404 }
405
406 if let Some(existing_time) = self.stats.times.get_mut(algo_name) {
407 *existing_time = (*existing_time * (attempts as u32 - 1) + time) / attempts as u32;
408 } else {
409 self.stats.times.insert(algo_name.to_string(), time);
410 }
411
412 self.stats.total_bytes += original_size as u64;
413 self.stats.total_time += time;
414 }
415
416 fn estimate_memory_usage(&self) -> usize {
418 std::mem::size_of::<Self>()
420 + self.stats.attempts.len() * 64
421 + self.stats.ratios.len() * 64
422 + self.stats.times.len() * 64
423 }
424
425 pub fn stats(&self) -> &CompressionStats {
427 &self.stats
428 }
429
430 pub fn reset_stats(&mut self) {
432 self.stats = CompressionStats::default();
433 }
434}
435
436#[derive(Debug)]
438pub struct DataCharacteristics {
439 pub entropy: f64,
441
442 pub repetition_ratio: f64,
444
445 pub pattern_density: f64,
447
448 pub ascii_ratio: f64,
450}
451
452impl Default for CompressionConfig {
453 fn default() -> Self {
454 Self {
455 default_level: 6,
456 adaptive_selection: true,
457 size_threshold: 1024, target_ratio: 0.7,
459 max_compression_time: std::time::Duration::from_secs(30),
460 prefer_speed: false,
461 }
462 }
463}
464
465impl CompressionStats {
466 pub fn overall_ratio(&self) -> f64 {
468 if self.ratios.is_empty() {
469 1.0
470 } else {
471 self.ratios.values().sum::<f64>() / self.ratios.len() as f64
472 }
473 }
474
475 pub fn overall_throughput(&self) -> f64 {
477 if self.total_time.as_secs_f64() > 0.0 {
478 self.total_bytes as f64 / self.total_time.as_secs_f64()
479 } else {
480 0.0
481 }
482 }
483
484 pub fn best_algorithm(&self) -> Option<String> {
486 self.ratios
487 .iter()
488 .min_by(|a, b| a.1.partial_cmp(b.1).unwrap_or(std::cmp::Ordering::Equal))
489 .map(|(name, _)| name.clone())
490 }
491}
492
493#[cfg(feature = "compression")]
494fn decompress_lz4_block(data: &[u8], original_size: usize) -> SnapshotResult<Vec<u8>> {
495 let size_hint = original_size.max(1);
496
497 #[cfg(target_arch = "wasm32")]
498 {
499 let min_uncompressed = size_hint.max(data.len().saturating_mul(4));
500 lz4_flex::block::decompress(data, min_uncompressed).map_err(|e| {
501 SnapshotError::Compression {
502 message: format!("LZ4 decompression failed: {e}"),
503 }
504 })
505 }
506 #[cfg(not(target_arch = "wasm32"))]
507 {
508 let clamped = size_hint.min(i32::MAX as usize);
509 let size_i32 = i32::try_from(clamped).unwrap_or(i32::MAX);
510 lz4::block::decompress(data, Some(size_i32)).map_err(|e| SnapshotError::Compression {
511 message: format!("LZ4 decompression failed: {e}"),
512 })
513 }
514}
515
516#[cfg(all(feature = "compression", target_arch = "wasm32"))]
517fn decompress_zstd_block(data: &[u8]) -> SnapshotResult<Vec<u8>> {
518 let cursor = Cursor::new(data);
519 let mut decoder = StreamingDecoder::new(cursor).map_err(|e| SnapshotError::Compression {
520 message: format!("ZSTD decompression failed: {e}"),
521 })?;
522 let mut output = Vec::new();
523 decoder
524 .read_to_end(&mut output)
525 .map_err(|e| SnapshotError::Compression {
526 message: format!("ZSTD decompression failed: {e}"),
527 })?;
528 Ok(output)
529}
530
531#[cfg(all(feature = "compression", not(target_arch = "wasm32")))]
532fn decompress_zstd_block(data: &[u8]) -> SnapshotResult<Vec<u8>> {
533 zstd::decode_all(data).map_err(|e| SnapshotError::Compression {
534 message: format!("ZSTD decompression failed: {e}"),
535 })
536}
537
538#[cfg(feature = "compression")]
539fn compress_lz4_block(data: &[u8], fast: bool) -> SnapshotResult<Vec<u8>> {
540 #[cfg(target_arch = "wasm32")]
541 {
542 let compressed = lz4_flex::block::compress(data);
543 if !fast {
544 log::trace!("High-compression LZ4 mode not available on wasm; using fast path");
545 }
546 Ok(compressed)
547 }
548 #[cfg(not(target_arch = "wasm32"))]
549 {
550 let result = if fast {
551 lz4::block::compress(data, None, false)
552 } else {
553 lz4::block::compress(
554 data,
555 Some(lz4::block::CompressionMode::HIGHCOMPRESSION(12)),
556 false,
557 )
558 };
559
560 result.map_err(|e| SnapshotError::Compression {
561 message: format!("LZ4 compression failed: {e}"),
562 })
563 }
564}
565
566#[cfg(all(feature = "compression", target_arch = "wasm32"))]
567fn compress_zstd_block(_data: &[u8], _level: i32) -> SnapshotResult<Vec<u8>> {
568 Err(SnapshotError::Configuration {
569 message: "ZSTD compression is not supported on wasm targets".to_string(),
570 })
571}
572
573#[cfg(all(feature = "compression", not(target_arch = "wasm32")))]
574fn compress_zstd_block(data: &[u8], level: i32) -> SnapshotResult<Vec<u8>> {
575 zstd::encode_all(data, level).map_err(|e| SnapshotError::Compression {
576 message: format!("ZSTD compression failed: {e}"),
577 })
578}
579
580#[cfg(test)]
581mod tests {
582 use super::*;
583
584 #[test]
585 fn test_compression_config_default() {
586 let config = CompressionConfig::default();
587 assert_eq!(config.default_level, 6);
588 assert!(config.adaptive_selection);
589 assert_eq!(config.size_threshold, 1024);
590 }
591
592 #[test]
593 fn test_compression_engine_creation() {
594 let config = CompressionConfig::default();
595 let engine = CompressionEngine::new(config);
596 assert_eq!(engine.stats.total_bytes, 0);
597 }
598
599 #[test]
600 fn test_small_data_compression() {
601 let mut engine = CompressionEngine::new(CompressionConfig::default());
602 let small_data = vec![1, 2, 3]; let result = engine.compress(&small_data).unwrap();
605 assert_eq!(result.data, small_data);
606 assert!(matches!(result.info.algorithm, CompressionAlgorithm::None));
607 }
608
609 #[cfg(feature = "compression")]
610 #[test]
611 fn test_lz4_compression() {
612 let mut engine = CompressionEngine::new(CompressionConfig {
613 adaptive_selection: false,
614 prefer_speed: true,
615 size_threshold: 10, ..CompressionConfig::default()
617 });
618
619 let data = b"Hello, World! This is a longer test string for LZ4 compression.".repeat(50);
620 let result = engine.compress(&data).unwrap();
621
622 assert!(
623 result.data.len() <= data.len(),
624 "Compression did not reduce size"
625 );
626
627 assert!(matches!(
629 result.info.algorithm,
630 CompressionAlgorithm::Lz4 { .. } | CompressionAlgorithm::None
631 ));
632
633 match engine.decompress(&result.data, &result.info) {
635 Ok(decompressed) => {
636 assert_eq!(decompressed, data);
637 }
638 Err(e) => {
639 if matches!(result.info.algorithm, CompressionAlgorithm::None) {
641 assert_eq!(result.data, data);
642 } else {
643 panic!("Decompression failed: {e:?}");
644 }
645 }
646 }
647 }
648
649 #[test]
650 fn test_compression_stats() {
651 let stats = CompressionStats::default();
652 assert_eq!(stats.overall_ratio(), 1.0);
653 assert_eq!(stats.overall_throughput(), 0.0);
654 assert!(stats.best_algorithm().is_none());
655 }
656
657 #[test]
658 fn test_data_characteristics() {
659 let engine = CompressionEngine::new(CompressionConfig::default());
660
661 let ascii_data = b"Hello, World!".to_vec();
663 let characteristics = engine.analyze_data(&ascii_data);
664 assert!(characteristics.ascii_ratio > 0.9);
665
666 let binary_data = vec![0u8, 1u8, 2u8, 255u8];
668 let characteristics = engine.analyze_data(&binary_data);
669 assert!(characteristics.ascii_ratio < 1.0);
670 }
671}