Skip to main content

memtrace_utils/
interpret.rs

1use crate::output::{Frame, Output};
2use crate::pipe_io::Record;
3use crate::resolver::Resolver;
4use crate::{executor, resolver};
5use indexmap::{IndexMap, IndexSet};
6use std::ffi::OsStr;
7use std::fs::OpenOptions;
8use std::io;
9use std::path::Path;
10use thiserror::Error;
11
12#[derive(Error, Debug)]
13pub enum Error {
14    #[error("Execution failed")]
15    Exec(#[from] executor::Error),
16    #[error("IO error")]
17    Io(#[from] io::Error),
18    #[error("Resolver")]
19    Resolver(#[from] resolver::Error),
20    #[error("Custom error: {0}")]
21    Custom(String),
22}
23
24#[derive(Default)]
25struct MemStats {
26    allocations: u64,
27    leaked_allocations: u64,
28    tmp_allocations: u64,
29}
30
31#[derive(Hash, PartialEq, Eq)]
32struct AllocationInfo {
33    size: u64,
34    trace_idx: u64,
35}
36
37const PAGE_SIZE: u64 = u16::MAX as u64 / 4;
38
39struct SplitPointer {
40    big: u64,
41    small: u16,
42}
43
44impl SplitPointer {
45    pub fn new(ptr: u64) -> Self {
46        Self {
47            big: ptr / PAGE_SIZE,
48            small: (ptr % PAGE_SIZE) as u16,
49        }
50    }
51}
52
53#[derive(Default)]
54struct Indices {
55    small_ptr_parts: Vec<u16>,
56    allocation_indices: Vec<usize>,
57}
58
59pub struct Interpreter {
60    output: Output,
61    strings: IndexSet<String>,
62    frames: IndexSet<u64>,
63    pointers: IndexMap<u64, Indices>,
64    allocation_info: IndexSet<AllocationInfo>,
65    resolver: Resolver,
66    stats: MemStats,
67    last_ptr: usize,
68}
69
70impl Interpreter {
71    pub fn new(out_filepath: impl AsRef<Path>) -> io::Result<Self> {
72        let file = OpenOptions::new()
73            .write(true)
74            .truncate(true)
75            .create(true)
76            .open(out_filepath)?;
77
78        Ok(Self {
79            output: Output::new(file),
80            strings: IndexSet::new(),
81            frames: IndexSet::new(),
82            pointers: IndexMap::new(),
83            allocation_info: IndexSet::new(),
84            resolver: Resolver::new(),
85            stats: MemStats::default(),
86            last_ptr: 0,
87        })
88    }
89
90    pub fn exec<S, P>(
91        &mut self,
92        program: S,
93        args: impl IntoIterator<Item = S>,
94        cwd: P,
95        lib_path: &str,
96    ) -> Result<(), Error>
97    where
98        S: AsRef<OsStr>,
99        P: AsRef<Path>,
100    {
101        let mut exec = executor::exec_cmd(program, args, cwd, lib_path);
102
103        while let Some(item) = exec.next() {
104            let record = item?;
105
106            self.handle_record(record)?;
107        }
108
109        self.write_comments()?;
110
111        self.output.flush()?;
112
113        Ok(())
114    }
115
116    fn handle_record(&mut self, record: Record) -> Result<(), Error> {
117        match record {
118            Record::Version(version) => {
119                self.output.write_version(version, 3)?;
120            }
121            Record::Exec(cmd) => {
122                self.output.write_exec(&cmd)?;
123            }
124            Record::Image {
125                name,
126                start_address,
127                size,
128            } => {
129                let module_id = self.write_string(&name)?;
130                _ = self.resolver.add_module(
131                    module_id,
132                    &name,
133                    start_address as u64,
134                    start_address as u64 + size as u64,
135                );
136            }
137            Record::PageInfo { size, pages } => {
138                self.output.write_page_info(size, pages as u64)?;
139            }
140            Record::Trace { ip, parent_idx } => {
141                let ip_id = self.add_frame(ip as u64)?;
142                self.output.write_trace(ip_id, parent_idx as u64)?;
143            }
144            Record::Alloc {
145                ptr,
146                size,
147                parent_idx,
148            } => {
149                self.stats.allocations += 1;
150                self.stats.leaked_allocations += 1;
151
152                let idx = self.add_alloc(size as u64, parent_idx as u64)?;
153
154                self.add_pointer(ptr as u64, idx as u64);
155                self.last_ptr = ptr;
156                self.output.write_alloc(idx)?;
157            }
158            Record::Free { ptr } => {
159                let temporary = self.last_ptr == ptr;
160                self.last_ptr = 0;
161
162                let Some(allocation_idx) = self.take_pointer(ptr as u64) else {
163                    return Ok(());
164                };
165
166                self.output.write_free(allocation_idx)?;
167
168                if temporary {
169                    self.stats.tmp_allocations += 1;
170                }
171                self.stats.leaked_allocations -= 1;
172            }
173            Record::Duration(duration) => {
174                self.output.write_duration(duration)?;
175            }
176            Record::RSS(rss) => {
177                self.output.write_rss(rss)?;
178            }
179        }
180
181        Ok(())
182    }
183
184    fn add_frame(&mut self, ip: u64) -> Result<usize, Error> {
185        match self.frames.get_full(&ip) {
186            None => {
187                let (id, _) = self.frames.insert_full(ip);
188
189                let Some(result) = self.resolver.lookup(ip) else {
190                    return Err(Error::Custom("ip locations not found".to_string()));
191                };
192
193                let mut frames = Vec::with_capacity(result.locations.len());
194
195                for location in result.locations {
196                    let function_idx = self.write_string(&location.function_name)?;
197
198                    let frame = if location.file_name.is_some() {
199                        let file_idx = self.write_string(
200                            &location
201                                .file_name
202                                .ok_or_else(|| Error::Custom("empty file name".into()))?,
203                        )?;
204
205                        let line_number = location
206                            .line_number
207                            .ok_or_else(|| Error::Custom("empty line number".into()))?;
208
209                        Frame::Multiple {
210                            function_idx,
211                            file_idx,
212                            line_number,
213                        }
214                    } else {
215                        Frame::Single { function_idx }
216                    };
217
218                    frames.push(frame);
219                }
220
221                self.output
222                    .write_instruction(ip, result.module_id, &frames)?;
223
224                Ok(id + 1)
225            }
226            Some((id, _)) => Ok(id + 1),
227        }
228    }
229
230    fn add_alloc(&mut self, size: u64, parent_idx: u64) -> Result<usize, Error> {
231        let info = AllocationInfo {
232            size,
233            trace_idx: parent_idx,
234        };
235
236        match self.allocation_info.get_full(&info) {
237            None => {
238                let (idx, _) = self.allocation_info.insert_full(info);
239
240                self.output.write_trace_alloc(size, parent_idx as usize)?;
241
242                Ok(idx)
243            }
244            Some((idx, _)) => Ok(idx),
245        }
246    }
247
248    fn add_pointer(&mut self, ptr: u64, allocation_idx: u64) {
249        let pointer = SplitPointer::new(ptr);
250
251        let indices = self.pointers.entry(pointer.big).or_default();
252
253        match indices
254            .small_ptr_parts
255            .iter()
256            .position(|&i| i == pointer.small)
257        {
258            None => {
259                indices.small_ptr_parts.push(pointer.small);
260                indices.allocation_indices.push(allocation_idx as usize);
261            }
262            Some(idx) => {
263                indices.allocation_indices[idx] = allocation_idx as usize;
264            }
265        }
266    }
267
268    fn take_pointer(&mut self, ptr: u64) -> Option<usize> {
269        let pointer = SplitPointer::new(ptr);
270        let indices = self.pointers.get_mut(&pointer.big)?;
271
272        let idx = indices
273            .small_ptr_parts
274            .iter()
275            .position(|&i| i == pointer.small)?;
276        let allocation_idx = indices.allocation_indices[idx];
277
278        indices.small_ptr_parts.swap_remove(idx);
279        indices.allocation_indices.swap_remove(idx);
280        if indices.allocation_indices.is_empty() {
281            self.pointers.swap_remove(&pointer.big);
282        }
283
284        Some(allocation_idx)
285    }
286
287    fn write_string(&mut self, value: &str) -> Result<usize, Error> {
288        match self.strings.get_full(value) {
289            None => {
290                let (id, _) = self.strings.insert_full(value.to_string());
291                self.output.write_string(value)?;
292
293                Ok(id + 1)
294            }
295            Some((id, _)) => Ok(id + 1),
296        }
297    }
298
299    fn write_comments(&mut self) -> Result<(), Error> {
300        self.output.write("")?;
301
302        self.output
303            .write_comment(&format!("strings: {}", self.strings.len()))?;
304        self.output
305            .write_comment(&format!("ips: {}", self.frames.len()))?;
306
307        Ok(())
308    }
309}