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