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