Sort and group network stats collected from eBPF.

Since eBPF use hash map to record stats, network stats collected
from eBPF will be out of order. And the performance of
findIndexHinted in NetworkStats would be impacted.

Furthermore, since the StatsKey contains iface index, the
network stats reported to framework would create items with the
same iface, uid, tag and set, which causes NetworkStats maps
wrong item to subtract.

Thus, the stats needs to be properly sorted and grouped before
reported.

Bug: 112226716
Test: 1. manually reconnect vpn
      2. atest libnetdbpf_test
      3. runtest frameworks-net
      4. cts-tradefed run cts -m CtsUsageStatsTestCases -t \
              android.app.usage.cts.NetworkUsageStatsTest

Change-Id: I7cb622bfcc643f6e1a0ab59c5aafa85319313493
diff --git a/libnetdbpf/BpfNetworkStats.cpp b/libnetdbpf/BpfNetworkStats.cpp
index 013e7ef..c8bfbeb 100644
--- a/libnetdbpf/BpfNetworkStats.cpp
+++ b/libnetdbpf/BpfNetworkStats.cpp
@@ -162,6 +162,17 @@
               strerror(res.code()));
         return -res.code();
     }
+
+    // Since eBPF use hash map to record stats, network stats collected from
+    // eBPF will be out of order. And the performance of findIndexHinted in
+    // NetworkStats will also be impacted.
+    //
+    // Furthermore, since the StatsKey contains iface index, the network stats
+    // reported to framework would create items with the same iface, uid, tag
+    // and set, which causes NetworkStats maps wrong item to subtract.
+    //
+    // Thus, the stats needs to be properly sorted and grouped before reported.
+    groupNetworkStats(lines);
     return 0;
 }
 
@@ -227,6 +238,8 @@
               strerror(res.code()));
         return -res.code();
     }
+
+    groupNetworkStats(lines);
     return 0;
 }
 
@@ -253,5 +266,66 @@
     return (uint64_t)uid << 32 | tag;
 }
 
+void groupNetworkStats(std::vector<stats_line>* lines) {
+    if (lines->size() <= 1) return;
+    std::sort(lines->begin(), lines->end());
+
+    // Similar to std::unique(), but aggregates the duplicates rather than discarding them.
+    size_t nextOutput = 0;
+    for (size_t i = 1; i < lines->size(); i++) {
+        if (lines->at(nextOutput) == lines->at(i)) {
+            lines->at(nextOutput) += lines->at(i);
+        } else {
+            nextOutput++;
+            if (nextOutput != i) {
+                lines->at(nextOutput) = lines->at(i);
+            }
+        }
+    }
+
+    if (lines->size() != nextOutput + 1) {
+        lines->resize(nextOutput + 1);
+    }
+}
+
+// True if lhs equals to rhs, only compare iface, uid, tag and set.
+bool operator==(const stats_line& lhs, const stats_line& rhs) {
+    return ((lhs.uid == rhs.uid) && (lhs.tag == rhs.tag) && (lhs.set == rhs.set) &&
+            !strncmp(lhs.iface, rhs.iface, sizeof(lhs.iface)));
+}
+
+// True if lhs is smaller then rhs, only compare iface, uid, tag and set.
+bool operator<(const stats_line& lhs, const stats_line& rhs) {
+    int ret = strncmp(lhs.iface, rhs.iface, sizeof(lhs.iface));
+    if (ret != 0) return ret < 0;
+    ret = lhs.uid - rhs.uid;
+    if (ret != 0) return ret < 0;
+    ret = lhs.tag - rhs.tag;
+    if (ret != 0) return ret < 0;
+    ret = lhs.set - rhs.set;
+    if (ret != 0) return ret < 0;
+    return false;
+}
+
+stats_line& stats_line::operator=(const stats_line& rhs) {
+    strlcpy(iface, rhs.iface, sizeof(iface));
+    uid = rhs.uid;
+    set = rhs.set;
+    tag = rhs.tag;
+    rxPackets = rhs.rxPackets;
+    txPackets = rhs.txPackets;
+    rxBytes = rhs.rxBytes;
+    txBytes = rhs.txBytes;
+    return *this;
+}
+
+stats_line& stats_line::operator+=(const stats_line& rhs) {
+    rxPackets += rhs.rxPackets;
+    txPackets += rhs.txPackets;
+    rxBytes += rhs.rxBytes;
+    txBytes += rhs.txBytes;
+    return *this;
+}
+
 }  // namespace bpf
 }  // namespace android
diff --git a/libnetdbpf/BpfNetworkStatsTest.cpp b/libnetdbpf/BpfNetworkStatsTest.cpp
index 64dc7d4..432d9fc 100644
--- a/libnetdbpf/BpfNetworkStatsTest.cpp
+++ b/libnetdbpf/BpfNetworkStatsTest.cpp
@@ -431,14 +431,108 @@
     ASSERT_EQ(0,
               parseBpfNetworkStatsDevInternal(&lines, mFakeIfaceStatsMap, mFakeIfaceIndexNameMap));
     ASSERT_EQ((unsigned long)4, lines.size());
-    std::sort(lines.begin(), lines.end(), [](const auto& line1, const auto& line2)-> bool {
-        return strcmp(line1.iface, line2.iface) < 0;
-    });
+
     expectStatsLineEqual(value1, IFACE_NAME1, UID_ALL, SET_ALL, TAG_NONE, lines[0]);
     expectStatsLineEqual(value1, IFACE_NAME3, UID_ALL, SET_ALL, TAG_NONE, lines[1]);
     expectStatsLineEqual(value2, IFACE_NAME2, UID_ALL, SET_ALL, TAG_NONE, lines[2]);
     ASSERT_EQ(0, strcmp(TRUNCATED_IFACE_NAME, lines[3].iface));
     expectStatsLineEqual(value2, TRUNCATED_IFACE_NAME, UID_ALL, SET_ALL, TAG_NONE, lines[3]);
 }
+
+TEST_F(BpfNetworkStatsHelperTest, TestGetStatsSortedAndGrouped) {
+    SKIP_IF_BPF_NOT_SUPPORTED;
+
+    // Create iface indexes with duplicate iface name.
+    updateIfaceMap(IFACE_NAME1, IFACE_INDEX1);
+    updateIfaceMap(IFACE_NAME2, IFACE_INDEX2);
+    updateIfaceMap(IFACE_NAME1, IFACE_INDEX3);  // Duplicate!
+
+    StatsValue value1 = {
+            .rxBytes = TEST_BYTES0,
+            .rxPackets = TEST_PACKET0,
+            .txBytes = TEST_BYTES1,
+            .txPackets = TEST_PACKET1,
+    };
+    StatsValue value2 = {
+            .rxBytes = TEST_BYTES1,
+            .rxPackets = TEST_PACKET1,
+            .txBytes = TEST_BYTES0,
+            .txPackets = TEST_PACKET0,
+    };
+    StatsValue value3 = {
+            .rxBytes = TEST_BYTES0 * 2,
+            .rxPackets = TEST_PACKET0 * 2,
+            .txBytes = TEST_BYTES1 * 2,
+            .txPackets = TEST_PACKET1 * 2,
+    };
+
+    std::vector<stats_line> lines;
+    std::vector<std::string> ifaces;
+
+    // Test empty stats.
+    ASSERT_EQ(0, parseBpfNetworkStatsDetailInternal(&lines, ifaces, TAG_ALL, UID_ALL,
+                                                    mFakeTagStatsMap, mFakeIfaceIndexNameMap));
+    ASSERT_EQ((size_t) 0, lines.size());
+    lines.clear();
+
+    // Test 1 line stats.
+    populateFakeStats(TEST_UID1, TEST_TAG, IFACE_INDEX1, TEST_COUNTERSET0, value1,
+                      mFakeTagStatsMap);
+    ASSERT_EQ(0, parseBpfNetworkStatsDetailInternal(&lines, ifaces, TAG_ALL, UID_ALL,
+                                                    mFakeTagStatsMap, mFakeIfaceIndexNameMap));
+    ASSERT_EQ((size_t) 1, lines.size());
+    expectStatsLineEqual(value1, IFACE_NAME1, TEST_UID1, TEST_COUNTERSET0, TEST_TAG, lines[0]);
+    lines.clear();
+
+    // These items should not be grouped.
+    populateFakeStats(TEST_UID1, TEST_TAG, IFACE_INDEX2, TEST_COUNTERSET0, value2,
+                      mFakeTagStatsMap);
+    populateFakeStats(TEST_UID1, TEST_TAG, IFACE_INDEX3, TEST_COUNTERSET1, value2,
+                      mFakeTagStatsMap);
+    populateFakeStats(TEST_UID1, TEST_TAG + 1, IFACE_INDEX1, TEST_COUNTERSET0, value2,
+                      mFakeTagStatsMap);
+    populateFakeStats(TEST_UID2, TEST_TAG, IFACE_INDEX1, TEST_COUNTERSET0, value1,
+                      mFakeTagStatsMap);
+    ASSERT_EQ(0, parseBpfNetworkStatsDetailInternal(&lines, ifaces, TAG_ALL, UID_ALL,
+                                                    mFakeTagStatsMap, mFakeIfaceIndexNameMap));
+    ASSERT_EQ((size_t) 5, lines.size());
+    lines.clear();
+
+    // These items should be grouped.
+    populateFakeStats(TEST_UID1, TEST_TAG, IFACE_INDEX3, TEST_COUNTERSET0, value1,
+                      mFakeTagStatsMap);
+    populateFakeStats(TEST_UID2, TEST_TAG, IFACE_INDEX3, TEST_COUNTERSET0, value1,
+                      mFakeTagStatsMap);
+
+    ASSERT_EQ(0, parseBpfNetworkStatsDetailInternal(&lines, ifaces, TAG_ALL, UID_ALL,
+                                                    mFakeTagStatsMap, mFakeIfaceIndexNameMap));
+    ASSERT_EQ((size_t) 5, lines.size());
+
+    // Verify Sorted & Grouped.
+    expectStatsLineEqual(value3, IFACE_NAME1, TEST_UID1, TEST_COUNTERSET0, TEST_TAG, lines[0]);
+    expectStatsLineEqual(value2, IFACE_NAME1, TEST_UID1, TEST_COUNTERSET1, TEST_TAG, lines[1]);
+    expectStatsLineEqual(value2, IFACE_NAME1, TEST_UID1, TEST_COUNTERSET0, TEST_TAG + 1, lines[2]);
+    expectStatsLineEqual(value3, IFACE_NAME1, TEST_UID2, TEST_COUNTERSET0, TEST_TAG, lines[3]);
+    expectStatsLineEqual(value2, IFACE_NAME2, TEST_UID1, TEST_COUNTERSET0, TEST_TAG, lines[4]);
+    lines.clear();
+
+    // Perform test on IfaceStats.
+    uint32_t ifaceStatsKey = IFACE_INDEX2;
+    EXPECT_TRUE(isOk(mFakeIfaceStatsMap.writeValue(ifaceStatsKey, value2, BPF_ANY)));
+    ifaceStatsKey = IFACE_INDEX1;
+    EXPECT_TRUE(isOk(mFakeIfaceStatsMap.writeValue(ifaceStatsKey, value1, BPF_ANY)));
+
+    // This should be grouped.
+    ifaceStatsKey = IFACE_INDEX3;
+    EXPECT_TRUE(isOk(mFakeIfaceStatsMap.writeValue(ifaceStatsKey, value1, BPF_ANY)));
+
+    ASSERT_EQ(0,
+              parseBpfNetworkStatsDevInternal(&lines, mFakeIfaceStatsMap, mFakeIfaceIndexNameMap));
+    ASSERT_EQ((size_t) 2, lines.size());
+
+    expectStatsLineEqual(value3, IFACE_NAME1, UID_ALL, SET_ALL, TAG_NONE, lines[0]);
+    expectStatsLineEqual(value2, IFACE_NAME2, UID_ALL, SET_ALL, TAG_NONE, lines[1]);
+    lines.clear();
+}
 }  // namespace bpf
 }  // namespace android
diff --git a/libnetdbpf/include/netdbpf/BpfNetworkStats.h b/libnetdbpf/include/netdbpf/BpfNetworkStats.h
index 80dff88..2ceae98 100644
--- a/libnetdbpf/include/netdbpf/BpfNetworkStats.h
+++ b/libnetdbpf/include/netdbpf/BpfNetworkStats.h
@@ -14,6 +14,9 @@
  * limitations under the License.
  */
 
+#ifndef _BPF_NETWORKSTATS_H
+#define _BPF_NETWORKSTATS_H
+
 #include <bpf/BpfMap.h>
 
 namespace android {
@@ -43,7 +46,14 @@
     int64_t rxPackets;
     int64_t txBytes;
     int64_t txPackets;
+
+    stats_line& operator=(const stats_line& rhs);
+    stats_line& operator+=(const stats_line& rhs);
 };
+
+bool operator==(const stats_line& lhs, const stats_line& rhs);
+bool operator<(const stats_line& lhs, const stats_line& rhs);
+
 // For test only
 int bpfGetUidStatsInternal(uid_t uid, struct Stats* stats,
                            const BpfMap<uint32_t, StatsValue>& appUidStatsMap);
@@ -107,6 +117,9 @@
                                int limitUid);
 
 int parseBpfNetworkStatsDev(std::vector<stats_line>* lines);
+void groupNetworkStats(std::vector<stats_line>* lines);
 int cleanStatsMap();
 }  // namespace bpf
 }  // namespace android
+
+#endif  // _BPF_NETWORKSTATS_H