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