Skip to main content

memscope_rs/analysis/relation_inference/
range_map.rs

1//! RangeMap - Binary search index for allocation address ranges.
2//!
3//! Maps memory addresses to allocation indices using a sorted list
4//! and binary search, enabling O(log n) pointer-to-allocation lookup.
5
6use crate::analysis::is_virtual_pointer;
7use crate::snapshot::types::ActiveAllocation;
8
9/// A single entry in the range map.
10#[derive(Debug, Clone)]
11struct RangeEntry {
12    /// Start address (inclusive).
13    start: usize,
14    /// End address (exclusive).
15    end: usize,
16    /// Index into the original allocations slice.
17    alloc_id: usize,
18}
19
20/// An index structure that maps memory addresses to allocation indices.
21///
22/// Built once from a list of allocations, then queried many times
23/// during pointer scanning.
24#[derive(Debug, Default)]
25pub struct RangeMap {
26    /// Sorted range entries.
27    entries: Vec<RangeEntry>,
28}
29
30impl RangeMap {
31    /// Build a RangeMap from a list of active allocations.
32    ///
33    /// # Arguments
34    ///
35    /// * `allocations` - Slice of allocations to index. Each allocation's
36    ///   index in this slice becomes its `alloc_id`.
37    pub fn new(allocations: &[ActiveAllocation]) -> Self {
38        let mut entries: Vec<RangeEntry> = allocations
39            .iter()
40            .enumerate()
41            .filter_map(|(id, alloc)| {
42                // Only include HeapOwner allocations with valid pointers
43                // Skip virtual pointers used for Container types
44                alloc.ptr.and_then(|ptr| {
45                    if !is_virtual_pointer(ptr) {
46                        Some(RangeEntry {
47                            start: ptr,
48                            end: ptr.saturating_add(alloc.size),
49                            alloc_id: id,
50                        })
51                    } else {
52                        None
53                    }
54                })
55            })
56            .collect();
57
58        // Sort by start address for binary search.
59        entries.sort_by_key(|e| e.start);
60
61        Self { entries }
62    }
63    /// Find the allocation ID that contains the given pointer.
64    ///
65    /// Returns `Some(alloc_id)` if `ptr` falls within `[start, end)` of
66    /// some allocation, or `None` if no allocation contains it.
67    ///
68    /// # Complexity
69    ///
70    /// O(log n) via binary search.
71    pub fn find_containing(&self, ptr: usize) -> Option<usize> {
72        // partition_point returns the index of the first entry where
73        // `entry.start > ptr` is false, i.e., the first entry with start > ptr.
74        // The candidate is the entry just before it (last entry with start <= ptr).
75        let idx = self.entries.partition_point(|e| e.start <= ptr);
76
77        if idx > 0 {
78            let candidate = &self.entries[idx - 1];
79            if ptr < candidate.end {
80                return Some(candidate.alloc_id);
81            }
82        }
83        None
84    }
85
86    /// Find the allocation ID where the pointer equals the start address.
87    ///
88    /// Returns `Some(alloc_id)` if `ptr == alloc.start` for some allocation.
89    pub fn find_exact_start(&self, ptr: usize) -> Option<usize> {
90        let idx = self.entries.partition_point(|e| e.start < ptr);
91        if idx < self.entries.len() && self.entries[idx].start == ptr {
92            Some(self.entries[idx].alloc_id)
93        } else {
94            None
95        }
96    }
97
98    /// Get the number of indexed allocations.
99    pub fn len(&self) -> usize {
100        self.entries.len()
101    }
102
103    /// Check if the range map is empty.
104    pub fn is_empty(&self) -> bool {
105        self.entries.is_empty()
106    }
107}
108
109#[cfg(test)]
110mod tests {
111    use super::*;
112    use crate::core::types::TrackKind::HeapOwner;
113
114    fn make_alloc(ptr: usize, size: usize) -> ActiveAllocation {
115        ActiveAllocation {
116            kind: HeapOwner { ptr, size },
117            ptr: Some(ptr),
118            size,
119            allocated_at: 0,
120            var_name: None,
121            type_name: None,
122            thread_id: 0,
123            call_stack_hash: None,
124        }
125    }
126
127    #[test]
128    fn test_rangemap_empty() {
129        let map = RangeMap::new(&[]);
130        assert!(map.is_empty());
131        assert_eq!(map.len(), 0);
132        assert!(map.find_containing(0x1000).is_none());
133    }
134
135    #[test]
136    fn test_rangemap_single_allocation() {
137        let allocs = vec![make_alloc(0x1000, 100)];
138        let map = RangeMap::new(&allocs);
139
140        assert_eq!(map.find_containing(0x1000), Some(0));
141        assert_eq!(map.find_containing(0x1050), Some(0));
142        assert_eq!(map.find_containing(0x1063), Some(0)); // last valid byte
143        assert_eq!(map.find_containing(0x1064), None); // out of range
144        assert_eq!(map.find_containing(0x0FFF), None); // before start
145    }
146
147    #[test]
148    fn test_rangemap_multiple_allocations() {
149        // Three non-overlapping allocations with gaps between them.
150        // alloc[0]: [0x1000, 0x1064) size=100=0x64
151        // alloc[1]: [0x2000, 0x20C8) size=200=0xC8
152        // alloc[2]: [0x3000, 0x3032) size=50=0x32
153        let allocs = vec![
154            make_alloc(0x1000, 100),
155            make_alloc(0x2000, 200),
156            make_alloc(0x3000, 50),
157        ];
158        let map = RangeMap::new(&allocs);
159
160        // Boundary tests: first byte, middle byte, last byte of each allocation.
161        assert_eq!(map.find_containing(0x1000), Some(0)); // first byte of alloc[0]
162        assert_eq!(map.find_containing(0x1050), Some(0)); // middle of alloc[0]
163        assert_eq!(map.find_containing(0x1063), Some(0)); // last valid byte of alloc[0]
164        assert_eq!(map.find_containing(0x1064), None); // just past alloc[0]
165
166        assert_eq!(map.find_containing(0x2000), Some(1)); // first byte of alloc[1]
167        assert_eq!(map.find_containing(0x2050), Some(1)); // middle of alloc[1]
168        assert_eq!(map.find_containing(0x20C7), Some(1)); // last valid byte of alloc[1]
169        assert_eq!(map.find_containing(0x20C8), None); // just past alloc[1]
170
171        assert_eq!(map.find_containing(0x3000), Some(2)); // first byte of alloc[2]
172        assert_eq!(map.find_containing(0x3031), Some(2)); // last valid byte of alloc[2]
173        assert_eq!(map.find_containing(0x3032), None); // just past alloc[2]
174
175        // Gap addresses should return None.
176        assert_eq!(map.find_containing(0x1500), None); // gap between alloc[0] and alloc[1]
177        assert_eq!(map.find_containing(0x2500), None); // gap between alloc[1] and alloc[2]
178    }
179
180    #[test]
181    fn test_rangemap_adjacent_allocations() {
182        let allocs = vec![make_alloc(0x1000, 100), make_alloc(0x1064, 100)];
183        let map = RangeMap::new(&allocs);
184
185        assert_eq!(map.find_containing(0x1063), Some(0));
186        assert_eq!(map.find_containing(0x1064), Some(1));
187    }
188
189    #[test]
190    fn test_rangemap_find_exact_start() {
191        let allocs = vec![make_alloc(0x1000, 100), make_alloc(0x2000, 200)];
192        let map = RangeMap::new(&allocs);
193
194        assert_eq!(map.find_exact_start(0x1000), Some(0));
195        assert_eq!(map.find_exact_start(0x2000), Some(1));
196        assert_eq!(map.find_exact_start(0x1050), None); // inside but not start
197        assert_eq!(map.find_exact_start(0x3000), None); // not present
198    }
199
200    #[test]
201    fn test_rangemap_unsorted_input() {
202        let allocs = vec![
203            make_alloc(0x3000, 10),
204            make_alloc(0x1000, 10),
205            make_alloc(0x2000, 10),
206        ];
207        let map = RangeMap::new(&allocs);
208
209        // Should still find the right allocation IDs.
210        assert_eq!(map.find_containing(0x1000), Some(1));
211        assert_eq!(map.find_containing(0x2000), Some(2));
212        assert_eq!(map.find_containing(0x3000), Some(0));
213    }
214
215    #[test]
216    fn test_rangemap_zero_size_allocation() {
217        let allocs = vec![make_alloc(0x1000, 0)];
218        let map = RangeMap::new(&allocs);
219
220        // Zero-size allocation: start == end, nothing is contained.
221        assert_eq!(map.find_containing(0x1000), None);
222    }
223
224    #[test]
225    fn test_rangemap_large_number_of_allocations() {
226        let allocs: Vec<ActiveAllocation> = (0..1000)
227            .map(|i| make_alloc(0x10000 + i * 0x1000, 0x100))
228            .collect();
229        let map = RangeMap::new(&allocs);
230
231        // Spot check.
232        assert_eq!(map.find_containing(0x10000), Some(0));
233        assert_eq!(map.find_containing(0x11000), Some(1));
234        assert_eq!(map.find_containing(0x1F000), Some(15));
235    }
236}