blob: 57c4cb4b20bab4bc7e99af5801d079a7f0ba7b48 [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 <sys/types.h>
#include <array>
#include <cstring>
#include <fstream>
#include <memory>
#include <numeric>
#include <string>
#include <unordered_map>
#include <unordered_set>
#include <vector>
#include "base/bind.h"
#include "base/files/file.h"
#include "base/files/file_util.h"
#include "base/files/scoped_temp_dir.h"
#include "base/format_macros.h"
#include "base/message_loop/message_loop.h"
#include "base/run_loop.h"
#include "base/strings/string_piece.h"
#include "base/strings/string_util.h"
#include "base/strings/stringprintf.h"
#include "base/task/post_task.h"
#include "base/test/scoped_task_environment.h"
#include "base/threading/platform_thread.h"
#include "base/time/time.h"
#include "base/timer/timer.h"
#include "chrome/browser/chromeos/power/process_data_collector.h"
#include "content/public/browser/browser_thread.h"
#include "testing/gtest/include/gtest/gtest.h"
// The tests are structured as follows; this is taken from |RunTest|:
// 1. First, there's a call to |SetUpProcfs| which sets up the dummy procfs that
// will be used during testing.
// 2. |TimeStepExpectedResult| structures are constructed, which contain the
// expected results for each time step that the |ProcessDataCollector| is
// sampled.
// 3. |RunTest| is called which calls |ValidateProcessList| to check whether the
// |TimeStepExpectedResult|s for each time step match; after a time step is
// checked, the procfs of the next time step is set up with |SetUpProcfs|.
namespace chromeos {
namespace {
// The path component for a custom stat; this corresponds to a real /proc/stat.
constexpr char kCustomCpuTimeFile[] = "stat";
// The path component for a custom file where Android's init PID lives.
constexpr char kCustomAndroidInitPidFile[] = "container.pid";
// The number of jiffies per iteration of the simulated procfs.
constexpr uint64_t kJiffiesPerIteration = 100;
using ProcessData = ProcessDataCollector::ProcessData;
using Config = ProcessDataCollector::Config;
using PowerConsumerType = ProcessDataCollector::PowerConsumerType;
using ProcessUsageData = ProcessDataCollector::ProcessUsageData;
// A process information container containing the minimum amount of information
// for testing.
struct ProcessTestData {
ProcessTestData() = default;
ProcessTestData(ProcessData process_data,
pid_t ppid,
double cpu_usage_fraction)
: process_data(process_data),
ppid(ppid),
cpu_usage_fraction(cpu_usage_fraction) {}
// Information about the process.
ProcessData process_data;
// The PPID of the process.
pid_t ppid = 0;
// The fraction of CPU used by this process.
double cpu_usage_fraction = 0.;
};
// The total number of entries that will be in the simulated /proc/%u/stat.
constexpr uint64_t kProcStatNumEntries = 52;
// The number of faked data entries in the following data. Update this number as
// necessary.
constexpr uint64_t kNumProcStatFakedEntries = 16;
// The number of zeroed entries.
constexpr uint64_t kNumProcStatZeroedEntries =
kProcStatNumEntries - kNumProcStatFakedEntries;
// Generates a suitable string for to be written in a /proc/%u/stat.
std::string GenerateProcPidStatString(const std::string& name,
pid_t ppid,
uint64_t user_time,
uint64_t kernel_time) {
return base::StringPrintf(
"0 (%s) S %" PRId32 " %s %" PRIu64 " %" PRIu64 " ", name.c_str(),
ppid,
// Pad 9 0's between the PPID and the user and
// kernel times.
base::JoinString(std::vector<std::string>(9, "0"), " ").c_str(),
user_time, kernel_time) +
base::JoinString(
std::vector<std::string>(kNumProcStatZeroedEntries, "0"), " ") +
"\n";
}
// Writes |data| to |file_path| and checks that the write succeeded.
void WriteStringToFile(const base::FilePath& file_path,
const std::string& data) {
ASSERT_EQ(base::WriteFile(file_path, data.c_str(), data.length()),
static_cast<int>(data.length()));
}
// Sets up a procfs-like directory with certain processes and stat files
// corresponding to a certain time step within a custom directory. For example,
// say a process has a fixed CPU usage of 0.03 or 4% of the total CPU usage.
// Then the number of jiffies used would be this;
// timestep: 3
// timestep: 6
// timestep: 9
// ...
// When written /proc/%u/stat, this total will be split into the user and kernel
// times. Additionally, each time step, the CPU itself uses 100 jiffies. So for
// the first time step this process will 3 / 100 = 0.03 of the total CPU, and
// the second time step, (3 + 3) / (100 + 100) = 6 / 200 = 0.03. Thus the
// fraction of CPU usage will remain fixed.
void SetUpProcfs(const base::FilePath& root_dir,
const std::vector<ProcessTestData>& processes,
int iteration) {
for (const auto& process_info : processes) {
uint64_t jiffies = (iteration + 1) * kJiffiesPerIteration *
process_info.cpu_usage_fraction;
std::string proc_stat_file_contents = GenerateProcPidStatString(
process_info.process_data.name, process_info.ppid,
// Above, the number of jiffies used in total by the process was
// calculated for this time step. If the number of jiffies is odd,
// division by 2 won't work since it'll be one short, so compensate for
// that.
jiffies / 2 /* user time */, (jiffies + 1) / 2 /* kernel time*/);
std::string proc_comm_file_contents = process_info.process_data.name + "\n";
ASSERT_TRUE(base::CreateDirectory(base::FilePath(base::StringPrintf(
"%s/%u", root_dir.value().c_str(), process_info.process_data.pid))));
WriteStringToFile(base::FilePath(base::StringPrintf(
// Corresponds to a real /proc/%u/cmdline.
"%s/%u/cmdline", root_dir.value().c_str(),
process_info.process_data.pid)),
process_info.process_data.cmdline);
WriteStringToFile(base::FilePath(base::StringPrintf(
// Correponds to a real /proc/%u/stat.
"%s/%u/stat", root_dir.value().c_str(),
process_info.process_data.pid)),
proc_stat_file_contents);
WriteStringToFile(base::FilePath(base::StringPrintf(
// Corresponds to a real /proc/%u/comm.
"%s/%u/comm", root_dir.value().c_str(),
process_info.process_data.pid)),
proc_comm_file_contents);
}
// Let each time step use |kJiffiesPerIteration| jiffies.
std::string stat_file_contents =
"cpu " + std::to_string(kJiffiesPerIteration * (iteration + 1)) +
// Pad some zeros.
" 0 0 0 0 0 0 0 0 0\n";
WriteStringToFile(base::FilePath(base::StringPrintf(
"%s/%s", root_dir.value().c_str(), kCustomCpuTimeFile)),
stat_file_contents);
// The hard coded PID used for Android's init process.
std::string android_init_pid_contents = "10\n";
WriteStringToFile(
base::FilePath(base::StringPrintf("%s/%s", root_dir.value().c_str(),
kCustomAndroidInitPidFile)),
android_init_pid_contents);
}
// Represents the expected results for a single time step.
struct TimeStepExpectedResult {
TimeStepExpectedResult() = default;
TimeStepExpectedResult(
const std::unordered_set<pid_t>& expected_pids,
const std::unordered_map<pid_t, double>& expected_avgs,
const std::unordered_map<pid_t, ProcessTestData>& pid_to_proc)
: expected_pids(expected_pids),
expected_averages(expected_avgs),
pid_infos(pid_to_proc) {}
// The expected PID's to be returned; these should only be valid PIDs.
std::unordered_set<pid_t> expected_pids;
// The expected averages.
std::unordered_map<pid_t, double> expected_averages;
// Maps PID's to |ProcessTestData| structs. This is used to valid process
// types and process names.
std::unordered_map<pid_t, ProcessTestData> pid_infos;
};
// Normalizes the average CPU usages of processes with |valid_pids| such
// that their sum is 1.0.
void NormalizeProcessAverages(
const std::unordered_set<pid_t>& valid_pids,
std::unordered_map<pid_t, double>* process_averages) {
double total = 0.;
for (const auto& pid : valid_pids)
total += (*process_averages)[pid];
if (total == 0.)
return; // No more work needs to be done.
for (auto& average : *process_averages)
average.second /= total;
}
} // namespace
class ProcessDataCollectorTest : public testing::Test {
public:
ProcessDataCollectorTest() {
InitializeStats();
// Create a temporary directory for the simulated procfs.
CHECK(proc_dir_.CreateUniqueTempDir());
}
~ProcessDataCollectorTest() override { ProcessDataCollector::Shutdown(); }
protected:
// Faked process data.
// The process information will be truncated a lot, since most of the data in
// a typical /proc/%u directory is unnecessary for testing.
// Below are processes of different kinds: Crostini, ARC, Base and System,
// both of which are system processes but Base corresponds to the root of the
// process hierarchy, and Chrome (corresponding to separate types of processes
// in |ProcessDataCollector::ProcessType|. These all contain trimmed down
// information from various files in procfs.
const std::vector<ProcessTestData> kAllProcessTestData = {
ProcessTestData(ProcessData(0, "", "", PowerConsumerType::SYSTEM),
0 /* ppid */,
0.02 /* cpu_usage_fraction */),
ProcessTestData(
ProcessData(1, "init", "/usr/bin/init\n", PowerConsumerType::SYSTEM),
0 /* ppid */,
0.01 /* cpu_usage_fraction */),
ProcessTestData(ProcessData(2,
"chrome",
"/opt/google/chrome/chrome\n",
PowerConsumerType::CHROME),
1 /* ppid */,
0.02 /* cpu_usage_fraction */),
ProcessTestData(
ProcessData(
3,
"chrome",
"/opt/google/chrome/chrome --other --commandline --options\n",
PowerConsumerType::CHROME),
1 /* ppid */,
0.02 /* cpu_usage_fraction */),
ProcessTestData(
ProcessData(4,
"chrome",
"/opt/google/chrome/chrome --even --more --commandline"
"--options\n",
PowerConsumerType::CHROME),
2 /* ppid */,
0.03 /* cpu_usage_fraction */),
ProcessTestData(ProcessData(5,
"chrome",
"/opt/google/chrome/chrome\n",
PowerConsumerType::CHROME),
1 /* ppid */,
0.02 /* cpu_usage_fraction */),
ProcessTestData(ProcessData(6,
"vm_concierge",
"/usr/bin/vm_concierge\n",
PowerConsumerType::CROSTINI),
1 /* ppid */,
0.1 /* cpu_usage_fraction */),
ProcessTestData(ProcessData(7,
"some_container",
"some_container\n",
PowerConsumerType::CROSTINI),
6 /* ppid */,
0.03 /* cpu_usage_fraction */),
ProcessTestData(ProcessData(8,
"nested_under_some_container",
"nested_under_some_container\n",
PowerConsumerType::CROSTINI),
7 /* ppid */,
0.04 /* cpu_usage_fraction */),
ProcessTestData(ProcessData(9,
"other_container",
"other_container\n",
PowerConsumerType::CROSTINI),
6 /* ppid */,
0.02 /* cpu_usage_fraction */),
ProcessTestData(
ProcessData(10, "init", "/init --android\n", PowerConsumerType::ARC),
1 /* ppid */,
0.02 /* cpu_usage_fraction */),
ProcessTestData(ProcessData(11,
"org.google.chromium.1",
"org.google.chromium.1\n",
PowerConsumerType::ARC),
10 /* ppid */,
0.02 /* cpu_usage_fraction */),
ProcessTestData(ProcessData(12,
"org.google.chromium.2",
"org.google.chromium.2\n",
PowerConsumerType::ARC),
11 /* ppid */,
0.02 /* cpu_usage_fraction */),
ProcessTestData(ProcessData(13,
"org.google.chromium.3",
"org.google.chromium.3\n",
PowerConsumerType::ARC),
10 /* ppid */,
0.02 /* cpu_usage_fraction */),
ProcessTestData(
ProcessData(14, "powerd", "powerd\n", PowerConsumerType::SYSTEM),
1 /* ppid */,
0.02 /* cpu_usage_fraction */),
ProcessTestData(ProcessData(15,
"powerd_loggerd",
"powerd_loggerd\n",
PowerConsumerType::SYSTEM),
14 /* ppid */,
0.02 /* cpu_usage_fraction */),
ProcessTestData(
ProcessData(16, "garcon", "garcon\n", PowerConsumerType::SYSTEM),
1 /* ppid */,
0.02 /* cpu_usage_fraction */),
ProcessTestData(ProcessData(17, "", "\n", PowerConsumerType::SYSTEM),
1 /* ppid */,
0.02 /* cpu_usage_fraction */)};
// Helper function to generate a |Config| struct that will be passed to the
// |ProcessDataCollector| to initialize it with an appropriate testing
// configuration.
Config CreateConfig(const base::FilePath& proc_path,
Config::AveragingTechnique technique);
// A helper function that wraps |ValidateProcessList| for a convenient
// testing setup.
void RunTest(const std::vector<TimeStepExpectedResult>& expected_results,
Config::AveragingTechnique averaging_technique);
// Contains the PIDs of all the valid processes from |kProcessTestData|.
std::unordered_set<pid_t> all_valid_pids_;
// Represents the root of the simulated procfs.
base::ScopedTempDir proc_dir_;
base::test::ScopedTaskEnvironment scoped_task_environment_;
// The current time step, used in testing functions.
int timestep_ = 0;
// Maps each PID to its respective |ProcessTestData| struct.
std::unordered_map<pid_t, ProcessTestData> pid_infos_;
// Contains the normalized averages of all the valid processes.
std::unordered_map<pid_t, double> all_process_averages_;
private:
// Initialize relevant statistics like process averages.
void InitializeStats();
// Checks whether all of the processes that are returned from
// |ProcessDataCollector::GetProcessUsages| are valid.
void CheckValidProcesses(const std::vector<ProcessUsageData>& process_list,
const std::unordered_set<pid_t>& expected_pids);
// Checks whether all given processes match their expected results. This
// should only be called after |CheckValidProcesses|.
void CheckProcessUsages(const std::vector<ProcessUsageData>& process_list,
const TimeStepExpectedResult& timestep_info);
// Makes sure that the calculated results match the expected results.
// |timestep_info| represents the expected results for a single time step.
// |process_list| contains the calculated results from the
// |ProcessDataCollector| which needs to compared against |timestep_info|.
void ValidateProcessList(const TimeStepExpectedResult& timestep_info,
const std::vector<ProcessUsageData>& process_list);
DISALLOW_COPY_AND_ASSIGN(ProcessDataCollectorTest);
};
ProcessDataCollector::Config ProcessDataCollectorTest::CreateConfig(
const base::FilePath& proc_path,
ProcessDataCollector::Config::AveragingTechnique technique) {
std::string cmdline_fmt = proc_path.value() + "/%u/cmdline";
std::string stat_fmt = proc_path.value() + "/%u/stat";
std::string comm_fmt = proc_path.value() + "/%u/comm";
// The delay is arbitrary and shouldn't actually be used since the
// |ProcessDataCollector| will be initialized with |InitializeForTesting|.
return ProcessDataCollector::Config(
proc_path, proc_path.AppendASCII(kCustomCpuTimeFile),
proc_path.AppendASCII(kCustomAndroidInitPidFile), cmdline_fmt, stat_fmt,
comm_fmt, base::TimeDelta(), technique);
}
void ProcessDataCollectorTest::RunTest(
const std::vector<TimeStepExpectedResult>& expected_results,
Config::AveragingTechnique averaging_technique) {
// Initialize the testing environment with all processes for the 0th time
// step.
SetUpProcfs(proc_dir_.GetPath(), kAllProcessTestData, 0);
ProcessDataCollector::Config config =
CreateConfig(proc_dir_.GetPath(), averaging_technique);
ProcessDataCollector::InitializeForTesting(config);
ProcessDataCollector* process_data_collector = ProcessDataCollector::Get();
for (timestep_ = 0; timestep_ < static_cast<int>(expected_results.size());
timestep_++) {
// Sample from the simulated procfs.
process_data_collector->SampleCpuUsageForTesting();
// Set up the next iteration of the procfs.
SetUpProcfs(proc_dir_.GetPath(), kAllProcessTestData, timestep_ + 1);
// If the 0th |timestep_| was just sampled, then there are no results to
// check. Just go to the next iteration. At least 2 samples from the
// simulated procfs need to be gathered before an average can be calculated.
if (timestep_ == 0)
continue;
const TimeStepExpectedResult& expected_result = expected_results[timestep_];
const std::vector<ProcessDataCollector::ProcessUsageData>& process_list =
process_data_collector->GetProcessUsages();
ValidateProcessList(expected_result, process_list);
}
}
void ProcessDataCollectorTest::InitializeStats() {
// Map each PID to a |ProcessTestData| and gather the PIDs of all the valid
// processes.
for (const auto& process_info : kAllProcessTestData) {
pid_infos_[process_info.process_data.pid] = process_info;
if (!process_info.process_data.name.empty() &&
!process_info.process_data.cmdline.empty()) {
all_valid_pids_.insert(process_info.process_data.pid);
}
}
// Make sure that the given fractions sum to less than 100% of the total CPU
// time.
double total_cpu_usage = 0.;
for (const auto& process_info : kAllProcessTestData)
total_cpu_usage += process_info.cpu_usage_fraction;
ASSERT_LE(total_cpu_usage, 1.);
// The average CPU usage for the first time step for each process.
for (const auto& process_info : kAllProcessTestData)
all_process_averages_[process_info.process_data.pid] =
process_info.cpu_usage_fraction;
// Normalize the averages for the first time step for a normal averaging
// technique.
NormalizeProcessAverages(all_valid_pids_, &all_process_averages_);
}
void ProcessDataCollectorTest::CheckValidProcesses(
const std::vector<ProcessUsageData>& process_list,
const std::unordered_set<pid_t>& expected_pids) {
std::unordered_set<pid_t> calculated_pids;
for (const auto& process : process_list)
calculated_pids.insert(process.process_data.pid);
ASSERT_EQ(expected_pids, calculated_pids);
}
void ProcessDataCollectorTest::CheckProcessUsages(
const std::vector<ProcessUsageData>& process_list,
const TimeStepExpectedResult& timestep_info) {
for (const auto& process : process_list) {
pid_t pid = process.process_data.pid;
EXPECT_DOUBLE_EQ(process.power_usage_fraction,
timestep_info.expected_averages.at(pid))
<< "For PID " << pid
<< " the expected and calculated average CPU "
"usages didn't match.";
const ProcessTestData& process_info = timestep_info.pid_infos.at(pid);
// Make sure that the |ProcessDataCollector::PowerConsumerType|s match.
EXPECT_EQ(process.process_data.type, process_info.process_data.type)
<< "For PID " << pid
<< " the expected and calculated process types "
"didn't match.";
// Make sure the process name matches what is expected.
EXPECT_EQ(process_info.process_data.name, process.process_data.name)
<< "For PID " << pid
<< " the expected and calculated process names "
"didn't match.";
}
}
void ProcessDataCollectorTest::ValidateProcessList(
const TimeStepExpectedResult& timestep_info,
const std::vector<ProcessUsageData>& process_list) {
CheckValidProcesses(process_list, timestep_info.expected_pids);
CheckProcessUsages(process_list, timestep_info);
}
// Averages for one time step and checks normal averages.
TEST_F(ProcessDataCollectorTest, AveragingBasic) {
std::vector<TimeStepExpectedResult> expected_results;
// For each time step, insert a dummy set of expected results for the 0th time
// step, since there will be no expected results.
expected_results.push_back(TimeStepExpectedResult());
expected_results.push_back(TimeStepExpectedResult(
all_valid_pids_, all_process_averages_, pid_infos_));
RunTest(expected_results, Config::AveragingTechnique::AVERAGE);
}
// Averages for one time steps and checks exponential averages.
TEST_F(ProcessDataCollectorTest, ExpAveragingBasic) {
std::vector<TimeStepExpectedResult> expected_results;
expected_results.push_back(TimeStepExpectedResult());
expected_results.push_back(TimeStepExpectedResult(
all_valid_pids_, all_process_averages_, pid_infos_));
RunTest(expected_results, Config::AveragingTechnique::EXPONENTIAL);
}
// Averages for two time steps and checks both normal averages.
TEST_F(ProcessDataCollectorTest, AveragingMultistep) {
std::vector<TimeStepExpectedResult> expected_results;
expected_results.push_back(TimeStepExpectedResult());
expected_results.push_back(TimeStepExpectedResult(
all_valid_pids_, all_process_averages_, pid_infos_));
expected_results.push_back(TimeStepExpectedResult(
all_valid_pids_, all_process_averages_, pid_infos_));
RunTest(expected_results, Config::AveragingTechnique::AVERAGE);
}
// Averages for two time steps and checks exponential moving averages.
TEST_F(ProcessDataCollectorTest, ExpAveragingMultistep) {
std::vector<TimeStepExpectedResult> expected_results;
expected_results.push_back(TimeStepExpectedResult());
expected_results.push_back(TimeStepExpectedResult(
all_valid_pids_, all_process_averages_, pid_infos_));
expected_results.push_back(TimeStepExpectedResult(
all_valid_pids_, all_process_averages_, pid_infos_));
RunTest(expected_results, Config::AveragingTechnique::EXPONENTIAL);
}
} // namespace chromeos