blob: ac63c00b20def1a22430e56b5c465ade58d206c0 [file] [log] [blame]
// Copyright 2018 The Chromium 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 "chrome/browser/chromeos/power/process_data_collector.h"
#include <sys/types.h>
#include <algorithm>
#include <fstream>
#include <iostream>
#include <limits>
#include <numeric>
#include <string>
#include <unordered_map>
#include <unordered_set>
#include <utility>
#include <vector>
#include "base/bind.h"
#include "base/files/file_enumerator.h"
#include "base/files/file_path.h"
#include "base/files/file_util.h"
#include "base/i18n/number_formatting.h"
#include "base/logging.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 "base/strings/utf_string_conversions.h"
#include "base/synchronization/lock.h"
#include "base/task/post_task.h"
#include "base/threading/platform_thread.h"
#include "base/threading/scoped_blocking_call.h"
#include "base/time/time.h"
#include "base/values.h"
#include "chrome/browser/chromeos/system/procfs_util.h"
#include "chromeos/dbus/dbus_thread_manager.h"
#include "chromeos/dbus/power_manager_client.h"
#include "content/public/browser/browser_thread.h"
namespace chromeos {
ProcessDataCollector* g_process_data_collector = nullptr;
namespace {
// The command line that will used to invoke the Crostini concierge daemon.
constexpr char kConciergeCmdline[] = "/usr/bin/vm_concierge";
// The full file path that will be used to invoke chrome.
constexpr char kChromeCmdPath[] = "/opt/google/chrome/chrome";
// Sampling frequency.
constexpr base::TimeDelta kSampleDelay = base::TimeDelta::FromSeconds(15);
// Time after which a sample is invalid. Must be greater than |kSampleDelay|.
constexpr base::TimeDelta kExcessiveDelay = base::TimeDelta::FromSeconds(30);
// Represents a map of all processes; maps a PPID to a PID.
using PpidToPidMap = std::unordered_multimap<pid_t, pid_t>;
// Represents the CPU and power exponential moving averages.
struct CpuUsageAndPowerAverage {
// Represents an accumulated average; this will change depending on the
// |AveragingTechnique| specified.
double accumulated_cpu_usages;
// Represents the the average power usage that is calculated from the
// |accumulated_cpu_usages|. How it is calculated depends on the
// |AveragingTechnique|.
double power_average;
};
// Represents the CPU time a process has used and its PPID.
using ProcCpuUsageAndPpid = std::pair<int64_t, pid_t>;
// Returns ARC's init PID.
base::Optional<pid_t> GetAndroidInitPid(
const base::FilePath& android_pid_file) {
// This function does I/O and must be done on a blocking thread.
base::ScopedBlockingCall scoped_blocking_call(FROM_HERE,
base::BlockingType::MAY_BLOCK);
std::string android_pid_contents;
if (!base::ReadFileToString(android_pid_file, &android_pid_contents))
return base::nullopt;
// This file contains a single number which contains the PID of the Android
// init PID.
pid_t android_pid;
base::TrimWhitespaceASCII(android_pid_contents, base::TRIM_ALL,
&android_pid_contents);
if (!base::StringToInt(android_pid_contents, &android_pid))
return base::nullopt;
return android_pid;
}
// Calculates the total CPU time used by a single process in jiffies and its
// PPID.
base::Optional<ProcCpuUsageAndPpid> ComputeProcCpuTimeJiffiesAndPpid(
const base::FilePath& proc_stat_file) {
// This function does I/O and must be done on a blocking thread.
base::ScopedBlockingCall scoped_blocking_call(FROM_HERE,
base::BlockingType::MAY_BLOCK);
base::Optional<system::SingleProcStat> stat =
system::GetSingleProcStat(proc_stat_file);
if (!stat.has_value())
return base::nullopt;
return std::make_pair(stat.value().utime + stat.value().stime,
stat.value().ppid);
}
// Reads a process' name from |comm_file|, a file like "/proc/%u/comm".
base::Optional<std::string> GetProcName(const base::FilePath& comm_file) {
// This function does I/O and must be done on a blocking thread.
base::ScopedBlockingCall scoped_blocking_call(FROM_HERE,
base::BlockingType::MAY_BLOCK);
std::string comm_contents;
if (!base::ReadFileToString(comm_file, &comm_contents))
return base::nullopt;
base::TrimWhitespaceASCII(comm_contents, base::TRIM_ALL, &comm_contents);
return comm_contents.empty() ? base::nullopt
: base::make_optional(comm_contents);
}
// Reads a process's command line from |cmdline|, a path like
// "/proc/%u/cmdline".
base::Optional<std::string> GetProcCmdline(const base::FilePath& cmdline) {
// This function does I/O and must be done on a blocking thread.
base::ScopedBlockingCall scoped_blocking_call(FROM_HERE,
base::BlockingType::MAY_BLOCK);
std::string cmdline_contents;
if (!base::ReadFileToString(cmdline, &cmdline_contents))
return base::nullopt;
base::TrimWhitespaceASCII(cmdline_contents, base::TRIM_ALL,
&cmdline_contents);
return cmdline_contents.empty() ? base::nullopt
: base::make_optional(cmdline_contents);
}
// Finds all children of a root PID recursively and stores the results in
// |visited|.
void GetChildPids(const PpidToPidMap& ppid_to_pid,
pid_t root,
std::unordered_set<pid_t>* visited) {
visited->insert(root);
auto adj = ppid_to_pid.equal_range(root);
for (auto iter = adj.first; iter != adj.second; iter++) {
pid_t pid = iter->second;
if (visited->find(pid) == visited->end())
GetChildPids(ppid_to_pid, pid, visited);
}
}
// Returns a new average given a |summed_averages| aggregated over |num_samples|
// samples and a |new_sample|.
CpuUsageAndPowerAverage ComputeNormalAverages(int64_t num_samples,
double summed_averages,
double new_sample) {
// When the |AveragingTechnique| used is |AveragingTechnique::AVERAGE|, the
// |summed_averages| that should be passed into this function should be
// accumulated sums of the average CPU usages over all of the previous
// intervals. Specifically, say that n averages over n intervals have been
// calculated, a1,...,an and the a(n+1)th sample is just passed, then:
// |num_samples| = n
// |summed_averages| = a1 + a2 + ... + an
// |new_sample| = a(n+1)
double accumulated_cpu_usages = summed_averages + new_sample;
double new_power_average = accumulated_cpu_usages / (num_samples + 1);
return {accumulated_cpu_usages, new_power_average};
}
// Returns new exponential moving averages calculated as follows:
// Let a = the weight for CPU averages and b = the weight for power averages.
// First calculate:
// c_new = (1 - a) * c_old + a * c_curr
// b_new = (1 - b) * b_old + b * c_new
// Where c_new is the new exponential moving average for CPU usages, c_old is
// the currently aggregated CPU usages, and c_curr is the new CPU average over a
// single interval. Similarly, b_old is the old power exponential moving
// average and b_new is the newly calculated exponential moving average. The
// function returns c_new and b_new.
CpuUsageAndPowerAverage ComputeExponentialMovingAverages(
double curr_cpu_average,
double curr_power_average,
double new_sample) {
double new_cpu_average =
(1 - ProcessDataCollector::kCpuUsageExponentialMovingAverageWeight) *
curr_cpu_average +
ProcessDataCollector::kCpuUsageExponentialMovingAverageWeight *
new_sample;
double new_power_average =
(1 - ProcessDataCollector::kPowerUsageExponentialMovingAverageWeight) *
curr_power_average +
ProcessDataCollector::kPowerUsageExponentialMovingAverageWeight *
new_cpu_average;
return {new_cpu_average, new_power_average};
}
} // namespace
ProcessDataCollector::ProcessUsageData::ProcessUsageData(
const ProcessData& process_data,
double power_usage_fraction)
: process_data(process_data), power_usage_fraction(power_usage_fraction) {}
ProcessDataCollector::ProcessUsageData::ProcessUsageData(
const ProcessUsageData& p) = default;
ProcessDataCollector::ProcessUsageData::~ProcessUsageData() = default;
ProcessDataCollector::Config::Config(const base::FilePath& procfs,
const base::FilePath& total_cpu_time,
const base::FilePath& android_init,
const std::string& cmdline_fmt,
const std::string& stat_fmt,
const std::string& comm_fmt,
base::TimeDelta delay,
AveragingTechnique technique)
: proc_dir(procfs),
total_cpu_time_path(total_cpu_time),
android_init_pid_path(android_init),
proc_cmdline_fmt(cmdline_fmt),
proc_stat_fmt(stat_fmt),
proc_comm_fmt(comm_fmt),
sample_delay(delay),
averaging_technique(technique) {}
ProcessDataCollector::Config::Config(const Config& config) = default;
ProcessDataCollector::Config::~Config() = default;
// static
void ProcessDataCollector::Initialize() {
DCHECK(!g_process_data_collector);
// A |ProcessDataCollector::Config| struct that contains a set of parameters
// to be used in a production environment.
const ProcessDataCollector::Config kRealConfig(
// /proc directory which stores process information.
base::FilePath("/proc"),
// Contains the total amount of CPU time used.
base::FilePath("/proc/stat"),
// Contains the PID of the ARC init process.
base::FilePath("/run/containers/android-run_oci/container.pid"),
// Contains the command used to start a process.
"/proc/%u/cmdline",
// Contains the amount of CPU time a process has used and its PPID.
"/proc/%u/stat",
// Contains the name of the process.
"/proc/%u/comm", kSampleDelay,
ProcessDataCollector::Config::AveragingTechnique::AVERAGE);
g_process_data_collector = new ProcessDataCollector(kRealConfig);
g_process_data_collector->StartSamplingCpuUsage();
}
// static
void ProcessDataCollector::InitializeForTesting(const Config& config) {
DCHECK(!g_process_data_collector);
g_process_data_collector = new ProcessDataCollector(config);
}
// static
ProcessDataCollector* ProcessDataCollector::Get() {
DCHECK(g_process_data_collector);
return g_process_data_collector;
}
// static
void ProcessDataCollector::Shutdown() {
DCHECK(g_process_data_collector);
delete g_process_data_collector;
g_process_data_collector = nullptr;
}
void ProcessDataCollector::SampleCpuUsageForTesting() {
DCHECK(g_process_data_collector);
SamplesAndSummaryInfo samples_and_summary_info =
g_process_data_collector->ComputeSampleAsync(
g_process_data_collector->config_,
g_process_data_collector->prev_samples_,
g_process_data_collector->curr_samples_,
g_process_data_collector->curr_summary_);
g_process_data_collector->SaveSamplesOnUIThread(samples_and_summary_info);
}
const std::vector<ProcessDataCollector::ProcessUsageData>
ProcessDataCollector::GetProcessUsages() {
std::vector<ProcessUsageData> process_list;
// Gather the summarized statistics and generate user-facing information.
for (const auto& proc : curr_summary_) {
process_list.emplace_back(ProcessDataCollector::ProcessUsageData(
ProcessData(proc.second.process_data),
proc.second.power_usage_fraction));
}
// Since the sampling will give us approximate CPU usages and some processes
// are discarded in |GetValidProcesses|, the total fraction of CPU usages in
// this list of processes may not sum up to 1. Normalizing ensures that the
// fractions make sense.
double total = std::accumulate(
process_list.begin(), process_list.end(), 0.,
[](const auto& i, const auto& s) { return i + s.power_usage_fraction; });
if (total != 0) {
std::for_each(process_list.begin(), process_list.end(),
[&total](auto& c) { c.power_usage_fraction /= total; });
}
return process_list;
}
ProcessDataCollector::ProcessData::ProcessData() = default;
ProcessDataCollector::ProcessData::ProcessData(pid_t pid,
const std::string& name,
const std::string& cmdline,
PowerConsumerType type)
: pid(pid), name(name), cmdline(cmdline), type(type) {}
ProcessDataCollector::ProcessData::ProcessData(const ProcessData& p) = default;
ProcessDataCollector::ProcessData::~ProcessData() = default;
ProcessDataCollector::ProcessSample::ProcessSample() = default;
ProcessDataCollector::ProcessSample::ProcessSample(const ProcessSample& p) =
default;
ProcessDataCollector::ProcessSample::~ProcessSample() = default;
ProcessDataCollector::ProcessStoredData::ProcessStoredData() = default;
ProcessDataCollector::ProcessStoredData::ProcessStoredData(
const ProcessStoredData& p) = default;
ProcessDataCollector::ProcessStoredData::~ProcessStoredData() = default;
ProcessDataCollector::ProcessDataCollector(const Config& config)
: config_(config), weak_ptr_factory_(this) {}
ProcessDataCollector::~ProcessDataCollector() = default;
void ProcessDataCollector::StartSamplingCpuUsage() {
cpu_data_task_runner_ = base::CreateSequencedTaskRunnerWithTraits(
{base::MayBlock(), base::TaskPriority::BEST_EFFORT});
cpu_data_timer_.Start(FROM_HERE, config_.sample_delay, this,
&ProcessDataCollector::SampleCpuUsage);
}
void ProcessDataCollector::SampleCpuUsage() {
base::PostTaskAndReplyWithResult(
cpu_data_task_runner_.get(), FROM_HERE,
base::BindOnce(&ProcessDataCollector::ComputeSampleAsync, config_,
prev_samples_, curr_samples_, curr_summary_),
base::BindOnce(&ProcessDataCollector::SaveSamplesOnUIThread,
weak_ptr_factory_.GetWeakPtr()));
}
// static
ProcessDataCollector::ProcessSampleMap ProcessDataCollector::GetValidProcesses(
const Config& config) {
// This function does I/O and must be done on a blocking thread.
base::ScopedBlockingCall scoped_blocking_call(FROM_HERE,
base::BlockingType::MAY_BLOCK);
base::FileEnumerator proc_files(config.proc_dir, false,
base::FileEnumerator::DIRECTORIES);
ProcessSampleMap procs;
for (base::FilePath path = proc_files.Next(); !path.empty();
path = proc_files.Next()) {
pid_t proc;
if (!base::StringToInt(path.BaseName().value(), &proc))
continue;
// Don't track if either the process name or cmdline are empty or
// non-existent.
base::Optional<std::string> proc_name = GetProcName(
base::FilePath(base::StringPrintf(config.proc_comm_fmt.c_str(), proc)));
if (!proc_name)
continue;
base::Optional<std::string> proc_cmdline = GetProcCmdline(base::FilePath(
base::StringPrintf(config.proc_cmdline_fmt.c_str(), proc)));
if (!proc_cmdline)
continue;
ProcessSample psample;
psample.valid = true;
psample.process_data.pid = proc;
psample.process_data.name = proc_name.value();
psample.process_data.cmdline = proc_cmdline.value();
// Set every process type to be a system process. Once procfs is sampled in
// more detail, the actual type of power consumer can be set and determined.
psample.process_data.type = ProcessDataCollector::PowerConsumerType::SYSTEM;
psample.now = base::TimeTicks::Now();
// |procs| starts off as an empty |ProcessSampleMap| and for every
// iteration of this loop, |proc| will correspond to the PID of a different
// process. Thus, this call to |std::unordered_map::emplace| should never be
// called with the same key twice.
procs.emplace(std::make_pair(proc, std::move(psample)));
}
return procs;
}
// static
ProcessDataCollector::ProcessSampleMap ProcessDataCollector::ComputeSample(
ProcessSampleMap curr_samples,
const Config& config) {
// First the amount of CPU time used by the machine is gathered. Then, for
// each process, the PPID and the amount of CPU time it has used is gathered.
// All this information is stored in the process sample so that an average can
// later be calculated. Additionally, a PPID to PID map is constructed so that
// different types of processes can be classified; this is needed to classify
// ARC process for example.
base::Optional<int64_t> total_cpu_time =
system::GetCpuTimeJiffies(config.total_cpu_time_path);
// If this can't be read, then the average CPU usage over this interval can't
// be calculated. Ignore these samples.
if (!total_cpu_time.value()) {
// Set all the samples to be invalid.
for (auto& sample : curr_samples)
sample.second.valid = false;
return curr_samples;
}
base::Optional<int64_t> concierge_pid = base::nullopt;
std::unordered_map<pid_t, int64_t> pid_to_cpu_usage_before;
std::unordered_set<uint64_t> chrome_pids;
PpidToPidMap proc_ppid_to_pid;
for (auto& sample : curr_samples) {
base::Optional<ProcCpuUsageAndPpid> proc_cpu_time_and_ppid =
ComputeProcCpuTimeJiffiesAndPpid(base::FilePath(
base::StringPrintf(config.proc_stat_fmt.c_str(), sample.first)));
// If this failed, it could be that a process terminated while sampling it.
// Ignore these processes; since the process will be invalid, it will never
// be aggregated into |ProcessStoredData|.
sample.second.valid = proc_cpu_time_and_ppid.has_value();
if (!sample.second.valid)
continue;
uint64_t proc_cpu_time = proc_cpu_time_and_ppid.value().first;
pid_t ppid = proc_cpu_time_and_ppid.value().second;
sample.second.total_cpu_time_jiffies = total_cpu_time.value();
sample.second.proc_cpu_time_jiffies = proc_cpu_time;
proc_ppid_to_pid.insert({ppid, sample.first});
// Get the PID of the Concierge daemon if it exists.
if (base::StartsWith(sample.second.process_data.cmdline, kConciergeCmdline,
base::CompareCase::SENSITIVE))
concierge_pid = sample.second.process_data.pid;
// Get all process that are Chrome processes.
if (base::StartsWith(sample.second.process_data.cmdline, kChromeCmdPath,
base::CompareCase::SENSITIVE))
chrome_pids.insert(sample.first);
}
std::unordered_set<pid_t> arc_pids;
base::Optional<pid_t> android_init_pid =
GetAndroidInitPid(config.android_init_pid_path);
// Compute all processes associated with ARC.
if (android_init_pid.has_value())
GetChildPids(proc_ppid_to_pid, android_init_pid.value(), &arc_pids);
std::unordered_set<pid_t> crostini_pids;
// Compute all process associated with Crostini.
if (concierge_pid.has_value())
GetChildPids(proc_ppid_to_pid, concierge_pid.value(), &crostini_pids);
// Change the process types appropriately depending on the previous
// information gathered. Currently, these are the only 3 types of process that
// are classified. Everything else is classified as a |SYSTEM| process, which
// was the default value set in |GetValidProcesses|.
for (auto& sample : curr_samples) {
if (arc_pids.count(sample.first)) {
sample.second.process_data.type =
ProcessDataCollector::PowerConsumerType::ARC;
} else if (crostini_pids.count(sample.first)) {
sample.second.process_data.type =
ProcessDataCollector::PowerConsumerType::CROSTINI;
} else if (chrome_pids.count(sample.first)) {
sample.second.process_data.type =
ProcessDataCollector::PowerConsumerType::CHROME;
}
}
return curr_samples;
}
// static
ProcessDataCollector::ProcessStoredDataMap ProcessDataCollector::ComputeSummary(
Config::AveragingTechnique averaging_technique,
const ProcessSampleMap& prev_samples,
const ProcessSampleMap& curr_samples,
const ProcessStoredDataMap& curr_summary) {
ProcessStoredDataMap next_summary;
// For each pair of valid samples, samples that are neither invalid nor
// separated by a time delay greater than a preset amount, across two
// timeslices, calculate the average CPU usage for each process. Once this has
// been calculated, aggregate this average of one interval to the average
// across all intervals this process has been sampled for; the average can be
// a normal average or an exponential moving average. Store the newly updated
// averages into ProcessStoredData and update |curr_summary_| with the latest
// calculations.
for (const auto& sample : curr_samples) {
auto prev_iter = prev_samples.find(sample.first);
// If the process has not been sampled for more than two timeslices, then
// there is no way to compute an average across an interval, so skip this
// process for now. When the next timeslice comes around, if the process is
// still running, it can be sampled again and there will be two samples in
// two different timeslices so that an average over an interval can be
// computed.
if (prev_iter == prev_samples.end())
continue;
const ProcessSample& curr_sample = sample.second;
const ProcessSample& prev_sample = prev_iter->second;
// If there's too much of a delay between the last sample and our current
// sample, then throw away all of the previous information, since there was
// some problem that caused it to delay.
if (base::TimeDelta(curr_sample.now - prev_sample.now) > kExcessiveDelay)
continue;
// Computes the average CPU usage over a single interval.
double cpu_usage =
(static_cast<double>(curr_sample.proc_cpu_time_jiffies -
prev_sample.proc_cpu_time_jiffies)) /
(static_cast<double>(curr_sample.total_cpu_time_jiffies -
prev_sample.total_cpu_time_jiffies));
ProcessStoredData& new_summary = next_summary[sample.first];
new_summary.process_data.pid = curr_sample.process_data.pid;
new_summary.process_data.name = curr_sample.process_data.name;
new_summary.process_data.type = curr_sample.process_data.type;
new_summary.process_data.cmdline = curr_sample.process_data.cmdline;
auto summary_iter = curr_summary.find(sample.first);
// If this is the first sample, there is no power usage and only one
// sample.
if (summary_iter == curr_summary.end()) {
new_summary.accumulated_cpu_usages = cpu_usage;
new_summary.power_usage_fraction = cpu_usage;
new_summary.num_samples = 1;
} else {
// Otherwise, depending on the averaging technique calculate the average
// CPU usage across all processes so far; e.g. for time step i + 1,
// ProcessStoredData should have aggregated the first i time steps.
if (averaging_technique ==
ProcessDataCollector::Config::AveragingTechnique ::EXPONENTIAL) {
const CpuUsageAndPowerAverage averages =
ComputeExponentialMovingAverages(
summary_iter->second.accumulated_cpu_usages,
summary_iter->second.power_usage_fraction, cpu_usage);
new_summary.accumulated_cpu_usages = averages.accumulated_cpu_usages;
new_summary.power_usage_fraction = averages.power_average;
} else if (averaging_technique ==
ProcessDataCollector::Config::AveragingTechnique ::AVERAGE) {
const CpuUsageAndPowerAverage averages = ComputeNormalAverages(
summary_iter->second.num_samples,
summary_iter->second.accumulated_cpu_usages, cpu_usage);
new_summary.accumulated_cpu_usages = averages.accumulated_cpu_usages;
new_summary.power_usage_fraction = averages.power_average;
} else {
NOTREACHED();
}
new_summary.num_samples = summary_iter->second.num_samples + 1;
}
}
return next_summary;
}
// static
ProcessDataCollector::SamplesAndSummaryInfo
ProcessDataCollector::ComputeSampleAsync(Config config,
ProcessSampleMap prev_samples,
ProcessSampleMap curr_samples,
ProcessStoredDataMap curr_summary) {
ProcessSampleMap procs = GetValidProcesses(config);
prev_samples = std::move(curr_samples);
curr_samples = ComputeSample(std::move(procs), config);
ProcessStoredDataMap next_summary = ComputeSummary(
config.averaging_technique, prev_samples, curr_samples, curr_summary);
return std::make_tuple(std::move(prev_samples), std::move(curr_samples),
std::move(next_summary));
}
void ProcessDataCollector::SaveSamplesOnUIThread(
const SamplesAndSummaryInfo& samples_and_summary_info) {
// Modifies |prev_samples_|, |curr_samples_|, and |next_summary|, thus this
// must be run on the UI thread.
DCHECK_CALLED_ON_VALID_SEQUENCE(sequence_checker_);
std::tie(prev_samples_, curr_samples_, curr_summary_) =
samples_and_summary_info;
}
} // namespace chromeos