Skip to main content

chronix_encoding/
gorilla.rs

1//! Gorilla XOR float encoding (Facebook, 2015).
2//!
3//! Classic XOR-based compression where each value is `XORed` with its predecessor
4//! and the meaningful bits of the XOR result are stored with a compact
5//! leading/trailing zero prefix scheme. Used as fallback when Chimp's
6//! bucket-based approach doesn't achieve sufficient compression.
7
8use crate::coding::{checked_count, checked_decode_count};
9use crate::delta::{BitReader, BitWriter};
10use crate::error::{EncodingError, Result};
11use crate::simd::{
12    batch_f64_to_bits, batch_leading_zeros, batch_trailing_zeros, batch_xor_adjacent,
13};
14
15/// Gorilla float encoder.
16#[derive(Debug, Clone, Copy)]
17pub struct GorillaEncoder;
18
19/// Reusable scratch buffers for Gorilla encoding.
20///
21/// Avoids allocating four intermediate `Vec`s per `encode()` call.
22/// Create once and reuse across multiple encode calls (e.g., during
23/// flush or compaction of many columns).
24#[derive(Debug, Default)]
25pub struct GorillaEncodeScratch {
26    bits: Vec<u64>,
27    xors: Vec<u64>,
28    leading: Vec<u32>,
29    trailing: Vec<u32>,
30}
31
32impl GorillaEncodeScratch {
33    /// Create a new empty scratch buffer.
34    #[must_use]
35    pub fn new() -> Self {
36        Self::default()
37    }
38}
39
40/// Gorilla float decoder.
41#[derive(Debug, Clone, Copy)]
42pub struct GorillaDecoder;
43
44impl GorillaEncoder {
45    /// Encode a sequence of `f64` values using Gorilla XOR encoding.
46    ///
47    /// # Errors
48    ///
49    /// Returns [`EncodingError::EmptyInput`] if `values` is empty.
50    pub fn encode(values: &[f64]) -> Result<Vec<u8>> {
51        let mut scratch = GorillaEncodeScratch::new();
52        Self::encode_with_scratch(values, &mut scratch)
53    }
54
55    /// Encode using caller-provided scratch buffers.
56    ///
57    /// Reuses buffers across multiple calls to avoid
58    /// repeated allocation during flush/compaction of many columns.
59    pub fn encode_with_scratch(
60        values: &[f64],
61        scratch: &mut GorillaEncodeScratch,
62    ) -> Result<Vec<u8>> {
63        if values.is_empty() {
64            return Err(EncodingError::EmptyInput {
65                context: "gorilla encoder",
66            });
67        }
68
69        let mut writer = BitWriter::with_capacity(4 + values.len() * 2);
70
71        // Value count (validated to fit in u32)
72        let count = checked_count(values.len())?;
73        writer.write_bits(u64::from(count), 32);
74
75        // ---- SIMD-friendly batch pre-computation ----
76        // Reuse scratch buffers — clear + refill avoids realloc
77        // when capacity is already sufficient from a prior call.
78        scratch.bits.clear();
79        batch_f64_to_bits(values, &mut scratch.bits);
80
81        scratch.xors.clear();
82        batch_xor_adjacent(&scratch.bits, &mut scratch.xors);
83
84        scratch.leading.clear();
85        batch_leading_zeros(&scratch.xors, &mut scratch.leading);
86
87        scratch.trailing.clear();
88        batch_trailing_zeros(&scratch.xors, &mut scratch.trailing);
89
90        // ---- Serial bit-packing (inherently sequential) ----
91        // First value: raw 64-bit (safe access avoids potential index panic)
92        let first_bits = *scratch.bits.first().ok_or(EncodingError::CorruptData {
93            detail: "gorilla: empty bits buffer after non-empty check".to_string(),
94        })?;
95        writer.write_bits(first_bits, 64);
96
97        let mut prev_leading: u32 = u32::MAX;
98        let mut prev_trailing: u32 = 0;
99
100        for i in 0..scratch.xors.len() {
101            let xor = scratch.xors[i];
102
103            if xor == 0 {
104                // Case 0: identical → single 0 bit
105                writer.write_bit(false);
106            } else {
107                let leading = scratch.leading[i];
108                let trailing = scratch.trailing[i];
109
110                // Check if current window fits within previous
111                if prev_leading != u32::MAX && leading >= prev_leading && trailing >= prev_trailing
112                {
113                    // Case 1: reuse window → prefix 10
114                    writer.write_bits(0b10, 2);
115                    let meaningful = 64u32
116                        .checked_sub(prev_leading)
117                        .and_then(|v| v.checked_sub(prev_trailing))
118                        .ok_or_else(|| EncodingError::CorruptData {
119                            detail: "gorilla case-1 encode: meaningful-bits underflow".to_string(),
120                        })?;
121                    let shifted = xor >> prev_trailing;
122                    writer.write_bits(shifted, meaningful as u8);
123                } else {
124                    // Case 2: new window → prefix 11
125                    writer.write_bits(0b11, 2);
126                    // 6-bit leading zeros (capped to 63)
127                    writer.write_bits(u64::from(leading.min(63)), 6);
128                    let meaningful = 64u32
129                        .checked_sub(leading)
130                        .and_then(|v| v.checked_sub(trailing))
131                        .ok_or_else(|| EncodingError::CorruptData {
132                            detail: format!(
133                                "gorilla case-2 encode: meaningful-bits underflow \
134                                 (leading={leading}, trailing={trailing})"
135                            ),
136                        })?;
137                    // 6-bit meaningful length, stored as meaningful-1
138                    // (so 1..=64 maps to 0..=63, fitting in 6 bits)
139                    let meaningful_m1 =
140                        meaningful
141                            .checked_sub(1)
142                            .ok_or_else(|| EncodingError::CorruptData {
143                                detail: "gorilla case-2 encode: zero meaningful bits \
144                                     for non-zero XOR"
145                                    .to_string(),
146                            })?;
147                    writer.write_bits(u64::from(meaningful_m1), 6);
148                    let shifted = xor >> trailing;
149                    writer.write_bits(shifted, meaningful as u8);
150
151                    prev_leading = leading;
152                    prev_trailing = trailing;
153                }
154            }
155        }
156
157        Ok(writer.finish())
158    }
159}
160
161impl GorillaDecoder {
162    /// Decode a Gorilla-encoded byte buffer back to `f64` values.
163    ///
164    /// # Errors
165    ///
166    /// Returns [`EncodingError::CorruptData`] if the data is malformed.
167    pub fn decode(data: &[u8]) -> Result<Vec<f64>> {
168        if data.len() < 4 {
169            return Err(EncodingError::CorruptData {
170                detail: "gorilla data too short for header".to_string(),
171            });
172        }
173
174        let mut reader = BitReader::new(data);
175
176        let count = checked_decode_count(reader.read_bits(32)? as usize, "Gorilla")?;
177        if count == 0 {
178            return Ok(Vec::new());
179        }
180
181        let mut result = Vec::with_capacity(count);
182
183        let first_bits = reader.read_bits(64)?;
184        result.push(f64::from_bits(first_bits));
185
186        let mut prev_bits = first_bits;
187        let mut prev_leading: u32 = u32::MAX;
188        let mut prev_trailing: u32 = 0;
189
190        for _ in 1..count {
191            if !reader.read_bit()? {
192                // Case 0: identical
193                result.push(f64::from_bits(prev_bits));
194                continue;
195            }
196
197            if reader.read_bit()? {
198                // Case 2: new window (prefix 11)
199                let leading = reader.read_bits(6)? as u32;
200                let meaningful = reader.read_bits(6)? as u32 + 1; // stored as m-1
201                let trailing = 64u32
202                    .checked_sub(leading)
203                    .and_then(|v| v.checked_sub(meaningful))
204                    .ok_or_else(|| EncodingError::CorruptData {
205                        detail: format!(
206                            "gorilla case-2: leading ({leading}) + meaningful \
207                             ({meaningful}) exceeds 64"
208                        ),
209                    })?;
210                let shifted = reader.read_bits(meaningful as u8)?;
211                let xor = shifted << trailing;
212                let bits = prev_bits ^ xor;
213                result.push(f64::from_bits(bits));
214                prev_bits = bits;
215
216                prev_leading = leading;
217                prev_trailing = trailing;
218            } else {
219                // Case 1: reuse window (prefix 10)
220                if prev_leading == u32::MAX {
221                    return Err(EncodingError::CorruptData {
222                        detail: "gorilla case-1 encountered before any case-2 window".to_string(),
223                    });
224                }
225                let meaningful = 64u32
226                    .checked_sub(prev_leading)
227                    .and_then(|v| v.checked_sub(prev_trailing))
228                    .ok_or_else(|| EncodingError::CorruptData {
229                        detail: format!(
230                            "gorilla case-1: invalid window \
231                             (leading={prev_leading}, trailing={prev_trailing})"
232                        ),
233                    })?;
234                let shifted = reader.read_bits(meaningful as u8)?;
235                let xor = shifted << prev_trailing;
236                let bits = prev_bits ^ xor;
237                result.push(f64::from_bits(bits));
238                prev_bits = bits;
239            }
240        }
241
242        Ok(result)
243    }
244}
245
246#[cfg(test)]
247mod tests {
248    use super::*;
249
250    #[test]
251    fn roundtrip_constant() {
252        let values = vec![42.0_f64; 100];
253        let encoded = GorillaEncoder::encode(&values).unwrap();
254        let decoded = GorillaDecoder::decode(&encoded).unwrap();
255        for (a, b) in values.iter().zip(decoded.iter()) {
256            assert_eq!(a.to_bits(), b.to_bits());
257        }
258    }
259
260    #[test]
261    fn roundtrip_special_values() {
262        let values = vec![
263            0.0,
264            -0.0,
265            f64::NAN,
266            f64::INFINITY,
267            f64::NEG_INFINITY,
268            f64::MIN,
269            f64::MAX,
270            f64::MIN_POSITIVE,
271            f64::EPSILON,
272        ];
273        let encoded = GorillaEncoder::encode(&values).unwrap();
274        let decoded = GorillaDecoder::decode(&encoded).unwrap();
275        for (a, b) in values.iter().zip(decoded.iter()) {
276            assert_eq!(a.to_bits(), b.to_bits(), "mismatch for value {a}");
277        }
278    }
279
280    #[test]
281    fn roundtrip_sinwave() {
282        let values: Vec<f64> = (0..1000).map(|i| (i as f64 * 0.01).sin() * 100.0).collect();
283        let encoded = GorillaEncoder::encode(&values).unwrap();
284        let decoded = GorillaDecoder::decode(&encoded).unwrap();
285        for (a, b) in values.iter().zip(decoded.iter()) {
286            assert_eq!(a.to_bits(), b.to_bits());
287        }
288    }
289
290    #[test]
291    fn empty_error() {
292        assert!(GorillaEncoder::encode(&[]).is_err());
293    }
294
295    #[test]
296    fn single_value() {
297        let values = vec![99.99];
298        let encoded = GorillaEncoder::encode(&values).unwrap();
299        let decoded = GorillaDecoder::decode(&encoded).unwrap();
300        assert_eq!(values[0].to_bits(), decoded[0].to_bits());
301    }
302}
303
304#[cfg(test)]
305mod proptests {
306    use super::*;
307    use proptest::prelude::*;
308
309    proptest! {
310        #[test]
311        fn roundtrip_arbitrary(values in proptest::collection::vec(any::<f64>(), 1..500)) {
312            let encoded = GorillaEncoder::encode(&values).unwrap();
313            let decoded = GorillaDecoder::decode(&encoded).unwrap();
314            prop_assert_eq!(values.len(), decoded.len());
315            for (a, b) in values.iter().zip(decoded.iter()) {
316                prop_assert_eq!(a.to_bits(), b.to_bits());
317            }
318        }
319    }
320}