heapdump_vmo/
stack_trace_compression.rs

1// Copyright 2023 The Fuchsia Authors. All rights reserved.
2// Use of this source code is governed by a BSD-style license that can be
3// found in the LICENSE file.
4
5/// Given the size of an uncompressed stack trace (expressed as the number of stack frames), returns
6/// an upper bound of its compressed size.
7pub const fn max_compressed_size(num_frames: usize) -> usize {
8    num_frames * VARINT_MAX_ENCODED_LEN
9}
10
11/// Compresses a stack trace into a preallocated buffer.
12///
13/// The destination buffer's size must be suitable for the input size (see `max_compressed_size` in
14/// this crate).
15///
16/// This function returns the number of bytes actually written into the destination buffer (i.e. the
17/// compressed size).
18pub fn compress_into(src: &[u64], dest: &mut [u8]) -> usize {
19    assert!(dest.len() >= max_compressed_size(src.len()), "dest buffer is not big enough");
20
21    let mut offset = 0;
22    let mut prev = 0;
23    for value in src {
24        let zigzag = zigzag_encode(value.wrapping_sub(prev));
25        offset += varint_encode(zigzag, &mut dest[offset..]);
26        prev = *value;
27    }
28
29    offset
30}
31
32/// Compresses a stack trace into a dynamically-allocated vector of bytes.
33pub fn compress(src: &[u64]) -> Vec<u8> {
34    let mut buf = vec![0; max_compressed_size(src.len())];
35    let compressed_size = compress_into(src, &mut buf);
36    buf.truncate(compressed_size);
37    buf
38}
39
40/// Uncompresses a stack trace.
41pub fn uncompress(src: &[u8]) -> Result<Vec<u64>, crate::Error> {
42    let mut result = Vec::new();
43
44    let mut offset = 0;
45    let mut value = 0;
46    while offset != src.len() {
47        let (zigzag, num_bytes) =
48            varint_decode(&src[offset..]).ok_or(crate::Error::InvalidInput)?;
49        offset += num_bytes;
50        value = zigzag_decode(zigzag).wrapping_add(value);
51        result.push(value);
52    }
53
54    Ok(result)
55}
56
57const VARINT_SHIFT: u32 = 7;
58const VARINT_VALUE_MASK: u8 = 0x7f;
59const VARINT_CONT_BIT: u8 = 0x80;
60const VARINT_MAX_ENCODED_LEN: usize = 10; // ceil(u64::BITS / VARINT_SHIFT)
61
62/// Encodes a value into the given buffer, returning the number of bytes that were written.
63fn varint_encode(mut value: u64, dest: &mut [u8]) -> usize {
64    assert!(dest.len() >= VARINT_MAX_ENCODED_LEN, "dest buffer is not big enough");
65
66    let mut offset = 0;
67    loop {
68        dest[offset] = value as u8 & VARINT_VALUE_MASK;
69        value >>= VARINT_SHIFT;
70
71        if value != 0 {
72            dest[offset] |= VARINT_CONT_BIT;
73            offset += 1;
74        } else {
75            return offset + 1;
76        }
77    }
78}
79
80/// Tries to decode a value from the given buffer, returning the value and the number of bytes that
81/// were consumed.
82fn varint_decode(src: &[u8]) -> Option<(u64, usize)> {
83    let mut result = 0;
84    let mut offset = 0;
85    let mut shift = 0;
86    loop {
87        // Read the next byte or return None if we are at the end of the array.
88        let input = src.get(offset)?;
89
90        // Left-shift the value at its final position, then right-shift it back to validate that
91        // we didn't lose any bit due to the left-shift overflowing.
92        let value = (input & VARINT_VALUE_MASK) as u64;
93        let value_shifted = value.checked_shl(shift)?;
94        if (value_shifted >> shift) != value {
95            return None; // overflow detected
96        }
97
98        result |= value_shifted;
99        if (input & VARINT_CONT_BIT) != 0 {
100            shift += VARINT_SHIFT;
101            offset += 1;
102        } else {
103            return Some((result, offset + 1));
104        }
105    }
106}
107
108const ZIGZAG_VALUE_MASK: u64 = !1;
109const ZIGZAG_SIGN_BIT: u64 = 1;
110
111fn zigzag_encode(mut value: u64) -> u64 {
112    value = value.rotate_left(1);
113    if (value & ZIGZAG_SIGN_BIT) != 0 {
114        value ^= ZIGZAG_VALUE_MASK;
115    }
116    value
117}
118
119fn zigzag_decode(mut value: u64) -> u64 {
120    if (value & ZIGZAG_SIGN_BIT) != 0 {
121        value ^= ZIGZAG_VALUE_MASK;
122    }
123    value.rotate_right(1)
124}
125
126#[cfg(test)]
127mod tests {
128    use super::*;
129    use test_case::test_case;
130
131    // Arbitrary constants, used instead of real code addresses in the tests below:
132    const A: u64 = 0x0000_008a_b3fe_9821;
133    const B: u64 = 0x0000_812c_6a4a_682e;
134    const C: u64 = 0x0048_b5a0_2d45_9e5a;
135    const D: u64 = 0xcccc_f148_39a5_9c5a;
136    const F: u64 = 0xffff_ffff_ffff_ffff; // all 1's
137    const Z: u64 = 0x0000_0000_0000_0000; // all 0's
138
139    #[test_case(&[] ; "empty")]
140    #[test_case(&[A] ; "A")]
141    #[test_case(&[B] ; "B")]
142    #[test_case(&[C] ; "C")]
143    #[test_case(&[D] ; "D")]
144    #[test_case(&[A, B] ; "AB")]
145    #[test_case(&[A, A] ; "AA")]
146    #[test_case(&[A, A, B] ; "AAB")]
147    #[test_case(&[A, B, C, D] ; "ABCD")]
148    #[test_case(&[A, A, B, B, C, C, D, D] ; "AABBCCDD")]
149    #[test_case(&[D, D, C, C, B, B, A, A] ; "DDCCBBAA")]
150    #[test_case(&[F, F, A, F, F] ; "FFAFF")]
151    #[test_case(&[Z, Z, A, Z, Z] ; "ZZAFF")]
152    #[test_case(&[F, Z, F] ; "FZF")]
153    #[test_case(&[Z, F, Z] ; "ZFZ")]
154    fn test_compress_and_uncompress(stack_trace: &[u64]) {
155        let compressed_data = compress(stack_trace);
156        assert_eq!(stack_trace, &uncompress(&compressed_data).unwrap());
157    }
158
159    #[test_case(&[] ; "empty")]
160    #[test_case(&[0xff, 0xff] ; "ends with CONT bit set")]
161    #[test_case(&[0x80, 0x80, 0x80, 0x80, 0x80, 0x80, 0x80, 0x80, 0x80, 0x02] ;
162        "valid encoding but too big (u64::MAX + 1)")]
163    #[test_case(&[0x80, 0x80, 0x80, 0x80, 0x80, 0x80, 0x80, 0x80, 0x80, 0x80, 0x80, 0x01] ;
164        "valid encoding but too big (u64::MAX * 2^14)")]
165    fn test_varint_decode_bad(data: &[u8]) {
166        assert_eq!(varint_decode(data), None);
167    }
168
169    #[test_case(0 ; "0")]
170    #[test_case(1 ; "1")]
171    #[test_case(-1 ; "negative 1")]
172    #[test_case(1000 ; "1000")]
173    #[test_case(-1000 ; "negative 1000")]
174    #[test_case(i64::MIN ; "min")]
175    #[test_case(i64::MAX ; "max")]
176    fn test_zigzag(value: i64) {
177        let encoded_value = zigzag_encode(value as u64);
178        let decoded_value = zigzag_decode(encoded_value) as i64;
179        assert_eq!(decoded_value, value, "encoded value: {:x}", encoded_value);
180    }
181}