1use crate::error::{OptimError, Result};
2use scirs2_core::ndarray::{Array, Dimension, ScalarOperand};
3use scirs2_core::numeric::Float;
4use scirs2_core::random::thread_rng;
5use std::fmt::Debug;
6
7#[derive(Debug, Clone, PartialEq)]
9pub enum CompressionStrategy {
10 None,
12 TopK {
14 k: usize,
16 },
17 RandomK {
19 k: usize,
21 },
22 Threshold {
24 threshold: f64,
26 },
27 Quantization {
29 bits: u8,
31 },
32 ErrorFeedback {
34 base_strategy: Box<CompressionStrategy>,
36 error_compensation: bool,
38 },
39 ClippedCompression {
41 base_strategy: Box<CompressionStrategy>,
43 clip_value: f64,
45 },
46}
47
48fn read_f64_le(bytes: &[u8]) -> Result<f64> {
52 let arr: [u8; 8] = bytes.try_into().map_err(|_| {
53 OptimError::InvalidConfig(
54 "corrupted compressed data: expected 8 bytes for an f64".to_string(),
55 )
56 })?;
57 Ok(f64::from_le_bytes(arr))
58}
59
60fn read_u32_le(bytes: &[u8]) -> Result<u32> {
63 let arr: [u8; 4] = bytes.try_into().map_err(|_| {
64 OptimError::InvalidConfig(
65 "corrupted compressed data: expected 4 bytes for a u32".to_string(),
66 )
67 })?;
68 Ok(u32::from_le_bytes(arr))
69}
70
71fn read_u16_le(bytes: &[u8]) -> Result<u16> {
74 let arr: [u8; 2] = bytes.try_into().map_err(|_| {
75 OptimError::InvalidConfig(
76 "corrupted compressed data: expected 2 bytes for a u16".to_string(),
77 )
78 })?;
79 Ok(u16::from_le_bytes(arr))
80}
81
82#[derive(Debug, Clone)]
84pub struct CompressedGradient<A: Float> {
85 pub data: Vec<u8>,
87 pub metadata: CompressionMetadata<A>,
89 pub shapes: Vec<Vec<usize>>,
91}
92
93#[derive(Debug, Clone)]
95pub struct CompressionMetadata<A: Float> {
96 pub strategy: CompressionStrategy,
98 pub compression_ratio: f64,
100 pub nnz_count: usize,
102 pub scale_factors: Vec<A>,
104 pub extra_data: Vec<u8>,
106}
107
108#[derive(Debug)]
110pub struct GradientCompressor<A: Float, D: Dimension> {
111 strategy: CompressionStrategy,
113 error_state: Option<Vec<Array<A, D>>>,
115 stats: CompressionStats,
117}
118
119impl<A: Float + ScalarOperand + Debug + Send + Sync, D: Dimension + Send + Sync>
120 GradientCompressor<A, D>
121{
122 pub fn new(strategy: CompressionStrategy) -> Self {
124 Self {
125 strategy,
126 error_state: None,
127 stats: CompressionStats::new(),
128 }
129 }
130
131 pub fn initialize_error_state(&mut self, gradientshapes: &[Array<A, D>]) {
133 self.error_state = Some(
134 gradientshapes
135 .iter()
136 .map(|g| Array::zeros(g.raw_dim()))
137 .collect(),
138 );
139 }
140
141 pub fn compress(&mut self, gradients: &[Array<A, D>]) -> Result<CompressedGradient<A>> {
143 let needs_lazy_init = matches!(
148 &self.strategy,
149 CompressionStrategy::ErrorFeedback {
150 error_compensation: true,
151 ..
152 }
153 ) && self.error_state.is_none();
154 if needs_lazy_init {
155 self.initialize_error_state(gradients);
156 }
157
158 let mut working_gradients: Vec<Array<A, D>> =
160 if let Some(ref error_state) = self.error_state {
161 gradients
162 .iter()
163 .zip(error_state.iter())
164 .map(|(grad, error)| grad + error)
165 .collect()
166 } else {
167 gradients.to_vec()
168 };
169
170 let (compressed_data, metadata) = match &self.strategy {
171 CompressionStrategy::None => self.compress_none(&working_gradients)?,
172 CompressionStrategy::TopK { k } => self.compress_topk(&working_gradients, *k)?,
173 CompressionStrategy::RandomK { k } => self.compress_randomk(&working_gradients, *k)?,
174 CompressionStrategy::Threshold { threshold } => self.compress_threshold(
175 &working_gradients,
176 A::from(*threshold).ok_or_else(|| {
177 OptimError::InvalidConfig(format!(
178 "threshold {threshold} could not be represented in the parameter type"
179 ))
180 })?,
181 )?,
182 CompressionStrategy::Quantization { bits } => {
183 self.compress_quantization(&working_gradients, *bits)?
184 }
185 CompressionStrategy::ErrorFeedback {
186 base_strategy,
187 error_compensation,
188 } => {
189 let mut temp_compressor = GradientCompressor::new((**base_strategy).clone());
191 let compressed = temp_compressor.compress(&working_gradients)?;
192
193 if *error_compensation {
196 let decompressed = temp_compressor.decompress(&compressed)?;
202 if let Some(ref mut error_state) = self.error_state {
203 for ((working, decompressed), error) in working_gradients
204 .iter()
205 .zip(decompressed.iter())
206 .zip(error_state.iter_mut())
207 {
208 *error = working - decompressed;
209 }
210 }
211 }
212
213 (compressed.data, compressed.metadata)
214 }
215 CompressionStrategy::ClippedCompression {
216 base_strategy,
217 clip_value,
218 } => {
219 let clip_val = A::from(*clip_value).ok_or_else(|| {
221 OptimError::InvalidConfig(format!(
222 "clip value {clip_value} could not be represented in the parameter type"
223 ))
224 })?;
225 for grad in &mut working_gradients {
226 grad.mapv_inplace(|x| {
227 if x > clip_val {
228 clip_val
229 } else if x < -clip_val {
230 -clip_val
231 } else {
232 x
233 }
234 });
235 }
236
237 let mut temp_compressor = GradientCompressor::new((**base_strategy).clone());
239 let compressed = temp_compressor.compress(&working_gradients)?;
240 (compressed.data, compressed.metadata)
241 }
242 };
243
244 let shapes = gradients.iter().map(|g| g.shape().to_vec()).collect();
246
247 let result = CompressedGradient {
248 data: compressed_data,
249 metadata,
250 shapes,
251 };
252
253 let original_size = self.calculate_size(gradients);
255 let compressed_size = result.data.len();
256 self.stats
257 .record_compression(original_size, compressed_size);
258
259 Ok(result)
260 }
261
262 pub fn decompress(&self, compressed: &CompressedGradient<A>) -> Result<Vec<Array<A, D>>> {
264 match &compressed.metadata.strategy {
265 CompressionStrategy::None => self.decompress_none(compressed),
266 CompressionStrategy::TopK { .. } => self.decompress_sparse(compressed),
267 CompressionStrategy::RandomK { .. } => self.decompress_sparse(compressed),
268 CompressionStrategy::Threshold { .. } => self.decompress_sparse(compressed),
269 CompressionStrategy::Quantization { bits } => {
270 self.decompress_quantization(compressed, *bits)
271 }
272 CompressionStrategy::ErrorFeedback { base_strategy, .. } => {
273 let temp_compressor = GradientCompressor::new((**base_strategy).clone());
274 temp_compressor.decompress(compressed)
275 }
276 CompressionStrategy::ClippedCompression { base_strategy, .. } => {
277 let temp_compressor = GradientCompressor::new((**base_strategy).clone());
278 temp_compressor.decompress(compressed)
279 }
280 }
281 }
282
283 fn compress_none(
285 &self,
286 gradients: &[Array<A, D>],
287 ) -> Result<(Vec<u8>, CompressionMetadata<A>)> {
288 let mut data = Vec::new();
289
290 for grad in gradients {
292 for &val in grad.iter() {
293 let bits = val.to_f64().ok_or_else(|| {
294 OptimError::InvalidConfig(
295 "gradient value could not be converted to f64 for serialization"
296 .to_string(),
297 )
298 })?;
299 data.extend_from_slice(&bits.to_le_bytes());
300 }
301 }
302
303 let metadata = CompressionMetadata {
304 strategy: CompressionStrategy::None,
305 compression_ratio: 1.0,
306 nnz_count: gradients.iter().map(|g| g.len()).sum(),
307 scale_factors: Vec::new(),
308 extra_data: Vec::new(),
309 };
310
311 Ok((data, metadata))
312 }
313
314 fn compress_topk(
316 &self,
317 gradients: &[Array<A, D>],
318 k: usize,
319 ) -> Result<(Vec<u8>, CompressionMetadata<A>)> {
320 let mut indices = Vec::new();
321 let mut values = Vec::new();
322 let mut total_elements = 0;
323
324 for (grad_idx, grad) in gradients.iter().enumerate() {
325 total_elements += grad.len();
326
327 let mut value_indices: Vec<(A, usize)> =
331 grad.iter().enumerate().map(|(i, &val)| (val, i)).collect();
332
333 value_indices.sort_by(|a, b| {
337 b.0.abs()
338 .partial_cmp(&a.0.abs())
339 .unwrap_or(std::cmp::Ordering::Equal)
340 });
341
342 let k_local = k.min(value_indices.len());
344 for &(val, orig_idx) in value_indices.iter().take(k_local) {
345 indices.push((grad_idx as u32, orig_idx as u32));
346 values.push(val);
347 }
348 }
349
350 let mut data = Vec::new();
352
353 data.extend_from_slice(&(indices.len() as u32).to_le_bytes());
355
356 for ((grad_idx, elem_idx), value) in indices.iter().zip(values.iter()) {
358 data.extend_from_slice(&grad_idx.to_le_bytes());
359 data.extend_from_slice(&elem_idx.to_le_bytes());
360 let bits = value.to_f64().ok_or_else(|| {
361 OptimError::InvalidConfig(
362 "gradient value could not be converted to f64 for serialization".to_string(),
363 )
364 })?;
365 data.extend_from_slice(&bits.to_le_bytes());
366 }
367
368 let metadata = CompressionMetadata {
369 strategy: CompressionStrategy::TopK { k },
370 compression_ratio: data.len() as f64
371 / (total_elements.max(1) * std::mem::size_of::<A>()) as f64,
372 nnz_count: indices.len(),
373 scale_factors: Vec::new(),
374 extra_data: Vec::new(),
375 };
376
377 Ok((data, metadata))
378 }
379
380 fn compress_randomk(
382 &self,
383 gradients: &[Array<A, D>],
384 k: usize,
385 ) -> Result<(Vec<u8>, CompressionMetadata<A>)> {
386 let mut indices = Vec::new();
387 let mut values = Vec::new();
388 let mut total_elements = 0;
389 let mut rng = thread_rng();
390
391 for (grad_idx, grad) in gradients.iter().enumerate() {
392 total_elements += grad.len();
393
394 let k_local = k.min(grad.len());
401 let mut selected_indices: Vec<usize> = (0..grad.len()).collect();
402 for i in 0..k_local {
403 let remaining = grad.len() - i;
404 let swap_idx = i + rng.gen_range(0..remaining);
405 selected_indices.swap(i, swap_idx);
406 }
407
408 let flat: Vec<A> = grad.iter().copied().collect();
411 for &idx in selected_indices.iter().take(k_local) {
412 indices.push((grad_idx as u32, idx as u32));
413 values.push(flat[idx]);
414 }
415 }
416
417 let mut data = Vec::new();
419 data.extend_from_slice(&(indices.len() as u32).to_le_bytes());
420
421 for ((grad_idx, elem_idx), value) in indices.iter().zip(values.iter()) {
422 data.extend_from_slice(&grad_idx.to_le_bytes());
423 data.extend_from_slice(&elem_idx.to_le_bytes());
424 let bits = value.to_f64().ok_or_else(|| {
425 OptimError::InvalidConfig(
426 "gradient value could not be converted to f64 for serialization".to_string(),
427 )
428 })?;
429 data.extend_from_slice(&bits.to_le_bytes());
430 }
431
432 let metadata = CompressionMetadata {
433 strategy: CompressionStrategy::RandomK { k },
434 compression_ratio: data.len() as f64
435 / (total_elements.max(1) * std::mem::size_of::<A>()) as f64,
436 nnz_count: indices.len(),
437 scale_factors: Vec::new(),
438 extra_data: Vec::new(),
439 };
440
441 Ok((data, metadata))
442 }
443
444 fn compress_threshold(
446 &self,
447 gradients: &[Array<A, D>],
448 threshold: A,
449 ) -> Result<(Vec<u8>, CompressionMetadata<A>)> {
450 let mut indices = Vec::new();
451 let mut values = Vec::new();
452 let mut total_elements = 0;
453
454 for (grad_idx, grad) in gradients.iter().enumerate() {
455 total_elements += grad.len();
456
457 for (elem_idx, &val) in grad.iter().enumerate() {
458 if val.abs() > threshold {
459 indices.push((grad_idx as u32, elem_idx as u32));
460 values.push(val);
461 }
462 }
463 }
464
465 let mut data = Vec::new();
467 data.extend_from_slice(&(indices.len() as u32).to_le_bytes());
468
469 for ((grad_idx, elem_idx), value) in indices.iter().zip(values.iter()) {
470 data.extend_from_slice(&grad_idx.to_le_bytes());
471 data.extend_from_slice(&elem_idx.to_le_bytes());
472 let bits = value.to_f64().ok_or_else(|| {
473 OptimError::InvalidConfig(
474 "gradient value could not be converted to f64 for serialization".to_string(),
475 )
476 })?;
477 data.extend_from_slice(&bits.to_le_bytes());
478 }
479
480 let metadata = CompressionMetadata {
481 strategy: CompressionStrategy::Threshold {
482 threshold: threshold.to_f64().ok_or_else(|| {
483 OptimError::InvalidConfig(
484 "threshold could not be converted to f64 for metadata".to_string(),
485 )
486 })?,
487 },
488 compression_ratio: data.len() as f64
489 / (total_elements.max(1) * std::mem::size_of::<A>()) as f64,
490 nnz_count: indices.len(),
491 scale_factors: Vec::new(),
492 extra_data: Vec::new(),
493 };
494
495 Ok((data, metadata))
496 }
497
498 fn compress_quantization(
500 &self,
501 gradients: &[Array<A, D>],
502 bits: u8,
503 ) -> Result<(Vec<u8>, CompressionMetadata<A>)> {
504 if bits == 0 || bits > 32 {
505 return Err(OptimError::InvalidConfig(
506 "Quantization bits must be in 1..=32".to_string(),
507 ));
508 }
509
510 let mut data = Vec::new();
511 let mut scale_factors = Vec::new();
512 let levels = (1u64 << bits) - 1;
513 let levels_a = A::from(levels).ok_or_else(|| {
514 OptimError::InvalidConfig(format!(
515 "quantization level count {levels} could not be represented in the parameter type"
516 ))
517 })?;
518
519 for grad in gradients {
520 if grad.iter().any(|v| !v.is_finite()) {
525 return Err(OptimError::InvalidConfig(
526 "gradient contains non-finite (NaN/inf) values; cannot quantize".to_string(),
527 ));
528 }
529
530 let min_val = grad.iter().fold(A::infinity(), |acc, &x| acc.min(x));
532 let max_val = grad.iter().fold(A::neg_infinity(), |acc, &x| acc.max(x));
533
534 let range = max_val - min_val;
535 let scale = if range > A::zero() {
536 range / levels_a
537 } else {
538 A::one()
539 };
540
541 scale_factors.push(scale);
542
543 for &val in grad.iter() {
547 let normalized = ((val - min_val) / scale)
548 .max(A::zero())
549 .min(levels_a)
550 .round();
551 let quantized = normalized.to_u64().unwrap_or(levels).min(levels) as u32;
552
553 match bits {
555 1..=8 => data.push(quantized as u8),
556 9..=16 => data.extend_from_slice(&(quantized as u16).to_le_bytes()),
557 17..=32 => data.extend_from_slice(&quantized.to_le_bytes()),
558 _ => unreachable!(),
559 }
560 }
561
562 let min_bits = min_val.to_f64().ok_or_else(|| {
568 OptimError::InvalidConfig(
569 "min value could not be converted to f64 for serialization".to_string(),
570 )
571 })?;
572 let scale_bits = scale.to_f64().ok_or_else(|| {
573 OptimError::InvalidConfig(
574 "scale factor could not be converted to f64 for serialization".to_string(),
575 )
576 })?;
577 data.extend_from_slice(&min_bits.to_le_bytes());
578 data.extend_from_slice(&scale_bits.to_le_bytes());
579 }
580
581 let total_elements: usize = gradients.iter().map(|g| g.len()).sum();
582 let metadata = CompressionMetadata {
583 strategy: CompressionStrategy::Quantization { bits },
584 compression_ratio: data.len() as f64
585 / (total_elements.max(1) * std::mem::size_of::<A>()) as f64,
586 nnz_count: total_elements,
587 scale_factors,
588 extra_data: Vec::new(),
589 };
590
591 Ok((data, metadata))
592 }
593
594 fn decompress_none(&self, compressed: &CompressedGradient<A>) -> Result<Vec<Array<A, D>>> {
596 let mut result = Vec::new();
597 let mut data_offset = 0;
598
599 for shape in &compressed.shapes {
600 let num_elements: usize = shape.iter().product();
601 let mut values = Vec::with_capacity(num_elements);
602
603 for _ in 0..num_elements {
604 if data_offset + 8 > compressed.data.len() {
605 return Err(OptimError::InvalidConfig(
606 "Insufficient data for decompression".to_string(),
607 ));
608 }
609
610 let value = read_f64_le(&compressed.data[data_offset..data_offset + 8])?;
611 values.push(A::from(value).ok_or_else(|| {
612 OptimError::InvalidConfig(
613 "decompressed value could not be represented in the parameter type"
614 .to_string(),
615 )
616 })?);
617 data_offset += 8;
618 }
619
620 let dynamic_array = Array::from_shape_vec(shape.as_slice(), values).map_err(|_| {
622 OptimError::InvalidConfig("Invalid shape for reconstruction".to_string())
623 })?;
624 let array = dynamic_array.into_dimensionality::<D>().map_err(|_| {
625 OptimError::InvalidConfig("Dimension conversion failed".to_string())
626 })?;
627 result.push(array);
628 }
629
630 Ok(result)
631 }
632
633 fn decompress_sparse(&self, compressed: &CompressedGradient<A>) -> Result<Vec<Array<A, D>>> {
635 let mut result = Vec::new();
636
637 for shape in &compressed.shapes {
639 let dynamic_array = Array::zeros(shape.as_slice());
640 let array = dynamic_array.into_dimensionality::<D>().map_err(|_| {
641 OptimError::InvalidConfig("Dimension conversion failed for zero array".to_string())
642 })?;
643 result.push(array);
644 }
645
646 if compressed.data.len() < 4 {
648 return Err(OptimError::InvalidConfig(
649 "Invalid compressed data format".to_string(),
650 ));
651 }
652
653 let num_elements = read_u32_le(&compressed.data[0..4])? as usize;
654 let mut data_offset = 4;
655
656 for _ in 0..num_elements {
658 if data_offset + 16 > compressed.data.len() {
659 return Err(OptimError::InvalidConfig(
660 "Insufficient data for sparse decompression".to_string(),
661 ));
662 }
663
664 let grad_idx = read_u32_le(&compressed.data[data_offset..data_offset + 4])? as usize;
665 let elem_idx =
666 read_u32_le(&compressed.data[data_offset + 4..data_offset + 8])? as usize;
667 let value_f64 = read_f64_le(&compressed.data[data_offset + 8..data_offset + 16])?;
668 let value = A::from(value_f64).ok_or_else(|| {
669 OptimError::InvalidConfig(
670 "decompressed value could not be represented in the parameter type".to_string(),
671 )
672 })?;
673
674 data_offset += 16;
675
676 if grad_idx >= result.len() {
677 return Err(OptimError::InvalidConfig(
678 "Invalid gradient index in compressed data".to_string(),
679 ));
680 }
681
682 let target = result[grad_idx].as_slice_mut().ok_or_else(|| {
686 OptimError::InvalidConfig(
687 "target array is not contiguous; cannot write decompressed element".to_string(),
688 )
689 })?;
690 match target.get_mut(elem_idx) {
691 Some(elem) => *elem = value,
692 None => {
693 return Err(OptimError::InvalidConfig(
694 "Invalid element index in compressed data".to_string(),
695 ));
696 }
697 }
698 }
699
700 Ok(result)
701 }
702
703 fn decompress_quantization(
705 &self,
706 compressed: &CompressedGradient<A>,
707 bits: u8,
708 ) -> Result<Vec<Array<A, D>>> {
709 let mut result = Vec::new();
710 let mut data_offset = 0;
711
712 for shape in compressed.shapes.iter() {
713 let num_elements: usize = shape.iter().product();
714 let mut values = Vec::with_capacity(num_elements);
715
716 for _ in 0..num_elements {
718 let quantized = match bits {
719 1..=8 => {
720 if data_offset >= compressed.data.len() {
721 return Err(OptimError::InvalidConfig(
722 "Insufficient quantized data".to_string(),
723 ));
724 }
725 let val = compressed.data[data_offset] as u32;
726 data_offset += 1;
727 val
728 }
729 9..=16 => {
730 if data_offset + 2 > compressed.data.len() {
731 return Err(OptimError::InvalidConfig(
732 "Insufficient quantized data".to_string(),
733 ));
734 }
735 let val =
736 read_u16_le(&compressed.data[data_offset..data_offset + 2])? as u32;
737 data_offset += 2;
738 val
739 }
740 17..=32 => {
741 if data_offset + 4 > compressed.data.len() {
742 return Err(OptimError::InvalidConfig(
743 "Insufficient quantized data".to_string(),
744 ));
745 }
746 let val = read_u32_le(&compressed.data[data_offset..data_offset + 4])?;
747 data_offset += 4;
748 val
749 }
750 _ => {
751 return Err(OptimError::InvalidConfig(
752 "Invalid quantization bits".to_string(),
753 ))
754 }
755 };
756
757 values.push(quantized);
758 }
759
760 if data_offset + 16 > compressed.data.len() {
767 return Err(OptimError::InvalidConfig(
768 "Missing min value/scale for quantization".to_string(),
769 ));
770 }
771 let min_val_f64 = read_f64_le(&compressed.data[data_offset..data_offset + 8])?;
772 let scale_f64 = read_f64_le(&compressed.data[data_offset + 8..data_offset + 16])?;
773 let min_val = A::from(min_val_f64).ok_or_else(|| {
774 OptimError::InvalidConfig(
775 "min value could not be represented in the parameter type".to_string(),
776 )
777 })?;
778 let scale = A::from(scale_f64).ok_or_else(|| {
779 OptimError::InvalidConfig(
780 "scale factor could not be represented in the parameter type".to_string(),
781 )
782 })?;
783 data_offset += 16;
784
785 let dequantized_values: Vec<A> = values
787 .into_iter()
788 .map(|q| -> Result<A> {
789 let q_a = A::from(q).ok_or_else(|| {
790 OptimError::InvalidConfig(
791 "quantized value could not be represented in the parameter type"
792 .to_string(),
793 )
794 })?;
795 Ok(min_val + q_a * scale)
796 })
797 .collect::<Result<Vec<A>>>()?;
798
799 let dynamic_array = Array::from_shape_vec(shape.as_slice(), dequantized_values)
800 .map_err(|_| {
801 OptimError::InvalidConfig(
802 "Invalid shape for quantized reconstruction".to_string(),
803 )
804 })?;
805 let array = dynamic_array.into_dimensionality::<D>().map_err(|_| {
806 OptimError::InvalidConfig(
807 "Dimension conversion failed for quantized array".to_string(),
808 )
809 })?;
810 result.push(array);
811 }
812
813 Ok(result)
814 }
815
816 fn calculate_size(&self, gradients: &[Array<A, D>]) -> usize {
818 gradients
819 .iter()
820 .map(|g| g.len() * std::mem::size_of::<A>())
821 .sum()
822 }
823
824 pub fn stats(&self) -> &CompressionStats {
826 &self.stats
827 }
828
829 pub fn reset_stats(&mut self) {
831 self.stats = CompressionStats::new();
832 }
833}
834
835#[derive(Debug, Clone)]
837pub struct CompressionStats {
838 pub compressions_count: usize,
840 pub total_original_bytes: usize,
842 pub total_compressed_bytes: usize,
844 pub average_compression_ratio: f64,
846 pub best_compression_ratio: f64,
848 pub worst_compression_ratio: f64,
850}
851
852impl CompressionStats {
853 pub fn new() -> Self {
855 Self {
856 compressions_count: 0,
857 total_original_bytes: 0,
858 total_compressed_bytes: 0,
859 average_compression_ratio: 0.0,
860 best_compression_ratio: f64::INFINITY,
861 worst_compression_ratio: 0.0,
862 }
863 }
864
865 pub fn record_compression(&mut self, original_bytes: usize, compressedbytes: usize) {
867 self.compressions_count += 1;
868 self.total_original_bytes += original_bytes;
869 self.total_compressed_bytes += compressedbytes;
870
871 let ratio = if original_bytes > 0 {
872 compressedbytes as f64 / original_bytes as f64
873 } else {
874 1.0
875 };
876
877 self.best_compression_ratio = self.best_compression_ratio.min(ratio);
878 self.worst_compression_ratio = self.worst_compression_ratio.max(ratio);
879
880 self.average_compression_ratio = if self.total_original_bytes > 0 {
881 self.total_compressed_bytes as f64 / self.total_original_bytes as f64
882 } else {
883 0.0
884 };
885 }
886
887 pub fn overall_compression_ratio(&self) -> f64 {
889 self.average_compression_ratio
890 }
891
892 pub fn bandwidth_savings(&self) -> f64 {
894 (1.0 - self.average_compression_ratio) * 100.0
895 }
896}
897
898impl Default for CompressionStats {
899 fn default() -> Self {
900 Self::new()
901 }
902}