Add a collector for per-process/per-thread stats.
- ProcPidStat collector will be integrated with IoPerfCollection in the
following CL.
- Added unit tests to test the scanning and parsing logics.
- Added unit tests to scan and parse the /proc directory from the device
but this test is commented as CarWatchDog doesn't have required SELinux
permission to read the /proc/PID directories.
Already approved in ag/10353452.
Bug: 148486340, 130382110
Test: Tested with unit tests.
Change-Id: I31aa63a109aed0361538bdb9c2a8ff5cd823dd5c
diff --git a/watchdog/server/Android.bp b/watchdog/server/Android.bp
index faa4e24..0cc5e57 100644
--- a/watchdog/server/Android.bp
+++ b/watchdog/server/Android.bp
@@ -45,8 +45,9 @@
],
srcs: [
"src/IoPerfCollection.cpp",
+ "src/ProcPidStat.cpp",
+ "src/ProcStat.cpp",
"src/UidIoStats.cpp",
- "src/ProcStat.cpp"
],
static_libs: [
"libgtest_prod",
@@ -66,6 +67,7 @@
srcs: [
"tests/IoPerfCollectionTest.cpp",
"tests/UidIoStatsTest.cpp",
+ "tests/ProcPidStatTest.cpp",
"tests/ProcStatTest.cpp",
],
static_libs: [
diff --git a/watchdog/server/src/IoPerfCollection.h b/watchdog/server/src/IoPerfCollection.h
index 1066ac4..259b186 100644
--- a/watchdog/server/src/IoPerfCollection.h
+++ b/watchdog/server/src/IoPerfCollection.h
@@ -125,15 +125,15 @@
// a collection, update the collection type, and generate collection dumps.
class IoPerfCollection {
public:
- IoPerfCollection()
- : mTopNStatsPerCategory(kTopNStatsPerCategory),
+ IoPerfCollection() :
+ mTopNStatsPerCategory(kTopNStatsPerCategory),
mBoottimeRecords({}),
mPeriodicRecords({}),
mCustomRecords({}),
mCurrCollectionEvent(CollectionEvent::NONE),
mUidToPackageNameMapping({}),
- mUidIoStats() {
- }
+ mUidIoStats(),
+ mProcStat() {}
// Starts the boot-time collection on a separate thread and returns immediately. Must be called
// only once. Otherwise, returns an error.
diff --git a/watchdog/server/src/ProcPidStat.cpp b/watchdog/server/src/ProcPidStat.cpp
new file mode 100644
index 0000000..b92c739
--- /dev/null
+++ b/watchdog/server/src/ProcPidStat.cpp
@@ -0,0 +1,315 @@
+/**
+ * Copyright (c) 2020, The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+#define LOG_TAG "carwatchdogd"
+
+#include "ProcPidStat.h"
+
+#include <android-base/file.h>
+#include <android-base/parseint.h>
+#include <android-base/strings.h>
+#include <dirent.h>
+#include <log/log.h>
+
+#include <string>
+#include <unordered_map>
+#include <vector>
+
+namespace android {
+namespace automotive {
+namespace watchdog {
+
+using android::base::EndsWith;
+using android::base::Error;
+using android::base::ParseInt;
+using android::base::ParseUint;
+using android::base::ReadFileToString;
+using android::base::Result;
+using android::base::Split;
+
+namespace {
+
+enum ReadError {
+ ERR_INVALID_FILE = 0,
+ ERR_FILE_OPEN_READ = 1,
+ 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...
+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
+ // ProcPidStatTest#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];
+ if (EndsWith(fields[i], ")")) {
+ commEndOffset = i - 1;
+ break;
+ }
+ pidStat->comm += " ";
+ }
+
+ if (pidStat->comm.front() != '(' || pidStat->comm.back() != ')') {
+ ALOGW("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 || !ParseUint(fields[0], &pidStat->pid) ||
+ !ParseUint(fields[3 + commEndOffset], &pidStat->ppid) ||
+ !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());
+ return false;
+ }
+ pidStat->state = fields[2 + commEndOffset];
+ return true;
+}
+
+Result<void> readPidStatFile(const std::string& path, PidStat* pidStat) {
+ std::string buffer;
+ if (!ReadFileToString(path, &buffer)) {
+ return Error(ERR_FILE_OPEN_READ) << "ReadFileToString failed for " << path;
+ }
+ std::vector<std::string> lines = Split(std::move(buffer), "\n");
+ if (lines.size() != 1 && (lines.size() != 2 || !lines[1].empty())) {
+ return Error(ERR_INVALID_FILE) << path << " contains " << lines.size() << " lines != 1";
+ }
+ if (!parsePidStatLine(std::move(lines[0]), pidStat)) {
+ return Error(ERR_INVALID_FILE) << "Failed to parse the contents of " << path;
+ }
+ return {};
+}
+
+} // namespace
+
+Result<std::vector<ProcessStats>> ProcPidStat::collect() {
+ if (!mEnabled) {
+ return Error() << "Can not access PID stat files under " << kProcDirPath;
+ }
+
+ Mutex::Autolock lock(mMutex);
+ const auto& processStats = getProcessStatsLocked();
+ if (!processStats) {
+ return Error() << processStats.error();
+ }
+
+ std::vector<ProcessStats> delta;
+ for (const auto& it : *processStats) {
+ const ProcessStats& curStats = it.second;
+ const auto& cachedIt = mLastProcessStats.find(it.first);
+ if (cachedIt == mLastProcessStats.end() ||
+ cachedIt->second.process.startTime != curStats.process.startTime) {
+ // New/reused PID so don't calculate the delta.
+ delta.emplace_back(curStats);
+ 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;
+ }
+ deltaThread.second.majorFaults -= cachedThread->second.majorFaults;
+ }
+ delta.emplace_back(deltaStats);
+ }
+ mLastProcessStats = *processStats;
+ return delta;
+}
+
+Result<std::unordered_map<uint32_t, ProcessStats>> ProcPidStat::getProcessStatsLocked() const {
+ std::unordered_map<uint32_t, ProcessStats> processStats;
+ 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.
+ uint32_t pid = 0;
+ if (pidDir->d_type != DT_DIR || !ParseUint(pidDir->d_name, &pid)) {
+ continue;
+ }
+ ProcessStats curStats;
+ std::string path = StringPrintf((mPath + kStatFileFormat).c_str(), pid);
+ const auto& ret = readPidStatFile(path, &curStats.process);
+ if (!ret) {
+ // 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();
+ }
+ ALOGW("Failed to read top-level per-process stat file %s: %s", path.c_str(),
+ ret.error().message().c_str());
+ continue;
+ }
+
+ // 2. When not found in the cache, fetch tgid/UID as soon as possible because processes
+ // may terminate during scanning.
+ const auto& it = mLastProcessStats.find(curStats.process.pid);
+ if (it == mLastProcessStats.end() ||
+ it->second.process.startTime != curStats.process.startTime || it->second.tgid == -1 ||
+ it->second.uid == -1) {
+ const auto& ret = getPidStatusLocked(&curStats);
+ if (!ret) {
+ 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());
+ // Default tgid and uid values are -1 (aka unknown).
+ }
+ } else {
+ // Fetch from cache.
+ curStats.tgid = it->second.tgid;
+ curStats.uid = it->second.uid;
+ }
+
+ 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;
+ }
+
+ // 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());
+ }
+ dirent* tidDir = nullptr;
+ bool didReadMainThread = false;
+ while (taskDirp != nullptr && (tidDir = readdir(taskDirp.get())) != nullptr) {
+ uint32_t tid = 0;
+ if (tidDir->d_type != DT_DIR || !ParseUint(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) {
+ 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;
+ }
+ 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,
+ };
+ }
+ processStats[curStats.process.pid] = curStats;
+ }
+ return processStats;
+}
+
+Result<void> ProcPidStat::getPidStatusLocked(ProcessStats* processStats) const {
+ std::string buffer;
+ std::string path = StringPrintf((mPath + kStatusFileFormat).c_str(), processStats->process.pid);
+ if (!ReadFileToString(path, &buffer)) {
+ return Error(ERR_FILE_OPEN_READ) << "ReadFileToString failed for " << path;
+ }
+ std::vector<std::string> lines = Split(std::move(buffer), "\n");
+ bool didReadUid = false;
+ bool didReadTgid = false;
+ for (size_t i = 0; i < lines.size(); ++i) {
+ if (lines[i].empty()) {
+ continue;
+ }
+ if (!lines[i].compare(0, 4, "Uid:")) {
+ if (didReadUid) {
+ return Error(ERR_INVALID_FILE)
+ << "Duplicate UID line: \"" << lines[i] << "\" in file " << path;
+ }
+ std::vector<std::string> fields = Split(lines[i], "\t");
+ if (fields.size() < 2 || !ParseInt(fields[1], &processStats->uid)) {
+ return Error(ERR_INVALID_FILE)
+ << "Invalid UID line: \"" << lines[i] << "\" in file " << path;
+ }
+ didReadUid = true;
+ } else if (!lines[i].compare(0, 5, "Tgid:")) {
+ if (didReadTgid) {
+ return Error(ERR_INVALID_FILE)
+ << "Duplicate Tgid line: \"" << lines[i] << "\" in file" << path;
+ }
+ std::vector<std::string> fields = Split(lines[i], "\t");
+ if (fields.size() != 2 || !ParseInt(fields[1], &processStats->tgid)) {
+ return Error(ERR_INVALID_FILE)
+ << "Invalid tgid line: \"" << lines[i] << "\" in file" << path;
+ }
+ didReadTgid = true;
+ }
+ }
+ if (!didReadUid || !didReadTgid) {
+ return Error(ERR_INVALID_FILE) << "Incomplete file " << mPath + kStatusFileFormat;
+ }
+ return {};
+}
+
+} // namespace watchdog
+} // namespace automotive
+} // namespace android
diff --git a/watchdog/server/src/ProcPidStat.h b/watchdog/server/src/ProcPidStat.h
new file mode 100644
index 0000000..5fa1fb7
--- /dev/null
+++ b/watchdog/server/src/ProcPidStat.h
@@ -0,0 +1,118 @@
+/**
+ * Copyright (c) 2020, 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 WATCHDOG_SERVER_SRC_PROCPIDSTAT_H_
+#define WATCHDOG_SERVER_SRC_PROCPIDSTAT_H_
+
+#include <android-base/result.h>
+#include <android-base/stringprintf.h>
+#include <gtest/gtest_prod.h>
+#include <inttypes.h>
+#include <stdint.h>
+#include <utils/Mutex.h>
+
+#include <string>
+#include <unordered_map>
+#include <vector>
+
+namespace android {
+namespace automotive {
+namespace watchdog {
+
+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";
+
+struct PidStat {
+ uint32_t pid = 0;
+ std::string comm = "";
+ std::string state = "";
+ uint32_t ppid = 0;
+ uint64_t majorFaults = 0;
+ uint32_t numThreads = 0;
+ uint64_t startTime = 0; // Useful when identifying PID/TID reuse
+};
+
+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
+ PidStat process = {}; // Aggregated stats across all the threads
+ std::unordered_map<uint32_t, PidStat> threads; // Per-thread stat including the main thread
+};
+
+// Collector/parser for `/proc/[pid]/stat`, `/proc/[pid]/task/[tid]/stat` and /proc/[pid]/status`
+// files.
+class ProcPidStat {
+public:
+ explicit ProcPidStat(const std::string& path = kProcDirPath) :
+ mLastProcessStats({}), mPath(path) {
+ std::string pidStatPath = StringPrintf((mPath + kStatFileFormat).c_str(), PID_FOR_INIT);
+ std::string tidStatPath = StringPrintf((mPath + kTaskDirFormat + kStatFileFormat).c_str(),
+ PID_FOR_INIT, PID_FOR_INIT);
+ std::string pidStatusPath = StringPrintf((mPath + kStatusFileFormat).c_str(), PID_FOR_INIT);
+
+ mEnabled = !access(pidStatPath.c_str(), R_OK) && !access(tidStatPath.c_str(), R_OK) &&
+ !access(pidStatusPath.c_str(), R_OK);
+ }
+
+ // Collects pid info delta since the last collection.
+ android::base::Result<std::vector<ProcessStats>> collect();
+
+ // Called by IoPerfCollection and tests.
+ bool enabled() { return mEnabled; }
+
+private:
+ // Reads the contents of the below files:
+ // 1. Pid stat file at |mPath| + |kStatFileFormat|
+ // 2. Tid stat file at |mPath| + |kTaskDirFormat| + |kStatFileFormat|
+ android::base::Result<std::unordered_map<uint32_t, ProcessStats>> getProcessStatsLocked() const;
+
+ // Reads the tgid and real UID for the given PID from |mPath| + |kStatusFileFormat|.
+ android::base::Result<void> getPidStatusLocked(ProcessStats* processStats) const;
+
+ // Makes sure only one collection is running at any given time.
+ Mutex mMutex;
+
+ // Last dump of per-process stats. Useful for calculating the delta and identifying PID/TID
+ // reuse.
+ std::unordered_map<uint32_t, ProcessStats> mLastProcessStats 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.
+ bool mEnabled;
+
+ // Proc directory path. Default value is |kProcDirPath|.
+ // Updated by tests to point to a different location when needed.
+ std::string mPath;
+
+ FRIEND_TEST(ProcPidStatTest, TestValidStatFiles);
+ FRIEND_TEST(ProcPidStatTest, TestHandlesProcessTerminationBetweenScanningAndParsing);
+ FRIEND_TEST(ProcPidStatTest, TestHandlesPidTidReuse);
+};
+
+} // namespace watchdog
+} // namespace automotive
+} // namespace android
+
+#endif // WATCHDOG_SERVER_SRC_PROCPIDSTAT_H_
diff --git a/watchdog/server/tests/ProcPidStatTest.cpp b/watchdog/server/tests/ProcPidStatTest.cpp
new file mode 100644
index 0000000..abd1c2b
--- /dev/null
+++ b/watchdog/server/tests/ProcPidStatTest.cpp
@@ -0,0 +1,671 @@
+/*
+ * Copyright 2020 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.
+ */
+
+#include "ProcPidStat.h"
+
+#include <android-base/file.h>
+#include <android-base/stringprintf.h>
+#include <errno.h>
+#include <inttypes.h>
+
+#include <algorithm>
+#include <string>
+
+#include "gmock/gmock.h"
+
+namespace android {
+namespace automotive {
+namespace watchdog {
+
+using android::base::Error;
+using android::base::Result;
+using android::base::StringAppendF;
+using android::base::StringPrintf;
+using android::base::WriteStringToFile;
+
+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 ", %s\n", stats.tgid, stats.uid,
+ 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 ||
+ !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;
+ });
+}
+
+Result<void> makeDir(std::string path) {
+ if (mkdir(path.c_str(), 0700) && errno != EEXIST) {
+ return Error() << "Could not mkdir " << path << ": " << strerror(errno);
+ }
+ return {};
+}
+
+Result<void> populateProcPidDir(
+ const std::string& procDirPath,
+ const std::unordered_map<uint32_t, std::vector<uint32_t>>& pidToTids,
+ const std::unordered_map<uint32_t, std::string>& processStat,
+ const std::unordered_map<uint32_t, std::string>& processStatus,
+ const std::unordered_map<uint32_t, std::string>& threadStat) {
+ for (const auto& it : pidToTids) {
+ // 1. Create /proc/PID dir.
+ const auto& pidDirRes = makeDir(StringPrintf("%s/%" PRIu32, procDirPath.c_str(), it.first));
+ if (!pidDirRes) {
+ return Error() << "Failed to create top-level per-process directory: "
+ << pidDirRes.error();
+ }
+
+ // 2. Create /proc/PID/stat file.
+ uint32_t pid = it.first;
+ if (processStat.find(pid) != processStat.end()) {
+ std::string path = StringPrintf((procDirPath + kStatFileFormat).c_str(), pid);
+ if (!WriteStringToFile(processStat.at(pid), path)) {
+ return Error() << "Failed to write pid stat file " << path;
+ }
+ }
+
+ // 3. Create /proc/PID/status file.
+ if (processStatus.find(pid) != processStatus.end()) {
+ std::string path = StringPrintf((procDirPath + kStatusFileFormat).c_str(), pid);
+ if (!WriteStringToFile(processStatus.at(pid), path)) {
+ return Error() << "Failed to write pid status file " << path;
+ }
+ }
+
+ // 4. Create /proc/PID/task dir.
+ const auto& taskDirRes = makeDir(StringPrintf((procDirPath + kTaskDirFormat).c_str(), pid));
+ if (!taskDirRes) {
+ return Error() << "Failed to create task directory: " << taskDirRes.error();
+ }
+
+ // 5. Create /proc/PID/task/TID dirs and /proc/PID/task/TID/stat files.
+ for (const auto& tid : it.second) {
+ const auto& tidDirRes = makeDir(
+ StringPrintf((procDirPath + kTaskDirFormat + "/%" PRIu32).c_str(), pid, tid));
+ if (!tidDirRes) {
+ return Error() << "Failed to create per-thread directory: " << tidDirRes.error();
+ }
+ if (threadStat.find(tid) != threadStat.end()) {
+ std::string path =
+ StringPrintf((procDirPath + kTaskDirFormat + kStatFileFormat).c_str(), pid,
+ tid);
+ if (!WriteStringToFile(threadStat.at(tid), path)) {
+ return Error() << "Failed to write thread stat file " << path;
+ }
+ }
+ }
+ }
+
+ return {};
+}
+
+} // namespace
+
+TEST(ProcPidStatTest, TestValidStatFiles) {
+ std::unordered_map<uint32_t, std::vector<uint32_t>> pidToTids = {
+ {1, {1, 453}},
+ {1000, {1000, 1100}},
+ };
+
+ std::unordered_map<uint32_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"},
+ };
+
+ std::unordered_map<uint32_t, std::string> perProcessStatus = {
+ {1, "Pid:\t1\nTgid:\t1\nUid:\t0\t0\t0\t0\n"},
+ {1000, "Pid:\t1000\nTgid:\t1000\nUid:\t10001234\t10001234\t10001234\t10001234\n"},
+ };
+
+ std::unordered_map<uint32_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,
+ .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,
+ .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}},
+ },
+ },
+ };
+
+ TemporaryDir firstSnapshot;
+ auto ret = populateProcPidDir(firstSnapshot.path, pidToTids, perProcessStat, perProcessStatus,
+ perThreadStat);
+ ASSERT_TRUE(ret) << "Failed to populate proc pid dir: " << ret.error();
+
+ ProcPidStat procPidStat(firstSnapshot.path);
+ ASSERT_TRUE(procPidStat.enabled())
+ << "Files under the path `" << firstSnapshot.path << "` are inaccessible";
+
+ auto actual = procPidStat.collect();
+ ASSERT_TRUE(actual) << "Failed to collect proc pid stat: " << actual.error();
+
+ EXPECT_TRUE(isEqual(&expected, &actual.value())) << "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"},
+ };
+
+ 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"},
+ // 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,
+ .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,
+ .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}},
+ },
+ },
+ };
+
+ TemporaryDir secondSnapshot;
+ ret = populateProcPidDir(secondSnapshot.path, pidToTids, perProcessStat, perProcessStatus,
+ perThreadStat);
+ ASSERT_TRUE(ret) << "Failed to populate proc pid dir: " << ret.error();
+
+ procPidStat.mPath = secondSnapshot.path;
+ ASSERT_TRUE(procPidStat.enabled())
+ << "Files under the path `" << secondSnapshot.path << "` are inaccessible";
+
+ actual = procPidStat.collect();
+ ASSERT_TRUE(actual) << "Failed to collect proc pid stat: " << actual.error();
+
+ EXPECT_TRUE(isEqual(&expected, &actual.value()))
+ << "Second snapshot doesn't match.\nExpected:\n"
+ << toString(expected) << "\nActual:\n"
+ << toString(*actual);
+}
+
+TEST(ProcPidStatTest, TestHandlesProcessTerminationBetweenScanningAndParsing) {
+ std::unordered_map<uint32_t, std::vector<uint32_t>> pidToTids = {
+ {1, {1}},
+ {100, {100}}, // Process terminates after scanning PID directory.
+ {1000, {1000}}, // Process terminates after reading stat file.
+ {2000, {2000}}, // Process terminates after scanning task directory.
+ {3000, {3000, 3300}}, // TID 3300 terminates after scanning task directory.
+ };
+
+ std::unordered_map<uint32_t, std::string> perProcessStat = {
+ {1, "1 (init) S 0 0 0 0 0 0 0 0 220 0 0 0 0 0 0 0 1 0 0\n"},
+ // Process 100 terminated.
+ {1000, "1000 (system_server) R 1 0 0 0 0 0 0 0 600 0 0 0 0 0 0 0 1 0 1000\n"},
+ {2000, "2000 (logd) R 1 0 0 0 0 0 0 0 1200 0 0 0 0 0 0 0 1 0 4567\n"},
+ {3000, "3000 (disk I/O) R 1 0 0 0 0 0 0 0 10300 0 0 0 0 0 0 0 2 0 67890\n"},
+ };
+
+ std::unordered_map<uint32_t, std::string> perProcessStatus = {
+ {1, "Pid:\t1\nTgid:\t1\nUid:\t0\t0\t0\t0\n"},
+ // Process 1000 terminated.
+ {2000, "Pid:\t2000\nTgid:\t2000\nUid:\t10001234\t10001234\t10001234\t10001234\n"},
+ {3000, "Pid:\t3000\nTgid:\t3000\nUid:\t10001234\t10001234\t10001234\t10001234\n"},
+ };
+
+ std::unordered_map<uint32_t, std::string> perThreadStat = {
+ {1, "1 (init) S 0 0 0 0 0 0 0 0 200 0 0 0 0 0 0 0 1 0 0\n"},
+ // Process 2000 terminated.
+ {3000, "3000 (disk I/O) R 1 0 0 0 0 0 0 0 2400 0 0 0 0 0 0 0 2 0 67890\n"},
+ // 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}}},
+ },
+ };
+
+ TemporaryDir procDir;
+ const auto& ret = populateProcPidDir(procDir.path, pidToTids, perProcessStat, perProcessStatus,
+ perThreadStat);
+ ASSERT_TRUE(ret) << "Failed to populate proc pid dir: " << ret.error();
+
+ ProcPidStat procPidStat(procDir.path);
+ ASSERT_TRUE(procPidStat.enabled())
+ << "Files under the path `" << procDir.path << "` are inaccessible";
+
+ auto actual = procPidStat.collect();
+ ASSERT_TRUE(actual) << "Failed to collect proc pid stat: " << actual.error();
+
+ EXPECT_TRUE(isEqual(&expected, &actual.value()))
+ << "Proc pid contents doesn't match.\nExpected:\n"
+ << toString(expected) << "\nActual:\n"
+ << toString(*actual);
+}
+
+TEST(ProcPidStatTest, TestHandlesPidTidReuse) {
+ std::unordered_map<uint32_t, std::vector<uint32_t>> pidToTids = {
+ {1, {1, 367, 453, 589}},
+ {1000, {1000}},
+ {2345, {2345}},
+ };
+
+ std::unordered_map<uint32_t, std::string> perProcessStat = {
+ {1, "1 (init) S 0 0 0 0 0 0 0 0 1200 0 0 0 0 0 0 0 4 0 0\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::unordered_map<uint32_t, std::string> perProcessStatus = {
+ {1, "Pid:\t1\nTgid:\t1\nUid:\t0\t0\t0\t0\n"},
+ {1000, "Pid:\t1000\nTgid:\t1000\nUid:\t10001234\t10001234\t10001234\t10001234\n"},
+ {2345, "Pid:\t2345\nTgid:\t2345\nUid:\t10001234\t10001234\t10001234\t10001234\n"},
+ };
+
+ std::unordered_map<uint32_t, std::string> perThreadStat = {
+ {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"},
+ {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}}},
+ },
+ };
+
+ TemporaryDir firstSnapshot;
+ auto ret = populateProcPidDir(firstSnapshot.path, pidToTids, perProcessStat, perProcessStatus,
+ perThreadStat);
+ ASSERT_TRUE(ret) << "Failed to populate proc pid dir: " << ret.error();
+
+ ProcPidStat procPidStat(firstSnapshot.path);
+ ASSERT_TRUE(procPidStat.enabled())
+ << "Files under the path `" << firstSnapshot.path << "` are inaccessible";
+
+ auto actual = procPidStat.collect();
+ ASSERT_TRUE(actual) << "Failed to collect proc pid stat: " << actual.error();
+
+ EXPECT_TRUE(isEqual(&expected, &actual.value())) << "First snapshot doesn't match.\nExpected:\n"
+ << toString(expected) << "\nActual:\n"
+ << toString(*actual);
+
+ pidToTids = {
+ {1, {1, 589}}, // TID 589 reused by the same process.
+ {367, {367, 2000}}, // TID 367 reused as a PID. PID 2000 reused as a TID.
+ // PID 1000 reused as a new PID. TID 453 reused by a different PID.
+ {1000, {1000, 453}},
+ };
+
+ perProcessStat = {
+ {1, "1 (init) S 0 0 0 0 0 0 0 0 1800 0 0 0 0 0 0 0 2 0 0\n"},
+ {367, "367 (system_server) R 1 0 0 0 0 0 0 0 100 0 0 0 0 0 0 0 2 0 3450\n"},
+ {1000, "1000 (logd) R 1 0 0 0 0 0 0 0 2000 0 0 0 0 0 0 0 2 0 4650\n"},
+ };
+
+ perProcessStatus = {
+ {1, "Pid:\t1\nTgid:\t1\nUid:\t0\t0\t0\t0\n"},
+ {367, "Pid:\t367\nTgid:\t367\nUid:\t10001234\t10001234\t10001234\t10001234\n"},
+ {1000, "Pid:\t1000\nTgid:\t1000\nUid:\t10001234\t10001234\t10001234\t10001234\n"},
+ };
+
+ perThreadStat = {
+ {1, "1 (init) S 0 0 0 0 0 0 0 0 500 0 0 0 0 0 0 0 2 0 0\n"},
+ {589, "589 (init) S 0 0 0 0 0 0 0 0 300 0 0 0 0 0 0 0 2 0 2345\n"},
+ {367, "367 (system_server) R 1 0 0 0 0 0 0 0 50 0 0 0 0 0 0 0 2 0 3450\n"},
+ {2000, "2000 (system_server) R 1 0 0 0 0 0 0 0 50 0 0 0 0 0 0 0 2 0 3670\n"},
+ {1000, "1000 (logd) R 1 0 0 0 0 0 0 0 200 0 0 0 0 0 0 0 2 0 4650\n"},
+ {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}},
+ },
+ },
+ };
+
+ TemporaryDir secondSnapshot;
+ ret = populateProcPidDir(secondSnapshot.path, pidToTids, perProcessStat, perProcessStatus,
+ perThreadStat);
+ ASSERT_TRUE(ret) << "Failed to populate proc pid dir: " << ret.error();
+
+ procPidStat.mPath = secondSnapshot.path;
+ ASSERT_TRUE(procPidStat.enabled())
+ << "Files under the path `" << secondSnapshot.path << "` are inaccessible";
+
+ actual = procPidStat.collect();
+ ASSERT_TRUE(actual) << "Failed to collect proc pid stat: " << actual.error();
+
+ EXPECT_TRUE(isEqual(&expected, &actual.value()))
+ << "Second snapshot doesn't match.\nExpected:\n"
+ << toString(expected) << "\nActual:\n"
+ << toString(*actual);
+}
+
+TEST(ProcPidStatTest, TestErrorOnCorruptedProcessStatFile) {
+ std::unordered_map<uint32_t, std::vector<uint32_t>> pidToTids = {
+ {1, {1}},
+ };
+
+ std::unordered_map<uint32_t, std::string> perProcessStat = {
+ {1, "1 (init) S 0 0 0 0 0 0 0 0 200 0 0 0 CORRUPTED DATA\n"},
+ };
+
+ std::unordered_map<uint32_t, std::string> perProcessStatus = {
+ {1, "Pid:\t1\nTgid:\t1\nUid:\t0\t0\t0\t0\n"},
+ };
+
+ std::unordered_map<uint32_t, std::string> perThreadStat = {
+ {1, "1 (init) S 0 0 0 0 0 0 0 0 200 0 0 0 0 0 0 0 1 0 0\n"},
+ };
+
+ TemporaryDir procDir;
+ const auto& ret = populateProcPidDir(procDir.path, pidToTids, perProcessStat, perProcessStatus,
+ perThreadStat);
+ ASSERT_TRUE(ret) << "Failed to populate proc pid dir: " << ret.error();
+
+ ProcPidStat procPidStat(procDir.path);
+ ASSERT_TRUE(procPidStat.enabled())
+ << "Files under the path `" << procDir.path << "` are inaccessible";
+
+ const auto& actual = procPidStat.collect();
+ ASSERT_FALSE(actual) << "No error returned for invalid process stat file";
+}
+
+TEST(ProcPidStatTest, TestErrorOnCorruptedProcessStatusFile) {
+ std::unordered_map<uint32_t, std::vector<uint32_t>> pidToTids = {
+ {1, {1}},
+ };
+
+ std::unordered_map<uint32_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"},
+ };
+
+ std::unordered_map<uint32_t, std::string> perProcessStatus = {
+ {1, "Pid:\t1\nTgid:\t1\nCORRUPTED DATA\n"},
+ };
+
+ std::unordered_map<uint32_t, std::string> perThreadStat = {
+ {1, "1 (init) S 0 0 0 0 0 0 0 0 200 0 0 0 0 0 0 0 1 0 0\n"},
+ };
+
+ TemporaryDir procDir;
+ const auto& ret = populateProcPidDir(procDir.path, pidToTids, perProcessStat, perProcessStatus,
+ perThreadStat);
+ ASSERT_TRUE(ret) << "Failed to populate proc pid dir: " << ret.error();
+
+ ProcPidStat procPidStat(procDir.path);
+ ASSERT_TRUE(procPidStat.enabled())
+ << "Files under the path `" << procDir.path << "` are inaccessible";
+
+ const auto& actual = procPidStat.collect();
+ ASSERT_FALSE(actual) << "No error returned for invalid process status file";
+}
+
+TEST(ProcPidStatTest, TestErrorOnCorruptedThreadStatFile) {
+ std::unordered_map<uint32_t, std::vector<uint32_t>> pidToTids = {
+ {1, {1}},
+ };
+
+ std::unordered_map<uint32_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"},
+ };
+
+ std::unordered_map<uint32_t, std::string> perProcessStatus = {
+ {1, "Pid:\t1\nTgid:\t1\nUid:\t0\t0\t0\t0\n"},
+ };
+
+ std::unordered_map<uint32_t, std::string> perThreadStat = {
+ {1, "1 (init) S 0 0 0 0 0 0 0 0 200 0 0 0 CORRUPTED DATA\n"},
+ };
+
+ TemporaryDir procDir;
+ const auto& ret = populateProcPidDir(procDir.path, pidToTids, perProcessStat, perProcessStatus,
+ perThreadStat);
+ ASSERT_TRUE(ret) << "Failed to populate proc pid dir: " << ret.error();
+
+ ProcPidStat procPidStat(procDir.path);
+ ASSERT_TRUE(procPidStat.enabled())
+ << "Files under the path `" << procDir.path << "` are inaccessible";
+
+ const auto& actual = procPidStat.collect();
+ ASSERT_FALSE(actual) << "No error returned for invalid thread stat file";
+}
+
+TEST(ProcPidStatTest, TestHandlesSpaceInCommName) {
+ std::unordered_map<uint32_t, std::vector<uint32_t>> pidToTids = {
+ {1, {1}},
+ };
+
+ std::unordered_map<uint32_t, std::string> perProcessStat = {
+ {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::unordered_map<uint32_t, std::string> perProcessStatus = {
+ {1, "Pid:\t1\nTgid:\t1\nUid:\t0\t0\t0\t0\n"},
+ };
+
+ std::unordered_map<uint32_t, std::string> perThreadStat = {
+ {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}}},
+ },
+ };
+
+ TemporaryDir procDir;
+ const auto& ret = populateProcPidDir(procDir.path, pidToTids, perProcessStat, perProcessStatus,
+ perThreadStat);
+ ASSERT_TRUE(ret) << "Failed to populate proc pid dir: " << ret.error();
+
+ ProcPidStat procPidStat(procDir.path);
+ ASSERT_TRUE(procPidStat.enabled())
+ << "Files under the path `" << procDir.path << "` are inaccessible";
+
+ auto actual = procPidStat.collect();
+ ASSERT_TRUE(actual) << "Failed to collect proc pid stat: " << actual.error();
+
+ EXPECT_TRUE(isEqual(&expected, &actual.value()))
+ << "Proc pid contents doesn't match.\nExpected:\n"
+ << toString(expected) << "\nActual:\n"
+ << toString(*actual);
+}
+
+TEST(ProcPidStatTest, TestProcPidStatContentsFromDevice) {
+ // TODO(b/148486340): Enable the test after appropriate SELinux privileges are available to
+ // read the proc file.
+ /*ProcPidStat procPidStat;
+ ASSERT_TRUE(procPidStat.enabled()) << "/proc/[pid]/.* files are inaccessible";
+
+ const auto& processStats = procPidStat.collect();
+ ASSERT_TRUE(processStats) << "Failed to collect proc pid stat: " << processStats.error();
+
+ // The below check should pass because there should be at least one process.
+ EXPECT_GT(processStats->size(), 0);*/
+}
+
+} // namespace watchdog
+} // namespace automotive
+} // namespace android