blob: 558fd2dc25e9d4479d7c9ba922a96ae3a2432b83 [file] [log] [blame]
/*
* Copyright (C) 2021 The Android Open Source Project
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
#define LOG_TAG "pixelstats: MmMetrics"
#include <aidl/android/frameworks/stats/IStats.h>
#include <android-base/file.h>
#include <android-base/parseint.h>
#include <android-base/properties.h>
#include <android-base/stringprintf.h>
#include <android-base/strings.h>
#include <android/binder_manager.h>
#include <hardware/google/pixel/pixelstats/pixelatoms.pb.h>
#include <pixelstats/MmMetricsReporter.h>
#include <sys/stat.h>
#include <sys/types.h>
#include <unistd.h>
#include <utils/Log.h>
#define SZ_4K 0x00001000
#define SZ_2M 0x00200000
namespace android {
namespace hardware {
namespace google {
namespace pixel {
using aidl::android::frameworks::stats::IStats;
using aidl::android::frameworks::stats::VendorAtom;
using aidl::android::frameworks::stats::VendorAtomValue;
using android::base::ReadFileToString;
using android::base::StartsWith;
using android::hardware::google::pixel::PixelAtoms::CmaStatus;
using android::hardware::google::pixel::PixelAtoms::CmaStatusExt;
using android::hardware::google::pixel::PixelAtoms::PixelMmMetricsPerDay;
using android::hardware::google::pixel::PixelAtoms::PixelMmMetricsPerHour;
const std::vector<MmMetricsReporter::MmMetricsInfo> MmMetricsReporter::kMmMetricsPerHourInfo = {
{"nr_free_pages", PixelMmMetricsPerHour::kFreePagesFieldNumber, false},
{"nr_anon_pages", PixelMmMetricsPerHour::kAnonPagesFieldNumber, false},
{"nr_file_pages", PixelMmMetricsPerHour::kFilePagesFieldNumber, false},
{"nr_slab_reclaimable", PixelMmMetricsPerHour::kSlabReclaimableFieldNumber, false},
{"nr_zspages", PixelMmMetricsPerHour::kZspagesFieldNumber, false},
{"nr_unevictable", PixelMmMetricsPerHour::kUnevictableFieldNumber, false},
};
const std::vector<MmMetricsReporter::MmMetricsInfo> MmMetricsReporter::kMmMetricsPerDayInfo = {
{"workingset_refault", PixelMmMetricsPerDay::kWorkingsetRefaultFieldNumber, true},
{"workingset_refault_file", PixelMmMetricsPerDay::kWorkingsetRefaultFieldNumber, true},
{"pswpin", PixelMmMetricsPerDay::kPswpinFieldNumber, true},
{"pswpout", PixelMmMetricsPerDay::kPswpoutFieldNumber, true},
{"allocstall_dma", PixelMmMetricsPerDay::kAllocstallDmaFieldNumber, true},
{"allocstall_dma32", PixelMmMetricsPerDay::kAllocstallDma32FieldNumber, true},
{"allocstall_normal", PixelMmMetricsPerDay::kAllocstallNormalFieldNumber, true},
{"allocstall_movable", PixelMmMetricsPerDay::kAllocstallMovableFieldNumber, true},
{"pgalloc_dma", PixelMmMetricsPerDay::kPgallocDmaFieldNumber, true},
{"pgalloc_dma32", PixelMmMetricsPerDay::kPgallocDma32FieldNumber, true},
{"pgalloc_normal", PixelMmMetricsPerDay::kPgallocNormalFieldNumber, true},
{"pgalloc_movable", PixelMmMetricsPerDay::kPgallocMovableFieldNumber, true},
{"pgsteal_kswapd", PixelMmMetricsPerDay::kPgstealKswapdFieldNumber, true},
{"pgsteal_direct", PixelMmMetricsPerDay::kPgstealDirectFieldNumber, true},
{"pgscan_kswapd", PixelMmMetricsPerDay::kPgscanKswapdFieldNumber, true},
{"pgscan_direct", PixelMmMetricsPerDay::kPgscanDirectFieldNumber, true},
{"oom_kill", PixelMmMetricsPerDay::kOomKillFieldNumber, true},
{"pgalloc_costly_order", PixelMmMetricsPerDay::kPgallocHighFieldNumber, true},
{"pgcache_hit", PixelMmMetricsPerDay::kPgcacheHitFieldNumber, true},
{"pgcache_miss", PixelMmMetricsPerDay::kPgcacheMissFieldNumber, true},
};
const std::vector<MmMetricsReporter::MmMetricsInfo> MmMetricsReporter::kCmaStatusInfo = {
{"alloc_pages_attempts", CmaStatus::kCmaAllocPagesAttemptsFieldNumber, true},
{"alloc_pages_failfast_attempts", CmaStatus::kCmaAllocPagesSoftAttemptsFieldNumber, true},
{"fail_pages", CmaStatus::kCmaFailPagesFieldNumber, true},
{"fail_failfast_pages", CmaStatus::kCmaFailSoftPagesFieldNumber, true},
{"migrated_pages", CmaStatus::kMigratedPagesFieldNumber, true},
};
const std::vector<MmMetricsReporter::MmMetricsInfo> MmMetricsReporter::kCmaStatusExtInfo = {
{"latency_low", CmaStatusExt::kCmaAllocLatencyLowFieldNumber, false},
{"latency_mid", CmaStatusExt::kCmaAllocLatencyMidFieldNumber, false},
{"latency_high", CmaStatusExt::kCmaAllocLatencyHighFieldNumber, false},
};
static bool file_exists(const char *path) {
struct stat sbuf;
return (stat(path, &sbuf) == 0);
}
bool MmMetricsReporter::checkKernelMMMetricSupport() {
const char *const require_all[] = {
kVmstatPath,
kGpuTotalPages,
kPixelStatMm,
};
const char *const require_one[] = {
kIonTotalPoolsPath,
kIonTotalPoolsPathForLegacy,
};
for (auto &path : require_all) {
if (!file_exists(path)) {
ALOGI("MM Metrics not supported - no %s.", path);
return false;
}
}
std::string err_msg;
for (auto &path : require_one) {
if (file_exists(path)) {
err_msg.clear();
break;
}
err_msg += path;
err_msg += ", ";
}
if (!err_msg.empty()) {
err_msg.pop_back(); // remove last space
err_msg.pop_back(); // remove last comma
ALOGI("MM Metrics not supported - no IonTotalPools path.");
return false;
}
return true;
}
static bool checkUserBuild() {
return android::base::GetProperty("ro.build.type", "") == "user";
}
MmMetricsReporter::MmMetricsReporter()
: kVmstatPath("/proc/vmstat"),
kIonTotalPoolsPath("/sys/kernel/dma_heap/total_pools_kb"),
kIonTotalPoolsPathForLegacy("/sys/kernel/ion/total_pools_kb"),
kGpuTotalPages("/sys/kernel/pixel_stat/gpu/mem/total_page_count"),
kPixelStatMm("/sys/kernel/pixel_stat/mm") {
is_user_build_ = checkUserBuild();
ker_mm_metrics_support_ = checkKernelMMMetricSupport();
}
bool MmMetricsReporter::ReadFileToUint(const char *const path, uint64_t *val) {
std::string file_contents;
if (!ReadFileToString(path, &file_contents)) {
// Don't print this log if the file doesn't exist, since logs will be printed repeatedly.
if (errno != ENOENT) {
ALOGI("Unable to read %s - %s", path, strerror(errno));
}
return false;
} else {
file_contents = android::base::Trim(file_contents);
if (!android::base::ParseUint(file_contents, val)) {
ALOGI("Unable to convert %s to uint - %s", path, strerror(errno));
return false;
}
}
return true;
}
bool MmMetricsReporter::reportVendorAtom(const std::shared_ptr<IStats> &stats_client, int atom_id,
const std::vector<VendorAtomValue> &values,
const std::string &atom_name) {
// Send vendor atom to IStats HAL
VendorAtom event = {.reverseDomainName = "",
.atomId = atom_id,
.values = std::move(values)};
const ndk::ScopedAStatus ret = stats_client->reportVendorAtom(event);
if (!ret.isOk()) {
ALOGE("Unable to report %s to Stats service", atom_name.c_str());
return false;
}
return true;
}
/**
* Parse the output of /proc/vmstat or the sysfs having the same output format.
* The map containing pairs of {field_string, data} will be returned.
*/
std::map<std::string, uint64_t> MmMetricsReporter::readVmStat(const char *path) {
std::string file_contents;
std::map<std::string, uint64_t> vmstat_data;
if (path == nullptr) {
ALOGI("vmstat path is not specified");
return vmstat_data;
}
if (!ReadFileToString(path, &file_contents)) {
ALOGE("Unable to read vmstat from %s, err: %s", path, strerror(errno));
return vmstat_data;
}
std::istringstream data(file_contents);
std::string line;
while (std::getline(data, line)) {
std::vector<std::string> words = android::base::Split(line, " ");
if (words.size() != 2)
continue;
uint64_t i;
if (!android::base::ParseUint(words[1], &i))
continue;
vmstat_data[words[0]] = i;
}
return vmstat_data;
}
uint64_t MmMetricsReporter::getIonTotalPools() {
uint64_t res;
if (!ReadFileToUint(kIonTotalPoolsPathForLegacy, &res) || (res == 0)) {
if (!ReadFileToUint(kIonTotalPoolsPath, &res)) {
return 0;
}
}
return res;
}
/**
* Collect GPU memory from kGpuTotalPages and return the total number of 4K page.
*/
uint64_t MmMetricsReporter::getGpuMemory() {
uint64_t gpu_size = 0;
if (!ReadFileToUint(kGpuTotalPages, &gpu_size)) {
return 0;
}
return gpu_size;
}
/**
* fillAtomValues() is used to copy Mm metrics to values
* metrics_info: This is a vector of MmMetricsInfo {field_string, atom_key, update_diff}
* field_string is used to get the data from mm_metrics.
* atom_key is the position where the data should be put into values.
* update_diff will be true if this is an accumulated data.
* metrics_info may have multiple entries with the same atom_key,
* e.g. workingset_refault and workingset_refault_file.
* mm_metrics: This map contains pairs of {field_string, cur_value} collected
* from /proc/vmstat or the sysfs for the pixel specific metrics.
* e.g. {"nr_free_pages", 200000}
* Some data in mm_metrics are accumulated, e.g. pswpin.
* We upload the difference instead of the accumulated value
* when update_diff of the field is true.
* prev_mm_metrics: The pointer to the metrics we collected last time.
* atom_values: The atom values that will be reported later.
*/
void MmMetricsReporter::fillAtomValues(const std::vector<MmMetricsInfo> &metrics_info,
const std::map<std::string, uint64_t> &mm_metrics,
std::map<std::string, uint64_t> *prev_mm_metrics,
std::vector<VendorAtomValue> *atom_values) {
VendorAtomValue tmp;
tmp.set<VendorAtomValue::longValue>(0);
// resize atom_values to add all fields defined in metrics_info
int max_idx = 0;
for (auto &entry : metrics_info) {
if (max_idx < entry.atom_key)
max_idx = entry.atom_key;
}
int size = max_idx - kVendorAtomOffset + 1;
if (atom_values->size() < size)
atom_values->resize(size, tmp);
for (auto &entry : metrics_info) {
int atom_idx = entry.atom_key - kVendorAtomOffset;
auto data = mm_metrics.find(entry.name);
if (data == mm_metrics.end())
continue;
uint64_t cur_value = data->second;
uint64_t prev_value = 0;
if (prev_mm_metrics->size() != 0) {
auto prev_data = prev_mm_metrics->find(entry.name);
if (prev_data != prev_mm_metrics->end())
prev_value = prev_data->second;
}
if (entry.update_diff) {
tmp.set<VendorAtomValue::longValue>(cur_value - prev_value);
} else {
tmp.set<VendorAtomValue::longValue>(cur_value);
}
(*atom_values)[atom_idx] = tmp;
}
(*prev_mm_metrics) = mm_metrics;
}
void MmMetricsReporter::logPixelMmMetricsPerHour(const std::shared_ptr<IStats> &stats_client) {
if (!MmMetricsSupported())
return;
std::map<std::string, uint64_t> vmstat = readVmStat(kVmstatPath);
if (vmstat.size() == 0)
return;
uint64_t ion_total_pools = getIonTotalPools();
uint64_t gpu_memory = getGpuMemory();
std::vector<VendorAtomValue> values;
bool is_first_atom = (prev_hour_vmstat_.size() == 0) ? true : false;
fillAtomValues(kMmMetricsPerHourInfo, vmstat, &prev_hour_vmstat_, &values);
// resize values to add the following fields
VendorAtomValue tmp;
tmp.set<VendorAtomValue::longValue>(0);
int size = PixelMmMetricsPerHour::kGpuMemoryFieldNumber - kVendorAtomOffset + 1;
if (values.size() < size) {
values.resize(size, tmp);
}
tmp.set<VendorAtomValue::longValue>(ion_total_pools);
values[PixelMmMetricsPerHour::kIonTotalPoolsFieldNumber - kVendorAtomOffset] = tmp;
tmp.set<VendorAtomValue::longValue>(gpu_memory);
values[PixelMmMetricsPerHour::kGpuMemoryFieldNumber - kVendorAtomOffset] = tmp;
// Don't report the first atom to avoid big spike in accumulated values.
if (!is_first_atom) {
// Send vendor atom to IStats HAL
reportVendorAtom(stats_client, PixelAtoms::Atom::kPixelMmMetricsPerHour, values,
"PixelMmMetricsPerHour");
}
}
void MmMetricsReporter::logPixelMmMetricsPerDay(const std::shared_ptr<IStats> &stats_client) {
if (!MmMetricsSupported())
return;
std::map<std::string, uint64_t> vmstat = readVmStat(kVmstatPath);
if (vmstat.size() == 0)
return;
std::vector<VendorAtomValue> values;
bool is_first_atom = (prev_day_vmstat_.size() == 0) ? true : false;
fillAtomValues(kMmMetricsPerDayInfo, vmstat, &prev_day_vmstat_, &values);
std::map<std::string, uint64_t> pixel_vmstat =
readVmStat(android::base::StringPrintf("%s/vmstat", kPixelStatMm).c_str());
fillAtomValues(kMmMetricsPerDayInfo, pixel_vmstat, &prev_day_pixel_vmstat_, &values);
fillProcessStime(PixelMmMetricsPerDay::kKswapdStimeClksFieldNumber, "kswapd0", &kswapd_pid_,
&prev_kswapd_stime_, &values);
fillProcessStime(PixelMmMetricsPerDay::kKcompactdStimeClksFieldNumber, "kcompactd0",
&kcompactd_pid_, &prev_kcompactd_stime_, &values);
// Don't report the first atom to avoid big spike in accumulated values.
if (!is_first_atom) {
// Send vendor atom to IStats HAL
reportVendorAtom(stats_client, PixelAtoms::Atom::kPixelMmMetricsPerDay, values,
"PixelMmMetricsPerDay");
}
}
/**
* Check if /proc/<pid>/comm is equal to name.
*/
bool MmMetricsReporter::isValidPid(int pid, const char *name) {
if (pid <= 0)
return false;
std::string file_contents;
std::string path = android::base::StringPrintf("/proc/%d/comm", pid);
if (!ReadFileToString(path, &file_contents)) {
ALOGI("Unable to read %s, err: %s", path.c_str(), strerror(errno));
return false;
}
file_contents = android::base::Trim(file_contents);
return !file_contents.compare(name);
}
/**
* Return pid if /proc/<pid>/comm is equal to name, or -1 if not found.
*/
int MmMetricsReporter::findPidByProcessName(const char *name) {
std::unique_ptr<DIR, int (*)(DIR *)> dir(opendir("/proc"), closedir);
if (!dir)
return -1;
int pid;
while (struct dirent *dp = readdir(dir.get())) {
if (dp->d_type != DT_DIR)
continue;
if (!android::base::ParseInt(dp->d_name, &pid))
continue;
// Avoid avc denial since pixelstats-vendor doesn't have the permission to access /proc/1
if (pid == 1)
continue;
std::string file_contents;
std::string path = android::base::StringPrintf("/proc/%s/comm", dp->d_name);
if (!ReadFileToString(path, &file_contents))
continue;
file_contents = android::base::Trim(file_contents);
if (file_contents.compare(name))
continue;
return pid;
}
return -1;
}
/**
* Get stime of a process from /proc/<pid>/stat
* stime is the 15th field.
*/
uint64_t MmMetricsReporter::getStimeByPid(int pid) {
const int stime_idx = 15;
uint64_t stime;
std::string file_contents;
std::string path = android::base::StringPrintf("/proc/%d/stat", pid);
if (!ReadFileToString(path, &file_contents)) {
ALOGI("Unable to read %s, err: %s", path.c_str(), strerror(errno));
return false;
}
std::vector<std::string> data = android::base::Split(file_contents, " ");
if (data.size() < stime_idx) {
ALOGI("Unable to find stime from %s. size: %lu", path.c_str(), data.size());
return false;
}
if (android::base::ParseUint(data[stime_idx - 1], &stime))
return stime;
else
return 0;
}
/**
* Find stime of the process and copy it into atom_values
* atom_key: Currently, it can only be kKswapdTimeFieldNumber or kKcompactdTimeFieldNumber
* name: process name
* pid: The pid of the process. It would be the pid we found last time,
* or -1 if not found.
* prev_stime: The stime of the process collected last time.
* atom_values: The atom we will report later.
*/
void MmMetricsReporter::fillProcessStime(int atom_key, const char *name, int *pid,
uint64_t *prev_stime,
std::vector<VendorAtomValue> *atom_values) {
// resize atom_values if there is no space for this stime field.
int atom_idx = atom_key - kVendorAtomOffset;
int size = atom_idx + 1;
VendorAtomValue tmp;
tmp.set<VendorAtomValue::longValue>(0);
if (atom_values->size() < size)
atom_values->resize(size, tmp);
if (!isValidPid(*pid, name)) {
(*pid) = findPidByProcessName(name);
if ((*pid) <= 0) {
ALOGI("Unable to find pid of %s, err: %s", name, strerror(errno));
return;
}
}
uint64_t stime = getStimeByPid(*pid);
tmp.set<VendorAtomValue::longValue>(stime - *prev_stime);
(*atom_values)[atom_idx] = tmp;
(*prev_stime) = stime;
}
/**
* Collect CMA metrics from kPixelStatMm/cma/<cma_type>/<metric>
* cma_type: CMA heap name
* metrics_info: This is a vector of MmMetricsInfo {metric, atom_key, update_diff}.
* Currently, we only collect CMA metrics defined in metrics_info
*/
std::map<std::string, uint64_t> MmMetricsReporter::readCmaStat(
const std::string &cma_type,
const std::vector<MmMetricsReporter::MmMetricsInfo> &metrics_info) {
uint64_t file_contents;
std::map<std::string, uint64_t> cma_stat;
for (auto &entry : metrics_info) {
std::string path = android::base::StringPrintf("%s/cma/%s/%s", kPixelStatMm,
cma_type.c_str(), entry.name.c_str());
if (!ReadFileToUint(path.c_str(), &file_contents))
continue;
cma_stat[entry.name] = file_contents;
}
return cma_stat;
}
/**
* This function is to collect CMA metrics and upload them.
* The CMA metrics are collected by readCmaStat(), copied into atom values
* by fillAtomValues(), and then uploaded by reportVendorAtom(). The collected
* metrics will be stored in prev_cma_stat_ and prev_cma_stat_ext_ according
* to its CmaType.
*
* stats_client: The Stats service
* atom_id: The id of atom. It can be PixelAtoms::Atom::kCmaStatus or kCmaStatusExt
* cma_type: The name of CMA heap.
* cma_name_offset: The offset of the field cma_heap_name in CmaStatus or CmaStatusExt
* type_idx: The id of the CMA heap. We add this id in atom values to identify
* the CMA status data.
* metrics_info: This is a vector of MmMetricsInfo {metric, atom_key, update_diff}.
* We only collect metrics defined in metrics_info from CMA heap path.
* all_prev_cma_stat: This is the CMA status collected last time.
* It is a map containing pairs of {type_idx, cma_stat}, and cma_stat is
* a map contains pairs of {metric, cur_value}.
* e.g. {CmaType::FARAWIMG, {"alloc_pages_attempts", 100000}, {...}, ....}
* is collected from kPixelStatMm/cma/farawimg/alloc_pages_attempts
*/
void MmMetricsReporter::reportCmaStatusAtom(
const std::shared_ptr<IStats> &stats_client, int atom_id, const std::string &cma_type,
int cma_name_offset, const std::vector<MmMetricsInfo> &metrics_info,
std::map<std::string, std::map<std::string, uint64_t>> *all_prev_cma_stat) {
std::map<std::string, uint64_t> cma_stat = readCmaStat(cma_type, metrics_info);
if (!cma_stat.empty()) {
std::vector<VendorAtomValue> values;
VendorAtomValue tmp;
// type is an enum value corresponding to the CMA heap name. Since CMA heap name
// can be added/removed/modified, it would take effort to maintain the mapping table.
// We would like to store CMA heap name directly, so just set type to 0.
tmp.set<VendorAtomValue::intValue>(0);
values.push_back(tmp);
std::map<std::string, uint64_t> prev_cma_stat;
auto entry = all_prev_cma_stat->find(cma_type);
if (entry != all_prev_cma_stat->end())
prev_cma_stat = entry->second;
bool is_first_atom = (prev_cma_stat.size() == 0) ? true : false;
fillAtomValues(metrics_info, cma_stat, &prev_cma_stat, &values);
int size = cma_name_offset - kVendorAtomOffset + 1;
if (values.size() < size) {
values.resize(size, tmp);
}
tmp.set<VendorAtomValue::stringValue>(cma_type);
values[cma_name_offset - kVendorAtomOffset] = tmp;
(*all_prev_cma_stat)[cma_type] = prev_cma_stat;
if (!is_first_atom)
reportVendorAtom(stats_client, atom_id, values, "CmaStatus");
}
}
/**
* Find the CMA heap defined in kCmaTypeInfo, and then call reportCmaStatusAtom()
* to collect the CMA metrics from kPixelStatMm/cma/<cma_type> and upload them.
*/
void MmMetricsReporter::logCmaStatus(const std::shared_ptr<IStats> &stats_client) {
if (!CmaMetricsSupported())
return;
std::string cma_root = android::base::StringPrintf("%s/cma", kPixelStatMm);
std::unique_ptr<DIR, int (*)(DIR *)> dir(opendir(cma_root.c_str()), closedir);
if (!dir)
return;
while (struct dirent *dp = readdir(dir.get())) {
if (dp->d_type != DT_DIR)
continue;
std::string cma_type(dp->d_name);
reportCmaStatusAtom(stats_client, PixelAtoms::Atom::kCmaStatus, cma_type,
CmaStatus::kCmaHeapNameFieldNumber, kCmaStatusInfo, &prev_cma_stat_);
reportCmaStatusAtom(stats_client, PixelAtoms::Atom::kCmaStatusExt, cma_type,
CmaStatusExt::kCmaHeapNameFieldNumber, kCmaStatusExtInfo,
&prev_cma_stat_ext_);
}
}
} // namespace pixel
} // namespace google
} // namespace hardware
} // namespace android