1use std::time::{Duration, Instant};
17
18use crate::compression::{
19 delta::DeltaEncoder, lz4::Lz4Compressor, run_length::RunLengthEncoder,
20 snappy::SnappyCompressor, zstd_compression::ZstdCompressor, CompressionAlgorithm,
21};
22
23#[derive(Debug, Clone, PartialEq, Eq, Hash)]
29pub enum CompressionAlgo {
30 Delta,
32 RunLength,
34 Lz4,
36 Zstd,
38 Snappy,
40}
41
42impl std::fmt::Display for CompressionAlgo {
43 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
44 let name = match self {
45 CompressionAlgo::Delta => "Delta",
46 CompressionAlgo::RunLength => "RunLength",
47 CompressionAlgo::Lz4 => "LZ4",
48 CompressionAlgo::Zstd => "Zstd",
49 CompressionAlgo::Snappy => "Snappy",
50 };
51 write!(f, "{}", name)
52 }
53}
54
55#[derive(Debug, Clone)]
61pub struct BenchmarkResult {
62 pub algo: CompressionAlgo,
64 pub ratio: f64,
67 pub compress_mb_s: f64,
69 pub decompress_mb_s: f64,
71 pub original_size: usize,
73 pub compressed_size: usize,
75}
76
77impl BenchmarkResult {
78 pub fn is_beneficial(&self) -> bool {
80 self.ratio < 1.0
81 }
82
83 pub fn space_savings_pct(&self) -> f64 {
85 (1.0 - self.ratio) * 100.0
86 }
87}
88
89impl std::fmt::Display for BenchmarkResult {
90 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
91 write!(
92 f,
93 "{}: ratio={:.3} compress={:.1}MB/s decompress={:.1}MB/s",
94 self.algo, self.ratio, self.compress_mb_s, self.decompress_mb_s
95 )
96 }
97}
98
99const MIN_NS: u128 = 1;
105
106fn mb_per_second(data_bytes: usize, elapsed: Duration) -> f64 {
108 let ns = elapsed.as_nanos().max(MIN_NS);
109 (data_bytes as f64 / 1_048_576.0) / (ns as f64 / 1_000_000_000.0)
110}
111
112fn unavailable_result(algo: CompressionAlgo, data: &[u8]) -> BenchmarkResult {
116 BenchmarkResult {
117 algo,
118 ratio: 1.0,
119 compress_mb_s: 0.0,
120 decompress_mb_s: 0.0,
121 original_size: data.len(),
122 compressed_size: data.len(),
123 }
124}
125
126pub struct CompressionBenchmark;
135
136impl CompressionBenchmark {
137 pub fn new() -> Self {
139 Self
140 }
141
142 pub fn benchmark_algorithm(algo: CompressionAlgo, data: &[u8]) -> BenchmarkResult {
147 if data.is_empty() {
148 return BenchmarkResult {
149 algo,
150 ratio: 1.0,
151 compress_mb_s: 0.0,
152 decompress_mb_s: 0.0,
153 original_size: 0,
154 compressed_size: 0,
155 };
156 }
157
158 match &algo {
159 CompressionAlgo::Delta => Self::bench_delta(data),
160 CompressionAlgo::RunLength => Self::bench_run_length(data),
161 CompressionAlgo::Lz4 => Self::bench_lz4(data),
162 CompressionAlgo::Zstd => Self::bench_zstd(data),
163 CompressionAlgo::Snappy => Self::bench_snappy(data),
164 }
165 }
166
167 pub fn benchmark_all(data: &[u8]) -> Vec<BenchmarkResult> {
171 vec![
172 Self::benchmark_algorithm(CompressionAlgo::Delta, data),
173 Self::benchmark_algorithm(CompressionAlgo::RunLength, data),
174 Self::benchmark_algorithm(CompressionAlgo::Lz4, data),
175 Self::benchmark_algorithm(CompressionAlgo::Zstd, data),
176 Self::benchmark_algorithm(CompressionAlgo::Snappy, data),
177 ]
178 }
179
180 pub fn best_ratio(results: &[BenchmarkResult]) -> Option<&BenchmarkResult> {
182 results.iter().min_by(|a, b| {
183 a.ratio
184 .partial_cmp(&b.ratio)
185 .unwrap_or(std::cmp::Ordering::Equal)
186 })
187 }
188
189 pub fn best_compress_speed(results: &[BenchmarkResult]) -> Option<&BenchmarkResult> {
191 results.iter().max_by(|a, b| {
192 a.compress_mb_s
193 .partial_cmp(&b.compress_mb_s)
194 .unwrap_or(std::cmp::Ordering::Equal)
195 })
196 }
197
198 pub fn best_decompress_speed(results: &[BenchmarkResult]) -> Option<&BenchmarkResult> {
200 results.iter().max_by(|a, b| {
201 a.decompress_mb_s
202 .partial_cmp(&b.decompress_mb_s)
203 .unwrap_or(std::cmp::Ordering::Equal)
204 })
205 }
206
207 fn bench_delta(data: &[u8]) -> BenchmarkResult {
210 let values: Vec<u64> = data
212 .chunks_exact(8)
213 .map(|c| u64::from_le_bytes([c[0], c[1], c[2], c[3], c[4], c[5], c[6], c[7]]))
214 .collect();
215
216 if values.is_empty() {
217 let t0 = Instant::now();
219 let enc = DeltaEncoder::encode_byte_sequence(data);
220 let compress_elapsed = t0.elapsed();
221
222 let compressed = match enc {
223 Ok(c) => c,
224 Err(_) => return unavailable_result(CompressionAlgo::Delta, data),
225 };
226
227 let t1 = Instant::now();
228 let _ = DeltaEncoder::decode_byte_sequence(&compressed);
229 let decompress_elapsed = t1.elapsed();
230
231 let ratio = compressed.len() as f64 / data.len() as f64;
232 return BenchmarkResult {
233 algo: CompressionAlgo::Delta,
234 ratio,
235 compress_mb_s: mb_per_second(data.len(), compress_elapsed),
236 decompress_mb_s: mb_per_second(data.len(), decompress_elapsed),
237 original_size: data.len(),
238 compressed_size: compressed.len(),
239 };
240 }
241
242 let t0 = Instant::now();
243 let encoded = DeltaEncoder::encode(&values);
244 let compress_elapsed = t0.elapsed();
245
246 let t1 = Instant::now();
247 let _ = DeltaEncoder::decode(&encoded);
248 let decompress_elapsed = t1.elapsed();
249
250 let ratio = encoded.len() as f64 / data.len() as f64;
251 BenchmarkResult {
252 algo: CompressionAlgo::Delta,
253 ratio,
254 compress_mb_s: mb_per_second(data.len(), compress_elapsed),
255 decompress_mb_s: mb_per_second(data.len(), decompress_elapsed),
256 original_size: data.len(),
257 compressed_size: encoded.len(),
258 }
259 }
260
261 fn bench_run_length(data: &[u8]) -> BenchmarkResult {
262 let t0 = Instant::now();
263 let enc = RunLengthEncoder::encode(data);
264 let compress_elapsed = t0.elapsed();
265
266 let compressed = match enc {
267 Ok(c) => c,
268 Err(_) => return unavailable_result(CompressionAlgo::RunLength, data),
269 };
270
271 let t1 = Instant::now();
272 let _ = RunLengthEncoder::decode(&compressed);
273 let decompress_elapsed = t1.elapsed();
274
275 let ratio = if data.is_empty() {
276 1.0
277 } else {
278 compressed.len() as f64 / data.len() as f64
279 };
280
281 BenchmarkResult {
282 algo: CompressionAlgo::RunLength,
283 ratio,
284 compress_mb_s: mb_per_second(data.len(), compress_elapsed),
285 decompress_mb_s: mb_per_second(data.len(), decompress_elapsed),
286 original_size: data.len(),
287 compressed_size: compressed.len(),
288 }
289 }
290
291 fn bench_lz4(data: &[u8]) -> BenchmarkResult {
292 let compressor = Lz4Compressor::new();
293
294 let t0 = Instant::now();
295 let compressed_data = compressor.compress(data);
296 let compress_elapsed = t0.elapsed();
297
298 let compressed_data = match compressed_data {
299 Ok(c) => c,
300 Err(_) => return unavailable_result(CompressionAlgo::Lz4, data),
301 };
302
303 let t1 = Instant::now();
304 let _ = compressor.decompress(&compressed_data);
305 let decompress_elapsed = t1.elapsed();
306
307 let ratio = compressed_data.data.len() as f64 / data.len() as f64;
308 BenchmarkResult {
309 algo: CompressionAlgo::Lz4,
310 ratio,
311 compress_mb_s: mb_per_second(data.len(), compress_elapsed),
312 decompress_mb_s: mb_per_second(data.len(), decompress_elapsed),
313 original_size: data.len(),
314 compressed_size: compressed_data.data.len(),
315 }
316 }
317
318 fn bench_zstd(data: &[u8]) -> BenchmarkResult {
319 let compressor = ZstdCompressor::new();
320
321 let t0 = Instant::now();
322 let compressed_data = compressor.compress(data);
323 let compress_elapsed = t0.elapsed();
324
325 let compressed_data = match compressed_data {
326 Ok(c) => c,
327 Err(_) => return unavailable_result(CompressionAlgo::Zstd, data),
328 };
329
330 let t1 = Instant::now();
331 let _ = compressor.decompress(&compressed_data);
332 let decompress_elapsed = t1.elapsed();
333
334 let ratio = compressed_data.data.len() as f64 / data.len() as f64;
335 BenchmarkResult {
336 algo: CompressionAlgo::Zstd,
337 ratio,
338 compress_mb_s: mb_per_second(data.len(), compress_elapsed),
339 decompress_mb_s: mb_per_second(data.len(), decompress_elapsed),
340 original_size: data.len(),
341 compressed_size: compressed_data.data.len(),
342 }
343 }
344
345 fn bench_snappy(data: &[u8]) -> BenchmarkResult {
346 let compressor = SnappyCompressor::new();
347
348 let t0 = Instant::now();
349 let compressed_data = compressor.compress(data);
350 let compress_elapsed = t0.elapsed();
351
352 let compressed_data = match compressed_data {
353 Ok(c) => c,
354 Err(_) => return unavailable_result(CompressionAlgo::Snappy, data),
355 };
356
357 let t1 = Instant::now();
358 let _ = compressor.decompress(&compressed_data);
359 let decompress_elapsed = t1.elapsed();
360
361 let ratio = compressed_data.data.len() as f64 / data.len() as f64;
362 BenchmarkResult {
363 algo: CompressionAlgo::Snappy,
364 ratio,
365 compress_mb_s: mb_per_second(data.len(), compress_elapsed),
366 decompress_mb_s: mb_per_second(data.len(), decompress_elapsed),
367 original_size: data.len(),
368 compressed_size: compressed_data.data.len(),
369 }
370 }
371}
372
373impl Default for CompressionBenchmark {
374 fn default() -> Self {
375 Self::new()
376 }
377}
378
379#[cfg(test)]
384mod tests {
385 use super::*;
386
387 fn repetitive_u64_data(count: usize) -> Vec<u8> {
389 let values: Vec<u64> = (0..count as u64).map(|i| i / 10).collect();
390 values.iter().flat_map(|v| v.to_le_bytes()).collect()
391 }
392
393 fn sorted_u64_data(count: usize) -> Vec<u8> {
395 let values: Vec<u64> = (0..count as u64).collect();
396 values.iter().flat_map(|v| v.to_le_bytes()).collect()
397 }
398
399 #[test]
402 fn test_benchmark_delta_valid_result() {
403 let data = sorted_u64_data(100);
404 let result = CompressionBenchmark::benchmark_algorithm(CompressionAlgo::Delta, &data);
405 assert_eq!(result.algo, CompressionAlgo::Delta);
406 assert!(result.original_size > 0);
407 assert!(result.ratio >= 0.0);
408 assert!(result.compress_mb_s >= 0.0);
409 assert!(result.decompress_mb_s >= 0.0);
410 }
411
412 #[test]
413 fn test_benchmark_run_length_valid_result() {
414 let data = repetitive_u64_data(100);
415 let result = CompressionBenchmark::benchmark_algorithm(CompressionAlgo::RunLength, &data);
416 assert_eq!(result.algo, CompressionAlgo::RunLength);
417 assert!(result.original_size > 0);
418 assert!(result.ratio >= 0.0);
419 }
420
421 #[test]
422 fn test_benchmark_lz4_valid_result() {
423 let data = b"hello world hello world hello world hello world".to_vec();
424 let result = CompressionBenchmark::benchmark_algorithm(CompressionAlgo::Lz4, &data);
425 assert_eq!(result.algo, CompressionAlgo::Lz4);
426 assert!(result.original_size == data.len());
427 assert!(result.ratio >= 0.0);
428 }
429
430 #[test]
431 fn test_benchmark_zstd_valid_result() {
432 let data = b"aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa".to_vec();
433 let result = CompressionBenchmark::benchmark_algorithm(CompressionAlgo::Zstd, &data);
434 assert_eq!(result.algo, CompressionAlgo::Zstd);
435 assert!(result.ratio >= 0.0);
436 }
437
438 #[test]
439 fn test_benchmark_snappy_valid_result() {
440 let data = b"snappy snappy snappy snappy snappy".to_vec();
441 let result = CompressionBenchmark::benchmark_algorithm(CompressionAlgo::Snappy, &data);
442 assert_eq!(result.algo, CompressionAlgo::Snappy);
443 assert!(result.ratio >= 0.0);
444 }
445
446 #[test]
449 fn test_benchmark_all_returns_five_results() {
450 let data = b"test data for benchmarking all algorithms".to_vec();
451 let results = CompressionBenchmark::benchmark_all(&data);
452 assert_eq!(results.len(), 5);
453 }
454
455 #[test]
456 fn test_benchmark_all_all_algos_present() {
457 let data = sorted_u64_data(50);
458 let results = CompressionBenchmark::benchmark_all(&data);
459 let algos: Vec<&CompressionAlgo> = results.iter().map(|r| &r.algo).collect();
460 assert!(algos.contains(&&CompressionAlgo::Delta));
461 assert!(algos.contains(&&CompressionAlgo::RunLength));
462 assert!(algos.contains(&&CompressionAlgo::Lz4));
463 assert!(algos.contains(&&CompressionAlgo::Zstd));
464 assert!(algos.contains(&&CompressionAlgo::Snappy));
465 }
466
467 #[test]
470 fn test_best_ratio_returns_minimum() {
471 let data = repetitive_u64_data(100);
472 let results = CompressionBenchmark::benchmark_all(&data);
473 let best = CompressionBenchmark::best_ratio(&results);
474 assert!(best.is_some());
475 let best = best.unwrap();
476 for r in &results {
478 assert!(best.ratio <= r.ratio + f64::EPSILON);
479 }
480 }
481
482 #[test]
483 fn test_best_ratio_empty_returns_none() {
484 let result = CompressionBenchmark::best_ratio(&[]);
485 assert!(result.is_none());
486 }
487
488 #[test]
491 fn test_best_compress_speed_returns_maximum() {
492 let data = sorted_u64_data(100);
493 let results = CompressionBenchmark::benchmark_all(&data);
494 let best = CompressionBenchmark::best_compress_speed(&results);
495 assert!(best.is_some());
496 let best = best.unwrap();
497 for r in &results {
498 assert!(best.compress_mb_s >= r.compress_mb_s - f64::EPSILON);
499 }
500 }
501
502 #[test]
503 fn test_best_compress_speed_empty_returns_none() {
504 let result = CompressionBenchmark::best_compress_speed(&[]);
505 assert!(result.is_none());
506 }
507
508 #[test]
511 fn test_best_decompress_speed_returns_maximum() {
512 let data = sorted_u64_data(100);
513 let results = CompressionBenchmark::benchmark_all(&data);
514 let best = CompressionBenchmark::best_decompress_speed(&results);
515 assert!(best.is_some());
516 }
517
518 #[test]
521 fn test_benchmark_result_ratio_beneficial() {
522 let r = BenchmarkResult {
523 algo: CompressionAlgo::Lz4,
524 ratio: 0.5,
525 compress_mb_s: 100.0,
526 decompress_mb_s: 200.0,
527 original_size: 1000,
528 compressed_size: 500,
529 };
530 assert!(r.is_beneficial());
531 assert!((r.space_savings_pct() - 50.0).abs() < f64::EPSILON);
532 }
533
534 #[test]
535 fn test_benchmark_result_ratio_not_beneficial() {
536 let r = BenchmarkResult {
537 algo: CompressionAlgo::RunLength,
538 ratio: 1.5,
539 compress_mb_s: 50.0,
540 decompress_mb_s: 80.0,
541 original_size: 100,
542 compressed_size: 150,
543 };
544 assert!(!r.is_beneficial());
545 assert!(r.space_savings_pct() < 0.0);
546 }
547
548 #[test]
549 fn test_benchmark_empty_data() {
550 let result = CompressionBenchmark::benchmark_algorithm(CompressionAlgo::Lz4, &[]);
551 assert_eq!(result.original_size, 0);
552 assert_eq!(result.compressed_size, 0);
553 }
554
555 #[test]
556 fn test_benchmark_result_display() {
557 let r = BenchmarkResult {
558 algo: CompressionAlgo::Zstd,
559 ratio: 0.3,
560 compress_mb_s: 120.0,
561 decompress_mb_s: 400.0,
562 original_size: 1000,
563 compressed_size: 300,
564 };
565 let s = r.to_string();
566 assert!(s.contains("Zstd"));
567 assert!(s.contains("0.300"));
568 }
569
570 #[test]
571 fn test_compression_algo_display() {
572 assert_eq!(CompressionAlgo::Delta.to_string(), "Delta");
573 assert_eq!(CompressionAlgo::RunLength.to_string(), "RunLength");
574 assert_eq!(CompressionAlgo::Lz4.to_string(), "LZ4");
575 assert_eq!(CompressionAlgo::Zstd.to_string(), "Zstd");
576 assert_eq!(CompressionAlgo::Snappy.to_string(), "Snappy");
577 }
578
579 #[test]
580 fn test_delta_compresses_sorted_ids_efficiently() {
581 let data = sorted_u64_data(200);
583 let result = CompressionBenchmark::benchmark_algorithm(CompressionAlgo::Delta, &data);
584 assert!(
586 result.ratio < 0.5,
587 "Expected ratio < 0.5, got {}",
588 result.ratio
589 );
590 }
591
592 #[test]
593 fn test_rle_compresses_repetitive_ids_efficiently() {
594 let data = vec![42u8; 1000];
596 let result = CompressionBenchmark::benchmark_algorithm(CompressionAlgo::RunLength, &data);
597 assert!(
598 result.ratio < 0.1,
599 "Expected ratio < 0.1 for repetitive data, got {}",
600 result.ratio
601 );
602 }
603}