Skip to main content

heapdump_vmo/
allocations_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::{
6    ReadOnlyMemoryMappedVmo, ReadWriteMemoryMappedArcVmo, ReadWriteMemoryMappedVmo,
7};
8use static_assertions::const_assert;
9use std::collections::HashSet;
10use std::mem::{align_of, size_of};
11use std::ops::Deref;
12use std::sync::Mutex;
13use std::sync::atomic::AtomicU32;
14use std::sync::atomic::Ordering::{Relaxed, SeqCst};
15use zerocopy::FromBytes;
16
17pub use crate::resources_table_v1::ResourceKey;
18
19type NodeIndex = u32;
20type BucketHeads = [AtomicNodeIndex; NUM_BUCKETS];
21const NUM_BUCKETS: usize = 1 << 16;
22const NODE_INVALID: NodeIndex = NodeIndex::MAX;
23
24/// Minimum memory alignment of an allocation table.
25pub const MIN_ALIGNMENT: usize = align_of::<Node>();
26const_assert!(MIN_ALIGNMENT % align_of::<BucketHeads>() == 0);
27
28/// Maximum number of free nodes maintained locally in each shard. They can be
29/// used without locking the centralized SharedAllocator.
30const LOCAL_CACHE_CAPACITY: usize = 24;
31
32/// Number of free nodes transferred at once to/from the SharedAllocator.
33const REFILL_SPILL_BATCH: usize = 12;
34
35// Define AtomicNodeIndex as a newtype so we can implement traits on it.
36#[repr(transparent)]
37#[derive(Debug, FromBytes)]
38struct AtomicNodeIndex(AtomicU32);
39
40impl AtomicNodeIndex {
41    const fn new(value: u32) -> AtomicNodeIndex {
42        AtomicNodeIndex(AtomicU32::new(value))
43    }
44}
45
46impl Deref for AtomicNodeIndex {
47    type Target = AtomicU32;
48
49    fn deref(&self) -> &AtomicU32 {
50        &self.0
51    }
52}
53
54#[repr(C)]
55#[derive(Debug, FromBytes)]
56pub struct Node {
57    next: AtomicNodeIndex,
58    pub address: u64,
59    pub size: u64,
60    pub timestamp: zx::MonotonicInstant,
61    pub thread_info_key: ResourceKey,
62    pub stack_trace_key: ResourceKey,
63}
64
65/// Computes the capacity (number of nodes) of a memory region given its size (bytes).
66fn compute_nodes_count(num_bytes: usize) -> Result<usize, crate::Error> {
67    let Some(nodes_size) = num_bytes.checked_sub(size_of::<BucketHeads>()) else {
68        return Err(crate::Error::BufferTooSmall);
69    };
70
71    let num_nodes = nodes_size / size_of::<Node>();
72    if num_nodes > NODE_INVALID as usize {
73        return Err(crate::Error::BufferTooBig);
74    }
75
76    Ok(num_nodes)
77}
78
79/// Precomputed shard and bucket indices for a given allocation address.
80#[derive(Clone, Copy, Debug, PartialEq, Eq)]
81pub struct AllocationKey<const NUM_SHARDS: usize> {
82    shard_index: usize,
83    bucket_index: usize,
84    address: u64,
85}
86
87impl<const NUM_SHARDS: usize> AllocationKey<NUM_SHARDS> {
88    /// Computes the key for `address` when the table is partitioned into `NUM_SHARDS` shards.
89    pub const fn make(address: u64) -> AllocationKey<NUM_SHARDS> {
90        let key = address ^ (address >> 33);
91        let hash = key.wrapping_mul(0x9e3779b97f4a7c15);
92
93        // Take the top N bits.
94        const { assert!(NUM_BUCKETS.is_power_of_two()) };
95        const { assert!(NUM_SHARDS.is_power_of_two()) };
96        const { assert!(NUM_BUCKETS >= NUM_SHARDS) };
97        let bucket_index = (hash >> (64 - NUM_BUCKETS.ilog2())) as usize;
98        let shard_index = bucket_index / (NUM_BUCKETS / NUM_SHARDS);
99
100        AllocationKey { shard_index, bucket_index, address }
101    }
102
103    pub const fn shard_index(&self) -> usize {
104        self.shard_index
105    }
106
107    pub const fn address(&self) -> u64 {
108        self.address
109    }
110}
111
112/// Mediates write access to a VMO containing an hash table of allocated blocks indexed by block
113/// address.
114///
115/// Operations can be "sharded" according to a hash function of the block address (see
116/// `AllocationKey`). All the shards operate on the same VMO concurrently, but their implementation
117/// is such that they never end up accessing the same data. Hence, callers do not need to worry
118/// about race conditions.
119///
120/// From the point of view of an external observer, all the updates happen atomically so that, at
121/// any time, snapshotting the VMO always results in a coherent snapshot of the table. The value of
122/// `NUM_SHARDS` does not affect the "wire format" of data stored in the VMO.
123///
124/// Hash collisions are handled by maintaining per-bucket linked lists. Specifically, an array of
125/// list heads, one for each bucket, is stored at the beginning of the VMO. The remaining part of
126/// the VMO contains the linked list nodes.
127pub struct AllocationsTableWriter<const NUM_SHARDS: usize> {
128    storage: ReadWriteMemoryMappedArcVmo<Mutex<AllocationsTableWriterSharedState>>,
129    shard_index: usize,
130    local_free_list_head: NodeIndex,
131    local_free_count: usize,
132}
133
134/// Writer state shared by all the shards, stored in the ReadWriteMemoryMappedArcVmo's metadata
135/// and protected by a mutex.
136///
137/// Each shard maintains a local cache of up to LOCAL_CACHE_CAPACITY free nodes. When this cache
138/// overflows/underflows, the centralized state is used to drain/refill it.
139///
140/// Information in this struct is not necessary for reading a snapshot. Therefore, we do not need
141/// to store them in the main VMO or to offer any snapshot guarantee about them.
142struct AllocationsTableWriterSharedState {
143    // New free nodes are minted by a simple watermark allocator and the capacity of the hash table
144    // (i.e. the maximum number of nodes) is fixed.
145    watermark: NodeIndex,
146    max_num_nodes: usize,
147
148    // In order to make it possible to reuse nodes that have been freed, we also keep a linked list
149    // of free nodes in addition to the watermark. When it is not empty, nodes are allocated by
150    // popping the head of this list instead of incrementing the watermark.
151    free_list_head: NodeIndex,
152}
153
154impl<const NUM_SHARDS: usize> AllocationsTableWriter<NUM_SHARDS> {
155    /// Initializes a VMO as an empty table and creates `NUM_SHARDS` writers that can operate on
156    /// disjoint shards of the table concurrently.
157    ///
158    /// Each address must be routed to the shard identified by `key.shard_index()`, where `key` is
159    /// computed with `AllocationKey::make(address)`.
160    ///
161    /// # Safety
162    /// The caller must guarantee that the `vmo` is not accessed by others while the returned
163    /// instances are alive. However, it is always safe to take a snapshot and read that instead.
164    pub unsafe fn new(
165        vmo: &zx::Vmo,
166        control_vmo_name: &zx::Name,
167    ) -> Result<[AllocationsTableWriter<NUM_SHARDS>; NUM_SHARDS], crate::Error> {
168        let mut raw_storage = unsafe { ReadWriteMemoryMappedVmo::new(vmo)? };
169        let max_num_nodes = compute_nodes_count(raw_storage.vmo_size())?;
170
171        // Clear the hash table.
172        let bucket_heads = raw_storage.get_object_mut::<BucketHeads>(0).unwrap();
173        for bucket_head in bucket_heads.iter() {
174            bucket_head.store(NODE_INVALID, SeqCst);
175        }
176
177        let shared_allocator = Mutex::new(AllocationsTableWriterSharedState {
178            max_num_nodes,
179            watermark: 0,
180            free_list_head: NODE_INVALID,
181        });
182        let storage = unsafe {
183            ReadWriteMemoryMappedArcVmo::wrap_with_name(
184                raw_storage,
185                shared_allocator,
186                control_vmo_name,
187            )?
188        };
189
190        Ok(std::array::from_fn(|shard_index| AllocationsTableWriter {
191            storage: storage.clone(),
192            shard_index,
193            local_free_list_head: NODE_INVALID,
194            local_free_count: 0,
195        }))
196    }
197
198    /// Returns a reference to the head of a given bucket's linked list.
199    fn bucket_head_at(&self, bucket_index: usize) -> &AtomicNodeIndex {
200        // The bucket heads are stored at the beginning of the VMO.
201        let bucket_heads = self.storage.get_object::<BucketHeads>(0).unwrap();
202        &bucket_heads[bucket_index]
203    }
204
205    /// Returns a reference to a given node.
206    fn node_at(&self, node_index: NodeIndex) -> &Node {
207        // The nodes are stored consecutively immediately after the bucket heads.
208        let byte_offset = size_of::<BucketHeads>() + node_index as usize * size_of::<Node>();
209        self.storage.get_object::<Node>(byte_offset).unwrap()
210    }
211
212    /// Returns a mutable reference to a given node.
213    fn node_at_mut(&mut self, node_index: NodeIndex) -> &mut Node {
214        // The nodes are stored consecutively immediately after the bucket heads.
215        let byte_offset = size_of::<BucketHeads>() + node_index as usize * size_of::<Node>();
216        self.storage.get_object_mut::<Node>(byte_offset).unwrap()
217    }
218
219    /// Inserts a new entry in the hash table.
220    ///
221    /// Returns Ok(true) if it has been inserted, Ok(false) if a previous entry with the same
222    /// address already existed, or an error if no free nodes are available.
223    pub fn insert_allocation(
224        &mut self,
225        key: AllocationKey<NUM_SHARDS>,
226        size: u64,
227        thread_info_key: ResourceKey,
228        stack_trace_key: ResourceKey,
229        timestamp: zx::MonotonicInstant,
230    ) -> Result<bool, crate::Error> {
231        assert_eq!(key.shard_index, self.shard_index);
232
233        let old_head = self.bucket_head_at(key.bucket_index).load(Relaxed);
234
235        // Verify that no entry with the same address already exists.
236        let mut curr_index = old_head;
237        while curr_index != NODE_INVALID {
238            let curr_data = self.node_at(curr_index);
239            if curr_data.address == key.address {
240                return Ok(false);
241            }
242            curr_index = curr_data.next.load(Relaxed);
243        }
244
245        // Insert a new entry at the head of the list.
246        let new_index = self.pop_free_node()?;
247        *self.node_at_mut(new_index) = Node {
248            address: key.address,
249            size,
250            timestamp,
251            thread_info_key,
252            stack_trace_key,
253            next: AtomicNodeIndex::new(old_head),
254        };
255        self.bucket_head_at(key.bucket_index).store(new_index, SeqCst);
256        Ok(true)
257    }
258
259    /// Removes an entry from the hash table and returns the value of the removed entry's size
260    /// field.
261    pub fn erase_allocation(&mut self, key: AllocationKey<NUM_SHARDS>) -> Option<u64> {
262        assert_eq!(key.shard_index, self.shard_index);
263
264        // Search the entry to be removed.
265        let mut prev_index = None;
266        let mut curr_index = self.bucket_head_at(key.bucket_index).load(Relaxed);
267        while curr_index != NODE_INVALID {
268            let curr_data = self.node_at(curr_index);
269            let curr_data_size = curr_data.size;
270            let curr_data_next = curr_data.next.load(Relaxed);
271
272            // Is this the entry we were looking for?
273            if curr_data.address == key.address {
274                if let Some(prev_index) = prev_index {
275                    self.node_at(prev_index).next.store(curr_data_next, SeqCst);
276                } else {
277                    self.bucket_head_at(key.bucket_index).store(curr_data_next, SeqCst);
278                }
279                self.push_free_node(curr_index);
280                return Some(curr_data_size);
281            }
282
283            prev_index = Some(curr_index);
284            curr_index = curr_data.next.load(Relaxed);
285        }
286
287        // Not found.
288        None
289    }
290
291    /// Atomically updates metadata for the given address in the hash table.
292    ///
293    /// Returns Ok(Some(old_size)) if it has been updated, Ok(None) if no entry with the given
294    /// address exists, or an error if no free nodes are available.
295    pub fn replace_allocation(
296        &mut self,
297        key: AllocationKey<NUM_SHARDS>,
298        size: u64,
299        thread_info_key: ResourceKey,
300        stack_trace_key: ResourceKey,
301        timestamp: zx::MonotonicInstant,
302    ) -> Result<Option<u64>, crate::Error> {
303        assert_eq!(key.shard_index, self.shard_index);
304
305        // Updates are implemented as an atomic insertion of a new node at the head followed by an
306        // atomic deletion of the old node. Therefore, while an update is in progress, two nodes
307        // with the same address may exist. `AllocationsTableReader`` is aware of this, and it
308        // only returns the first one (i.e. the newer one) in such a case.
309        // Note: the body of this function has been split in two parts to test reads of the
310        // intermediate state.
311
312        // Check preconditions and insert the new node.
313        let Some((new_node, old_node, old_size)) =
314            self.replace_allocation_begin(key, size, thread_info_key, stack_trace_key, timestamp)?
315        else {
316            return Ok(None);
317        };
318
319        // Remove the old node.
320        self.replace_allocation_end(new_node, old_node);
321        Ok(Some(old_size))
322    }
323
324    /// First part of `replace_allocation`. Locates the old node and inserts the new one at the head
325    fn replace_allocation_begin(
326        &mut self,
327        key: AllocationKey<NUM_SHARDS>,
328        size: u64,
329        thread_info_key: ResourceKey,
330        stack_trace_key: ResourceKey,
331        timestamp: zx::MonotonicInstant,
332    ) -> Result<Option<(NodeIndex, NodeIndex, u64)>, crate::Error> {
333        let old_head = self.bucket_head_at(key.bucket_index).load(Relaxed);
334
335        // Search the existing entry with the requested address.
336        let (old_index, old_size) = 'search: {
337            let mut curr_index = old_head;
338            while curr_index != NODE_INVALID {
339                let curr_data = self.node_at(curr_index);
340                if curr_data.address == key.address {
341                    break 'search (curr_index, curr_data.size); // Found.
342                }
343                curr_index = curr_data.next.load(Relaxed);
344            }
345
346            return Ok(None); // Not found.
347        };
348
349        // Insert a new entry at the head of the list.
350        let new_index = self.pop_free_node()?;
351        *self.node_at_mut(new_index) = Node {
352            address: key.address,
353            size,
354            timestamp,
355            thread_info_key,
356            stack_trace_key,
357            next: AtomicNodeIndex::new(old_head),
358        };
359        self.bucket_head_at(key.bucket_index).store(new_index, SeqCst);
360        Ok(Some((new_index, old_index, old_size)))
361    }
362
363    // Second part of `replace_allocation`. Removes the old node.
364    fn replace_allocation_end(&mut self, new_node: NodeIndex, old_node: NodeIndex) {
365        let tail = self.node_at(old_node).next.load(Relaxed);
366
367        // Note: `replace_allocation_begin` always inserts the new node at the head; therefore, the
368        // old node will always be one of its successors.
369        let mut scan_node = new_node;
370        loop {
371            assert_ne!(scan_node, NODE_INVALID, "the old node must be a successor of new node");
372            let scan_data = self.node_at(scan_node);
373            if scan_data.next.load(Relaxed) == old_node {
374                scan_data.next.store(tail, SeqCst);
375                self.push_free_node(old_node);
376                return;
377            }
378            scan_node = scan_data.next.load(Relaxed);
379        }
380    }
381
382    /// Inserts a node into the free list, spilling half the local cache to the shared free list if
383    /// the local cache exceeds `LOCAL_CACHE_CAPACITY`.
384    fn push_free_node(&mut self, index: NodeIndex) {
385        self.node_at(index).next.store(self.local_free_list_head, Relaxed);
386        self.local_free_list_head = index;
387        self.local_free_count += 1;
388
389        if self.local_free_count > LOCAL_CACHE_CAPACITY {
390            let mut shared = self.storage.metadata().lock().unwrap();
391
392            // Transfer REFILL_SPILL_BATCH nodes from the local free list to the shared free list.
393            for _ in 0..REFILL_SPILL_BATCH {
394                let node_index = self.local_free_list_head;
395                let node_data = self.node_at(node_index);
396                let next_local = node_data.next.load(Relaxed);
397
398                node_data.next.store(shared.free_list_head, Relaxed);
399                shared.free_list_head = node_index;
400
401                self.local_free_list_head = next_local;
402                self.local_free_count -= 1;
403            }
404        }
405    }
406
407    /// Takes a node from the local free list, refilling `REFILL_SPILL_BATCH` nodes from the shared
408    /// free list or watermark allocator when the local cache is empty.
409    fn pop_free_node(&mut self) -> Result<NodeIndex, crate::Error> {
410        if self.local_free_list_head == NODE_INVALID {
411            let mut shared = self.storage.metadata().lock().unwrap();
412
413            // Obtain up to REFILL_SPILL_BATCH nodes from the shared allocator.
414            while self.local_free_count < REFILL_SPILL_BATCH {
415                let new_node = if shared.free_list_head != NODE_INVALID {
416                    // Pop a node from the free list.
417                    let index = shared.free_list_head;
418                    shared.free_list_head = self.node_at(index).next.load(Relaxed);
419                    index
420                } else if (shared.watermark as usize) < shared.max_num_nodes {
421                    // Allocate one node with the watermark allocator.
422                    let index = shared.watermark;
423                    shared.watermark += 1;
424                    index
425                } else {
426                    break;
427                };
428
429                self.node_at(new_node).next.store(self.local_free_list_head, Relaxed);
430                self.local_free_list_head = new_node;
431                self.local_free_count += 1;
432            }
433        }
434
435        if self.local_free_list_head != NODE_INVALID {
436            let result = self.local_free_list_head;
437            self.local_free_list_head = self.node_at(result).next.load(Relaxed);
438            self.local_free_count -= 1;
439            Ok(result)
440        } else {
441            // We are out of space.
442            Err(crate::Error::OutOfSpace)
443        }
444    }
445}
446
447/// Mediates read access to a VMO written by AllocationsTableWriter.
448pub struct AllocationsTableReader {
449    storage: ReadOnlyMemoryMappedVmo,
450    max_num_nodes: usize,
451}
452
453impl AllocationsTableReader {
454    /// # Safety
455    /// The caller must guarantee that the `vmo` is not accessed by others while the returned
456    /// instance is alive, usually by taking a snapshot of the VMO that AllocationsTableWriter
457    /// operates on and then reading the snapshot instead.
458    pub unsafe fn new(vmo: &zx::Vmo) -> Result<AllocationsTableReader, crate::Error> {
459        let storage = unsafe { ReadOnlyMemoryMappedVmo::new(vmo)? };
460        let max_num_nodes = compute_nodes_count(storage.vmo_size())?;
461
462        Ok(AllocationsTableReader { storage, max_num_nodes })
463    }
464
465    /// Iterates all the allocated blocks that are present in the hash table.
466    pub fn iter(&self) -> impl Iterator<Item = Result<&Node, crate::Error>> {
467        // The bucket heads are stored at the beginning of the VMO.
468        let bucket_heads = self.storage.get_object::<BucketHeads>(0).unwrap();
469        bucket_heads.iter().map(|head| self.iterate_bucket(head.load(Relaxed))).flatten()
470    }
471
472    fn iterate_bucket(&self, head: NodeIndex) -> impl Iterator<Item = Result<&Node, crate::Error>> {
473        let mut curr_index = head;
474        let mut seen_addresses = HashSet::new();
475        std::iter::from_fn(move || {
476            while curr_index != NODE_INVALID {
477                if (curr_index as usize) < self.max_num_nodes {
478                    // The nodes are stored consecutively immediately after the bucket heads.
479                    let byte_offset =
480                        size_of::<BucketHeads>() + curr_index as usize * size_of::<Node>();
481                    let curr_data = self.storage.get_object::<Node>(byte_offset).unwrap();
482                    curr_index = curr_data.next.load(Relaxed);
483
484                    // Only return the first occurrence of each address. This ensures that only the
485                    // newer record is returned if an update is in progress.
486                    if seen_addresses.insert(curr_data.address) {
487                        return Some(Ok(curr_data));
488                    }
489                } else {
490                    return Some(Err(crate::Error::InvalidInput));
491                };
492            }
493
494            None
495        })
496    }
497}
498
499#[cfg(test)]
500mod tests {
501    use super::*;
502    use assert_matches::assert_matches;
503    use std::alloc::Layout;
504    use std::collections::HashMap;
505
506    // Some tests below use this constant to ensure that each bucket is been used at least 10 times.
507    const NUM_ITERATIONS: usize = NUM_BUCKETS * 10;
508
509    // Ensure we allocate enough nodes to store all the blocks the tests need plus some buffer.
510    const NUM_NODES: usize = NUM_ITERATIONS + 100;
511
512    // Placeholder values used in the tests below:
513    const THREAD_INFO_RESOURCE_KEY_1: ResourceKey = ResourceKey::from_raw(0x1122);
514    const THREAD_INFO_RESOURCE_KEY_2: ResourceKey = ResourceKey::from_raw(0x3344);
515    const STACK_TRACE_RESOURCE_KEY_1: ResourceKey = ResourceKey::from_raw(0x1212);
516    const STACK_TRACE_RESOURCE_KEY_2: ResourceKey = ResourceKey::from_raw(0x3434);
517
518    struct TestStorage {
519        vmo: zx::Vmo,
520    }
521
522    impl TestStorage {
523        pub fn new(num_nodes: usize) -> (TestStorage, AllocationsTableWriter<1>) {
524            let (storage, [writer]) = Self::new_sharded::<1>(num_nodes);
525            (storage, writer)
526        }
527
528        pub fn new_sharded<const NUM_SHARDS: usize>(
529            num_nodes: usize,
530        ) -> (TestStorage, [AllocationsTableWriter<NUM_SHARDS>; NUM_SHARDS]) {
531            let nodes_layout = Layout::array::<Node>(num_nodes).unwrap();
532            let (layout, nodes_offset) = Layout::new::<BucketHeads>().extend(nodes_layout).unwrap();
533            assert_eq!(nodes_offset, size_of::<BucketHeads>());
534
535            let vmo = zx::Vmo::create(layout.size() as u64).unwrap();
536
537            // SAFETY: Each shard writer in the returned array operates on a disjoint shard.
538            let writers =
539                unsafe { AllocationsTableWriter::new(&vmo, &zx::Name::default()) }.unwrap();
540
541            (TestStorage { vmo }, writers)
542        }
543
544        fn create_reader(&self) -> AllocationsTableReader {
545            let snapshot = self
546                .vmo
547                .create_child(
548                    zx::VmoChildOptions::SNAPSHOT | zx::VmoChildOptions::NO_WRITE,
549                    0,
550                    self.vmo.get_size().unwrap(),
551                )
552                .unwrap();
553
554            // SAFETY: Readers operate on the read-only VMO snapshot we just created.
555            unsafe { AllocationsTableReader::new(&snapshot) }.unwrap()
556        }
557    }
558
559    #[test]
560    fn test_cannot_insert_twice() {
561        let (_storage, mut writer) = TestStorage::new(NUM_NODES);
562
563        let result = writer.insert_allocation(
564            AllocationKey::make(0x1234),
565            0x5678,
566            THREAD_INFO_RESOURCE_KEY_1,
567            STACK_TRACE_RESOURCE_KEY_1,
568            zx::MonotonicInstant::ZERO,
569        );
570        assert_eq!(result, Ok(true));
571
572        let result = writer.insert_allocation(
573            AllocationKey::make(0x1234),
574            0x5678,
575            THREAD_INFO_RESOURCE_KEY_1,
576            STACK_TRACE_RESOURCE_KEY_1,
577            zx::MonotonicInstant::ZERO,
578        );
579        assert_eq!(result, Ok(false));
580    }
581
582    #[test]
583    fn test_cannot_erase_twice() {
584        let (_storage, mut writer) = TestStorage::new(NUM_NODES);
585
586        let result = writer.insert_allocation(
587            AllocationKey::make(0x1234),
588            0x5678,
589            THREAD_INFO_RESOURCE_KEY_1,
590            STACK_TRACE_RESOURCE_KEY_1,
591            zx::MonotonicInstant::ZERO,
592        );
593        assert_eq!(result, Ok(true));
594
595        let result = writer.erase_allocation(AllocationKey::make(0x1234));
596        assert_eq!(result, Some(0x5678));
597
598        let result = writer.erase_allocation(AllocationKey::make(0x1234));
599        assert_eq!(result, None);
600    }
601
602    #[test]
603    fn test_out_of_space() {
604        let (_storage, mut writer) = TestStorage::new(NUM_NODES);
605
606        // Test that inserting up to `NUM_NODES` works.
607        for i in 0..NUM_NODES {
608            let result = writer.insert_allocation(
609                AllocationKey::make(i as u64),
610                1,
611                THREAD_INFO_RESOURCE_KEY_1,
612                STACK_TRACE_RESOURCE_KEY_1,
613                zx::MonotonicInstant::ZERO,
614            );
615            assert_eq!(result, Ok(true));
616        }
617
618        // Test that inserting an extra node fails.
619        let result = writer.insert_allocation(
620            AllocationKey::make(NUM_NODES as u64),
621            1,
622            THREAD_INFO_RESOURCE_KEY_1,
623            STACK_TRACE_RESOURCE_KEY_1,
624            zx::MonotonicInstant::ZERO,
625        );
626        assert_eq!(result, Err(crate::Error::OutOfSpace));
627
628        // Test that removing an element and then inserting again succeeds.
629        let result = writer.erase_allocation(AllocationKey::make(0));
630        assert_eq!(result, Some(1));
631        let result = writer.insert_allocation(
632            AllocationKey::make(NUM_NODES as u64),
633            1,
634            THREAD_INFO_RESOURCE_KEY_1,
635            STACK_TRACE_RESOURCE_KEY_1,
636            zx::MonotonicInstant::ZERO,
637        );
638        assert_eq!(result, Ok(true));
639    }
640
641    #[test]
642    fn test_loop_insert_then_erase() {
643        let (_storage, mut writer) = TestStorage::new(NUM_NODES);
644
645        for i in 0..NUM_ITERATIONS {
646            let result = writer.insert_allocation(
647                AllocationKey::make(i as u64),
648                1,
649                THREAD_INFO_RESOURCE_KEY_1,
650                STACK_TRACE_RESOURCE_KEY_1,
651                zx::MonotonicInstant::ZERO,
652            );
653            assert_eq!(result, Ok(true), "failed to insert 0x{:x}", i);
654
655            let result = writer.erase_allocation(AllocationKey::make(i as u64));
656            assert_eq!(result, Some(1), "failed to erase 0x{:x}", i);
657        }
658    }
659
660    #[test]
661    fn test_bulk_insert_then_erase_same_order() {
662        let (_storage, mut writer) = TestStorage::new(NUM_NODES);
663
664        for i in 0..NUM_ITERATIONS {
665            let result = writer.insert_allocation(
666                AllocationKey::make(i as u64),
667                1,
668                THREAD_INFO_RESOURCE_KEY_1,
669                STACK_TRACE_RESOURCE_KEY_1,
670                zx::MonotonicInstant::ZERO,
671            );
672            assert_eq!(result, Ok(true), "failed to insert 0x{:x}", i);
673        }
674        for i in 0..NUM_ITERATIONS {
675            let result = writer.erase_allocation(AllocationKey::make(i as u64));
676            assert_eq!(result, Some(1), "failed to erase 0x{:x}", i);
677        }
678    }
679
680    #[test]
681    fn test_bulk_insert_then_erase_reverse_order() {
682        let (_storage, mut writer) = TestStorage::new(NUM_NODES);
683
684        for i in 0..NUM_ITERATIONS {
685            let result = writer.insert_allocation(
686                AllocationKey::make(i as u64),
687                1,
688                THREAD_INFO_RESOURCE_KEY_1,
689                STACK_TRACE_RESOURCE_KEY_1,
690                zx::MonotonicInstant::ZERO,
691            );
692            assert_eq!(result, Ok(true), "failed to insert 0x{:x}", i);
693        }
694        for i in (0..NUM_ITERATIONS).rev() {
695            let result = writer.erase_allocation(AllocationKey::make(i as u64));
696            assert_eq!(result, Some(1), "failed to erase 0x{:x}", i);
697        }
698    }
699
700    #[test]
701    fn test_read_empty() {
702        let (storage, _writer) = TestStorage::new(NUM_NODES);
703
704        // Verify that it is empty.
705        let reader = storage.create_reader();
706        assert_eq!(reader.iter().count(), 0);
707    }
708
709    #[test]
710    fn test_read_populated() {
711        let (storage, mut writer) = TestStorage::new(NUM_NODES);
712
713        // Fill the hash table.
714        let mut expected_map = HashMap::new();
715        for i in 0..NUM_ITERATIONS as u64 {
716            let thread_info_key =
717                if i % 4 >= 2 { THREAD_INFO_RESOURCE_KEY_1 } else { THREAD_INFO_RESOURCE_KEY_2 };
718            let stack_trace_key =
719                if i % 2 == 0 { STACK_TRACE_RESOURCE_KEY_1 } else { STACK_TRACE_RESOURCE_KEY_2 };
720            let timestamp =
721                zx::MonotonicInstant::from_nanos((NUM_ITERATIONS as i64 / 2) - (i as i64)); // test negative values too
722            let result = writer.insert_allocation(
723                AllocationKey::make(i),
724                1,
725                thread_info_key,
726                stack_trace_key,
727                timestamp,
728            );
729            assert_eq!(result, Ok(true), "failed to insert 0x{:x}", i);
730
731            expected_map.insert(i, (1, thread_info_key, stack_trace_key, timestamp));
732        }
733
734        // Read it back and verify.
735        let reader = storage.create_reader();
736        let mut actual_map = HashMap::new();
737        for node in reader.iter() {
738            let Node { address, size, thread_info_key, stack_trace_key, timestamp, .. } =
739                node.unwrap();
740            assert!(
741                actual_map
742                    .insert(*address, (*size, *thread_info_key, *stack_trace_key, *timestamp))
743                    .is_none(),
744                "address 0x{:x} was read more than once",
745                address
746            );
747        }
748
749        assert_eq!(actual_map, expected_map);
750    }
751
752    #[test]
753    fn test_read_bad_bucket_head() {
754        let (storage, writer) = TestStorage::new(NUM_NODES);
755
756        // Initialize the hash table and corrupt one of the heads.
757        writer.bucket_head_at(NUM_BUCKETS / 2).store(NODE_INVALID - 1, SeqCst);
758
759        // Try to read it back and verify that the iterator returns an error.
760        let reader = storage.create_reader();
761        let contains_error = reader.iter().any(|e| e.is_err());
762
763        assert!(contains_error);
764    }
765
766    #[test]
767    fn test_read_bad_node_next() {
768        let (storage, mut writer) = TestStorage::new(NUM_NODES);
769
770        // Initialize the hash table and insert a node with a bad next pointer.
771        writer.bucket_head_at(NUM_BUCKETS / 2).store(0, SeqCst);
772        *writer.node_at_mut(0) = Node {
773            next: AtomicNodeIndex::new(NODE_INVALID - 1),
774            address: 0x1234,
775            size: 0x5678,
776            thread_info_key: THREAD_INFO_RESOURCE_KEY_1,
777            stack_trace_key: STACK_TRACE_RESOURCE_KEY_1,
778            timestamp: zx::MonotonicInstant::from_nanos(99999999),
779        };
780
781        // Try to read it back and verify that the iterator returns an error.
782        let reader = storage.create_reader();
783        let contains_error = reader.iter().any(|e| e.is_err());
784
785        assert!(contains_error);
786    }
787
788    #[test]
789    fn test_replace() {
790        let (storage, mut writer) = TestStorage::new(NUM_NODES);
791
792        let result = writer.insert_allocation(
793            AllocationKey::make(0x1234),
794            0x1111,
795            THREAD_INFO_RESOURCE_KEY_1,
796            STACK_TRACE_RESOURCE_KEY_1,
797            zx::MonotonicInstant::ZERO,
798        );
799        assert_eq!(result, Ok(true));
800
801        // Verify initial contents.
802        let reader = storage.create_reader();
803        let mut iter = reader.iter();
804        assert_matches!(iter.next(), Some(Ok(Node { address: 0x1234, size: 0x1111, .. })));
805        assert_matches!(iter.next(), None);
806
807        let result = writer.replace_allocation_begin(
808            AllocationKey::make(0x1234),
809            0x2222,
810            THREAD_INFO_RESOURCE_KEY_2,
811            STACK_TRACE_RESOURCE_KEY_2,
812            zx::MonotonicInstant::ZERO,
813        );
814        let Ok(Some((new_node, old_node, old_size))) = result else {
815            panic!("Update begin is supposed to succeed in this test, got {:?} instead", result)
816        };
817        assert_eq!(old_size, 0x1111);
818
819        // Verify that intermediate contents already reflect the update.
820        let reader = storage.create_reader();
821        let mut iter = reader.iter();
822        assert_matches!(iter.next(), Some(Ok(Node { address: 0x1234, size: 0x2222, .. })));
823        assert_matches!(iter.next(), None);
824
825        writer.replace_allocation_end(new_node, old_node);
826
827        // Verify that final contents reflect the update.
828        let reader = storage.create_reader();
829        let mut iter = reader.iter();
830        assert_matches!(iter.next(), Some(Ok(Node { address: 0x1234, size: 0x2222, .. })));
831        assert_matches!(iter.next(), None);
832    }
833
834    #[test]
835    fn test_cannot_replace_nonexisting() {
836        let (_storage, mut writer) = TestStorage::new(NUM_NODES);
837
838        let result = writer.replace_allocation(
839            AllocationKey::make(0x1234),
840            0x5678,
841            THREAD_INFO_RESOURCE_KEY_1,
842            STACK_TRACE_RESOURCE_KEY_1,
843            zx::MonotonicInstant::ZERO,
844        );
845        assert_eq!(result, Ok(None));
846    }
847
848    #[test]
849    fn test_sharded_spill_and_refill() {
850        let (storage, [mut writer0, mut writer1]) = TestStorage::new_sharded::<2>(NUM_NODES);
851
852        // Helper to generate keys belonging to shard 0 or shard 1.
853        let key_for_shard = |shard: usize, i: usize| -> AllocationKey<2> {
854            AllocationKey {
855                shard_index: shard,
856                bucket_index: (shard * NUM_BUCKETS + i % NUM_BUCKETS) / 2,
857                address: ((i * 2 + shard) as u64) << 4,
858            }
859        };
860
861        // Exhaust all `NUM_NODES` nodes on shard 0.
862        for i in 0..NUM_NODES {
863            assert_eq!(
864                writer0.insert_allocation(
865                    key_for_shard(0, i),
866                    1,
867                    THREAD_INFO_RESOURCE_KEY_1,
868                    STACK_TRACE_RESOURCE_KEY_1,
869                    zx::MonotonicInstant::ZERO,
870                ),
871                Ok(true)
872            );
873        }
874
875        // Shard 1 cannot allocate yet because watermark is at NUM_NODES and nothing was spilled.
876        assert_eq!(
877            writer1.insert_allocation(
878                key_for_shard(1, 0),
879                1,
880                THREAD_INFO_RESOURCE_KEY_1,
881                STACK_TRACE_RESOURCE_KEY_1,
882                zx::MonotonicInstant::ZERO,
883            ),
884            Err(crate::Error::OutOfSpace)
885        );
886
887        // Free `LOCAL_CACHE_CAPACITY + 1` nodes on shard 0 to trigger a spill of
888        // `REFILL_SPILL_BATCH` nodes to the shared free list.
889        for i in 0..(LOCAL_CACHE_CAPACITY + 1) {
890            assert_eq!(writer0.erase_allocation(key_for_shard(0, i)), Some(1));
891        }
892
893        // Shard 1 can now allocate `REFILL_SPILL_BATCH` nodes from the shared free list.
894        for i in 0..REFILL_SPILL_BATCH {
895            assert_eq!(
896                writer1.insert_allocation(
897                    key_for_shard(1, i),
898                    2,
899                    THREAD_INFO_RESOURCE_KEY_2,
900                    STACK_TRACE_RESOURCE_KEY_2,
901                    zx::MonotonicInstant::ZERO,
902                ),
903                Ok(true)
904            );
905        }
906
907        let reader = storage.create_reader();
908        assert_eq!(
909            reader.iter().count(),
910            NUM_NODES - (LOCAL_CACHE_CAPACITY + 1) + REFILL_SPILL_BATCH
911        );
912    }
913}