Skip to main content

heapdump_vmo/
resources_table_v1.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
5use memory_mapped_vmo::{ReadOnlyMemoryMappedVmo, ReadWriteMemoryMappedVmo};
6use std::alloc::Layout;
7use std::mem::{align_of, size_of, size_of_val};
8use zerocopy::FromBytes;
9
10/// An offset within the VMO.
11type Offset = u32;
12const OFFSET_INVALID: Offset = Offset::MAX;
13
14// Stack traces are stored in compressed form as an array of u8 prefixed by a u16 stating its
15// length. The u16 is guaranteed to be aligned.
16type StackTraceLength = u16;
17
18// Known stack traces are indexed by a hash table, whose linked list heads (one for each bucket) are
19// stored at the beginning of the VMO.
20//
21// Note that the presence and the format of the hash table is meant to be an internal detail of the
22// current ResourcesTableWriter implementation: ResourcesTableReader does not depend on it.
23const NUM_STACK_BUCKETS: usize = 1 << 13;
24type StackBucketHeads = [Offset; NUM_STACK_BUCKETS];
25
26const CRC32_TABLE: crc::Crc<u32> = crc::Crc::<u32>::new(&crc::CRC_32_ISO_HDLC);
27
28/// A resource key is just an offset into the VMO.
29#[derive(Clone, Copy, Debug, Eq, FromBytes, Hash, Ord, PartialEq, PartialOrd)]
30#[repr(transparent)]
31pub struct ResourceKey(Offset);
32
33impl ResourceKey {
34    /// Used by tests in this crate to construct placeholder values.
35    #[cfg(test)]
36    pub(crate) const fn from_raw(offset: Offset) -> ResourceKey {
37        ResourceKey(offset)
38    }
39
40    pub const fn into_raw(self) -> Offset {
41        self.0
42    }
43}
44
45#[repr(C)]
46#[derive(Debug, FromBytes)]
47pub struct ThreadInfo {
48    pub koid: zx::sys::zx_koid_t,
49    pub name: zx::Name,
50}
51
52/// Mediates write access to a VMO containing compressed stack traces and thread info structs.
53///
54/// Compressed stack traces are stored as a hash table for efficient deduplication. Thread
55/// information structures are not deduplicated (it is expected that deduplication happens at a
56/// higher level in the stack). Apart from the hash table's bucket heads, all data is immutable:
57/// neither stack traces nor thread info structs can be modified or deleted after having been
58/// inserted.
59///
60/// Hash collisions are handled by maintaining per-bucket linked lists. Specifically, an array of
61/// list heads, one for each bucket, is stored at the beginning of the VMO. The remaining part of
62/// the VMO contains the linked list nodes. Since nodes are immutable, insertion always happen at
63/// the head of the list.
64///
65/// Thanks to the fact that inserted data is immutable, other readers are allowed to read data from
66/// this VMO while ResourcesTableWriter is still alive. In particular, all insertion functions
67/// return a ResourceKey, which is simply the offset of the just-inserted now-immutable data, and
68/// that can be used by ResourcesTableReader to read data back without even having to know about the
69/// hash table.
70pub struct ResourcesTableWriter {
71    storage: ReadWriteMemoryMappedVmo,
72    watermark: usize, // Offset of the first unallocated byte.
73}
74
75impl ResourcesTableWriter {
76    /// Initializes a VMO as an empty table and creates an AllocationsTableWriter to write into it.
77    ///
78    /// # Safety
79    /// The caller must guarantee that data accesses to the `vmo` do not generate data conflicts,
80    /// i.e. 1) only one ResourcesTableWriter can exist for this VMO 2) as this an append-only data
81    /// structure, readers should only read through the ResourceKeys we create, that are guaranteed
82    /// to point to VMO sections that have already been populated.
83    pub unsafe fn new(vmo: &zx::Vmo) -> Result<ResourcesTableWriter, crate::Error> {
84        let storage = unsafe { ReadWriteMemoryMappedVmo::new(vmo)? };
85        if storage.vmo_size() < size_of::<StackBucketHeads>() {
86            return Err(crate::Error::BufferTooSmall);
87        } else if storage.vmo_size() - 1 > Offset::MAX as usize {
88            return Err(crate::Error::BufferTooBig);
89        }
90
91        let mut result = ResourcesTableWriter { storage, watermark: size_of::<StackBucketHeads>() };
92
93        // Clear the hash table.
94        for bucket_index in 0..NUM_STACK_BUCKETS {
95            *result.stack_bucket_head_at(bucket_index) = OFFSET_INVALID;
96        }
97
98        Ok(result)
99    }
100
101    /// Allocates space in the VMO and returns the base offset of the allocated range.
102    fn allocate(&mut self, layout: Layout) -> Result<Offset, crate::Error> {
103        // Forbid alignment requirements greater than the page size, as they would have implications
104        // on how the VMO can be mapped for reading.
105        if layout.align() > zx::system_get_page_size() as usize {
106            return Err(crate::Error::InvalidInput);
107        }
108
109        let result_start = (self.watermark + layout.align() - 1) & !(layout.align() - 1);
110        let result_end = result_start + layout.size();
111
112        if result_end <= self.storage.vmo_size() {
113            self.watermark = result_end;
114            Ok(result_start as Offset)
115        } else {
116            Err(crate::Error::OutOfSpace)
117        }
118    }
119
120    /// This is the hash function for stack traces.
121    fn compute_bucket_index(compressed_stack_trace: &[u8]) -> usize {
122        let crc = CRC32_TABLE.checksum(compressed_stack_trace);
123        crc as usize % NUM_STACK_BUCKETS
124    }
125
126    /// Returns a mutable reference to the head of a given bucket's linked list.
127    fn stack_bucket_head_at(&mut self, bucket_index: usize) -> &mut Offset {
128        // The bucket heads are always stored at the beginning of the VMO.
129        let bucket_heads = self.storage.get_object_mut::<StackBucketHeads>(0).unwrap();
130        &mut bucket_heads[bucket_index]
131    }
132
133    /// Tries to find an already-inserted stack trace in the given bucket by scanning its linked
134    /// list.
135    fn find_in_bucket(
136        &mut self,
137        bucket_index: usize,
138        compressed_stack_trace: &[u8],
139    ) -> Option<Offset> {
140        let mut curr = *self.stack_bucket_head_at(bucket_index);
141        while curr != OFFSET_INVALID {
142            // Read the "next" field in the current node and its compressed stack trace, which is
143            // stored immediately afterwards.
144            let curr_next: Offset = *self.storage.get_object(curr as usize).unwrap();
145            let payload_offset = curr as usize + size_of_val(&curr_next);
146            let curr_payload = get_compressed_stack_trace(&self.storage, payload_offset).unwrap();
147
148            // Is this stack trace the one we were looking for?
149            if *curr_payload == *compressed_stack_trace {
150                return Some(curr);
151            }
152
153            curr = curr_next;
154        }
155
156        // Not found.
157        None
158    }
159
160    fn insert_in_bucket(
161        &mut self,
162        bucket_index: usize,
163        compressed_stack_trace: &[u8],
164    ) -> Result<Offset, crate::Error> {
165        // Allocate space for:
166        // - The "next" field
167        // - The stack trace length
168        // - The actual stack trace
169        let alloc_bytes =
170            size_of::<Offset>() + size_of::<StackTraceLength>() + compressed_stack_trace.len();
171        let alloc_align = align_of::<Offset>();
172        let new = self.allocate(Layout::from_size_align(alloc_bytes, alloc_align).unwrap())?;
173
174        let old_head = *self.stack_bucket_head_at(bucket_index);
175
176        // Write them.
177        *self.storage.get_object_mut(new as usize).unwrap() = old_head;
178        set_compressed_stack_trace(
179            &mut self.storage,
180            new as usize + size_of::<Offset>(),
181            compressed_stack_trace,
182        )
183        .unwrap();
184
185        // Update the bucket's head pointer.
186        *self.stack_bucket_head_at(bucket_index) = new;
187
188        Ok(new)
189    }
190
191    /// Appends a compressed stack trace and returns its offset into the VMO.
192    ///
193    /// This function also applies deduplication: if a copy of the given stack trace is already
194    /// present, the offset of the existing copy is returned without modifying the VMO contents.
195    pub fn intern_compressed_stack_trace(
196        &mut self,
197        compressed_stack_trace: &[u8],
198    ) -> Result<(ResourceKey, bool), crate::Error> {
199        // Verify that the length fits in StackTraceLength and return error if it does not.
200        if compressed_stack_trace.len() > StackTraceLength::MAX as usize {
201            return Err(crate::Error::BufferTooBig);
202        }
203
204        // Find/insert a StackNode and get its offset within the memory region.
205        let bucket_index = Self::compute_bucket_index(compressed_stack_trace);
206        let (offset, inserted) = match self.find_in_bucket(bucket_index, compressed_stack_trace) {
207            Some(offset) => (offset, false),
208            None => (self.insert_in_bucket(bucket_index, compressed_stack_trace)?, true),
209        };
210
211        // Adjust the returned offset to skip the "next" field (which is an internal
212        // ResourcesTableWriter implementation detail) and point directly to the StackTraceLength
213        // field (which is what ResourcesTableReader expects to receive).
214        let resource_key = ResourceKey(offset + size_of::<Offset>() as Offset);
215        Ok((resource_key, inserted))
216    }
217
218    /// Appends a thread information entry and returns its offset into the VMO.
219    pub fn insert_thread_info(
220        &mut self,
221        koid: zx::sys::zx_koid_t,
222        name: &zx::Name,
223    ) -> Result<ResourceKey, crate::Error> {
224        let offset = self.allocate(Layout::new::<ThreadInfo>())?;
225        *self.storage.get_object_mut(offset as usize).unwrap() = ThreadInfo { koid, name: *name };
226        Ok(ResourceKey(offset))
227    }
228}
229
230/// Mediates read access to a VMO written by ResourcesTableWriter.
231pub struct ResourcesTableReader {
232    storage: ReadOnlyMemoryMappedVmo,
233}
234
235impl ResourcesTableReader {
236    /// # Safety
237    /// The caller must guarantee that data in the `vmo` is not concurrently written by others.
238    /// Only using ResourceKeys received from the corresponding ResourcesTableWriter satisfies
239    /// this requirement.
240    pub unsafe fn new(vmo: &zx::Vmo) -> Result<ResourcesTableReader, crate::Error> {
241        let storage = unsafe { ReadOnlyMemoryMappedVmo::new(vmo)? };
242        Ok(ResourcesTableReader { storage })
243    }
244
245    /// Gets the compressed stack trace identified by `resource_key`.
246    pub fn get_compressed_stack_trace(
247        &self,
248        resource_key: ResourceKey,
249    ) -> Result<&[u8], crate::Error> {
250        let ResourceKey(offset) = resource_key;
251        get_compressed_stack_trace(&self.storage, offset as usize)
252    }
253
254    /// Gets the thread info entry identified by `resource_key`.
255    pub fn get_thread_info(&self, resource_key: ResourceKey) -> Result<&ThreadInfo, crate::Error> {
256        let ResourceKey(offset) = resource_key;
257        Ok(self.storage.get_object(offset as usize)?)
258    }
259}
260
261fn get_compressed_stack_trace(
262    storage: &ReadOnlyMemoryMappedVmo,
263    byte_offset: usize,
264) -> Result<&[u8], crate::Error> {
265    // Read the length.
266    let header: StackTraceLength = *storage.get_object(byte_offset)?;
267
268    // Get actual data, which is stored immediately after the length, as a slice.
269    Ok(storage.get_slice(byte_offset + size_of_val(&header), header as usize)?)
270}
271
272fn set_compressed_stack_trace(
273    storage: &mut ReadWriteMemoryMappedVmo,
274    byte_offset: usize,
275    compressed_stack_trace: &[u8],
276) -> Result<(), crate::Error> {
277    let header: StackTraceLength =
278        compressed_stack_trace.len().try_into().map_err(|_| crate::Error::BufferTooBig)?;
279
280    // Write the length.
281    *storage.get_object_mut(byte_offset)? = header;
282
283    // Write actual data immediately after the length.
284    storage
285        .get_slice_mut(byte_offset + size_of_val(&header), compressed_stack_trace.len())?
286        .copy_from_slice(compressed_stack_trace);
287
288    Ok(())
289}
290
291#[cfg(test)]
292mod tests {
293    use super::*;
294    use assert_matches::assert_matches;
295
296    // Some tests below use this constant to create a VMO with a known size.
297    const VMO_SIZE: usize = 4 * 1024 * 1024; // 4 MiB
298
299    struct TestStorage {
300        vmo: zx::Vmo,
301    }
302
303    impl TestStorage {
304        pub fn new(vmo_size: usize) -> (TestStorage, ResourcesTableWriter) {
305            let vmo = zx::Vmo::create(vmo_size as u64).unwrap();
306
307            // SAFETY: only one ResourcesTableWriter can ever be created.
308            let writer = unsafe { ResourcesTableWriter::new(&vmo) }.unwrap();
309
310            (TestStorage { vmo }, writer)
311        }
312
313        fn create_reader(&self) -> ResourcesTableReader {
314            // SAFETY: Tests only use ResourceKeys created by the writer to access it.
315            unsafe { ResourcesTableReader::new(&self.vmo) }.unwrap()
316        }
317    }
318
319    #[test]
320    fn test_stack_trace_deduplication() {
321        let (storage, mut writer) = TestStorage::new(VMO_SIZE);
322
323        // Insert different distinct stack traces and store the corresponding resource keys.
324        // The number of stack traces is chosen so that at least one bucket contains at least three
325        // stack traces.
326        const COUNT: usize = 2 * NUM_STACK_BUCKETS + 1;
327        let mut pairs = Vec::new();
328        for i in 0..COUNT {
329            // Generate a unique array of bytes and pretend it is a compressed stack trace.
330            let stack_trace = i.to_ne_bytes();
331
332            let (resource_key, inserted) =
333                writer.intern_compressed_stack_trace(&stack_trace).unwrap();
334            assert!(inserted, "expected true because the stack trace was not present");
335
336            pairs.push((stack_trace, resource_key));
337        }
338
339        // Verify that trying to insert them again returns the same resource keys.
340        for (stack_trace, expected_resource_key) in &pairs {
341            let (actual_resource_key, inserted) =
342                writer.intern_compressed_stack_trace(stack_trace).unwrap();
343            assert!(!inserted, "expected false because the stack trace is already present");
344            assert_eq!(actual_resource_key, *expected_resource_key);
345        }
346
347        // Verify that they can be read back.
348        let reader = storage.create_reader();
349        for (expected_stack_trace, resource_key) in &pairs {
350            let actual_stack_trace = reader.get_compressed_stack_trace(*resource_key).unwrap();
351            assert_eq!(actual_stack_trace, *expected_stack_trace);
352        }
353    }
354
355    #[test]
356    fn test_empty_stack_trace() {
357        let (storage, mut writer) = TestStorage::new(VMO_SIZE);
358
359        // It must be possible to insert the empty stack trace.
360        let (resource_key, inserted) = writer.intern_compressed_stack_trace(&[]).unwrap();
361        assert!(inserted);
362
363        // Verify that is can be read back correctly.
364        let reader = storage.create_reader();
365        let read_result = reader.get_compressed_stack_trace(resource_key).unwrap();
366        assert_eq!(read_result, []);
367    }
368
369    #[test]
370    fn test_long_stack_traces() {
371        let (storage, mut writer) = TestStorage::new(VMO_SIZE);
372
373        // Inserting a stack trace whose length cannot be represented in the length field (u16).
374        // should fail.
375        let stack_trace_too_long = vec![0xAA; u16::MAX as usize + 1];
376        let result = writer.intern_compressed_stack_trace(&stack_trace_too_long);
377        assert_matches!(result, Err(crate::Error::BufferTooBig));
378
379        // Inserting a stack trace with the maximum representable length should succeed.
380        let stack_trace_max_len = vec![0x55; u16::MAX as usize];
381        let (resource_key, _) = writer.intern_compressed_stack_trace(&stack_trace_max_len).unwrap();
382
383        // And it must be possible to read it back.
384        let reader = storage.create_reader();
385        let read_result = reader.get_compressed_stack_trace(resource_key).unwrap();
386        assert_eq!(read_result, stack_trace_max_len);
387    }
388
389    #[test]
390    fn test_write_until_out_of_space() {
391        let (_storage, mut writer) = TestStorage::new(VMO_SIZE);
392
393        // Insert many distinct stack traces and verify that, at some point, we get an OutOfSpace
394        // error. Instead of estimating exactly how many stack traces can fit, we just use VMO_SIZE
395        // as an upper bound before declaring failure (each distinct stack trace obviously requires
396        // at least one byte of storage).
397        for i in 0..=VMO_SIZE {
398            // Generate a unique array of bytes and pretend it is a compressed stack trace.
399            let stack_trace = i.to_ne_bytes();
400
401            if let Err(crate::Error::OutOfSpace) =
402                writer.intern_compressed_stack_trace(&stack_trace)
403            {
404                return; // Test passed
405            }
406        }
407
408        unreachable!("Inserted more than {} distinct stack traces", VMO_SIZE);
409    }
410
411    #[test]
412    fn test_thread_info() {
413        let (storage, mut writer) = TestStorage::new(VMO_SIZE);
414
415        // Insert a thread info struct with placeholder values (the name must be padded to the
416        // expected length).
417        const FAKE_KOID: zx::sys::zx_koid_t = 1234;
418        const FAKE_NAME: zx::Name = zx::Name::new_lossy("fake-name");
419        let resource_key = writer.insert_thread_info(FAKE_KOID, &FAKE_NAME).unwrap();
420
421        // Verify that it can be read back correctly.
422        let reader = storage.create_reader();
423        let thread_info = reader.get_thread_info(resource_key).unwrap();
424        assert_eq!(thread_info.koid, FAKE_KOID);
425        assert_eq!(thread_info.name, FAKE_NAME);
426    }
427}