Skip to main content

profile/
system_reporter.rs

1/* This Source Code Form is subject to the terms of the Mozilla Public
2 * License, v. 2.0. If a copy of the MPL was not distributed with this
3 * file, You can obtain one at https://mozilla.org/MPL/2.0/. */
4
5#[cfg(target_os = "macos")]
6use std::ptr;
7
8#[cfg(all(target_os = "linux", target_env = "gnu"))]
9use libc::c_int;
10use profile_traits::mem::{ProcessReports, Report, ReportKind, ReporterRequest};
11use profile_traits::path;
12
13const SYSTEM_HEAP_ALLOCATED_STR: &str = "system-heap-allocated";
14const SYSTEM_HEAP_RESERVED_STR: &str = "system-heap-reserved";
15
16struct SystemHeapInfo {
17    allocated: Option<usize>,
18    reserved: Option<usize>,
19}
20
21/// Collects global measurements from the OS and heap allocators.
22pub fn collect_reports(request: ReporterRequest) {
23    let mut reports = vec![];
24    {
25        let mut report = |path, size| {
26            if let Some(size) = size {
27                reports.push(Report {
28                    path,
29                    kind: ReportKind::NonExplicitSize,
30                    size,
31                });
32            }
33        };
34
35        // Virtual and physical memory usage, as reported by the OS.
36        report(path!["vsize"], vsize());
37        report(path!["resident"], resident());
38        report(path!["pss"], proportional_set_size());
39
40        // Memory segments, as reported by the OS.
41        // Notice that the sum of this should be more accurate according to
42        // the manpage of /proc/pid/statm
43        for seg in resident_segments() {
44            report(path!["resident-according-to-smaps", seg.0], Some(seg.1));
45        }
46
47        // Total number of bytes allocated by the application on the system
48        // heap. Even if we use a custom global allocator, this doesn't affect
49        // everything, e.g. C/C++ libraries might still use the default system allocator
50        // unless we explicitly patch  / configure them.
51        // Hence we always check system-heap info, since it allows us to know
52        // how much memory bypasses the global allocator we defined.
53        let system_heap = system_heap_info();
54        report(path![SYSTEM_HEAP_ALLOCATED_STR], system_heap.allocated);
55        report(path![SYSTEM_HEAP_RESERVED_STR], system_heap.reserved);
56
57        for heap_report in servo_allocator::heap_reports() {
58            report(path![heap_report.path], heap_report.size);
59        }
60    }
61
62    request.reports_channel.send(ProcessReports::new(reports));
63}
64
65#[cfg(all(target_os = "linux", target_env = "gnu"))]
66unsafe extern "C" {
67    fn mallinfo() -> struct_mallinfo;
68}
69
70#[cfg(all(target_os = "linux", target_env = "gnu"))]
71#[repr(C)]
72pub struct struct_mallinfo {
73    arena: c_int,
74    ordblks: c_int,
75    smblks: c_int,
76    hblks: c_int,
77    hblkhd: c_int,
78    usmblks: c_int,
79    fsmblks: c_int,
80    uordblks: c_int,
81    fordblks: c_int,
82    keepcost: c_int,
83}
84
85#[cfg(all(target_os = "linux", target_env = "gnu"))]
86fn system_heap_info() -> SystemHeapInfo {
87    let info: struct_mallinfo = unsafe { mallinfo() };
88
89    // https://man7.org/linux/man-pages/man3/mallinfo.3.html
90    // TODO: Switch to mallinfo2 or malloc_info.
91    // The documentation in the glibc man page makes it sound like |uordblks| would suffice,
92    // but that only gets the small allocations that are put in the brk heap. We need |hblkhd|
93    // as well to get the larger allocations that are mmapped.
94    //
95    // These fields are unfortunately |int| and so can overflow (becoming negative) if memory
96    // usage gets high enough. So don't report anything in that case. In the non-overflow case
97    // we cast the two values to usize before adding them to make sure the sum also doesn't
98    // overflow.
99    let allocated = if info.hblkhd >= 0 && info.uordblks >= 0 {
100        Some(info.hblkhd as usize + info.uordblks as usize)
101    } else {
102        None
103    };
104
105    let reserved = if info.arena >= 0 && info.hblkhd >= 0 {
106        Some(info.arena as usize + info.hblkhd as usize)
107    } else {
108        None
109    };
110
111    SystemHeapInfo {
112        allocated,
113        reserved,
114    }
115}
116
117#[cfg(target_os = "macos")]
118fn macos_malloc_statistics() -> libc::malloc_statistics_t {
119    let mut stats = libc::malloc_statistics_t {
120        blocks_in_use: 0,
121        size_in_use: 0,
122        max_size_in_use: 0,
123        size_allocated: 0,
124    };
125    unsafe {
126        // A null zone aggregates statistics across all malloc zones.
127        libc::malloc_zone_statistics(ptr::null_mut(), &mut stats);
128    }
129    stats
130}
131
132#[cfg(target_os = "macos")]
133fn system_heap_info() -> SystemHeapInfo {
134    let stats = macos_malloc_statistics();
135    SystemHeapInfo {
136        allocated: Some(stats.size_in_use),
137        reserved: Some(stats.size_allocated),
138    }
139}
140
141#[cfg(not(any(all(target_os = "linux", target_env = "gnu"), target_os = "macos")))]
142fn system_heap_info() -> SystemHeapInfo {
143    SystemHeapInfo {
144        allocated: None,
145        reserved: None,
146    }
147}
148
149#[cfg(target_os = "linux")]
150fn page_size() -> usize {
151    unsafe { ::libc::sysconf(::libc::_SC_PAGESIZE) as usize }
152}
153
154#[cfg(target_os = "linux")]
155fn proc_self_statm_field(field: usize) -> Option<usize> {
156    use std::fs::File;
157    use std::io::Read;
158
159    let mut f = File::open("/proc/self/statm").ok()?;
160    let mut contents = String::new();
161    f.read_to_string(&mut contents).ok()?;
162    let s = contents.split_whitespace().nth(field)?;
163    let npages = s.parse::<usize>().ok()?;
164    Some(npages * page_size())
165}
166
167#[cfg(target_os = "linux")]
168fn vsize() -> Option<usize> {
169    proc_self_statm_field(0)
170}
171
172#[cfg(target_os = "linux")]
173fn resident() -> Option<usize> {
174    proc_self_statm_field(1)
175}
176#[cfg(target_os = "linux")]
177fn proportional_set_size() -> Option<usize> {
178    use std::fs::File;
179    use std::io::Read;
180    let mut file = File::open("/proc/self/smaps_rollup").ok()?;
181    let mut contents = String::new();
182    file.read_to_string(&mut contents).ok()?;
183    let pss_line = contents
184        .split("\n")
185        .find(|string| string.contains("Pss:"))?;
186
187    // String looks like: "Pss:                 227 kB"
188    let pss_str = pss_line.split_whitespace().nth(1)?;
189    pss_str.parse().ok()
190}
191
192#[cfg(not(target_os = "linux"))]
193fn proportional_set_size() -> Option<usize> {
194    None
195}
196
197#[cfg(target_os = "macos")]
198fn task_basic_info() -> Option<mach2::task_info::task_basic_info> {
199    use mach2::kern_return::KERN_SUCCESS;
200    use mach2::task::task_info;
201    use mach2::task_info::{TASK_BASIC_INFO, TASK_BASIC_INFO_COUNT, task_basic_info};
202    use mach2::traps::mach_task_self;
203
204    let mut info = task_basic_info::default();
205    let mut count = TASK_BASIC_INFO_COUNT;
206    if unsafe {
207        task_info(
208            mach_task_self(),
209            TASK_BASIC_INFO,
210            std::ptr::from_mut(&mut info).cast(),
211            std::ptr::from_mut(&mut count),
212        )
213    } != KERN_SUCCESS
214    {
215        return None;
216    }
217    Some(info)
218}
219
220#[cfg(target_os = "macos")]
221fn vsize() -> Option<usize> {
222    task_basic_info().map(|task_basic_info| task_basic_info.virtual_size)
223}
224
225#[cfg(target_os = "macos")]
226fn resident() -> Option<usize> {
227    task_basic_info().map(|task_basic_info| task_basic_info.resident_size)
228}
229
230#[cfg(not(any(target_os = "linux", target_os = "macos")))]
231fn vsize() -> Option<usize> {
232    None
233}
234
235#[cfg(not(any(target_os = "linux", target_os = "macos")))]
236fn resident() -> Option<usize> {
237    None
238}
239
240#[cfg(target_os = "linux")]
241fn resident_segments() -> Vec<(String, usize)> {
242    use std::collections::HashMap;
243    use std::collections::hash_map::Entry;
244    use std::fs::File;
245    use std::io::{BufRead, BufReader};
246
247    use regex::Regex;
248
249    // The first line of an entry in /proc/<pid>/smaps looks just like an entry
250    // in /proc/<pid>/maps:
251    //
252    //   address           perms offset  dev   inode  pathname
253    //   02366000-025d8000 rw-p 00000000 00:00 0      [heap]
254    //
255    // Each of the following lines contains a key and a value, separated
256    // by ": ", where the key does not contain either of those characters.
257    // For example:
258    //
259    //   Rss:           132 kB
260    // See https://www.kernel.org/doc/Documentation/filesystems/proc.txt
261
262    let f = match File::open("/proc/self/smaps") {
263        Ok(f) => BufReader::new(f),
264        Err(_) => return vec![],
265    };
266
267    let seg_re = Regex::new(
268        r"^[[:xdigit:]]+-[[:xdigit:]]+ (....) [[:xdigit:]]+ [[:xdigit:]]+:[[:xdigit:]]+ \d+ +(.*)",
269    )
270    .unwrap();
271    let rss_re = Regex::new(r"^Rss: +(\d+) kB").unwrap();
272
273    // We record each segment's resident size.
274    let mut seg_map: HashMap<String, usize> = HashMap::new();
275
276    #[derive(PartialEq)]
277    enum LookingFor {
278        Segment,
279        Rss,
280    }
281    let mut looking_for = LookingFor::Segment;
282
283    let mut curr_seg_name = String::new();
284
285    // Parse the file.
286    for line in f.lines() {
287        let line = match line {
288            Ok(line) => line,
289            Err(_) => continue,
290        };
291        if looking_for == LookingFor::Segment {
292            // Look for a segment info line.
293            let cap = match seg_re.captures(&line) {
294                Some(cap) => cap,
295                None => continue,
296            };
297            let perms = cap.get(1).unwrap().as_str();
298            let pathname = cap.get(2).unwrap().as_str();
299
300            // Construct the segment name from its pathname and permissions.
301            curr_seg_name.clear();
302            if pathname.is_empty() || pathname.starts_with("[stack:") {
303                // Anonymous memory. Entries marked with "[stack:nnn]"
304                // look like thread stacks but they may include other
305                // anonymous mappings, so we can't trust them and just
306                // treat them as entirely anonymous.
307                curr_seg_name.push_str("anonymous");
308            } else {
309                curr_seg_name.push_str(pathname);
310            }
311            curr_seg_name.push_str(" (");
312            curr_seg_name.push_str(perms);
313            curr_seg_name.push(')');
314
315            looking_for = LookingFor::Rss;
316        } else {
317            // Look for an "Rss:" line.
318            let cap = match rss_re.captures(&line) {
319                Some(cap) => cap,
320                None => continue,
321            };
322            let rss = cap.get(1).unwrap().as_str().parse::<usize>().unwrap() * 1024;
323
324            if rss > 0 {
325                // Aggregate small segments into "other".
326                let seg_name = if rss < 512 * 1024 {
327                    "other".to_owned()
328                } else {
329                    curr_seg_name.clone()
330                };
331                match seg_map.entry(seg_name) {
332                    Entry::Vacant(entry) => {
333                        entry.insert(rss);
334                    },
335                    Entry::Occupied(mut entry) => *entry.get_mut() += rss,
336                }
337            }
338
339            looking_for = LookingFor::Segment;
340        }
341    }
342
343    // Note that the sum of all these segments' RSS values differs from the "resident"
344    // measurement obtained via /proc/<pid>/statm in resident(). It's unclear why this
345    // difference occurs; for some processes the measurements match, but for Servo they do not.
346    seg_map.into_iter().collect()
347}
348
349#[cfg(not(target_os = "linux"))]
350fn resident_segments() -> Vec<(String, usize)> {
351    vec![]
352}