| // Copyright 2024 The ChromiumOS Authors | |
| // Use of this source code is governed by a BSD-style license that can be | |
| // found in the LICENSE file. | |
| use std::collections::HashMap; | |
| use std::collections::HashSet; | |
| use std::io::Error as IoError; | |
| use std::os::fd::AsRawFd; | |
| use std::os::fd::FromRawFd; | |
| use std::os::fd::OwnedFd; | |
| use std::path::Component; | |
| use std::path::Path; | |
| use std::path::PathBuf; | |
| use std::sync::OnceLock; | |
| use anyhow::anyhow; | |
| use anyhow::bail; | |
| use anyhow::Context; | |
| use anyhow::Result; | |
| use log::error; | |
| use log::info; | |
| use log::warn; | |
| const INIT_PID: u32 = 1; | |
| #[derive(Debug, Eq, Hash, PartialEq, Clone, Copy)] | |
| #[repr(usize)] | |
| #[allow(clippy::upper_case_acronyms)] // Needs to match UMA metric name | |
| pub enum ProcessGroupKind { | |
| Browser = 0, | |
| Gpu = 1, | |
| Renderers = 2, | |
| ARC = 3, | |
| VMs = 4, // Except for ARCVM | |
| Daemons = 5, // Everything else | |
| Count = 6, | |
| } | |
| impl From<usize> for ProcessGroupKind { | |
| fn from(val: usize) -> ProcessGroupKind { | |
| match val { | |
| 0 => Self::Browser, | |
| 1 => Self::Gpu, | |
| 2 => Self::Renderers, | |
| 3 => Self::ARC, | |
| 4 => Self::VMs, | |
| 5 => Self::Daemons, | |
| _ => panic!("Tried to convert {} to ProcessGroupKind", val), | |
| } | |
| } | |
| } | |
| #[derive(Debug, Eq, Hash, PartialEq, Clone, Copy)] | |
| #[repr(usize)] | |
| pub enum MemKind { | |
| Total = 0, | |
| Anon = 1, | |
| File = 2, | |
| Shmem = 3, | |
| Swap = 4, | |
| Count = 5, | |
| } | |
| impl MemKind { | |
| fn smaps_prefix(&self) -> &'static str { | |
| match self { | |
| Self::Total => "Pss:", | |
| Self::Anon => "Pss_Anon:", | |
| Self::File => "Pss_File:", | |
| Self::Shmem => "Pss_Shmem:", | |
| Self::Swap => "SwapPss:", | |
| _ => panic!("Instantiated MemKind::Count"), | |
| } | |
| } | |
| } | |
| impl From<usize> for MemKind { | |
| fn from(val: usize) -> MemKind { | |
| match val { | |
| 0 => Self::Total, | |
| 1 => Self::Anon, | |
| 2 => Self::File, | |
| 3 => Self::Shmem, | |
| 4 => Self::Swap, | |
| _ => panic!("Tried to convert {} to MemKind", val), | |
| } | |
| } | |
| } | |
| /// Returns the UMA metric name based on the given process and memory kinds. | |
| pub fn get_metric_name(process_kind: usize, mem_kind: usize) -> String { | |
| let process_kind = ProcessGroupKind::from(process_kind); | |
| let mem_kind = MemKind::from(mem_kind); | |
| format!("Platform.Memory.{:?}.{:?}", process_kind, mem_kind) | |
| } | |
| // Parses comm and ppid from the stats file content. | |
| fn parse_stats_file(bytes: Vec<u8>) -> Result<u32> { | |
| // The format of the stats file is: | |
| // pid (comm) run_state ppid ... | |
| // <comm> can contain spaces and parentheses, but there are no parentheses | |
| // anywhere else in the file. Rather than doing complicated regex, just find | |
| // search backwards for the last parentheses to skip over <comm>. | |
| let Some(comm_end) = bytes.iter().rposition(|c| *c == b')') else { | |
| bail!("failed to find comm parentheses"); | |
| }; | |
| let find_next_space = |start_idx| { | |
| bytes[start_idx..] | |
| .iter() | |
| .position(|c| *c == b' ') | |
| .map(|offset| start_idx + offset) | |
| }; | |
| let run_state_start = comm_end + 2; // Advance past ") " | |
| let run_state_end = find_next_space(run_state_start).context("failed to skip run_state")?; | |
| let ppid_start = run_state_end + 1; // Advance past " " | |
| let ppid_end = find_next_space(ppid_start).context("failed to skip ppid")?; | |
| let ppid = String::from_utf8(bytes[ppid_start..ppid_end].to_vec()) | |
| .context("failed to convert ppid to string")? | |
| .parse::<u32>() | |
| .context("failed to parse ppid")?; | |
| Ok(ppid) | |
| } | |
| // Constructs a map from pid -> Vec<child pids> | |
| fn build_pid_map(procfs_path: &str) -> Result<HashMap<u32, Vec<u32>>> { | |
| let mut found_processes: HashSet<u32> = HashSet::new(); | |
| let mut pid_map: HashMap<u32, Vec<u32>> = HashMap::new(); | |
| let dir_entries = | |
| std::fs::read_dir(Path::new(procfs_path)).context("procfs read_dir failed")?; | |
| for dir_entry in dir_entries { | |
| let mut path = dir_entry.context("process enumeration error")?.path(); | |
| // Skip any directories that aren't a process. | |
| let Some(pid) = path | |
| .file_name() | |
| .and_then(|name| name.to_str()) | |
| .and_then(|pid| pid.parse::<u32>().ok()) | |
| else { | |
| continue; | |
| }; | |
| path.push("stat"); | |
| let Ok(bytes) = std::fs::read(path) else { | |
| // Assume the read failed because the process exited. | |
| continue; | |
| }; | |
| let ppid = match parse_stats_file(bytes) { | |
| Ok(res) => res, | |
| Err(e) => { | |
| warn!("failed to parse stats file {:?}", e); | |
| continue; | |
| } | |
| }; | |
| if !found_processes.insert(pid) { | |
| warn!("duplicate process {}", pid); | |
| } | |
| if ppid != 0 { | |
| pid_map.entry(ppid).or_default().push(pid); | |
| } | |
| } | |
| if !found_processes.contains(&INIT_PID) { | |
| bail!("No init process found"); | |
| } | |
| // Reparent any children whose parent we didn't find to init. This | |
| // probably means there was a race, but it should be rare. | |
| let mut need_reparenting = Vec::new(); | |
| pid_map.retain(|pid, children| { | |
| let is_found = found_processes.contains(pid); | |
| if !is_found { | |
| info!("Parent {} for children {:?} not found", pid, children); | |
| need_reparenting.append(children); | |
| } | |
| is_found | |
| }); | |
| pid_map | |
| .get_mut(&INIT_PID) | |
| .expect("init disappeared") | |
| .append(&mut need_reparenting); | |
| Ok(pid_map) | |
| } | |
| fn read_cmdline(procfs_path: &str, pid: u32) -> Option<Vec<u8>> { | |
| match std::fs::read(Path::new(&format!("{}/{}/cmdline", procfs_path, pid))) { | |
| Ok(bytes) => Some(bytes), | |
| Err(err) => { | |
| let os_err = err.raw_os_error(); | |
| // If cmdline doesn't exist or we got ESRCH reading it, then we raced | |
| // with the process dying. Otherwise something unexpected happened, so | |
| // log an error. | |
| if os_err != Some(libc::ENOENT) && os_err != Some(libc::ESRCH) { | |
| warn!("Failed to read {}'s cmdline: {:?}", pid, err); | |
| } | |
| None | |
| } | |
| } | |
| } | |
| // Classifies a pid in a daemon tree. Returns the pid's type as well as | |
| // the type of the tree of its descendants. | |
| fn classify_daemon( | |
| procfs_path: &str, | |
| pid: u32, | |
| arc_container_pid: Option<u32>, | |
| ) -> Option<(TreeKind, ProcessGroupKind)> { | |
| if Some(pid) == arc_container_pid { | |
| return Some((TreeKind::Arc, ProcessGroupKind::ARC)); | |
| } | |
| let exe = get_exe(procfs_path, pid)?; | |
| let exe = exe.as_os_str(); | |
| if exe == "/opt/google/chrome/chrome" { | |
| return Some((TreeKind::Chrome, ProcessGroupKind::Browser)); | |
| } else if exe == "/usr/bin/vm_concierge" { | |
| return Some((TreeKind::Concierge, ProcessGroupKind::VMs)); | |
| } else if exe == "/usr/bin/seneschal" { | |
| return Some((TreeKind::Vm, ProcessGroupKind::VMs)); | |
| } | |
| Some((TreeKind::Daemon, ProcessGroupKind::Daemons)) | |
| } | |
| fn strip_chrome_directory(exe: &Path) -> Option<PathBuf> { | |
| // Check for ash or running lacros from rootfs. | |
| if let Ok(exe) = exe | |
| .strip_prefix(Path::new("/opt/google/chrome")) | |
| .or_else(|_| exe.strip_prefix(Path::new("/run/lacros"))) | |
| { | |
| return Some(exe.to_path_buf()); | |
| } | |
| // Check for lacros from the stateful partition, which is of the form: | |
| // /run/imageloader/lacros-<channel>/104.0.xxxx.xx | |
| if let Ok(stripped) = exe.strip_prefix(Path::new("/run/imageloader")) { | |
| let mut components = stripped.components(); | |
| let Some(Component::Normal(dir_name)) = components.next() else { | |
| return None; | |
| }; | |
| if dir_name | |
| .to_str() | |
| .map_or(false, |dir_name| dir_name.starts_with("lacros-")) | |
| { | |
| return Some(components.skip(1).collect::<PathBuf>()); | |
| } | |
| } | |
| None | |
| } | |
| // Chrome process classification. We rely on the "--type=xyz" command line flag | |
| // to processes. A partial list of types is in | |
| // content/public/common/content_switches.cc. We classify them as shown: | |
| // | |
| // const char kGpuProcess[] = "gpu-process"; // GPU | |
| // const char kPpapiBrokerProcess[] = "ppapi-broker"; // browser | |
| // const char kPpapiPluginProcess[] = "ppapi"; // renderer | |
| // const char kRenderersProcess[] = "renderer"; // renderer | |
| // const char kUtilityProcess[] = "utility"; // renderer | |
| // | |
| // (PPAPI stands for "pepper plugin API", which includes Flash). Additionally | |
| // there is "zygote" and "broker", which we classify as browser. | |
| // | |
| // The browser process does not have a --type==xyz flag. | |
| fn classify_chrome(procfs_path: &str, pid: u32) -> Option<ProcessGroupKind> { | |
| let Some(exe) = get_exe(procfs_path, pid) else { | |
| return Some(ProcessGroupKind::Browser); | |
| }; | |
| let Some(exe) = strip_chrome_directory(&exe) else { | |
| warn!("Unknown chrome process {} running {:?}", pid, exe); | |
| return Some(ProcessGroupKind::Browser); | |
| }; | |
| if exe == Path::new("nacl_helper") { | |
| return Some(ProcessGroupKind::Browser); | |
| } else if exe != Path::new("chrome") { | |
| warn!("Unknown chrome process {} running {:?}", pid, exe); | |
| return Some(ProcessGroupKind::Browser); | |
| } | |
| let cmdline = read_cmdline(procfs_path, pid)?; | |
| let Some(type_switch) = find_cmdline_arg_by_prefix(&cmdline, b"--type") else { | |
| return Some(ProcessGroupKind::Browser); | |
| }; | |
| Some(match type_switch.as_str() { | |
| "--type=broker" => ProcessGroupKind::Browser, | |
| "--type=ppapi-broker" => ProcessGroupKind::Browser, | |
| "--type=zygote" => ProcessGroupKind::Browser, | |
| "--type=renderer" => ProcessGroupKind::Renderers, | |
| "--type=ppapi" => ProcessGroupKind::Renderers, | |
| "--type=sandbox" => ProcessGroupKind::Renderers, | |
| "--type=utility" => ProcessGroupKind::Renderers, | |
| "--type=gpu-process" => ProcessGroupKind::Gpu, | |
| _ => { | |
| warn!("Unknown chrome process {} with type {}", pid, type_switch); | |
| ProcessGroupKind::Browser | |
| } | |
| }) | |
| } | |
| // Shmem that has been swapped is not included in the SwapPss of any process's | |
| // smaps_rollup. In the kernel, PSS and shmem are abstractions in different | |
| // layers (address space and file systems), which makes resolving this issue | |
| // in the kernel very difficult. | |
| // | |
| // On ChromeOS, the lack of shmem in SwapPss is primarily a problem because | |
| // crosvm uses shmem for guest memory. Rather than trying to solve the general | |
| // case in the kernel, we can deal with this specific problem by parsing the | |
| // smaps of any root crosvm processes and summing up the Swap field of any | |
| // memfd regions. | |
| struct ProcessInfo { | |
| pid: u32, | |
| is_root_crosvm: bool, | |
| } | |
| // Classifies a pid in a concierge tree. Returns the pid's type, the type | |
| // of the tree of its descendants, and whether or not the process is a root | |
| // crosvm process (see ProcessInfo). | |
| fn classify_concierge(procfs_path: &str, pid: u32) -> Option<(TreeKind, ProcessGroupKind, bool)> { | |
| let cmdline = read_cmdline(procfs_path, pid)?; | |
| if find_cmdline_arg_by_prefix(&cmdline, b"androidboot.hardware=bertha").is_some() { | |
| Some((TreeKind::Arc, ProcessGroupKind::ARC, true)) | |
| } else { | |
| let is_root_crosvm = get_exe(procfs_path, pid) | |
| .and_then(|exe| exe.file_name().map(|name| name == "crosvm")) | |
| .unwrap_or(false); | |
| Some((TreeKind::Vm, ProcessGroupKind::VMs, is_root_crosvm)) | |
| } | |
| } | |
| fn is_seperator_byte(b: u8) -> bool { | |
| b == b'\0' || b.is_ascii_whitespace() | |
| } | |
| // Given the raw cmdline bytes, finds the first cmdline entry which starts | |
| // with |prefix|. This function only converts the matching entry to a String, | |
| // so other parts of the cmdline do not have to be valid UTF8. | |
| fn find_cmdline_arg_by_prefix(cmdline: &[u8], prefix: &[u8]) -> Option<String> { | |
| let mut idx = 0; | |
| let mut iter = cmdline.iter(); | |
| while let Some(start_offset) = iter.position(|b| !is_seperator_byte(*b)) { | |
| let start = idx + start_offset; | |
| idx += start_offset + 1; // +1 to move past the non-space character | |
| let substr = match iter.position(|b| is_seperator_byte(*b)) { | |
| Some(space_offset) => { | |
| idx += space_offset + 1; // +1 to move past the space | |
| &cmdline[start..=(start + space_offset)] | |
| } | |
| None => &cmdline[start..], | |
| }; | |
| if substr.len() >= prefix.len() && &substr[..prefix.len()] == prefix { | |
| return String::from_utf8(substr.to_vec()).ok(); | |
| } | |
| } | |
| None | |
| } | |
| // Read the exe path from /proc/pid/exe. | |
| fn get_exe(procfs_path: &str, pid: u32) -> Option<PathBuf> { | |
| match std::fs::read_link(Path::new(&format!("{}/{}/exe", procfs_path, pid))) { | |
| Ok(path) => Some(path.to_path_buf()), | |
| Err(err) => { | |
| let os_err = err.raw_os_error(); | |
| // ENOENT just means we raced with the process dying. Otherwise log the error. | |
| if os_err != Some(libc::ENOENT) { | |
| warn!("Failed to read {}'s cmdline: {:?}", pid, err); | |
| } | |
| None | |
| } | |
| } | |
| } | |
| // The type of the process tree that we're walking, which tells us the | |
| // function that should be to classify a given process. | |
| #[derive(Clone, Copy)] | |
| enum TreeKind { | |
| Daemon, | |
| Chrome, | |
| Concierge, | |
| Arc, | |
| Vm, | |
| } | |
| impl TreeKind { | |
| // Returns the pid's type, the type of the tree of its descendants, and whether | |
| // the process is a root crosvm process (see ProcessInfo). | |
| fn classify_process( | |
| &self, | |
| procfs_path: &str, | |
| pid: u32, | |
| arc_container_pid: Option<u32>, | |
| ) -> Option<(TreeKind, ProcessGroupKind, bool)> { | |
| match self { | |
| Self::Daemon => classify_daemon(procfs_path, pid, arc_container_pid) | |
| .map(|(tree, process)| (tree, process, false)), | |
| Self::Chrome => classify_chrome(procfs_path, pid).map(|k| (Self::Chrome, k, false)), | |
| Self::Concierge => classify_concierge(procfs_path, pid), | |
| Self::Arc => Some((Self::Arc, ProcessGroupKind::ARC, false)), | |
| Self::Vm => Some((Self::Vm, ProcessGroupKind::VMs, false)), | |
| } | |
| } | |
| } | |
| // Helper function for recursively classifying the a process tree. | |
| fn classify_processes_walker( | |
| procfs_path: &str, | |
| pid: u32, | |
| tree_kind: TreeKind, | |
| arc_container_pid: Option<u32>, | |
| pid_map: &HashMap<u32, Vec<u32>>, | |
| kind_map: &mut [Vec<ProcessInfo>; ProcessGroupKind::Count as usize], | |
| ) { | |
| let tree_kind = match tree_kind.classify_process(procfs_path, pid, arc_container_pid) { | |
| Some((tree_kind, process_kind, is_root_crosvm)) => { | |
| kind_map[process_kind as usize].push(ProcessInfo { | |
| pid, | |
| is_root_crosvm, | |
| }); | |
| tree_kind | |
| } | |
| None => tree_kind, | |
| }; | |
| if let Some(children) = pid_map.get(&pid) { | |
| for child in children.iter() { | |
| classify_processes_walker( | |
| procfs_path, | |
| *child, | |
| tree_kind, | |
| arc_container_pid, | |
| pid_map, | |
| kind_map, | |
| ); | |
| } | |
| }; | |
| } | |
| fn get_arc_container_init_pid(run_path: &str) -> Result<Option<u32>> { | |
| const ARC_CONTAINER_FILE: &str = "containers/android-run_oci/container.pid"; | |
| let arc_container_path = Path::new(run_path).join(ARC_CONTAINER_FILE); | |
| if !Path::exists(&arc_container_path) { | |
| return Ok(None); | |
| } | |
| let pid_bytes = std::fs::read(arc_container_path).context("failed to read arc pid ")?; | |
| let pid_str = String::from_utf8(pid_bytes).context("failed to get utf8 arc pid")?; | |
| pid_str | |
| .trim() | |
| .parse::<u32>() | |
| .map(Some) | |
| .context("failed to parse arc pid") | |
| } | |
| fn classify_processes( | |
| procfs_path: &str, | |
| run_path: &str, | |
| pid_map: HashMap<u32, Vec<u32>>, | |
| ) -> [Vec<ProcessInfo>; ProcessGroupKind::Count as usize] { | |
| let arc_container_pid = match get_arc_container_init_pid(run_path) { | |
| Ok(pid) => pid, | |
| Err(e) => { | |
| error!("Failed to read ARC pid {:?}", e); | |
| None | |
| } | |
| }; | |
| let mut kind_map: [Vec<ProcessInfo>; ProcessGroupKind::Count as usize] = Default::default(); | |
| // Walking from init is an easy way to skip kthreadd and its descendants. | |
| classify_processes_walker( | |
| procfs_path, | |
| INIT_PID, | |
| TreeKind::Daemon, | |
| arc_container_pid, | |
| &pid_map, | |
| &mut kind_map, | |
| ); | |
| kind_map | |
| } | |
| fn parse_dev_id_from_vma_header(line: &str) -> Option<&str> { | |
| // Extract the 4th column from lines of this form: | |
| // 7e44556a8000-7e44556a9000 rw-s 00000000 00:01 4716441 /memfd:name (deleted) | |
| line.split(' ').filter(|s| !s.is_empty()).nth(3) | |
| } | |
| fn parse_memfd_dev_id(addr: *const libc::c_void) -> Result<String> { | |
| let smaps_bytes = std::fs::read("/proc/self/smaps")?; | |
| let smaps = String::from_utf8_lossy(&smaps_bytes); | |
| for line in smaps.split('\n') { | |
| if let Some(idx) = line.find('-') { | |
| if let Ok(vma_start) = usize::from_str_radix(&line[..idx], 16) { | |
| if vma_start == addr as usize { | |
| if let Some(id) = parse_dev_id_from_vma_header(line) { | |
| return Ok(id.to_string()); | |
| } else { | |
| bail!("Failed to parse smaps header {}", line); | |
| } | |
| } | |
| } | |
| } | |
| } | |
| Err(anyhow!("Failed to find target VMA region")) | |
| } | |
| fn get_memfd_dev_id_from_smaps() -> Result<String> { | |
| // SAFETY: Safe because memfd_create doesn't affect memory and we check the return. | |
| let memfd = unsafe { | |
| libc::syscall( | |
| libc::SYS_memfd_create, | |
| b"memfd\0".as_ptr() as *const libc::c_char, | |
| 0, | |
| ) | |
| }; | |
| if memfd < 0 { | |
| bail!("memfd_create failed {:?}", IoError::last_os_error()); | |
| } | |
| // SAFETY: Safe because we checked that memfd is a valid fd. | |
| let memfd = unsafe { OwnedFd::from_raw_fd(memfd as i32) }; | |
| const MAP_SIZE: usize = 4096; | |
| // SAFETY: Safe because mmap64 doesn't affect any rust-controlled memory | |
| // and we check the return value. | |
| let addr = unsafe { | |
| libc::mmap64( | |
| std::ptr::null_mut(), | |
| MAP_SIZE, | |
| libc::PROT_NONE, | |
| libc::MAP_SHARED, | |
| memfd.as_raw_fd(), | |
| 0, | |
| ) | |
| }; | |
| if addr == libc::MAP_FAILED { | |
| bail!("failed to mmap memfd {:?}", IoError::last_os_error()); | |
| } | |
| let dev_id = parse_memfd_dev_id(addr); | |
| // SAFETY: Safe because this only affects the non-rust controlled | |
| // memory we allocated above with mmap64. | |
| let res = unsafe { libc::munmap(addr, MAP_SIZE) }; | |
| if res != 0 { | |
| warn!("failed to munmap memfd {:?}", IoError::last_os_error()); | |
| } | |
| dev_id | |
| } | |
| static MEMFD_DEV_ID: OnceLock<Option<String>> = OnceLock::new(); | |
| fn get_memfd_dev_id() -> Option<String> { | |
| MEMFD_DEV_ID | |
| .get_or_init(|| match get_memfd_dev_id_from_smaps() { | |
| Ok(dev_id) => Some(dev_id), | |
| Err(err) => { | |
| error!("Failed to parse memfd dev_id: {:?}", err); | |
| None | |
| } | |
| }) | |
| .clone() | |
| } | |
| fn parse_smaps_line(line: &str) -> Option<u64> { | |
| let parts: Vec<&str> = line.split(' ').filter(|s| !s.is_empty()).collect(); | |
| if parts.len() != 3 { | |
| return None; | |
| } | |
| parts[1].parse::<u64>().ok().map(|kb| kb * 1024) | |
| } | |
| // Parse shmem swap from full smaps for the given pid. | |
| fn parse_shmem_swap_from_smaps(procfs_path: &str, pid: u32) -> Option<u64> { | |
| let smaps_bytes = std::fs::read(format!("{}/{}/smaps", procfs_path, pid)).ok()?; | |
| // smaps contains file names, so we may not necessarily be able to convert it | |
| // to a utf8 string. However, we only care about the ASCII parts of the string | |
| // returned by the kernel, so a lossy conversion is fine. | |
| let smaps = String::from_utf8_lossy(&smaps_bytes); | |
| let mut res = 0; | |
| let dev_id = get_memfd_dev_id()?; | |
| let mut prev_line: Option<&str> = None; | |
| let mut processing_shmem = false; | |
| for line in smaps.split('\n') { | |
| let local_prev_line = prev_line; | |
| prev_line = Some(line); | |
| // Rather than trying to match the address in the VMA headers, it's easier | |
| // to match the 'Size:' prefix on the second line. | |
| if line.starts_with("Size:") { | |
| processing_shmem = local_prev_line.map_or(false, |header| { | |
| parse_dev_id_from_vma_header(header) == Some(&dev_id) | |
| }); | |
| } | |
| if processing_shmem && line.starts_with("Swap:") { | |
| if let Some(bytes) = parse_smaps_line(line) { | |
| res += bytes; | |
| } | |
| } | |
| } | |
| Some(res) | |
| } | |
| // Gets memory stats for the given process. | |
| fn get_memory_stats( | |
| procfs_path: &str, | |
| info: &ProcessInfo, | |
| ) -> Option<[u64; MemKind::Count as usize]> { | |
| // If this fails, the process is probably dead, so just return. | |
| let smaps_rollup_bytes = | |
| std::fs::read(format!("{}/{}/smaps_rollup", procfs_path, info.pid)).ok()?; | |
| let smaps_rollup = String::from_utf8(smaps_rollup_bytes).ok()?; | |
| let mut res: [u64; MemKind::Count as usize] = Default::default(); | |
| let mut mem_kind_idx = 0; | |
| for line in smaps_rollup.split('\n') { | |
| let kind = MemKind::from(mem_kind_idx); | |
| if !line.starts_with(kind.smaps_prefix()) { | |
| continue; | |
| } | |
| let bytes = parse_smaps_line(line).unwrap_or_else(|| panic!("bad line in rollup {}", line)); | |
| res[mem_kind_idx] += bytes; | |
| mem_kind_idx += 1; | |
| if mem_kind_idx == MemKind::Count as usize { | |
| break; | |
| } | |
| } | |
| if info.is_root_crosvm { | |
| if let Some(bytes) = parse_shmem_swap_from_smaps(procfs_path, info.pid) { | |
| res[MemKind::Swap as usize] += bytes; | |
| } | |
| } | |
| Some(res) | |
| } | |
| // Accumulates memory statistics for all classified processes. | |
| fn accumulate_memory_stats( | |
| procfs_path: &str, | |
| processes: [Vec<ProcessInfo>; ProcessGroupKind::Count as usize], | |
| ) -> MemStats { | |
| let mut res: MemStats = Default::default(); | |
| for (process_kind, processes) in processes.into_iter().enumerate() { | |
| for process in processes.iter() { | |
| if let Some(stats) = get_memory_stats(procfs_path, process) { | |
| for (i, stat) in stats.iter().enumerate() { | |
| res[process_kind][i] += stat; | |
| } | |
| } | |
| } | |
| } | |
| res | |
| } | |
| pub type MemStats = [[u64; MemKind::Count as usize]; ProcessGroupKind::Count as usize]; | |
| /// Gather the Platform.Memory.<ProcessGroupKind>.<MemKind> UMA statistics. | |
| pub fn get_all_memory_stats(procfs_path: &str, run_path: &str) -> Result<MemStats> { | |
| let process_map = build_pid_map(procfs_path)?; | |
| let classified_processes = classify_processes(procfs_path, run_path, process_map); | |
| let res = accumulate_memory_stats(procfs_path, classified_processes); | |
| Ok(res) | |
| } | |
| #[cfg(test)] | |
| mod tests { | |
| use tempfile::TempDir; | |
| use super::*; | |
| const MIB: u64 = 1024 * 1024; | |
| #[allow(clippy::too_many_arguments)] | |
| fn create_proc_entry( | |
| procfs_path: &Path, | |
| pid: u32, | |
| ppid: u32, | |
| name: &[u8], | |
| cmdline: Option<(&str, &str)>, | |
| total_mib: u64, | |
| anon_mib: u64, | |
| file_mib: u64, | |
| shmem_mib: u64, | |
| swap_mib: u64, | |
| shmem_swap_mib: u64, | |
| ) { | |
| let memfd_dev_id = MEMFD_DEV_ID | |
| .get_or_init(|| Some("00:01".to_string())) | |
| .as_ref() | |
| .unwrap(); | |
| let pid_path = procfs_path.join(format!("{}", pid)); | |
| std::fs::create_dir(&pid_path).unwrap(); | |
| if let Some(cmdline) = cmdline { | |
| std::os::unix::fs::symlink(Path::new(cmdline.0), pid_path.join("exe")).unwrap(); | |
| std::fs::write( | |
| pid_path.join("cmdline"), | |
| format!("{} {}", cmdline.0, cmdline.1), | |
| ) | |
| .unwrap(); | |
| } else { | |
| std::fs::File::create(pid_path.join("cmdline")).unwrap(); | |
| } | |
| let stats_bytes = [ | |
| format!("{} (", pid).as_bytes(), | |
| name, | |
| format!(") R {} 33 44 a b c \n", ppid).as_bytes(), | |
| ] | |
| .concat(); | |
| std::fs::write(pid_path.join("stat"), stats_bytes).unwrap(); | |
| if total_mib != 0 { | |
| let smaps_rollup_content = format!( | |
| "blah\n\ | |
| Rss: 123 kB\n\ | |
| Pss: {} kB\n\ | |
| Pss_Anon: {} kB\n\ | |
| Pss_File: {} kB\n\ | |
| Pss_Shmem: {} kB\n\ | |
| Shared_Clean: 456 kB\n\ | |
| Swap: 789 kB\n\ | |
| SwapPss: {} kB\n\ | |
| Locked: 987 kB", | |
| total_mib * 1024, | |
| anon_mib * 1024, | |
| file_mib * 1024, | |
| shmem_mib * 1024, | |
| swap_mib * 1024 | |
| ); | |
| std::fs::write(pid_path.join("smaps_rollup"), smaps_rollup_content).unwrap(); | |
| let smaps_content = format!( | |
| "7ffcd41f5000-7ffcd41f8000 r-xp 00000000 00:00 0 [vdso]\n\ | |
| Size: 123 kB\n\ | |
| Pss: 456 kB\n\ | |
| Swap: 789 kb\n\ | |
| 7ffcd41f8000-7ffcd41f9000 rw-s 00000000 {} 4988838 /memfd:name (deleted)\n\ | |
| Size: 321 kB\n\ | |
| Pss: 654 kB\n\ | |
| Swap: {} kb\n\ | |
| 7bf540e00000-7bf541e00000 r--p 001db000 103:05 12012 /lib64/libc.so.6\n\ | |
| Size: 987 kB\n\ | |
| Pss: 654 kB\n\ | |
| Swap: 321 kB\n", | |
| memfd_dev_id, | |
| shmem_swap_mib * 1024 | |
| ); | |
| std::fs::write(pid_path.join("smaps"), smaps_content).unwrap(); | |
| } else { | |
| std::fs::File::create(pid_path.join("smaps_rollup")).unwrap(); | |
| } | |
| } | |
| #[test] | |
| fn report_process_stats_container() { | |
| let temp_dir = TempDir::new().unwrap(); | |
| let proc_dir_path = temp_dir.path().join("proc"); | |
| let run_dir_path = temp_dir.path().join("run"); | |
| std::fs::create_dir(&proc_dir_path).unwrap(); | |
| std::fs::create_dir(&run_dir_path).unwrap(); | |
| let arc_init_pid = 22; | |
| let arc_pid_path = run_dir_path.join("containers/android-run_oci"); | |
| std::fs::create_dir_all(&arc_pid_path).unwrap(); | |
| std::fs::write( | |
| arc_pid_path.join("container.pid"), | |
| format!("{}", arc_init_pid), | |
| ) | |
| .unwrap(); | |
| #[rustfmt::skip] | |
| { | |
| // init. | |
| create_proc_entry( | |
| &proc_dir_path, 1, 0, | |
| b"init", Some(("/sbin/init", "")), | |
| 10, 5, 5, 0, 7, 3, | |
| ); | |
| // kthreadd (kernel daemon) | |
| create_proc_entry(&proc_dir_path, 2, 0, b"kthreadd", None, 0, 0, 0, 0, 0, 0); | |
| // kworker with a space in its name | |
| create_proc_entry( | |
| &proc_dir_path, 3, 2, | |
| b"kworker/0:0-My worker", None, | |
| 0, 0, 0, 0, 0, 0, | |
| ); | |
| // ARC init. | |
| create_proc_entry( | |
| &proc_dir_path, arc_init_pid, 1, | |
| b"arc-init", Some(("/blah/arc/init", "")), | |
| 10, 5, 5, 0, 1, 0, | |
| ); | |
| // ARC child process | |
| create_proc_entry( | |
| &proc_dir_path, 300, arc_init_pid, | |
| b"system_server", Some(("/blah/arc/system_server", "")), | |
| 100, 7, 6, 13, 10, 0, | |
| ); | |
| // Browser processes. | |
| create_proc_entry( | |
| &proc_dir_path, 100, 1, | |
| b"chrome", Some(("/opt/google/chrome/chrome", "blah")), | |
| 300, 200, 90, 10, 2, 0, | |
| ); | |
| create_proc_entry( | |
| &proc_dir_path, 101, 100, | |
| b"chrome", Some(("/opt/google/chrome/chrome", "--type=broker")), | |
| 5, 4, 3, 2, 1, 0, | |
| ); | |
| // Other spawned-from-chrome processes with a ) in the name. | |
| // Anything spawned from the Chrome browser process will count under browser | |
| // if it doesn't count under one of the other categories. | |
| create_proc_entry( | |
| &proc_dir_path, 102, 100, | |
| b"bash (stuff)", Some(("/bin/bash /usr/bin/somescript", "")), | |
| 400, 50, 245, 100, 5, 0, | |
| ); | |
| create_proc_entry( | |
| &proc_dir_path, 103, 100, | |
| b"corrupt )))) R Q", Some(("/bin/bash /usr/bin/somescript", "")), | |
| 100, 33, 33, 33, 1, 0, | |
| ); | |
| // GPU. | |
| create_proc_entry( | |
| &proc_dir_path, 110, 100, | |
| b"chrome", Some(("/opt/google/chrome/chrome", "--type=gpu-process")), | |
| 400, 70, 30, 300, 3, 0, | |
| ); | |
| // Renderers. | |
| create_proc_entry( | |
| &proc_dir_path, 120, 100, | |
| b"chrome", Some(("/opt/google/chrome/chrome", "--type=renderer")), | |
| 500, 450, 30, 20, 13, 0, | |
| ); | |
| create_proc_entry( | |
| &proc_dir_path, 121, 100, | |
| b"chrome", Some(("/opt/google/chrome/chrome", "--type=renderer")), | |
| 500, 450, 30, 20, 13, 0, | |
| ); | |
| // Name not UTF-8, but still a child of the browser | |
| create_proc_entry( | |
| &proc_dir_path, 122, 100, | |
| b"p\xb9Q\xc8", Some(("/opt/google/chrome/chrome", "--type=renderer")), | |
| 113, 33, 80, 0, 0, 0, | |
| ); | |
| // Daemons. | |
| create_proc_entry( | |
| &proc_dir_path, 200, 1, | |
| b"shill", Some(("/usr/bin/shill", "")), | |
| 100, 30, 70, 0, 0, 0, | |
| ); | |
| // Parent has died, will get reparented as a daemon | |
| create_proc_entry( | |
| &proc_dir_path, 45, 151, | |
| b"frecon", Some(("/usr/bin/frecon", "")), | |
| 213, 133, 80, 32, 48, 0, | |
| ); | |
| }; | |
| let stats = get_all_memory_stats( | |
| &proc_dir_path.to_string_lossy(), | |
| &run_dir_path.to_string_lossy(), | |
| ) | |
| .unwrap(); | |
| let expected = [ | |
| // browser | |
| [805 * MIB, 287 * MIB, 371 * MIB, 145 * MIB, 9 * MIB], | |
| // gpu | |
| [400 * MIB, 70 * MIB, 30 * MIB, 300 * MIB, 3 * MIB], | |
| // renderers | |
| [1113 * MIB, 933 * MIB, 140 * MIB, 40 * MIB, 26 * MIB], | |
| // arc | |
| [110 * MIB, 12 * MIB, 11 * MIB, 13 * MIB, 11 * MIB], | |
| // vms | |
| [0, 0, 0, 0, 0], | |
| // daemons | |
| [323 * MIB, 168 * MIB, 155 * MIB, 32 * MIB, 55 * MIB], | |
| ]; | |
| assert_eq!(stats, expected); | |
| } | |
| #[test] | |
| fn report_process_stats_arcvm() { | |
| let temp_dir = TempDir::new().unwrap(); | |
| let proc_dir_path = temp_dir.path().join("proc"); | |
| let run_dir_path = temp_dir.path().join("run"); | |
| std::fs::create_dir(&proc_dir_path).unwrap(); | |
| std::fs::create_dir(&run_dir_path).unwrap(); | |
| #[rustfmt::skip] | |
| { | |
| // init. | |
| create_proc_entry( | |
| &proc_dir_path, 1, 0, | |
| b"init", Some(("/sbin/init", "")), | |
| 10, 5, 5, 0, 7, 0); | |
| // vm_concierge | |
| create_proc_entry( | |
| &proc_dir_path, 100, 1, | |
| b"vm_concierge", Some(("/usr/bin/vm_concierge", "")), | |
| 10, 5, 5, 0, 1, 0, | |
| ); | |
| // ARCVM | |
| create_proc_entry( | |
| &proc_dir_path, 200, 100, | |
| b"crosvm", Some(("/usr/bin/crosvm", "androidboot.hardware=bertha vmlinux")), | |
| 100, 50, 50, 10, 10, 645, | |
| ); | |
| create_proc_entry( | |
| &proc_dir_path, 201, 200, | |
| b"crosvm", Some(("/usr/bin/crosvm", "androidboot.hardware=bertha vmlinux")), | |
| 100, 50, 50, 10, 10, 321, | |
| ); | |
| create_proc_entry( | |
| &proc_dir_path, 202, 201, | |
| b"crosvm", Some(("/usr/libexec/virgl_render_server", "")), | |
| 12, 31, 56, 78, 90, 321, | |
| ); | |
| create_proc_entry( | |
| &proc_dir_path, 203, 201, | |
| b"crosvm", Some(("/usr/bin/crosvm", "androidboot.hardware=bertha vmlinux")), | |
| 98, 76, 54, 32, 10, 321, | |
| ); | |
| // Other VMs | |
| create_proc_entry( | |
| &proc_dir_path, 300, 100, | |
| b"crosvm", Some(("/usr/bin/crosvm", "vmlinux")), | |
| 10, 5, 5, 0, 1, 478, | |
| ); | |
| create_proc_entry( | |
| &proc_dir_path, 301, 300, | |
| b"crosvm", Some(("/usr/bin/crosvm", "vmlinux")), | |
| 10, 5, 5, 0, 1, 213, | |
| ); | |
| // seneschal | |
| create_proc_entry( | |
| &proc_dir_path, 101, 1, | |
| b"seneschal", Some(("/usr/bin/seneschal", "")), | |
| 35, 2, 91, 11, 13, 0, | |
| ); | |
| }; | |
| let stats = get_all_memory_stats( | |
| &proc_dir_path.to_string_lossy(), | |
| &run_dir_path.to_string_lossy(), | |
| ) | |
| .unwrap(); | |
| let expected = [ | |
| // browser | |
| [0, 0, 0, 0, 0], | |
| // gpu | |
| [0, 0, 0, 0, 0], | |
| // renderers | |
| [0, 0, 0, 0, 0], | |
| // arc | |
| [310 * MIB, 207 * MIB, 210 * MIB, 130 * MIB, 765 * MIB], | |
| // vms | |
| [65 * MIB, 17 * MIB, 106 * MIB, 11 * MIB, 494 * MIB], | |
| // daemons | |
| [10 * MIB, 5 * MIB, 5 * MIB, 0, 7 * MIB], | |
| ]; | |
| assert_eq!(stats, expected); | |
| } | |
| #[test] | |
| fn report_process_stats_lacros() { | |
| let temp_dir = TempDir::new().unwrap(); | |
| let proc_dir_path = temp_dir.path().join("proc"); | |
| let run_dir_path = temp_dir.path().join("run"); | |
| std::fs::create_dir(&proc_dir_path).unwrap(); | |
| std::fs::create_dir(&run_dir_path).unwrap(); | |
| #[rustfmt::skip] | |
| { | |
| // init. | |
| create_proc_entry( | |
| &proc_dir_path, 1, 0, | |
| b"init", Some(("/sbin/init", "")), | |
| 10, 5, 5, 0, 7, 0); | |
| // Browser processes. | |
| create_proc_entry( | |
| &proc_dir_path, 100, 1, | |
| b"chrome", Some(("/opt/google/chrome/chrome", "blah")), | |
| 300, 200, 90, 10, 2, 0, | |
| ); | |
| // GPU. | |
| create_proc_entry( | |
| &proc_dir_path, 110, 100, | |
| b"chrome", Some(("/opt/google/chrome/chrome", "--type=gpu-process")), | |
| 400, 70, 30, 300, 3, 0, | |
| ); | |
| // Renderers. | |
| create_proc_entry( | |
| &proc_dir_path, 120, 100, | |
| b"chrome", Some(("/opt/google/chrome/chrome", "--type=renderer")), | |
| 500, 450, 30, 20, 13, 0, | |
| ); | |
| // Lacros browser processes. | |
| create_proc_entry( | |
| &proc_dir_path, 200, 100, | |
| b"chrome", Some(("/run/lacros/chrome", "blah")), | |
| 30, 20, 9, 1, 3, 0, | |
| ); | |
| create_proc_entry( | |
| &proc_dir_path, 201, 200, | |
| b"chrome", Some(("/run/lacros/nacl_helper", "blah")), | |
| 10, 30, 87, 42, 2, 0, | |
| ); | |
| // Lacros GPU. | |
| create_proc_entry( | |
| &proc_dir_path, 210, 200, | |
| b"chrome", Some(("/opt/google/chrome/chrome", "--type=gpu-process")), | |
| 40, 7, 3, 30, 5, 0, | |
| ); | |
| // Lacros Renderers. | |
| create_proc_entry( | |
| &proc_dir_path, 220, 200, | |
| b"chrome", Some(("/opt/google/chrome/chrome", "--type=renderer")), | |
| 50, 45, 3, 2, 7, 0, | |
| ); | |
| }; | |
| let stats = get_all_memory_stats( | |
| &proc_dir_path.to_string_lossy(), | |
| &run_dir_path.to_string_lossy(), | |
| ) | |
| .unwrap(); | |
| let expected = [ | |
| // browser | |
| [340 * MIB, 250 * MIB, 186 * MIB, 53 * MIB, 7 * MIB], | |
| // gpu | |
| [440 * MIB, 77 * MIB, 33 * MIB, 330 * MIB, 8 * MIB], | |
| // renderers | |
| [550 * MIB, 495 * MIB, 33 * MIB, 22 * MIB, 20 * MIB], | |
| // arc | |
| [0, 0, 0, 0, 0], | |
| // vms | |
| [0, 0, 0, 0, 0], | |
| // daemons | |
| [10 * MIB, 5 * MIB, 5 * MIB, 0, 7 * MIB], | |
| ]; | |
| assert_eq!(stats, expected); | |
| } | |
| #[test] | |
| fn check_metric_names() { | |
| assert_eq!( | |
| "Platform.Memory.Browser.Total", | |
| get_metric_name(ProcessGroupKind::Browser as usize, MemKind::Total as usize) | |
| ); | |
| assert_eq!( | |
| "Platform.Memory.Gpu.Anon", | |
| get_metric_name(ProcessGroupKind::Gpu as usize, MemKind::Anon as usize) | |
| ); | |
| assert_eq!( | |
| "Platform.Memory.Renderers.File", | |
| get_metric_name(ProcessGroupKind::Renderers as usize, MemKind::File as usize) | |
| ); | |
| assert_eq!( | |
| "Platform.Memory.ARC.Shmem", | |
| get_metric_name(ProcessGroupKind::ARC as usize, MemKind::Shmem as usize) | |
| ); | |
| assert_eq!( | |
| "Platform.Memory.VMs.Swap", | |
| get_metric_name(ProcessGroupKind::VMs as usize, MemKind::Swap as usize) | |
| ); | |
| assert_eq!( | |
| "Platform.Memory.Daemons.Total", | |
| get_metric_name(ProcessGroupKind::Daemons as usize, MemKind::Total as usize) | |
| ); | |
| } | |
| } |