blob: 371422679d89465d63ecdbbefb42e6035852fbf6 [file] [log] [blame]
// Copyright 2019 The Chromium OS Authors. All rights reserved.
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.
#include "metrics/process_meter.h"
#include <pcrecpp.h>
#include <string>
#include <unordered_map>
#include <vector>
#include <base/command_line.h>
#include <base/files/file_enumerator.h>
#include <base/files/file_path.h>
#include <base/files/file_util.h>
#include <base/macros.h>
#include <base/strings/string_number_conversions.h>
#include <base/strings/string_split.h>
#include <base/strings/string_util.h>
#include <base/strings/stringprintf.h>
#include <gtest/gtest_prod.h> // for FRIEND_TEST
#include "metrics/metrics_library.h"
namespace chromeos_metrics {
// UMA histogram names for process memory usage, split by process groups and
// types of memory. They must match MemoryStatKind and ProcessGroupKind in
// process_meter.h. C++ doesn't have C-style static array initializers, so the
// unit test checks this.
constexpr char const* kProcessMemoryUMANames[PG_KINDS_COUNT][MEM_KINDS_COUNT] =
{{
"Platform.Memory.Browser.Total",
"Platform.Memory.Browser.Anon",
"Platform.Memory.Browser.File",
"Platform.Memory.Browser.Shmem",
"Platform.Memory.Browser.Swap",
},
{
"Platform.Memory.Gpu.Total",
"Platform.Memory.Gpu.Anon",
"Platform.Memory.Gpu.File",
"Platform.Memory.Gpu.Shmem",
"Platform.Memory.Gpu.Swap",
},
{
"Platform.Memory.Renderers.Total",
"Platform.Memory.Renderers.Anon",
"Platform.Memory.Renderers.File",
"Platform.Memory.Renderers.Shmem",
"Platform.Memory.Renderers.Swap",
},
{
"Platform.Memory.ARC.Total",
"Platform.Memory.ARC.Anon",
"Platform.Memory.ARC.File",
"Platform.Memory.ARC.Shmem",
"Platform.Memory.ARC.Swap",
},
{
"Platform.Memory.VMs.Total",
"Platform.Memory.VMs.Anon",
"Platform.Memory.VMs.File",
"Platform.Memory.VMs.Shmem",
"Platform.Memory.VMs.Swap",
},
{
"Platform.Memory.Daemons.Total",
"Platform.Memory.Daemons.Anon",
"Platform.Memory.Daemons.File",
"Platform.Memory.Daemons.Shmem",
"Platform.Memory.Daemons.Swap",
}};
// 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 kRendererProcess[] = "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.
ChromeProcessKind GetChromeKind(const base::CommandLine& cmdline) {
// Assume all Chrome binaries are in /opt/google/chrome.
auto program = cmdline.GetProgram().MaybeAsASCII();
// Chrome execs a bunch of other binaries (for instance, crossystem) so we
// can't have a complete list.
if (!base::StartsWith(program, "/opt/google/chrome",
base::CompareCase::SENSITIVE)) {
return CHROME_OTHER;
}
if (program.find("/opt/google/chrome/nacl_helper") == 0)
return CHROME_BROWSER_HELPER;
// The Browser process needs to be identified as a binary named "chrome"
// in addition to not having a "type" because there are other binaries
// in that directory which may be running.
if (!cmdline.HasSwitch("type") && (program == "/opt/google/chrome/chrome"))
return CHROME_BROWSER;
auto type = cmdline.GetSwitchValueASCII("type");
// TODO(chromium:963210): remove the following "if" and let the next one
// handle the "broker" case.
if (strcmp(type.c_str(), "broker") == 0)
return CHROME_BROWSER_HELPER;
if (type == "broker" || type == "ppapi-broker" || type == "zygote") {
return CHROME_BROWSER_HELPER;
}
// clang-format off
if (type == "renderer" ||
type == "ppapi" ||
type == "sandbox" ||
type == "utility") {
return CHROME_RENDERER;
}
// clang-format on
if (type == "gpu-process")
return CHROME_GPU;
return CHROME_OTHER;
}
bool GetARCInitPID(const base::FilePath& run_root, int* pid_out) {
// ARC init may have stopped and restarted, so look up its PID.
std::string file_content;
if (!base::ReadFileToString(run_root.Append(kMetricsARCInitPIDFile),
&file_content)) {
// ARC is not running.
return false;
}
base::TrimWhitespaceASCII(file_content, base::TRIM_TRAILING, &file_content);
if (!base::StringToInt(file_content, pid_out)) {
LOG(FATAL) << "invalid integer in ARC init pid file: " << file_content;
}
return true;
}
bool FindProcessWithPrefix(
const std::string& prefix,
const std::unordered_map<int, std::unique_ptr<ProcessNode>>& processes,
ProcessNode** process) {
for (const auto& pit : processes) {
if (pit.second->HasPrefix(prefix)) {
*process = pit.second.get();
return true;
}
}
return false;
}
const bool ProcessNode::HasPrefix(const std::string& prefix) const {
return cmdline_.GetProgram().MaybeAsASCII().find(prefix) == 0;
}
const void ProcessNode::CollectSubtree(std::vector<ProcessNode*>* processes) {
processes->push_back(this);
for (const auto& child : children_) {
child->CollectSubtree(processes);
}
}
void ProcessInfo::Classify() {
// Find all ARC processes starting from ARC init.
int arc_init_pid;
if (GetARCInitPID(run_root_, &arc_init_pid)) {
if (process_map_.find(arc_init_pid) == process_map_.end()) {
LOG(WARNING) << "ARC init disappeared";
} else {
process_map_[arc_init_pid]->CollectSubtree(&groups_[PG_ARC]);
}
}
// Find VM processes starting from vm_concierge and seneschal processes.
ProcessNode* concierge;
if (FindProcessWithPrefix("/usr/bin/vm_concierge", process_map_, &concierge))
concierge->CollectSubtree(&groups_[PG_VMS]);
ProcessNode* seneschal;
if (FindProcessWithPrefix("/usr/bin/seneschal", process_map_, &seneschal)) {
seneschal->CollectSubtree(&groups_[PG_VMS]);
}
// Find the browser process.
ProcessNode* browser_process = nullptr;
for (const auto& pit : process_map_) {
if (GetChromeKind(pit.second->GetCmdline()) == CHROME_BROWSER) {
browser_process = pit.second.get();
}
}
// Find all descendants of the chrome browser.
std::vector<ProcessNode*> chrome_processes;
if (browser_process != nullptr)
browser_process->CollectSubtree(&chrome_processes);
// Classify the chrome processes.
for (const auto& process : chrome_processes) {
switch (GetChromeKind(process->GetCmdline())) {
case CHROME_RENDERER:
groups_[PG_RENDERERS].push_back(process);
break;
case CHROME_GPU:
groups_[PG_GPU].push_back(process);
break;
case CHROME_BROWSER:
case CHROME_BROWSER_HELPER:
groups_[PG_BROWSER].push_back(process);
break;
case CHROME_OTHER:
// Treat other as a browser process.
LOG(WARNING) << "Unknown chrome process type in "
<< process->GetCmdlineString();
groups_[PG_BROWSER].push_back(process);
break;
case CHROME_NOT_CHROME:
LOG(FATAL) << "Unexpected chrome process: "
<< process->GetCmdlineString();
break;
}
}
// Compute daemon processes. Start by making a copy of the map of all
// processes. Then remove ARC, VMs, and Chrome processes.
std::unordered_map<int, ProcessNode*> daemon_processes_map;
for (const auto& pit : process_map_) {
daemon_processes_map[pit.first] = pit.second.get();
}
for (const auto& process : groups_[PG_ARC]) {
daemon_processes_map.erase(process->GetPID());
}
for (const auto& process : groups_[PG_VMS]) {
daemon_processes_map.erase(process->GetPID());
}
for (const auto& process : chrome_processes) {
daemon_processes_map.erase(process->GetPID());
}
for (const auto& pit : daemon_processes_map) {
groups_[PG_DAEMONS].push_back(pit.second);
}
// Make sure there is at most one GPU process.
CHECK_LE(groups_[PG_GPU].size(), 1);
}
bool ProcessNode::RetrieveProcessData(const base::FilePath& procfs_root) {
std::string file_content;
// Get PPID and name from /proc/#/stat.
const std::string stat_name = base::StringPrintf("%d/stat", pid_);
const base::FilePath stat_path = procfs_root.Append(stat_name);
if (!base::ReadFileToString(stat_path, &file_content)) {
// Assume process has exited.
return false;
}
// stat: pid (comm) run_state ppid etc. The only parentheses in the file
// are around <comm>.
pcrecpp::RE re(R"(.*\((.*)\) \w+ (\d+)(.|\n)*)");
if (!re.FullMatch(file_content, &name_, &ppid_))
LOG(FATAL) << "cannot parse /proc/pid/stat: " << file_content;
// Get command line from /proc/#/cmdline and parse it.
const std::string cmdline_name = base::StringPrintf("%d/cmdline", pid_);
const base::FilePath cmdline_path = procfs_root.Append(cmdline_name);
if (!base::ReadFileToString(cmdline_path, &file_content)) {
// Assume process has exited.
return false;
}
cmdline_string_ = file_content;
cmdline_ = base::CommandLine(base::SplitString(
file_content, " ", base::TRIM_WHITESPACE, base::SPLIT_WANT_NONEMPTY));
return true;
}
void ProcessNode::LinkToParent(
const std::unordered_map<int, std::unique_ptr<ProcessNode>>& processes) {
if (ppid_ == 0) {
// Not every process has a parent.
return;
}
auto pit = processes.find(ppid_);
if (pit == processes.end()) {
// Parent process does not exist. This might happen on a race, before the
// orphan is reparented to init. At worst, this should be rare. We do the
// reparenting for consistency.
LOG(WARNING) << "PID " << pid_ << ": parent " << ppid_ << " not found";
ppid_ = 1;
pit = processes.find(ppid_);
}
// |pit| is now guaranteed to be valid.
parent_ = pit->second.get();
parent_->children_.push_back(this);
}
void ProcessInfo::Collect() {
// Collect all processes.
base::FileEnumerator proc_enum(procfs_root_, false,
base::FileEnumerator::DIRECTORIES);
for (base::FilePath path = proc_enum.Next(); !path.empty();
path = proc_enum.Next()) {
std::string pid_string = path.BaseName().MaybeAsASCII();
// Skip directories that do not represent processes.
int pid;
if (!base::StringToInt(pid_string, &pid))
continue;
if (process_map_.find(pid) == process_map_.end()) {
process_map_.emplace(pid, std::make_unique<ProcessNode>(pid));
} else {
// This seems rather unlikely, but just in case.
LOG(WARNING) << "duplicate PID: " << pid;
}
}
// Sanity check.
if (process_map_.find(1) == process_map_.end())
LOG(FATAL) << "cannot find init process";
// Construct process tree.
for (const auto& pit : process_map_) {
ProcessNode* process = pit.second.get();
if (!process->RetrieveProcessData(procfs_root_)) {
// Process went away, so ignore it.
continue;
}
// Set up parent/children links.
process->LinkToParent(process_map_);
}
}
const std::vector<ProcessNode*>& ProcessInfo::GetGroup(
ProcessGroupKind group_kind) {
return groups_[group_kind];
}
void GetMemoryUsage(const base::FilePath& procfs_path,
int pid,
ProcessMemoryStats* stats) {
std::string file_content;
const std::string file_name = base::StringPrintf("%d/totmaps", pid);
const base::FilePath file_path = procfs_path.Append(file_name);
if (!base::ReadFileToString(file_path, &file_content))
return;
const std::vector<std::string> lines = base::SplitString(
file_content, "\n", base::KEEP_WHITESPACE, base::SPLIT_WANT_NONEMPTY);
struct NameValuePair {
const std::string name;
uint64_t value;
};
std::vector<NameValuePair> pairs = {{"Pss:", 0},
{"Pss_Anon:", 0},
{"Pss_File:", 0},
{"Pss_Shmem:", 0},
{"Swap:", 0}};
int index = 0;
for (const auto& line : lines) {
if (base::StartsWith(line, pairs[index].name,
base::CompareCase::SENSITIVE)) {
std::vector<std::string> fields = base::SplitString(
line, " ", base::TRIM_WHITESPACE, base::SPLIT_WANT_NONEMPTY);
if (fields.size() != 3)
LOG(FATAL) << "bad rollup line: " << line;
if (!base::StringToUint64(fields[1], &pairs[index].value))
LOG(FATAL) << "bad integer in rollup line: " << line;
index++;
if (index == pairs.size())
break;
}
}
if (index < pairs.size() && index != 0) {
// If some fields aren't present, return zeros instead of crashing.
return;
}
stats->rss_sizes[MEM_TOTAL] = pairs[0].value * 1024;
stats->rss_sizes[MEM_ANON] = pairs[1].value * 1024;
stats->rss_sizes[MEM_FILE] = pairs[2].value * 1024;
stats->rss_sizes[MEM_SHMEM] = pairs[3].value * 1024;
stats->rss_sizes[MEM_SWAP] = pairs[4].value * 1024;
}
void AccumulateProcessGroupStats(const base::FilePath& procfs_path,
const std::vector<ProcessNode*>& processes,
ProcessMemoryStats* stats) {
for (const auto& process : processes) {
ProcessMemoryStats process_stats;
GetMemoryUsage(procfs_path, process->GetPID(), &process_stats);
// If GetMemoryUsage fails (which will happen if the process has
// exited), process_stats are all 0 and the accumulation is a no-op.
for (int i = 0; i < MEM_KINDS_COUNT; i++) {
stats->rss_sizes[i] += process_stats.rss_sizes[i];
}
}
}
} // namespace chromeos_metrics