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
diff --git a/cpp/watchdog/server/src/UidProcStatsCollector.cpp b/cpp/watchdog/server/src/UidProcStatsCollector.cpp
index 8c1ecaf..da3bffc 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::ReadFileToString;
 using ::android::base::Result;
 using ::android::base::Split;
+using ::android::base::StringAppendF;
 using ::android::base::Trim;
 
 namespace {
@@ -50,25 +53,36 @@
     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 @@
     }
 
     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 @@
     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();
             }
-            ALOGW("Failed to read top-level per-process stat file %s: %s", path.c_str(),
-                  ret.error().message().c_str());
+            /* |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());
+            }
             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] = {};
         }
-
-        // 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;
-            }
-        }
-
-        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) {
-            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;
-            }
-            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;
+        UidProcStats* uidProcStats = &uidProcStatsByUid[uid];
+        uidProcStats->totalMajorFaults += processStats.totalMajorFaults;
+        uidProcStats->totalTasksCount += processStats.totalTasksCount;
+        uidProcStats->ioBlockedTasksCount += processStats.ioBlockedTasksCount;
+        uidProcStats->processStatsByPid[pid] = std::move(processStats);
     }
-    return processStats;
+    return uidProcStatsByUid;
+}
+
+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 (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. 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;
+        }
+
+        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();
+            }
+            /* Maybe the thread terminated before reading the file so skip this thread and
+             * continue with scanning the next thread's stat.
+             */
+            continue;
+        }
+        processStats.ioBlockedTasksCount += tidStat.state == "D" ? 1 : 0;
+        processStats.totalTasksCount += 1;
+    }
+    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 04e4ca3..d0ec3c0 100644
--- a/cpp/watchdog/server/src/UidProcStatsCollector.h
+++ b/cpp/watchdog/server/src/UidProcStatsCollector.h
@@ -38,38 +38,52 @@
 
 #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;
-    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
-};
-
+// Per-process stats.
 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
+    std::string comm = "";
+    uint64_t startTime = 0;  // Useful when identifying PID reuse
+    uint64_t totalMajorFaults = 0;
+    int totalTasksCount = 0;
+    int ioBlockedTasksCount = 0;
+    std::string toString() const;
 };
 
-// Collector/parser for `/proc/[pid]/stat`, `/proc/[pid]/task/[tid]/stat` and /proc/[pid]/status`
-// files.
-class UidProcStatsCollector : public RefBase {
+// 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 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 @@
                 !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 4cd6539..a16fa37 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 68c9d21..c9eb6d4 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 @@
 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 @@
                         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 @@
 
     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"},
+            {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::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}}}},
-    };
+    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 @@
     };
 
     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 @@
             // 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 @@
             {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 @@
             {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 @@
                                         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 @@
                                         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, 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 @@
     };
 
     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 @@
                                         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 @@
             {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 0000000..6e5a409
--- /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_