Skip to main content

nodedb_codec/
delta.rs

1// SPDX-License-Identifier: Apache-2.0
2
3//! Delta codec for monotonic counter columns.
4//!
5//! Monotonic counters (bytes_sent, request_count) have small positive deltas
6//! that compress well with simple delta encoding + varint packing.
7//!
8//! Wire format:
9//! ```text
10//! [4 bytes] sample count (LE u32)
11//! [8 bytes] first value (LE i64)
12//! [N bytes] varint-encoded deltas (ZigZag + LEB128)
13//! ```
14//!
15//! Varint encoding uses ZigZag to handle negative deltas (counter resets)
16//! efficiently: small absolute values → 1-2 bytes regardless of sign.
17//!
18//! Compression: monotonic counters with small increments → ~1-2 bytes/sample.
19//! Non-monotonic data → ~2-4 bytes/sample (still better than raw 8 bytes).
20
21use std::mem::size_of;
22
23use crate::bounds::{
24    checked_add, checked_capacity, checked_mul, decoded_len, encode_input_len, u32_to_usize,
25};
26use crate::error::CodecError;
27
28// ---------------------------------------------------------------------------
29// ZigZag + LEB128 varint encoding
30// ---------------------------------------------------------------------------
31
32/// ZigZag-encode a signed i64 into an unsigned u64.
33///
34/// Maps signed integers to unsigned so small absolute values have small
35/// representations: 0→0, -1→1, 1→2, -2→3, 2→4, ...
36#[inline]
37fn zigzag_encode(v: i64) -> u64 {
38    ((v << 1) ^ (v >> 63)) as u64
39}
40
41/// ZigZag-decode an unsigned u64 back to signed i64.
42#[inline]
43fn zigzag_decode(v: u64) -> i64 {
44    ((v >> 1) as i64) ^ -((v & 1) as i64)
45}
46
47/// Write a varint (LEB128-encoded u64) to a buffer.
48fn write_varint(buf: &mut Vec<u8>, mut value: u64) {
49    loop {
50        let mut byte = (value & 0x7F) as u8;
51        value >>= 7;
52        if value != 0 {
53            byte |= 0x80;
54        }
55        buf.push(byte);
56        if value == 0 {
57            break;
58        }
59    }
60}
61
62/// Read a varint (LEB128-encoded u64) from a byte slice.
63///
64/// Returns `(value, bytes_consumed)`.
65fn read_varint(data: &[u8]) -> Result<(u64, usize), CodecError> {
66    let mut value = 0u64;
67    for (index, &byte) in data.iter().enumerate() {
68        if index >= 10 {
69            return Err(CodecError::Corrupt {
70                detail: "varint exceeds ten bytes".into(),
71            });
72        }
73        let payload = u64::from(byte & 0x7f);
74        if index == 9 && (byte & 0x80 != 0 || payload > 1) {
75            return Err(CodecError::Corrupt {
76                detail: "varint overflows u64".into(),
77            });
78        }
79        value |= payload << (index * 7);
80        if byte & 0x80 == 0 {
81            if index > 0 && payload == 0 {
82                return Err(CodecError::Corrupt {
83                    detail: "varint is noncanonical".into(),
84                });
85            }
86            return Ok((value, index + 1));
87        }
88    }
89
90    Err(CodecError::Truncated {
91        expected: checked_add(data.len(), 1, "Delta varint length")?,
92        actual: data.len(),
93    })
94}
95
96// ---------------------------------------------------------------------------
97// Public encode / decode API
98// ---------------------------------------------------------------------------
99
100/// Encode a slice of i64 values using Delta + ZigZag-varint compression.
101pub fn encode(values: &[i64]) -> Result<Vec<u8>, CodecError> {
102    let count = encode_value_count(values.len(), "Delta")?;
103    let estimated = checked_add(
104        12,
105        checked_mul(values.len(), 2, "Delta output estimate")?,
106        "Delta output estimate",
107    )?;
108    let mut out = Vec::with_capacity(estimated);
109
110    out.extend_from_slice(&count.to_le_bytes());
111
112    if values.is_empty() {
113        return Ok(out);
114    }
115
116    out.extend_from_slice(&values[0].to_le_bytes());
117
118    for i in 1..values.len() {
119        let delta = values[i].wrapping_sub(values[i - 1]);
120        write_varint(&mut out, zigzag_encode(delta));
121    }
122
123    Ok(out)
124}
125
126/// Decode Delta-compressed bytes back to i64 values.
127pub fn decode(data: &[u8]) -> Result<Vec<i64>, CodecError> {
128    if data.len() < 4 {
129        return Err(CodecError::Truncated {
130            expected: 4,
131            actual: data.len(),
132        });
133    }
134
135    let count = u32_to_usize(
136        u32::from_le_bytes(data[0..4].try_into().map_err(|_| CodecError::Corrupt {
137            detail: "invalid header".into(),
138        })?),
139        "Delta value count",
140    )?;
141    validate_value_count(count, "Delta")?;
142
143    if count == 0 {
144        if data.len() != 4 {
145            return Err(CodecError::Corrupt {
146                detail: "trailing bytes after empty Delta frame".into(),
147            });
148        }
149        return Ok(Vec::new());
150    }
151
152    if data.len() < 12 {
153        return Err(CodecError::Truncated {
154            expected: 12,
155            actual: data.len(),
156        });
157    }
158
159    let first_value =
160        i64::from_le_bytes(data[4..12].try_into().map_err(|_| CodecError::Corrupt {
161            detail: "invalid first value".into(),
162        })?);
163
164    let value_capacity = checked_capacity(count, size_of::<i64>(), "Delta values")?;
165    let mut values = Vec::with_capacity(value_capacity);
166    values.push(first_value);
167
168    let mut offset = 12;
169    for _ in 1..count {
170        if offset >= data.len() {
171            return Err(CodecError::Truncated {
172                expected: checked_add(offset, 1, "Delta varint offset")?,
173                actual: data.len(),
174            });
175        }
176        let tail = data.get(offset..).ok_or(CodecError::Truncated {
177            expected: checked_add(offset, 1, "Delta varint offset")?,
178            actual: data.len(),
179        })?;
180        let (encoded_delta, consumed) = read_varint(tail)?;
181        let delta = zigzag_decode(encoded_delta);
182        let value = values[values.len() - 1].wrapping_add(delta);
183        values.push(value);
184        offset = checked_add(offset, consumed, "Delta varint offset")?;
185    }
186    if offset != data.len() {
187        return Err(CodecError::Corrupt {
188            detail: "trailing bytes after Delta frame".into(),
189        });
190    }
191
192    Ok(values)
193}
194
195fn encode_value_count(len: usize, codec: &str) -> Result<u32, CodecError> {
196    validate_value_count(len, codec)?;
197    encode_input_len(len, codec)
198}
199
200fn validate_value_count(count: usize, codec: &str) -> Result<(), CodecError> {
201    let bytes = checked_mul(count, size_of::<i64>(), "integer decoded bytes")?;
202    decoded_len(bytes, codec)?;
203    Ok(())
204}
205
206// ---------------------------------------------------------------------------
207// Streaming encoder / decoder types
208// ---------------------------------------------------------------------------
209
210/// Streaming Delta encoder. Accumulates values and produces compressed
211/// bytes on `finish()`.
212pub struct DeltaEncoder {
213    values: Vec<i64>,
214}
215
216impl DeltaEncoder {
217    pub fn new() -> Self {
218        Self {
219            values: Vec::with_capacity(4096),
220        }
221    }
222
223    pub fn push(&mut self, value: i64) -> Result<(), CodecError> {
224        let next = checked_add(self.values.len(), 1, "Delta streaming count")?;
225        validate_value_count(next, "Delta streaming input")?;
226        self.values.push(value);
227        Ok(())
228    }
229
230    pub fn push_batch(&mut self, values: &[i64]) -> Result<(), CodecError> {
231        let next = checked_add(self.values.len(), values.len(), "Delta streaming count")?;
232        validate_value_count(next, "Delta streaming input")?;
233        self.values.extend_from_slice(values);
234        Ok(())
235    }
236
237    pub fn count(&self) -> usize {
238        self.values.len()
239    }
240
241    pub fn finish(self) -> Result<Vec<u8>, CodecError> {
242        encode(&self.values)
243    }
244}
245
246impl Default for DeltaEncoder {
247    fn default() -> Self {
248        Self::new()
249    }
250}
251
252/// Streaming Delta decoder.
253pub struct DeltaDecoder {
254    values: Vec<i64>,
255    pos: usize,
256}
257
258impl DeltaDecoder {
259    pub fn new(data: &[u8]) -> Result<Self, CodecError> {
260        let values = decode(data)?;
261        Ok(Self { values, pos: 0 })
262    }
263
264    pub fn decode_all(data: &[u8]) -> Result<Vec<i64>, CodecError> {
265        decode(data)
266    }
267
268    pub fn next_value(&mut self) -> Option<i64> {
269        if self.pos < self.values.len() {
270            let v = self.values[self.pos];
271            self.pos += 1;
272            Some(v)
273        } else {
274            None
275        }
276    }
277
278    pub fn remaining(&self) -> usize {
279        self.values.len() - self.pos
280    }
281}
282
283#[cfg(test)]
284mod tests {
285    use super::*;
286
287    fn encode(values: &[i64]) -> Vec<u8> {
288        super::encode(values).expect("test Delta encode")
289    }
290
291    #[test]
292    fn zigzag_roundtrip() {
293        for v in [0i64, 1, -1, 2, -2, 63, -63, 127, -128, i64::MAX, i64::MIN] {
294            assert_eq!(zigzag_decode(zigzag_encode(v)), v, "zigzag failed for {v}");
295        }
296    }
297
298    #[test]
299    fn varint_roundtrip() {
300        for v in [0u64, 1, 127, 128, 255, 16383, 16384, u64::MAX / 2, u64::MAX] {
301            let mut buf = Vec::new();
302            write_varint(&mut buf, v);
303            let (decoded, consumed) = read_varint(&buf).unwrap();
304            assert_eq!(decoded, v, "varint failed for {v}");
305            assert_eq!(consumed, buf.len());
306        }
307    }
308
309    #[test]
310    fn empty_roundtrip() {
311        let encoded = encode(&[]);
312        let decoded = decode(&encoded).unwrap();
313        assert!(decoded.is_empty());
314    }
315
316    #[test]
317    fn single_value() {
318        let encoded = encode(&[42i64]);
319        let decoded = decode(&encoded).unwrap();
320        assert_eq!(decoded, vec![42i64]);
321        assert_eq!(encoded.len(), 12); // 4 + 8
322    }
323
324    #[test]
325    fn monotonic_counter() {
326        // Bytes sent: monotonically increasing by ~1000 each step.
327        let values: Vec<i64> = (0..10_000).map(|i| i * 1000).collect();
328        let encoded = encode(&values);
329        let decoded = decode(&encoded).unwrap();
330        assert_eq!(decoded, values);
331
332        // All deltas are exactly 1000 → zigzag(1000) = 2000 → 2 bytes each.
333        let bytes_per_sample = encoded.len() as f64 / values.len() as f64;
334        assert!(
335            bytes_per_sample < 3.0,
336            "monotonic counter should compress to <3 bytes/sample, got {bytes_per_sample:.2}"
337        );
338    }
339
340    #[test]
341    fn counter_with_small_increments() {
342        // Request count: increment by 1 each step.
343        let values: Vec<i64> = (0..10_000).collect();
344        let encoded = encode(&values);
345        let decoded = decode(&encoded).unwrap();
346        assert_eq!(decoded, values);
347
348        // Delta = 1 → zigzag(1) = 2 → 1 byte each.
349        let bytes_per_sample = encoded.len() as f64 / values.len() as f64;
350        assert!(
351            bytes_per_sample < 2.0,
352            "unit-increment counter should compress to <2 bytes/sample, got {bytes_per_sample:.2}"
353        );
354    }
355
356    #[test]
357    fn counter_reset() {
358        // Counter with a reset (wrap-around): monotonic then drops to 0.
359        let mut values: Vec<i64> = (0..500).map(|i| i * 100).collect();
360        values.push(0); // reset
361        values.extend((1..500).map(|i| i * 100));
362
363        let encoded = encode(&values);
364        let decoded = decode(&encoded).unwrap();
365        assert_eq!(decoded, values);
366    }
367
368    #[test]
369    fn non_monotonic_gauge() {
370        // CPU gauge: fluctuates around a value.
371        let mut values = Vec::with_capacity(10_000);
372        let mut val = 50i64;
373        let mut rng: u64 = 12345;
374        for _ in 0..10_000 {
375            values.push(val);
376            rng = rng.wrapping_mul(6364136223846793005).wrapping_add(1);
377            let delta = ((rng >> 33) as i64 % 11) - 5; // -5 to +5
378            val += delta;
379        }
380        let encoded = encode(&values);
381        let decoded = decode(&encoded).unwrap();
382        assert_eq!(decoded, values);
383
384        // Small deltas → 1 byte each → ~2 bytes/sample.
385        let bytes_per_sample = encoded.len() as f64 / values.len() as f64;
386        assert!(
387            bytes_per_sample < 3.0,
388            "small-delta gauge should compress to <3 bytes/sample, got {bytes_per_sample:.2}"
389        );
390    }
391
392    #[test]
393    fn negative_values() {
394        let values: Vec<i64> = vec![-1000, -999, -998, -997, -996];
395        let encoded = encode(&values);
396        let decoded = decode(&encoded).unwrap();
397        assert_eq!(decoded, values);
398    }
399
400    #[test]
401    fn large_values() {
402        let values: Vec<i64> = vec![i64::MAX, i64::MAX - 1, i64::MAX - 2];
403        let encoded = encode(&values);
404        let decoded = decode(&encoded).unwrap();
405        assert_eq!(decoded, values);
406    }
407
408    #[test]
409    fn boundary_values() {
410        let values: Vec<i64> = vec![i64::MIN, 0, i64::MAX];
411        let encoded = encode(&values);
412        let decoded = decode(&encoded).unwrap();
413        assert_eq!(decoded, values);
414    }
415
416    #[test]
417    fn streaming_encoder_matches_batch() {
418        let values: Vec<i64> = (0..1000).map(|i| i * 7).collect();
419        let batch = encode(&values);
420
421        let mut enc = DeltaEncoder::new();
422        for &v in &values {
423            enc.push(v).expect("push");
424        }
425        assert_eq!(enc.finish().expect("finish"), batch);
426    }
427
428    #[test]
429    fn streaming_decoder() {
430        let values: Vec<i64> = (0..100).map(|i| i * 10).collect();
431        let encoded = encode(&values);
432        let mut dec = DeltaDecoder::new(&encoded).unwrap();
433
434        for &expected in &values {
435            assert_eq!(dec.next_value(), Some(expected));
436        }
437        assert_eq!(dec.next_value(), None);
438    }
439
440    #[test]
441    fn truncated_input_errors() {
442        assert!(decode(&[]).is_err());
443        assert!(decode(&[1, 0, 0, 0]).is_err()); // count=1, no value
444    }
445
446    #[test]
447    fn rejects_overflowed_noncanonical_and_huge_input_before_allocation() {
448        assert!(matches!(
449            read_varint(&[0x80, 0x00]),
450            Err(CodecError::Corrupt { .. })
451        ));
452        assert!(matches!(
453            read_varint(&[0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0x02]),
454            Err(CodecError::Corrupt { .. })
455        ));
456        assert!(matches!(
457            read_varint(&[0x80; 10]),
458            Err(CodecError::Corrupt { .. })
459        ));
460        assert!(matches!(
461            read_varint(&[0x80]),
462            Err(CodecError::Truncated { .. })
463        ));
464        let mut huge = u32::MAX.to_le_bytes().to_vec();
465        huge.extend_from_slice(&[0; 8]);
466        assert!(matches!(
467            decode(&huge),
468            Err(CodecError::ResourceLimit { .. })
469        ));
470    }
471
472    #[test]
473    fn compression_vs_raw() {
474        let values: Vec<i64> = (0..100_000).map(|i| i * 1000).collect();
475        let encoded = encode(&values);
476        let raw_size = values.len() * 8;
477        let ratio = raw_size as f64 / encoded.len() as f64;
478        assert!(
479            ratio > 3.0,
480            "expected >3x compression for monotonic counter, got {ratio:.1}x"
481        );
482    }
483}