diff options
author | Lakshman Annadorai <lakshmana@google.com> | 2021-09-14 10:56:57 -0700 |
---|---|---|
committer | Lakshman Annadorai <lakshmana@google.com> | 2021-09-20 15:00:00 -0700 |
commit | 08309492fb97f8e3ed3179f640198b1a680efa76 (patch) | |
tree | f39c48e8d75cf13d8abaa77843125b684811817c /cpp/watchdog | |
parent | 2b0161ce3763a469e7288d8853b01b65cbe93c66 (diff) | |
download | Car-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.cpp | 360 | ||||
-rw-r--r-- | cpp/watchdog/server/src/UidProcStatsCollector.h | 123 | ||||
-rw-r--r-- | cpp/watchdog/server/tests/MockUidProcStatsCollector.h | 11 | ||||
-rw-r--r-- | cpp/watchdog/server/tests/UidProcStatsCollectorTest.cpp | 351 | ||||
-rw-r--r-- | cpp/watchdog/server/tests/UidProcStatsCollectorTestUtils.h | 60 |
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_ |