aboutsummaryrefslogtreecommitdiff
path: root/cpp/watchdog
diff options
context:
space:
mode:
authorLakshman Annadorai <lakshmana@google.com>2021-09-14 10:56:57 -0700
committerLakshman Annadorai <lakshmana@google.com>2021-09-20 15:00:00 -0700
commit08309492fb97f8e3ed3179f640198b1a680efa76 (patch)
treef39c48e8d75cf13d8abaa77843125b684811817c /cpp/watchdog
parent2b0161ce3763a469e7288d8853b01b65cbe93c66 (diff)
downloadCar-08309492fb97f8e3ed3179f640198b1a680efa76.tar.gz
Aggregate per-UID stats during /proc/PID stats collection.
- Refactor UidProcStatsCollector to perform per-UID stats aggregation as soon as per-PID stats are collected. - Add UidProcStatsCollectorInterface for mocking in the test because marking implementation methods as virtual for testing purposes has performance overhead in prod. - Return only aggregated per-UID and per-PID stats and drop unnecessary per-thread stats. - Remove per process memory stats read from /proc/[PID]/status file because they are inaccurate and memory stats will be read with libmeminfo. - Replace isEqual test methods with test matchers. - Add UidProcStatsCollector's test matchers to a separate test utils. This will be also used by UidStatsCollectorTest in the following change. Test: atest libwatchdog_test Bug: 199782126 Change-Id: I30a20abf8fe882b3e401889d67a1eb4f494a25ab
Diffstat (limited to 'cpp/watchdog')
-rw-r--r--cpp/watchdog/server/src/UidProcStatsCollector.cpp360
-rw-r--r--cpp/watchdog/server/src/UidProcStatsCollector.h123
-rw-r--r--cpp/watchdog/server/tests/MockUidProcStatsCollector.h11
-rw-r--r--cpp/watchdog/server/tests/UidProcStatsCollectorTest.cpp351
-rw-r--r--cpp/watchdog/server/tests/UidProcStatsCollectorTestUtils.h60
5 files changed, 463 insertions, 442 deletions
diff --git a/cpp/watchdog/server/src/UidProcStatsCollector.cpp b/cpp/watchdog/server/src/UidProcStatsCollector.cpp
index 8c1ecafe61..da3bffc05d 100644
--- a/cpp/watchdog/server/src/UidProcStatsCollector.cpp
+++ b/cpp/watchdog/server/src/UidProcStatsCollector.cpp
@@ -15,11 +15,13 @@
*/
#define LOG_TAG "carwatchdogd"
+#define DEBUG false // STOPSHIP if true.
#include "UidProcStatsCollector.h"
#include <android-base/file.h>
#include <android-base/parseint.h>
+#include <android-base/stringprintf.h>
#include <android-base/strings.h>
#include <log/log.h>
@@ -40,6 +42,7 @@ using ::android::base::ParseUint;
using ::android::base::ReadFileToString;
using ::android::base::Result;
using ::android::base::Split;
+using ::android::base::StringAppendF;
using ::android::base::Trim;
namespace {
@@ -50,25 +53,36 @@ enum ReadError {
NUM_ERRORS = 2,
};
-// /proc/PID/stat or /proc/PID/task/TID/stat format:
-// <pid> <comm> <state> <ppid> <pgrp ID> <session ID> <tty_nr> <tpgid> <flags> <minor faults>
-// <children minor faults> <major faults> <children major faults> <user mode time>
-// <system mode time> <children user mode time> <children kernel mode time> <priority> <nice value>
-// <num threads> <start time since boot> <virtual memory size> <resident set size> <rss soft limit>
-// <start code addr> <end code addr> <start stack addr> <ESP value> <EIP> <bitmap of pending sigs>
-// <bitmap of blocked sigs> <bitmap of ignored sigs> <waiting channel> <num pages swapped>
-// <cumulative pages swapped> <exit signal> <processor #> <real-time prio> <agg block I/O delays>
-// <guest time> <children guest time> <start data addr> <end data addr> <start break addr>
-// <cmd line args start addr> <amd line args end addr> <env start addr> <env end addr> <exit code>
-// Example line: 1 (init) S 0 0 0 0 0 0 0 0 220 0 0 0 0 0 0 0 2 0 0 ...etc...
+// Per-pid/tid stats.
+struct PidStat {
+ std::string comm = "";
+ std::string state = "";
+ uint64_t startTime = 0;
+ uint64_t majorFaults = 0;
+};
+
+/**
+ * /proc/PID/stat or /proc/PID/task/TID/stat format:
+ * <pid> <comm> <state> <ppid> <pgrp ID> <session ID> <tty_nr> <tpgid> <flags> <minor faults>
+ * <children minor faults> <major faults> <children major faults> <user mode time>
+ * <system mode time> <children user mode time> <children kernel mode time> <priority> <nice value>
+ * <num threads> <start time since boot> <virtual memory size> <resident set size> <rss soft limit>
+ * <start code addr> <end code addr> <start stack addr> <ESP value> <EIP> <bitmap of pending sigs>
+ * <bitmap of blocked sigs> <bitmap of ignored sigs> <waiting channel> <num pages swapped>
+ * <cumulative pages swapped> <exit signal> <processor #> <real-time prio> <agg block I/O delays>
+ * <guest time> <children guest time> <start data addr> <end data addr> <start break addr>
+ * <cmd line args start addr> <amd line args end addr> <env start addr> <env end addr> <exit code>
+ * Example line: 1 (init) S 0 0 0 0 0 0 0 0 220 0 0 0 0 0 0 0 2 0 0 ...etc...
+ */
bool parsePidStatLine(const std::string& line, PidStat* pidStat) {
std::vector<std::string> fields = Split(line, " ");
- // Note: Regex parsing for the below logic increased the time taken to run the
- // UidProcStatsCollectorTest#TestProcPidStatContentsFromDevice from 151.7ms to 1.3 seconds.
-
- // Comm string is enclosed with ( ) brackets and may contain space(s). Thus calculate the
- // commEndOffset based on the field that contains the closing bracket.
+ /* Note: Regex parsing for the below logic increased the time taken to run the
+ * UidProcStatsCollectorTest#TestProcPidStatContentsFromDevice from 151.7ms to 1.3 seconds.
+ *
+ * Comm string is enclosed with ( ) brackets and may contain space(s). Thus calculate the
+ * commEndOffset based on the field that contains the closing bracket.
+ */
size_t commEndOffset = 0;
for (size_t i = 1; i < fields.size(); ++i) {
pidStat->comm += fields[i];
@@ -80,20 +94,16 @@ bool parsePidStatLine(const std::string& line, PidStat* pidStat) {
}
if (pidStat->comm.front() != '(' || pidStat->comm.back() != ')') {
- ALOGW("Comm string `%s` not enclosed in brackets", pidStat->comm.c_str());
+ ALOGD("Comm string `%s` not enclosed in brackets", pidStat->comm.c_str());
return false;
}
pidStat->comm.erase(pidStat->comm.begin());
pidStat->comm.erase(pidStat->comm.end() - 1);
- // The required data is in the first 22 + |commEndOffset| fields so make sure there are at least
- // these many fields in the file.
- if (fields.size() < 22 + commEndOffset || !ParseInt(fields[0], &pidStat->pid) ||
- !ParseInt(fields[3 + commEndOffset], &pidStat->ppid) ||
+ if (fields.size() < 22 + commEndOffset ||
!ParseUint(fields[11 + commEndOffset], &pidStat->majorFaults) ||
- !ParseUint(fields[19 + commEndOffset], &pidStat->numThreads) ||
!ParseUint(fields[21 + commEndOffset], &pidStat->startTime)) {
- ALOGW("Invalid proc pid stat contents: \"%s\"", line.c_str());
+ ALOGD("Invalid proc pid stat contents: \"%s\"", line.c_str());
return false;
}
pidStat->state = fields[2 + commEndOffset];
@@ -144,205 +154,213 @@ Result<std::unordered_map<std::string, std::string>> readKeyValueFile(
return contents;
}
-// /proc/PID/status file format(*):
-// Tgid: <Thread group ID of the process>
-// Uid: <Read UID> <Effective UID> <Saved set UID> <Filesystem UID>
-// VmPeak: <Peak virtual memory size> kB
-// VmSize: <Virtual memory size> kB
-// VmHWM: <Peak resident set size> kB
-// VmRSS: <Resident set size> kB
-//
-// (*) - Included only the fields that are parsed from the file.
-Result<void> readPidStatusFile(const std::string& path, ProcessStats* processStats) {
- auto ret = readKeyValueFile(path, ":\t");
- if (!ret.ok()) {
- return Error(ret.error().code()) << ret.error();
+/**
+ * /proc/PID/status file format:
+ * Tgid: <Thread group ID of the process>
+ * Uid: <Read UID> <Effective UID> <Saved set UID> <Filesystem UID>
+ *
+ * Note: Included only the fields that are parsed from the file.
+ */
+Result<std::tuple<uid_t, pid_t>> readPidStatusFile(const std::string& path) {
+ auto result = readKeyValueFile(path, ":\t");
+ if (!result.ok()) {
+ return Error(result.error().code()) << result.error();
}
- auto contents = ret.value();
+ auto contents = result.value();
if (contents.empty()) {
return Error(ERR_INVALID_FILE) << "Empty file " << path;
}
+ int64_t uid = 0;
+ int64_t tgid = 0;
if (contents.find("Uid") == contents.end() ||
- !ParseInt(Split(contents["Uid"], "\t")[0], &processStats->uid)) {
- return Error(ERR_INVALID_FILE) << "Failed to read 'UIDs' from file " << path;
+ !ParseInt(Split(contents["Uid"], "\t")[0], &uid)) {
+ return Error(ERR_INVALID_FILE) << "Failed to read 'UID' from file " << path;
}
- if (contents.find("Tgid") == contents.end() ||
- !ParseInt(contents["Tgid"], &processStats->tgid)) {
+ if (contents.find("Tgid") == contents.end() || !ParseInt(contents["Tgid"], &tgid)) {
return Error(ERR_INVALID_FILE) << "Failed to read 'Tgid' from file " << path;
}
- // Below Vm* fields may not be present for some processes so don't fail when they are missing.
- if (contents.find("VmPeak") != contents.end() &&
- !ParseUint(Split(contents["VmPeak"], " ")[0], &processStats->vmPeakKb)) {
- return Error(ERR_INVALID_FILE) << "Failed to parse 'VmPeak' from file " << path;
- }
- if (contents.find("VmSize") != contents.end() &&
- !ParseUint(Split(contents["VmSize"], " ")[0], &processStats->vmSizeKb)) {
- return Error(ERR_INVALID_FILE) << "Failed to parse 'VmSize' from file " << path;
- }
- if (contents.find("VmHWM") != contents.end() &&
- !ParseUint(Split(contents["VmHWM"], " ")[0], &processStats->vmHwmKb)) {
- return Error(ERR_INVALID_FILE) << "Failed to parse 'VmHWM' from file " << path;
- }
- if (contents.find("VmRSS") != contents.end() &&
- !ParseUint(Split(contents["VmRSS"], " ")[0], &processStats->vmRssKb)) {
- return Error(ERR_INVALID_FILE) << "Failed to parse 'VmRSS' from file " << path;
- }
- return {};
+ return std::make_tuple(uid, tgid);
}
} // namespace
+std::string ProcessStats::toString() const {
+ return StringPrintf("{comm: %s, startTime: %" PRIu64 ", totalMajorFaults: %" PRIu64
+ ", totalTasksCount: %d, ioBlockedTasksCount: %d}",
+ comm.c_str(), startTime, totalMajorFaults, totalTasksCount,
+ ioBlockedTasksCount);
+}
+
+std::string UidProcStats::toString() const {
+ std::string buffer;
+ StringAppendF(&buffer,
+ "UidProcStats{totalMajorFaults: %" PRIu64 ", totalTasksCount: %d,"
+ "ioBlockedTasksCount: %d, processStatsByPid: {",
+ totalMajorFaults, totalTasksCount, ioBlockedTasksCount);
+ for (const auto& [pid, processStats] : processStatsByPid) {
+ StringAppendF(&buffer, "{pid: %" PRIi32 ", processStats: %s},", pid,
+ processStats.toString().c_str());
+ }
+ StringAppendF(&buffer, "}");
+ return buffer;
+}
+
Result<void> UidProcStatsCollector::collect() {
if (!mEnabled) {
return Error() << "Can not access PID stat files under " << kProcDirPath;
}
Mutex::Autolock lock(mMutex);
- const auto& processStats = getProcessStatsLocked();
- if (!processStats.ok()) {
- return Error() << processStats.error();
+ auto uidProcStatsByUid = readUidProcStatsLocked();
+ if (!uidProcStatsByUid.ok()) {
+ return Error() << uidProcStatsByUid.error();
}
- mDeltaProcessStats.clear();
- for (const auto& it : *processStats) {
- const ProcessStats& curStats = it.second;
- const auto& cachedIt = mLatestProcessStats.find(it.first);
- if (cachedIt == mLatestProcessStats.end() ||
- cachedIt->second.process.startTime != curStats.process.startTime) {
- // New/reused PID so don't calculate the delta.
- mDeltaProcessStats.emplace_back(curStats);
+ mDeltaStats.clear();
+ for (const auto& [uid, currStats] : *uidProcStatsByUid) {
+ if (const auto& it = mLatestStats.find(uid); it == mLatestStats.end()) {
+ mDeltaStats[uid] = currStats;
continue;
}
-
- ProcessStats deltaStats = curStats;
- const ProcessStats& cachedStats = cachedIt->second;
- deltaStats.process.majorFaults -= cachedStats.process.majorFaults;
- for (auto& deltaThread : deltaStats.threads) {
- const auto& cachedThread = cachedStats.threads.find(deltaThread.first);
- if (cachedThread == cachedStats.threads.end() ||
- cachedThread->second.startTime != deltaThread.second.startTime) {
- // New TID or TID reused by the same PID so don't calculate the delta.
- continue;
+ const auto& prevStats = mLatestStats[uid];
+ UidProcStats deltaStats = {
+ .totalTasksCount = currStats.totalTasksCount,
+ .ioBlockedTasksCount = currStats.ioBlockedTasksCount,
+ };
+ for (const auto& [pid, processStats] : currStats.processStatsByPid) {
+ ProcessStats deltaProcessStats = processStats;
+ if (const auto& it = prevStats.processStatsByPid.find(pid);
+ it != prevStats.processStatsByPid.end() &&
+ it->second.startTime == processStats.startTime &&
+ it->second.totalMajorFaults <= deltaProcessStats.totalMajorFaults) {
+ deltaProcessStats.totalMajorFaults =
+ deltaProcessStats.totalMajorFaults - it->second.totalMajorFaults;
}
- deltaThread.second.majorFaults -= cachedThread->second.majorFaults;
+ deltaStats.totalMajorFaults += deltaProcessStats.totalMajorFaults;
+ deltaStats.processStatsByPid[pid] = deltaProcessStats;
}
- mDeltaProcessStats.emplace_back(deltaStats);
+ mDeltaStats[uid] = std::move(deltaStats);
}
- mLatestProcessStats = *processStats;
+ mLatestStats = std::move(*uidProcStatsByUid);
return {};
}
-Result<std::unordered_map<pid_t, ProcessStats>> UidProcStatsCollector::getProcessStatsLocked()
+Result<std::unordered_map<uid_t, UidProcStats>> UidProcStatsCollector::readUidProcStatsLocked()
const {
- std::unordered_map<pid_t, ProcessStats> processStats;
+ std::unordered_map<uid_t, UidProcStats> uidProcStatsByUid;
auto procDirp = std::unique_ptr<DIR, int (*)(DIR*)>(opendir(mPath.c_str()), closedir);
if (!procDirp) {
return Error() << "Failed to open " << mPath << " directory";
}
- dirent* pidDir = nullptr;
- while ((pidDir = readdir(procDirp.get())) != nullptr) {
- // 1. Read top-level pid stats.
+ for (dirent* pidDir = nullptr; (pidDir = readdir(procDirp.get())) != nullptr;) {
pid_t pid = 0;
if (pidDir->d_type != DT_DIR || !ParseInt(pidDir->d_name, &pid)) {
continue;
}
- ProcessStats curStats;
- std::string path = StringPrintf((mPath + kStatFileFormat).c_str(), pid);
- auto ret = readPidStatFile(path, &curStats.process);
- if (!ret.ok()) {
- // PID may disappear between scanning the directory and parsing the stat file.
- // Thus treat ERR_FILE_OPEN_READ errors as soft errors.
- if (ret.error().code() != ERR_FILE_OPEN_READ) {
- return Error() << "Failed to read top-level per-process stat file: "
- << ret.error().message().c_str();
+ auto result = readProcessStatsLocked(pid);
+ if (!result.ok()) {
+ if (result.error().code() != ERR_FILE_OPEN_READ) {
+ return Error() << result.error();
+ }
+ /* |ERR_FILE_OPEN_READ| is a soft-error because PID may disappear between scanning and
+ * reading directory/files.
+ */
+ if (DEBUG) {
+ ALOGD("%s", result.error().message().c_str());
}
- ALOGW("Failed to read top-level per-process stat file %s: %s", path.c_str(),
- ret.error().message().c_str());
continue;
}
-
- // 2. Read aggregated process status.
- path = StringPrintf((mPath + kStatusFileFormat).c_str(), curStats.process.pid);
- ret = readPidStatusFile(path, &curStats);
- if (!ret.ok()) {
- if (ret.error().code() != ERR_FILE_OPEN_READ) {
- return Error() << "Failed to read pid status for pid " << curStats.process.pid
- << ": " << ret.error().message().c_str();
- }
- ALOGW("Failed to read pid status for pid %" PRIu32 ": %s", curStats.process.pid,
- ret.error().message().c_str());
+ uid_t uid = std::get<0>(*result);
+ ProcessStats processStats = std::get<ProcessStats>(*result);
+ if (uidProcStatsByUid.find(uid) == uidProcStatsByUid.end()) {
+ uidProcStatsByUid[uid] = {};
}
+ UidProcStats* uidProcStats = &uidProcStatsByUid[uid];
+ uidProcStats->totalMajorFaults += processStats.totalMajorFaults;
+ uidProcStats->totalTasksCount += processStats.totalTasksCount;
+ uidProcStats->ioBlockedTasksCount += processStats.ioBlockedTasksCount;
+ uidProcStats->processStatsByPid[pid] = std::move(processStats);
+ }
+ return uidProcStatsByUid;
+}
- // 3. When failed to read tgid or uid, copy these from the previous collection.
- if (curStats.tgid == -1 || curStats.uid == -1) {
- const auto& it = mLatestProcessStats.find(curStats.process.pid);
- if (it != mLatestProcessStats.end() &&
- it->second.process.startTime == curStats.process.startTime) {
- curStats.tgid = it->second.tgid;
- curStats.uid = it->second.uid;
+Result<std::tuple<uid_t, ProcessStats>> UidProcStatsCollector::readProcessStatsLocked(
+ pid_t pid) const {
+ // 1. Read top-level pid stats.
+ PidStat pidStat = {};
+ std::string path = StringPrintf((mPath + kStatFileFormat).c_str(), pid);
+ if (auto result = readPidStatFile(path, &pidStat); !result.ok()) {
+ return Error(result.error().code())
+ << "Failed to read top-level per-process stat file '%s': %s"
+ << result.error().message().c_str();
+ }
+
+ // 2. Read aggregated process status.
+ pid_t tgid = -1;
+ uid_t uid = -1;
+ path = StringPrintf((mPath + kStatusFileFormat).c_str(), pid);
+ if (auto result = readPidStatusFile(path); !result.ok()) {
+ if (result.error().code() != ERR_FILE_OPEN_READ) {
+ return Error() << "Failed to read pid status for pid " << pid << ": "
+ << result.error().message().c_str();
+ }
+ for (const auto& [curUid, uidProcStats] : mLatestStats) {
+ if (const auto it = uidProcStats.processStatsByPid.find(pid);
+ it != uidProcStats.processStatsByPid.end() &&
+ it->second.startTime == pidStat.startTime) {
+ tgid = pid;
+ uid = curUid;
+ break;
}
}
+ } else {
+ uid = std::get<0>(*result);
+ tgid = std::get<1>(*result);
+ }
- if (curStats.tgid != -1 && curStats.tgid != curStats.process.pid) {
- ALOGW("Skipping non-process (i.e., Tgid != PID) entry for PID %" PRIu32,
- curStats.process.pid);
- continue;
- }
+ if (uid == -1 || tgid != pid) {
+ return Error(ERR_FILE_OPEN_READ)
+ << "Skipping PID '" << pid << "' because either Tgid != PID or invalid UID";
+ }
+
+ ProcessStats processStats = {
+ .comm = std::move(pidStat.comm),
+ .startTime = pidStat.startTime,
+ .totalTasksCount = 1,
+ /* Top-level process stats has the aggregated major page faults count and this should be
+ * persistent across thread creation/termination. Thus use the value from this field.
+ */
+ .totalMajorFaults = pidStat.majorFaults,
+ .ioBlockedTasksCount = pidStat.state == "D" ? 1 : 0,
+ };
- // 3. Fetch per-thread stats.
- std::string taskDir = StringPrintf((mPath + kTaskDirFormat).c_str(), pid);
- auto taskDirp = std::unique_ptr<DIR, int (*)(DIR*)>(opendir(taskDir.c_str()), closedir);
- if (!taskDirp) {
- // Treat this as a soft error so at least the process stats will be collected.
- ALOGW("Failed to open %s directory", taskDir.c_str());
+ // 3. Read per-thread stats.
+ std::string taskDir = StringPrintf((mPath + kTaskDirFormat).c_str(), pid);
+ bool didReadMainThread = false;
+ auto taskDirp = std::unique_ptr<DIR, int (*)(DIR*)>(opendir(taskDir.c_str()), closedir);
+ for (dirent* tidDir = nullptr;
+ taskDirp != nullptr && (tidDir = readdir(taskDirp.get())) != nullptr;) {
+ pid_t tid = 0;
+ if (tidDir->d_type != DT_DIR || !ParseInt(tidDir->d_name, &tid) || tid == pid) {
+ continue;
}
- dirent* tidDir = nullptr;
- bool didReadMainThread = false;
- while (taskDirp != nullptr && (tidDir = readdir(taskDirp.get())) != nullptr) {
- pid_t tid = 0;
- if (tidDir->d_type != DT_DIR || !ParseInt(tidDir->d_name, &tid)) {
- continue;
- }
- if (processStats.find(tid) != processStats.end()) {
- return Error() << "Process stats already exists for TID " << tid
- << ". Stats will be double counted";
- }
- PidStat curThreadStat = {};
- path = StringPrintf((taskDir + kStatFileFormat).c_str(), tid);
- const auto& ret = readPidStatFile(path, &curThreadStat);
- if (!ret.ok()) {
- if (ret.error().code() != ERR_FILE_OPEN_READ) {
- return Error() << "Failed to read per-thread stat file: "
- << ret.error().message().c_str();
- }
- // Maybe the thread terminated before reading the file so skip this thread and
- // continue with scanning the next thread's stat.
- ALOGW("Failed to read per-thread stat file %s: %s", path.c_str(),
- ret.error().message().c_str());
- continue;
- }
- if (curThreadStat.pid == curStats.process.pid) {
- didReadMainThread = true;
+ PidStat tidStat = {};
+ path = StringPrintf((taskDir + kStatFileFormat).c_str(), tid);
+ if (const auto& result = readPidStatFile(path, &tidStat); !result.ok()) {
+ if (result.error().code() != ERR_FILE_OPEN_READ) {
+ return Error() << "Failed to read per-thread stat file: "
+ << result.error().message().c_str();
}
- curStats.threads[curThreadStat.pid] = curThreadStat;
- }
- if (!didReadMainThread) {
- // In the event of failure to read main-thread info (mostly because the process
- // terminated during scanning/parsing), fill out the stat that are common between main
- // thread and the process.
- curStats.threads[curStats.process.pid] = PidStat{
- .pid = curStats.process.pid,
- .comm = curStats.process.comm,
- .state = curStats.process.state,
- .ppid = curStats.process.ppid,
- .numThreads = curStats.process.numThreads,
- .startTime = curStats.process.startTime,
- };
+ /* Maybe the thread terminated before reading the file so skip this thread and
+ * continue with scanning the next thread's stat.
+ */
+ continue;
}
- processStats[curStats.process.pid] = curStats;
+ processStats.ioBlockedTasksCount += tidStat.state == "D" ? 1 : 0;
+ processStats.totalTasksCount += 1;
}
- return processStats;
+ return std::make_tuple(uid, processStats);
}
} // namespace watchdog
diff --git a/cpp/watchdog/server/src/UidProcStatsCollector.h b/cpp/watchdog/server/src/UidProcStatsCollector.h
index 04e4ca3b81..d0ec3c03e4 100644
--- a/cpp/watchdog/server/src/UidProcStatsCollector.h
+++ b/cpp/watchdog/server/src/UidProcStatsCollector.h
@@ -38,38 +38,52 @@ using ::android::base::StringPrintf;
#define PID_FOR_INIT 1
-constexpr const char* kProcDirPath = "/proc";
-constexpr const char* kStatFileFormat = "/%" PRIu32 "/stat";
-constexpr const char* kTaskDirFormat = "/%" PRIu32 "/task";
-constexpr const char* kStatusFileFormat = "/%" PRIu32 "/status";
+constexpr const char kProcDirPath[] = "/proc";
+constexpr const char kStatFileFormat[] = "/%" PRIu32 "/stat";
+constexpr const char kTaskDirFormat[] = "/%" PRIu32 "/task";
+constexpr const char kStatusFileFormat[] = "/%" PRIu32 "/status";
-struct PidStat {
- pid_t pid = 0;
+// Per-process stats.
+struct ProcessStats {
std::string comm = "";
- std::string state = "";
- pid_t ppid = 0;
- uint64_t majorFaults = 0;
- uint32_t numThreads = 0;
- uint64_t startTime = 0; // Useful when identifying PID/TID reuse
+ uint64_t startTime = 0; // Useful when identifying PID reuse
+ uint64_t totalMajorFaults = 0;
+ int totalTasksCount = 0;
+ int ioBlockedTasksCount = 0;
+ std::string toString() const;
};
-struct ProcessStats {
- int64_t tgid = -1; // -1 indicates a failure to read this value
- int64_t uid = -1; // -1 indicates a failure to read this value
- uint64_t vmPeakKb = 0;
- uint64_t vmSizeKb = 0;
- uint64_t vmHwmKb = 0;
- uint64_t vmRssKb = 0;
- PidStat process = {}; // Aggregated stats across all the threads
- std::unordered_map<pid_t, PidStat> threads; // Per-thread stat including the main thread
+// Per-UID stats.
+struct UidProcStats {
+ uint64_t totalMajorFaults = 0;
+ int totalTasksCount = 0;
+ int ioBlockedTasksCount = 0;
+ std::unordered_map<pid_t, ProcessStats> processStatsByPid = {};
+ std::string toString() const;
};
-// Collector/parser for `/proc/[pid]/stat`, `/proc/[pid]/task/[tid]/stat` and /proc/[pid]/status`
-// files.
-class UidProcStatsCollector : public RefBase {
+/**
+ * Collector/parser for `/proc/[pid]/stat`, `/proc/[pid]/task/[tid]/stat` and /proc/[pid]/status`
+ * files.
+ */
+class UidProcStatsCollectorInterface : public RefBase {
+public:
+ // Collects the per-uid stats from /proc directory.
+ virtual android::base::Result<void> collect() = 0;
+ // Returns the latest per-uid process stats.
+ virtual const std::unordered_map<uid_t, UidProcStats> latestStats() const = 0;
+ // Returns the delta of per-uid process stats since the last before collection.
+ virtual const std::unordered_map<uid_t, UidProcStats> deltaStats() const = 0;
+ // Returns true only when the /proc files for the init process are accessible.
+ virtual bool enabled() const = 0;
+ // Returns the /proc files common ancestor directory path.
+ virtual const std::string dirPath() const = 0;
+};
+
+class UidProcStatsCollector final : public UidProcStatsCollectorInterface {
public:
explicit UidProcStatsCollector(const std::string& path = kProcDirPath) :
- mLatestProcessStats({}),
+ mLatestStats({}),
mPath(path) {
std::string pidStatPath = StringPrintf((mPath + kStatFileFormat).c_str(), PID_FOR_INIT);
std::string tidStatPath = StringPrintf((mPath + kTaskDirFormat + kStatFileFormat).c_str(),
@@ -80,54 +94,57 @@ public:
!access(pidStatusPath.c_str(), R_OK);
}
- virtual ~UidProcStatsCollector() {}
+ ~UidProcStatsCollector() {}
- // Collects per-process stats.
- virtual android::base::Result<void> collect();
+ android::base::Result<void> collect() override;
- // Returns the latest per-process stats collected.
- virtual const std::unordered_map<pid_t, ProcessStats> latestStats() const {
+ const std::unordered_map<uid_t, UidProcStats> latestStats() const {
Mutex::Autolock lock(mMutex);
- return mLatestProcessStats;
+ return mLatestStats;
}
- // Returns the delta of per-process stats since the last before collection.
- virtual const std::vector<ProcessStats> deltaStats() const {
+ const std::unordered_map<uid_t, UidProcStats> deltaStats() const {
Mutex::Autolock lock(mMutex);
- return mDeltaProcessStats;
+ return mDeltaStats;
}
- // Called by WatchdogPerfService and tests.
- virtual bool enabled() { return mEnabled; }
+ bool enabled() const { return mEnabled; }
- virtual std::string dirPath() { return mPath; }
+ const std::string dirPath() const { return mPath; }
private:
- // Reads the contents of the below files:
- // 1. Pid stat file at |mPath| + |kStatFileFormat|
- // 2. Aggregated per-process status at |mPath| + |kStatusFileFormat|
- // 3. Tid stat file at |mPath| + |kTaskDirFormat| + |kStatFileFormat|
- android::base::Result<std::unordered_map<pid_t, ProcessStats>> getProcessStatsLocked() const;
+ android::base::Result<std::unordered_map<uid_t, UidProcStats>> readUidProcStatsLocked() const;
+
+ /**
+ * Reads the contents of the below files:
+ * 1. Pid stat file at |mPath| + |kStatFileFormat|
+ * 2. Aggregated per-process status at |mPath| + |kStatusFileFormat|
+ * 3. Tid stat file at |mPath| + |kTaskDirFormat| + |kStatFileFormat|
+ */
+ android::base::Result<std::tuple<uid_t, ProcessStats>> readProcessStatsLocked(pid_t pid) const;
// Makes sure only one collection is running at any given time.
mutable Mutex mMutex;
- // Latest dump of per-process stats. Useful for calculating the delta and identifying PID/TID
- // reuse.
- std::unordered_map<pid_t, ProcessStats> mLatestProcessStats GUARDED_BY(mMutex);
+ // Latest dump of per-UID stats.
+ std::unordered_map<uid_t, UidProcStats> mLatestStats GUARDED_BY(mMutex);
- // Latest delta of per-process stats.
- std::vector<ProcessStats> mDeltaProcessStats GUARDED_BY(mMutex);
+ // Latest delta of per-uid stats.
+ std::unordered_map<uid_t, UidProcStats> mDeltaStats GUARDED_BY(mMutex);
- // True if the below files are accessible:
- // 1. Pid stat file at |mPath| + |kTaskStatFileFormat|
- // 2. Tid stat file at |mPath| + |kTaskDirFormat| + |kStatFileFormat|
- // 3. Pid status file at |mPath| + |kStatusFileFormat|
- // Otherwise, set to false.
+ /**
+ * True if the below files are accessible:
+ * 1. Pid stat file at |mPath| + |kTaskStatFileFormat|
+ * 2. Tid stat file at |mPath| + |kTaskDirFormat| + |kStatFileFormat|
+ * 3. Pid status file at |mPath| + |kStatusFileFormat|
+ * Otherwise, set to false.
+ */
bool mEnabled;
- // Proc directory path. Default value is |kProcDirPath|.
- // Updated by tests to point to a different location when needed.
+ /**
+ * Proc directory path. Default value is |kProcDirPath|.
+ * Updated by tests to point to a different location when needed.
+ */
std::string mPath;
FRIEND_TEST(IoPerfCollectionTest, TestValidProcPidContents);
diff --git a/cpp/watchdog/server/tests/MockUidProcStatsCollector.h b/cpp/watchdog/server/tests/MockUidProcStatsCollector.h
index 4cd6539da9..a16fa371d2 100644
--- a/cpp/watchdog/server/tests/MockUidProcStatsCollector.h
+++ b/cpp/watchdog/server/tests/MockUidProcStatsCollector.h
@@ -24,23 +24,22 @@
#include <string>
#include <unordered_map>
-#include <vector>
namespace android {
namespace automotive {
namespace watchdog {
-class MockUidProcStatsCollector : public UidProcStatsCollector {
+class MockUidProcStatsCollector : public UidProcStatsCollectorInterface {
public:
MockUidProcStatsCollector() {
ON_CALL(*this, enabled()).WillByDefault(::testing::Return(true));
}
- MOCK_METHOD(bool, enabled, (), (override));
MOCK_METHOD(android::base::Result<void>, collect, (), (override));
- MOCK_METHOD((const std::unordered_map<pid_t, ProcessStats>), latestStats, (),
+ MOCK_METHOD((const std::unordered_map<uid_t, UidProcStats>), latestStats, (),
(const, override));
- MOCK_METHOD(const std::vector<ProcessStats>, deltaStats, (), (const, override));
- MOCK_METHOD(std::string, dirPath, (), (override));
+ MOCK_METHOD((const std::unordered_map<uid_t, UidProcStats>), deltaStats, (), (const, override));
+ MOCK_METHOD(bool, enabled, (), (const, override));
+ MOCK_METHOD(const std::string, dirPath, (), (const, override));
};
} // namespace watchdog
diff --git a/cpp/watchdog/server/tests/UidProcStatsCollectorTest.cpp b/cpp/watchdog/server/tests/UidProcStatsCollectorTest.cpp
index 68c9d21039..c9eb6d4b94 100644
--- a/cpp/watchdog/server/tests/UidProcStatsCollectorTest.cpp
+++ b/cpp/watchdog/server/tests/UidProcStatsCollectorTest.cpp
@@ -16,6 +16,7 @@
#include "ProcPidDir.h"
#include "UidProcStatsCollector.h"
+#include "UidProcStatsCollectorTestUtils.h"
#include <android-base/file.h>
#include <android-base/stringprintf.h>
@@ -33,76 +34,15 @@ namespace watchdog {
using ::android::automotive::watchdog::testing::populateProcPidDir;
using ::android::base::StringAppendF;
using ::android::base::StringPrintf;
+using ::testing::UnorderedPointwise;
namespace {
-std::string toString(const PidStat& stat) {
- return StringPrintf("PID: %" PRIu32 ", PPID: %" PRIu32 ", Comm: %s, State: %s, "
- "Major page faults: %" PRIu64 ", Num threads: %" PRIu32
- ", Start time: %" PRIu64,
- stat.pid, stat.ppid, stat.comm.c_str(), stat.state.c_str(),
- stat.majorFaults, stat.numThreads, stat.startTime);
-}
-
-std::string toString(const ProcessStats& stats) {
- std::string buffer;
- StringAppendF(&buffer,
- "Tgid: %" PRIi64 ", UID: %" PRIi64 ", VmPeak: %" PRIu64 ", VmSize: %" PRIu64
- ", VmHWM: %" PRIu64 ", VmRSS: %" PRIu64 ", %s\n",
- stats.tgid, stats.uid, stats.vmPeakKb, stats.vmSizeKb, stats.vmHwmKb,
- stats.vmRssKb, toString(stats.process).c_str());
- StringAppendF(&buffer, "\tThread stats:\n");
- for (const auto& it : stats.threads) {
- StringAppendF(&buffer, "\t\t%s\n", toString(it.second).c_str());
- }
- StringAppendF(&buffer, "\n");
- return buffer;
-}
-
-std::string toString(const std::vector<ProcessStats>& stats) {
- std::string buffer;
- StringAppendF(&buffer, "Number of processes: %d\n", static_cast<int>(stats.size()));
- for (const auto& it : stats) {
- StringAppendF(&buffer, "%s", toString(it).c_str());
- }
- return buffer;
-}
-
-bool isEqual(const PidStat& lhs, const PidStat& rhs) {
- return lhs.pid == rhs.pid && lhs.comm == rhs.comm && lhs.state == rhs.state &&
- lhs.ppid == rhs.ppid && lhs.majorFaults == rhs.majorFaults &&
- lhs.numThreads == rhs.numThreads && lhs.startTime == rhs.startTime;
-}
-
-bool isEqual(std::vector<ProcessStats>* lhs, std::vector<ProcessStats>* rhs) {
- if (lhs->size() != rhs->size()) {
- return false;
- }
- std::sort(lhs->begin(), lhs->end(), [&](const ProcessStats& l, const ProcessStats& r) -> bool {
- return l.process.pid < r.process.pid;
- });
- std::sort(rhs->begin(), rhs->end(), [&](const ProcessStats& l, const ProcessStats& r) -> bool {
- return l.process.pid < r.process.pid;
- });
- return std::equal(lhs->begin(), lhs->end(), rhs->begin(),
- [&](const ProcessStats& l, const ProcessStats& r) -> bool {
- if (l.tgid != r.tgid || l.uid != r.uid || l.vmPeakKb != r.vmPeakKb ||
- l.vmSizeKb != r.vmSizeKb || l.vmHwmKb != r.vmHwmKb ||
- l.vmRssKb != r.vmRssKb || !isEqual(l.process, r.process) ||
- l.threads.size() != r.threads.size()) {
- return false;
- }
- for (const auto& lIt : l.threads) {
- const auto& rIt = r.threads.find(lIt.first);
- if (rIt == r.threads.end()) {
- return false;
- }
- if (!isEqual(lIt.second, rIt->second)) {
- return false;
- }
- }
- return true;
- });
+MATCHER(UidProcStatsByUidEq, "") {
+ const auto& actual = std::get<0>(arg);
+ const auto& expected = std::get<1>(arg);
+ return actual.first == expected.first &&
+ ExplainMatchResult(UidProcStatsEq(expected.second), actual.second, result_listener);
}
std::string pidStatusStr(pid_t pid, uid_t uid) {
@@ -110,11 +50,14 @@ std::string pidStatusStr(pid_t pid, uid_t uid) {
uid);
}
-std::string pidStatusStr(pid_t pid, uid_t uid, uint64_t vmPeakKb, uint64_t vmSizeKb,
- uint64_t vmHwmKb, uint64_t vmRssKb) {
- return StringPrintf("%sVmPeak:\t%" PRIu64 "\nVmSize:\t%" PRIu64 "\nVmHWM:\t%" PRIu64
- "\nVmRSS:\t%" PRIu64 "\n",
- pidStatusStr(pid, uid).c_str(), vmPeakKb, vmSizeKb, vmHwmKb, vmRssKb);
+std::string toString(const std::unordered_map<uid_t, UidProcStats>& uidProcStatsByUid) {
+ std::string buffer;
+ StringAppendF(&buffer, "Number of UIDs: %" PRIi32 "\n",
+ static_cast<int>(uidProcStatsByUid.size()));
+ for (const auto& [uid, stats] : uidProcStatsByUid) {
+ StringAppendF(&buffer, "{UID: %d, %s}", uid, stats.toString().c_str());
+ }
+ return buffer;
}
} // namespace
@@ -127,107 +70,93 @@ TEST(UidProcStatsCollectorTest, TestValidStatFiles) {
std::unordered_map<pid_t, std::string> perProcessStat = {
{1, "1 (init) S 0 0 0 0 0 0 0 0 220 0 0 0 0 0 0 0 2 0 0\n"},
- {1000, "1000 (system_server) R 1 0 0 0 0 0 0 0 600 0 0 0 0 0 0 0 2 0 1000\n"},
+ {1000, "1000 (system_server) D 1 0 0 0 0 0 0 0 600 0 0 0 0 0 0 0 2 0 13400\n"},
};
std::unordered_map<pid_t, std::string> perProcessStatus = {
- {1, pidStatusStr(1, 0, 123, 456, 789, 345)},
- {1000, pidStatusStr(1000, 10001234, 234, 567, 890, 123)},
+ {1, pidStatusStr(1, 0)},
+ {1000, pidStatusStr(1000, 10001234)},
};
std::unordered_map<pid_t, std::string> perThreadStat = {
{1, "1 (init) S 0 0 0 0 0 0 0 0 200 0 0 0 0 0 0 0 2 0 0\n"},
- {453, "453 (init) S 0 0 0 0 0 0 0 0 20 0 0 0 0 0 0 0 2 0 275\n"},
- {1000, "1000 (system_server) R 1 0 0 0 0 0 0 0 250 0 0 0 0 0 0 0 2 0 1000\n"},
- {1100, "1100 (system_server) S 1 0 0 0 0 0 0 0 350 0 0 0 0 0 0 0 2 0 1200\n"},
- };
-
- std::vector<ProcessStats> expected = {
- {.tgid = 1,
- .uid = 0,
- .vmPeakKb = 123,
- .vmSizeKb = 456,
- .vmHwmKb = 789,
- .vmRssKb = 345,
- .process = {1, "init", "S", 0, 220, 2, 0},
- .threads = {{1, {1, "init", "S", 0, 200, 2, 0}},
- {453, {453, "init", "S", 0, 20, 2, 275}}}},
- {.tgid = 1000,
- .uid = 10001234,
- .vmPeakKb = 234,
- .vmSizeKb = 567,
- .vmHwmKb = 890,
- .vmRssKb = 123,
- .process = {1000, "system_server", "R", 1, 600, 2, 1000},
- .threads = {{1000, {1000, "system_server", "R", 1, 250, 2, 1000}},
- {1100, {1100, "system_server", "S", 1, 350, 2, 1200}}}},
- };
+ {453, "453 (init) D 0 0 0 0 0 0 0 0 20 0 0 0 0 0 0 0 2 0 275\n"},
+ {1000, "1000 (system_server) D 1 0 0 0 0 0 0 0 250 0 0 0 0 0 0 0 2 0 13400\n"},
+ {1100, "1100 (system_server) D 1 0 0 0 0 0 0 0 350 0 0 0 0 0 0 0 2 0 13900\n"},
+ };
+
+ std::unordered_map<uid_t, UidProcStats> expected =
+ {{0,
+ UidProcStats{.totalMajorFaults = 220,
+ .totalTasksCount = 2,
+ .ioBlockedTasksCount = 1,
+ .processStatsByPid = {{1, {"init", 0, 220, 2, 1}}}}},
+ {10001234,
+ UidProcStats{.totalMajorFaults = 600,
+ .totalTasksCount = 2,
+ .ioBlockedTasksCount = 2,
+ .processStatsByPid = {{1000, {"system_server", 13'400, 600, 2, 2}}}}}};
TemporaryDir firstSnapshot;
ASSERT_RESULT_OK(populateProcPidDir(firstSnapshot.path, pidToTids, perProcessStat,
perProcessStatus, perThreadStat));
UidProcStatsCollector collector(firstSnapshot.path);
+
ASSERT_TRUE(collector.enabled())
<< "Files under the path `" << firstSnapshot.path << "` are inaccessible";
ASSERT_RESULT_OK(collector.collect());
- auto actual = std::vector<ProcessStats>(collector.deltaStats());
- EXPECT_TRUE(isEqual(&expected, &actual)) << "First snapshot doesn't match.\nExpected:\n"
- << toString(expected) << "\nActual:\n"
- << toString(actual);
+ auto actual = collector.deltaStats();
+
+ EXPECT_THAT(actual, UnorderedPointwise(UidProcStatsByUidEq(), expected))
+ << "First snapshot doesn't match.\nExpected:\n"
+ << toString(expected) << "\nActual:\n"
+ << toString(actual);
pidToTids = {
{1, {1, 453}}, {1000, {1000, 1400}}, // TID 1100 terminated and 1400 instantiated.
};
perProcessStat = {
{1, "1 (init) S 0 0 0 0 0 0 0 0 920 0 0 0 0 0 0 0 2 0 0\n"},
- {1000, "1000 (system_server) R 1 0 0 0 0 0 0 0 1550 0 0 0 0 0 0 0 2 0 1000\n"},
+ {1000, "1000 (system_server) R 1 0 0 0 0 0 0 0 1550 0 0 0 0 0 0 0 2 0 13400\n"},
};
perThreadStat = {
{1, "1 (init) S 0 0 0 0 0 0 0 0 600 0 0 0 0 0 0 0 2 0 0\n"},
{453, "453 (init) S 0 0 0 0 0 0 0 0 320 0 0 0 0 0 0 0 2 0 275\n"},
- {1000, "1000 (system_server) R 1 0 0 0 0 0 0 0 600 0 0 0 0 0 0 0 2 0 1000\n"},
+ {1000, "1000 (system_server) R 1 0 0 0 0 0 0 0 600 0 0 0 0 0 0 0 2 0 13400\n"},
// TID 1100 hits +400 major page faults before terminating. This is counted against
// PID 1000's perProcessStat.
{1400, "1400 (system_server) S 1 0 0 0 0 0 0 0 200 0 0 0 0 0 0 0 2 0 8977476\n"},
};
- expected = {
- {.tgid = 1,
- .uid = 0,
- .vmPeakKb = 123,
- .vmSizeKb = 456,
- .vmHwmKb = 789,
- .vmRssKb = 345,
- .process = {1, "init", "S", 0, 700, 2, 0},
- .threads = {{1, {1, "init", "S", 0, 400, 2, 0}},
- {453, {453, "init", "S", 0, 300, 2, 275}}}},
- {.tgid = 1000,
- .uid = 10001234,
- .vmPeakKb = 234,
- .vmSizeKb = 567,
- .vmHwmKb = 890,
- .vmRssKb = 123,
- .process = {1000, "system_server", "R", 1, 950, 2, 1000},
- .threads = {{1000, {1000, "system_server", "R", 1, 350, 2, 1000}},
- {1400, {1400, "system_server", "S", 1, 200, 2, 8977476}}}},
- };
+ expected = {{0,
+ {.totalMajorFaults = 700,
+ .totalTasksCount = 2,
+ .ioBlockedTasksCount = 0,
+ .processStatsByPid = {{1, {"init", 0, 700, 2, 0}}}}},
+ {10001234,
+ {.totalMajorFaults = 950,
+ .totalTasksCount = 2,
+ .ioBlockedTasksCount = 0,
+ .processStatsByPid = {{1000, {"system_server", 13'400, 950, 2, 0}}}}}};
TemporaryDir secondSnapshot;
ASSERT_RESULT_OK(populateProcPidDir(secondSnapshot.path, pidToTids, perProcessStat,
perProcessStatus, perThreadStat));
collector.mPath = secondSnapshot.path;
+
ASSERT_TRUE(collector.enabled())
<< "Files under the path `" << secondSnapshot.path << "` are inaccessible";
ASSERT_RESULT_OK(collector.collect());
- actual = std::vector<ProcessStats>(collector.deltaStats());
- EXPECT_TRUE(isEqual(&expected, &actual)) << "Second snapshot doesn't match.\nExpected:\n"
- << toString(expected) << "\nActual:\n"
- << toString(actual);
+ actual = collector.deltaStats();
+ EXPECT_THAT(actual, UnorderedPointwise(UidProcStatsByUidEq(), expected))
+ << "Second snapshot doesn't match.\nExpected:\n"
+ << toString(expected) << "\nActual:\n"
+ << toString(actual);
}
TEST(UidProcStatsCollectorTest, TestHandlesProcessTerminationBetweenScanningAndParsing) {
@@ -248,7 +177,7 @@ TEST(UidProcStatsCollectorTest, TestHandlesProcessTerminationBetweenScanningAndP
};
std::unordered_map<pid_t, std::string> perProcessStatus = {
- {1, "Pid:\t1\nTgid:\t1\nUid:\t0\t0\t0\t0\n"},
+ {1, pidStatusStr(1, 0)},
// Process 1000 terminated.
{2000, pidStatusStr(2000, 10001234)},
{3000, pidStatusStr(3000, 10001234)},
@@ -261,40 +190,34 @@ TEST(UidProcStatsCollectorTest, TestHandlesProcessTerminationBetweenScanningAndP
// TID 3300 terminated.
};
- std::vector<ProcessStats> expected = {
- {.tgid = 1,
- .uid = 0,
- .process = {1, "init", "S", 0, 220, 1, 0},
- .threads = {{1, {1, "init", "S", 0, 200, 1, 0}}}},
- {.tgid = -1,
- .uid = -1,
- .process = {1000, "system_server", "R", 1, 600, 1, 1000},
- // Stats common between process and main-thread are copied when
- // main-thread stats are not available.
- .threads = {{1000, {1000, "system_server", "R", 1, 0, 1, 1000}}}},
- {.tgid = 2000,
- .uid = 10001234,
- .process = {2000, "logd", "R", 1, 1200, 1, 4567},
- .threads = {{2000, {2000, "logd", "R", 1, 0, 1, 4567}}}},
- {.tgid = 3000,
- .uid = 10001234,
- .process = {3000, "disk I/O", "R", 1, 10300, 2, 67890},
- .threads = {{3000, {3000, "disk I/O", "R", 1, 2400, 2, 67890}}}},
- };
+ std::unordered_map<uid_t, UidProcStats> expected =
+ {{0,
+ UidProcStats{.totalMajorFaults = 220,
+ .totalTasksCount = 1,
+ .ioBlockedTasksCount = 0,
+ .processStatsByPid = {{1, {"init", 0, 220, 1, 0}}}}},
+ {10001234,
+ UidProcStats{.totalMajorFaults = 11500,
+ .totalTasksCount = 2,
+ .ioBlockedTasksCount = 0,
+ .processStatsByPid = {{2000, {"logd", 4567, 1200, 1, 0}},
+ {3000, {"disk I/O", 67890, 10'300, 1, 0}}}}}};
TemporaryDir procDir;
ASSERT_RESULT_OK(populateProcPidDir(procDir.path, pidToTids, perProcessStat, perProcessStatus,
perThreadStat));
UidProcStatsCollector collector(procDir.path);
+
ASSERT_TRUE(collector.enabled())
<< "Files under the path `" << procDir.path << "` are inaccessible";
ASSERT_RESULT_OK(collector.collect());
- auto actual = std::vector<ProcessStats>(collector.deltaStats());
- EXPECT_TRUE(isEqual(&expected, &actual)) << "Proc pid contents doesn't match.\nExpected:\n"
- << toString(expected) << "\nActual:\n"
- << toString(actual);
+ auto actual = collector.deltaStats();
+ EXPECT_THAT(actual, UnorderedPointwise(UidProcStatsByUidEq(), expected))
+ << "Proc pid contents doesn't match.\nExpected:\n"
+ << toString(expected) << "\nActual:\n"
+ << toString(actual);
}
TEST(UidProcStatsCollectorTest, TestHandlesPidTidReuse) {
@@ -320,42 +243,40 @@ TEST(UidProcStatsCollectorTest, TestHandlesPidTidReuse) {
{1, "1 (init) S 0 0 0 0 0 0 0 0 200 0 0 0 0 0 0 0 4 0 0\n"},
{367, "367 (init) S 0 0 0 0 0 0 0 0 400 0 0 0 0 0 0 0 4 0 100\n"},
{453, "453 (init) S 0 0 0 0 0 0 0 0 100 0 0 0 0 0 0 0 4 0 275\n"},
- {589, "589 (init) S 0 0 0 0 0 0 0 0 500 0 0 0 0 0 0 0 4 0 600\n"},
+ {589, "589 (init) D 0 0 0 0 0 0 0 0 500 0 0 0 0 0 0 0 4 0 600\n"},
{1000, "1000 (system_server) R 1 0 0 0 0 0 0 0 250 0 0 0 0 0 0 0 1 0 1000\n"},
{2345, "2345 (logd) R 1 0 0 0 0 0 0 0 54354 0 0 0 0 0 0 0 1 0 456\n"},
};
- std::vector<ProcessStats> expected = {
- {.tgid = 1,
- .uid = 0,
- .process = {1, "init", "S", 0, 1200, 4, 0},
- .threads = {{1, {1, "init", "S", 0, 200, 4, 0}},
- {367, {367, "init", "S", 0, 400, 4, 100}},
- {453, {453, "init", "S", 0, 100, 4, 275}},
- {589, {589, "init", "S", 0, 500, 4, 600}}}},
- {.tgid = 1000,
- .uid = 10001234,
- .process = {1000, "system_server", "R", 1, 250, 1, 1000},
- .threads = {{1000, {1000, "system_server", "R", 1, 250, 1, 1000}}}},
- {.tgid = 2345,
- .uid = 10001234,
- .process = {2345, "logd", "R", 1, 54354, 1, 456},
- .threads = {{2345, {2345, "logd", "R", 1, 54354, 1, 456}}}},
- };
+ std::unordered_map<uid_t, UidProcStats> expected =
+ {{0,
+ UidProcStats{.totalMajorFaults = 1200,
+ .totalTasksCount = 4,
+ .ioBlockedTasksCount = 1,
+ .processStatsByPid = {{1, {"init", 0, 1200, 4, 1}}}}},
+ {10001234,
+ UidProcStats{.totalMajorFaults = 54'604,
+ .totalTasksCount = 2,
+ .ioBlockedTasksCount = 0,
+ .processStatsByPid = {{1000, {"system_server", 1000, 250, 1, 0}},
+ {2345, {"logd", 456, 54'354, 1, 0}}}}}};
TemporaryDir firstSnapshot;
ASSERT_RESULT_OK(populateProcPidDir(firstSnapshot.path, pidToTids, perProcessStat,
perProcessStatus, perThreadStat));
UidProcStatsCollector collector(firstSnapshot.path);
+
ASSERT_TRUE(collector.enabled())
<< "Files under the path `" << firstSnapshot.path << "` are inaccessible";
ASSERT_RESULT_OK(collector.collect());
- auto actual = std::vector<ProcessStats>(collector.deltaStats());
- EXPECT_TRUE(isEqual(&expected, &actual)) << "First snapshot doesn't match.\nExpected:\n"
- << toString(expected) << "\nActual:\n"
- << toString(actual);
+ auto actual = collector.deltaStats();
+
+ EXPECT_THAT(actual, UnorderedPointwise(UidProcStatsByUidEq(), expected))
+ << "First snapshot doesn't match.\nExpected:\n"
+ << toString(expected) << "\nActual:\n"
+ << toString(actual);
pidToTids = {
{1, {1, 589}}, // TID 589 reused by the same process.
@@ -385,37 +306,34 @@ TEST(UidProcStatsCollectorTest, TestHandlesPidTidReuse) {
{453, "453 (logd) D 1 0 0 0 0 0 0 0 1800 0 0 0 0 0 0 0 2 0 4770\n"},
};
- expected = {
- {.tgid = 1,
- .uid = 0,
- .process = {1, "init", "S", 0, 600, 2, 0},
- .threads = {{1, {1, "init", "S", 0, 300, 2, 0}},
- {589, {589, "init", "S", 0, 300, 2, 2345}}}},
- {.tgid = 367,
- .uid = 10001234,
- .process = {367, "system_server", "R", 1, 100, 2, 3450},
- .threads = {{367, {367, "system_server", "R", 1, 50, 2, 3450}},
- {2000, {2000, "system_server", "R", 1, 50, 2, 3670}}}},
- {.tgid = 1000,
- .uid = 10001234,
- .process = {1000, "logd", "R", 1, 2000, 2, 4650},
- .threads = {{1000, {1000, "logd", "R", 1, 200, 2, 4650}},
- {453, {453, "logd", "D", 1, 1800, 2, 4770}}}},
- };
+ expected = {{0,
+ UidProcStats{.totalMajorFaults = 600,
+ .totalTasksCount = 2,
+ .ioBlockedTasksCount = 0,
+ .processStatsByPid = {{1, {"init", 0, 600, 2, 0}}}}},
+ {10001234,
+ UidProcStats{.totalMajorFaults = 2100,
+ .totalTasksCount = 4,
+ .ioBlockedTasksCount = 1,
+ .processStatsByPid = {{367, {"system_server", 3450, 100, 2, 0}},
+ {1000, {"logd", 4650, 2000, 2, 1}}}}}};
TemporaryDir secondSnapshot;
ASSERT_RESULT_OK(populateProcPidDir(secondSnapshot.path, pidToTids, perProcessStat,
perProcessStatus, perThreadStat));
collector.mPath = secondSnapshot.path;
+
ASSERT_TRUE(collector.enabled())
<< "Files under the path `" << secondSnapshot.path << "` are inaccessible";
ASSERT_RESULT_OK(collector.collect());
- actual = std::vector<ProcessStats>(collector.deltaStats());
- EXPECT_TRUE(isEqual(&expected, &actual)) << "Second snapshot doesn't match.\nExpected:\n"
- << toString(expected) << "\nActual:\n"
- << toString(actual);
+ actual = collector.deltaStats();
+
+ EXPECT_THAT(actual, UnorderedPointwise(UidProcStatsByUidEq(), expected))
+ << "Second snapshot doesn't match.\nExpected:\n"
+ << toString(expected) << "\nActual:\n"
+ << toString(actual);
}
TEST(UidProcStatsCollectorTest, TestErrorOnCorruptedProcessStatFile) {
@@ -440,6 +358,7 @@ TEST(UidProcStatsCollectorTest, TestErrorOnCorruptedProcessStatFile) {
perThreadStat));
UidProcStatsCollector collector(procDir.path);
+
ASSERT_TRUE(collector.enabled())
<< "Files under the path `" << procDir.path << "` are inaccessible";
ASSERT_FALSE(collector.collect().ok()) << "No error returned for invalid process stat file";
@@ -467,6 +386,7 @@ TEST(UidProcStatsCollectorTest, TestErrorOnCorruptedProcessStatusFile) {
perThreadStat));
UidProcStatsCollector collector(procDir.path);
+
ASSERT_TRUE(collector.enabled())
<< "Files under the path `" << procDir.path << "` are inaccessible";
ASSERT_FALSE(collector.collect().ok()) << "No error returned for invalid process status file";
@@ -474,11 +394,11 @@ TEST(UidProcStatsCollectorTest, TestErrorOnCorruptedProcessStatusFile) {
TEST(UidProcStatsCollectorTest, TestErrorOnCorruptedThreadStatFile) {
std::unordered_map<pid_t, std::vector<pid_t>> pidToTids = {
- {1, {1}},
+ {1, {1, 234}},
};
std::unordered_map<pid_t, std::string> perProcessStat = {
- {1, "1 (init) S 0 0 0 0 0 0 0 0 200 0 0 0 0 0 0 0 1 0 0\n"},
+ {1, "1 (init) S 0 0 0 0 0 0 0 0 200 0 0 0 0 0 0 0 2 0 678\n"},
};
std::unordered_map<pid_t, std::string> perProcessStatus = {
@@ -486,7 +406,8 @@ TEST(UidProcStatsCollectorTest, TestErrorOnCorruptedThreadStatFile) {
};
std::unordered_map<pid_t, std::string> perThreadStat = {
- {1, "1 (init) S 0 0 0 0 0 0 0 0 200 0 0 0 CORRUPTED DATA\n"},
+ {1, "1 (init) S 0 0 0 0 0 0 0 0 200 0 0 0 0 0 0 0 2 0 678\n"},
+ {234, "234 (init) D 0 0 0 0 0 0 0 0 200 0 0 0 CORRUPTED DATA\n"},
};
TemporaryDir procDir;
@@ -494,6 +415,7 @@ TEST(UidProcStatsCollectorTest, TestErrorOnCorruptedThreadStatFile) {
perThreadStat));
UidProcStatsCollector collector(procDir.path);
+
ASSERT_TRUE(collector.enabled())
<< "Files under the path `" << procDir.path << "` are inaccessible";
ASSERT_FALSE(collector.collect().ok()) << "No error returned for invalid thread stat file";
@@ -516,34 +438,39 @@ TEST(UidProcStatsCollectorTest, TestHandlesSpaceInCommName) {
{1, "1 (random process name with space) S 0 0 0 0 0 0 0 0 200 0 0 0 0 0 0 0 1 0 0\n"},
};
- std::vector<ProcessStats> expected = {
- {.tgid = 1,
- .uid = 0,
- .process = {1, "random process name with space", "S", 0, 200, 1, 0},
- .threads = {{1, {1, "random process name with space", "S", 0, 200, 1, 0}}}},
- };
+ std::unordered_map<uid_t, UidProcStats> expected = {
+ {0,
+ UidProcStats{.totalMajorFaults = 200,
+ .totalTasksCount = 1,
+ .ioBlockedTasksCount = 0,
+ .processStatsByPid = {
+ {1, {"random process name with space", 0, 200, 1, 0}}}}}};
TemporaryDir procDir;
ASSERT_RESULT_OK(populateProcPidDir(procDir.path, pidToTids, perProcessStat, perProcessStatus,
perThreadStat));
UidProcStatsCollector collector(procDir.path);
+
ASSERT_TRUE(collector.enabled())
<< "Files under the path `" << procDir.path << "` are inaccessible";
ASSERT_RESULT_OK(collector.collect());
- auto actual = std::vector<ProcessStats>(collector.deltaStats());
- EXPECT_TRUE(isEqual(&expected, &actual)) << "Proc pid contents doesn't match.\nExpected:\n"
- << toString(expected) << "\nActual:\n"
- << toString(actual);
+ auto actual = collector.deltaStats();
+
+ EXPECT_THAT(actual, UnorderedPointwise(UidProcStatsByUidEq(), expected))
+ << "Proc pid contents doesn't match.\nExpected:\n"
+ << toString(expected) << "\nActual:\n"
+ << toString(actual);
}
-TEST(UidProcStatsCollectorTest, TestProcPidStatContentsFromDevice) {
+TEST(UidProcStatsCollectorTest, TestUidProcStatsCollectorContentsFromDevice) {
UidProcStatsCollector collector;
ASSERT_TRUE(collector.enabled()) << "/proc/[pid]/.* files are inaccessible";
ASSERT_RESULT_OK(collector.collect());
const auto& processStats = collector.deltaStats();
+
// The below check should pass because there should be at least one process.
EXPECT_GT(processStats.size(), 0);
}
diff --git a/cpp/watchdog/server/tests/UidProcStatsCollectorTestUtils.h b/cpp/watchdog/server/tests/UidProcStatsCollectorTestUtils.h
new file mode 100644
index 0000000000..6e5a409f5b
--- /dev/null
+++ b/cpp/watchdog/server/tests/UidProcStatsCollectorTestUtils.h
@@ -0,0 +1,60 @@
+/*
+ * 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.
+ */
+
+#ifndef CPP_WATCHDOG_SERVER_TESTS_UIDPROCSTATSCOLLECTORTESTUTILS_H_
+#define CPP_WATCHDOG_SERVER_TESTS_UIDPROCSTATSCOLLECTORTESTUTILS_H_
+
+#include "UidProcStatsCollector.h"
+
+#include <gmock/gmock.h>
+
+namespace android {
+namespace automotive {
+namespace watchdog {
+
+MATCHER_P(ProcessStatsEq, expected, "") {
+ const auto& actual = arg;
+ return ::testing::Value(actual.comm, ::testing::Eq(expected.comm)) &&
+ ::testing::Value(actual.startTime, ::testing::Eq(expected.startTime)) &&
+ ::testing::Value(actual.totalMajorFaults, ::testing::Eq(expected.totalMajorFaults)) &&
+ ::testing::Value(actual.totalTasksCount, ::testing::Eq(expected.totalTasksCount)) &&
+ ::testing::Value(actual.ioBlockedTasksCount,
+ ::testing::Eq(expected.ioBlockedTasksCount));
+}
+
+MATCHER(ProcessStatsByPidEq, "") {
+ const auto& actual = std::get<0>(arg);
+ const auto& expected = std::get<1>(arg);
+ return actual.first == expected.first &&
+ ::testing::Value(actual.second, ProcessStatsEq(expected.second));
+}
+
+MATCHER_P(UidProcStatsEq, expected, "") {
+ const auto& actual = arg;
+ return ::testing::Value(actual.totalMajorFaults, ::testing::Eq(expected.totalMajorFaults)) &&
+ ::testing::Value(actual.totalTasksCount, ::testing::Eq(expected.totalTasksCount)) &&
+ ::testing::Value(actual.ioBlockedTasksCount,
+ ::testing::Eq(expected.ioBlockedTasksCount)) &&
+ ::testing::Value(actual.processStatsByPid,
+ ::testing::UnorderedPointwise(ProcessStatsByPidEq(),
+ expected.processStatsByPid));
+}
+
+} // namespace watchdog
+} // namespace automotive
+} // namespace android
+
+#endif // CPP_WATCHDOG_SERVER_TESTS_UIDPROCSTATSCOLLECTORTESTUTILS_H_