NetworkStatsService: Fix getDetailedUidStats to take VPNs into account.
This API is similar to one provided by NetworkStatsFactory with the
difference that NSS also migrates traffic from VPN UID to other apps.
Since traffic can only be migrated over NetworkStats delta, NSS
therefore maintains NetworkStats snapshot across all UIDs/ifaces/tags.
This snapshot gets updated whenever NSS records a new snapshot
(based on various hooks such as VPN updating its underlying networks,
network getting lost, etc.), or getDetailedUidStats API is invoked by
one of its callers.
Bug: 113122541
Bug: 120145746
Test: atest FrameworksNetTests
Test: manually verified that battery stats are migrating traffic off of
TUN (after patching above CL where we point BatteryStats to use this
API).
Change-Id: Ib0f0c2d4d41ee1d7a027ea9da457baaf198d649e
diff --git a/core/java/android/net/NetworkStats.java b/core/java/android/net/NetworkStats.java
index 9e79606..27e0414 100644
--- a/core/java/android/net/NetworkStats.java
+++ b/core/java/android/net/NetworkStats.java
@@ -34,6 +34,7 @@
import java.io.CharArrayWriter;
import java.io.PrintWriter;
import java.util.Arrays;
+import java.util.function.Predicate;
import java.util.HashSet;
import java.util.Map;
import java.util.Objects;
@@ -994,23 +995,33 @@
if (limitUid == UID_ALL && limitTag == TAG_ALL && limitIfaces == INTERFACES_ALL) {
return;
}
+ filter(e -> (limitUid == UID_ALL || limitUid == e.uid)
+ && (limitTag == TAG_ALL || limitTag == e.tag)
+ && (limitIfaces == INTERFACES_ALL
+ || ArrayUtils.contains(limitIfaces, e.iface)));
+ }
+ /**
+ * Only keep entries with {@link #set} value less than {@link #SET_DEBUG_START}.
+ *
+ * <p>This mutates the original structure in place.
+ */
+ public void filterDebugEntries() {
+ filter(e -> e.set < SET_DEBUG_START);
+ }
+
+ private void filter(Predicate<Entry> predicate) {
Entry entry = new Entry();
int nextOutputEntry = 0;
for (int i = 0; i < size; i++) {
entry = getValues(i, entry);
- final boolean matches =
- (limitUid == UID_ALL || limitUid == entry.uid)
- && (limitTag == TAG_ALL || limitTag == entry.tag)
- && (limitIfaces == INTERFACES_ALL
- || ArrayUtils.contains(limitIfaces, entry.iface));
-
- if (matches) {
- setValues(nextOutputEntry, entry);
+ if (predicate.test(entry)) {
+ if (nextOutputEntry != i) {
+ setValues(nextOutputEntry, entry);
+ }
nextOutputEntry++;
}
}
-
size = nextOutputEntry;
}
diff --git a/core/java/com/android/internal/net/NetworkStatsFactory.java b/core/java/com/android/internal/net/NetworkStatsFactory.java
index f848346..1f3b4c4 100644
--- a/core/java/com/android/internal/net/NetworkStatsFactory.java
+++ b/core/java/com/android/internal/net/NetworkStatsFactory.java
@@ -256,6 +256,11 @@
return stats;
}
+ /**
+ * @deprecated Use NetworkStatsService#getDetailedUidStats which also accounts for
+ * VPN traffic
+ */
+ @Deprecated
public NetworkStats readNetworkStatsDetail() throws IOException {
return readNetworkStatsDetail(UID_ALL, null, TAG_ALL, null);
}
diff --git a/services/core/java/com/android/server/net/NetworkStatsService.java b/services/core/java/com/android/server/net/NetworkStatsService.java
index 8fa435c..4c07678 100644
--- a/services/core/java/com/android/server/net/NetworkStatsService.java
+++ b/services/core/java/com/android/server/net/NetworkStatsService.java
@@ -292,6 +292,22 @@
/** Data layer operation counters for splicing into other structures. */
private NetworkStats mUidOperations = new NetworkStats(0L, 10);
+ /**
+ * Snapshot containing most recent network stats for all UIDs across all interfaces and tags
+ * since boot.
+ *
+ * <p>Maintains migrated VPN stats which are result of performing TUN migration on {@link
+ * #mLastUidDetailSnapshot}.
+ */
+ @GuardedBy("mStatsLock")
+ private NetworkStats mTunAdjustedStats;
+ /**
+ * Used by {@link #mTunAdjustedStats} to migrate VPN traffic over delta between this snapshot
+ * and latest snapshot.
+ */
+ @GuardedBy("mStatsLock")
+ private NetworkStats mLastUidDetailSnapshot;
+
/** Must be set in factory by calling #setHandler. */
private Handler mHandler;
private Handler.Callback mHandlerCallback;
@@ -805,15 +821,39 @@
@Override
public NetworkStats getDetailedUidStats(String[] requiredIfaces) {
try {
+ // Get the latest snapshot from NetworkStatsFactory.
+ // TODO: Querying for INTERFACES_ALL may incur performance penalty. Consider restricting
+ // this to limited set of ifaces.
+ NetworkStats uidDetailStats = getNetworkStatsUidDetail(INTERFACES_ALL);
+
+ // Migrate traffic from VPN UID over delta and update mTunAdjustedStats.
+ NetworkStats result;
+ synchronized (mStatsLock) {
+ migrateTunTraffic(uidDetailStats, mVpnInfos);
+ result = mTunAdjustedStats.clone();
+ }
+
+ // Apply filter based on ifacesToQuery.
final String[] ifacesToQuery =
NetworkStatsFactory.augmentWithStackedInterfaces(requiredIfaces);
- return getNetworkStatsUidDetail(ifacesToQuery);
+ result.filter(UID_ALL, ifacesToQuery, TAG_ALL);
+ return result;
} catch (RemoteException e) {
Log.wtf(TAG, "Error compiling UID stats", e);
return new NetworkStats(0L, 0);
}
}
+ @VisibleForTesting
+ NetworkStats getTunAdjustedStats() {
+ synchronized (mStatsLock) {
+ if (mTunAdjustedStats == null) {
+ return null;
+ }
+ return mTunAdjustedStats.clone();
+ }
+ }
+
@Override
public String[] getMobileIfaces() {
return mMobileIfaces;
@@ -1288,6 +1328,34 @@
// a race condition between the service handler thread and the observer's
mStatsObservers.updateStats(xtSnapshot, uidSnapshot, new ArrayMap<>(mActiveIfaces),
new ArrayMap<>(mActiveUidIfaces), vpnArray, currentTime);
+
+ migrateTunTraffic(uidSnapshot, vpnArray);
+ }
+
+ /**
+ * Updates {@link #mTunAdjustedStats} with the delta containing traffic migrated off of VPNs.
+ */
+ @GuardedBy("mStatsLock")
+ private void migrateTunTraffic(NetworkStats uidDetailStats, VpnInfo[] vpnInfoArray) {
+ if (mTunAdjustedStats == null) {
+ // Either device booted or system server restarted, hence traffic cannot be migrated
+ // correctly without knowing the past state of VPN's underlying networks.
+ mTunAdjustedStats = uidDetailStats;
+ mLastUidDetailSnapshot = uidDetailStats;
+ return;
+ }
+ // Migrate delta traffic from VPN to other apps.
+ NetworkStats delta = uidDetailStats.subtract(mLastUidDetailSnapshot);
+ for (VpnInfo info : vpnInfoArray) {
+ delta.migrateTun(info.ownerUid, info.vpnIface, info.underlyingIfaces);
+ }
+ // Filter out debug entries as that may lead to over counting.
+ delta.filterDebugEntries();
+ // Update #mTunAdjustedStats with migrated delta.
+ mTunAdjustedStats.combineAllValues(delta);
+ mTunAdjustedStats.setElapsedRealtime(uidDetailStats.getElapsedRealtime());
+ // Update last snapshot.
+ mLastUidDetailSnapshot = uidDetailStats;
}
/**