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