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