diff --git a/service-t/src/com/android/server/net/NetworkStatsFactory.java b/service-t/src/com/android/server/net/NetworkStatsFactory.java index 5ff708d6b6..c5a69c0551 100644 --- a/service-t/src/com/android/server/net/NetworkStatsFactory.java +++ b/service-t/src/com/android/server/net/NetworkStatsFactory.java @@ -19,6 +19,7 @@ package com.android.server.net; import static android.net.NetworkStats.INTERFACES_ALL; import static android.net.NetworkStats.TAG_ALL; import static android.net.NetworkStats.UID_ALL; +import static android.provider.DeviceConfig.NAMESPACE_TETHERING; import android.annotation.NonNull; import android.content.Context; @@ -26,15 +27,26 @@ import android.net.NetworkStats; import android.net.UnderlyingNetworkInfo; import android.os.ServiceSpecificException; import android.os.SystemClock; +import android.util.ArraySet; +import android.util.IndentingPrintWriter; +import android.util.Log; +import android.util.Pair; +import android.util.SparseArray; +import android.util.SparseBooleanArray; import com.android.internal.annotations.GuardedBy; import com.android.internal.annotations.VisibleForTesting; +import com.android.net.module.util.DeviceConfigUtils; import com.android.server.BpfNetMaps; import com.android.server.connectivity.InterfaceTracker; import java.io.IOException; import java.net.ProtocolException; +import java.util.ArrayList; +import java.util.Collections; +import java.util.List; import java.util.Map; +import java.util.Set; import java.util.concurrent.ConcurrentHashMap; /** @@ -65,6 +77,18 @@ public class NetworkStatsFactory { /** Set containing info about active VPNs and their underlying networks. */ private volatile UnderlyingNetworkInfo[] mUnderlyingNetworkInfos = new UnderlyingNetworkInfo[0]; + static final String CONFIG_PER_UID_TAG_THROTTLING = "per_uid_tag_throttling"; + static final String CONFIG_PER_UID_TAG_THROTTLING_THRESHOLD = + "per_uid_tag_throttling_threshold"; + private static final int DEFAULT_TAGS_PER_UID_THRESHOLD = 1000; + private static final int DUMP_TAGS_PER_UID_COUNT = 20; + private final boolean mSupportPerUidTagThrottling; + private final int mPerUidTagThrottlingThreshold; + + // Map for set of distinct tags per uid. Used for tag count limiting. + @GuardedBy("mPersistentDataLock") + private final SparseArray mUidTagSets = new SparseArray<>(); + // A persistent snapshot of cumulative stats since device start @GuardedBy("mPersistentDataLock") private NetworkStats mPersistSnapshot; @@ -110,6 +134,26 @@ public class NetworkStatsFactory { public BpfNetMaps createBpfNetMaps(@NonNull Context ctx) { return new BpfNetMaps(ctx, new InterfaceTracker(ctx)); } + + /** + * Check whether one specific feature is not disabled. + * @param name Flag name of the experiment in the tethering namespace. + * @see DeviceConfigUtils#isTetheringFeatureNotChickenedOut(Context, String) + */ + public boolean isFeatureNotChickenedOut(@NonNull Context context, @NonNull String name) { + return DeviceConfigUtils.isTetheringFeatureNotChickenedOut(context, name); + } + + /** + * Wrapper method for DeviceConfigUtils#getDeviceConfigPropertyInt for test injections. + * + * See {@link DeviceConfigUtils#getDeviceConfigPropertyInt(String, String, int)} + * for more detailed information. + */ + public int getDeviceConfigPropertyInt(@NonNull String name, int defaultValue) { + return DeviceConfigUtils.getDeviceConfigPropertyInt( + NAMESPACE_TETHERING, name, defaultValue); + } } /** @@ -162,6 +206,10 @@ public class NetworkStatsFactory { } mContext = ctx; mDeps = deps; + mSupportPerUidTagThrottling = mDeps.isFeatureNotChickenedOut( + ctx, CONFIG_PER_UID_TAG_THROTTLING); + mPerUidTagThrottlingThreshold = mDeps.getDeviceConfigPropertyInt( + CONFIG_PER_UID_TAG_THROTTLING_THRESHOLD, DEFAULT_TAGS_PER_UID_THRESHOLD); } /** @@ -210,10 +258,13 @@ public class NetworkStatsFactory { requestSwapActiveStatsMapLocked(); // Stats are always read from the inactive map, so they must be read after the // swap - final NetworkStats stats = mDeps.getNetworkStatsDetail(); + final NetworkStats diff = mDeps.getNetworkStatsDetail(); + // Filter based on UID tag set before merging. + final NetworkStats filteredDiff = mSupportPerUidTagThrottling + ? filterStatsByUidTagSets(diff) : diff; // BPF stats are incremental; fold into mPersistSnapshot. - mPersistSnapshot.setElapsedRealtime(stats.getElapsedRealtime()); - mPersistSnapshot.combineAllValues(stats); + mPersistSnapshot.setElapsedRealtime(diff.getElapsedRealtime()); + mPersistSnapshot.combineAllValues(filteredDiff); NetworkStats adjustedStats = adjustForTunAnd464Xlat(mPersistSnapshot, prev, vpnArray); @@ -223,6 +274,41 @@ public class NetworkStatsFactory { } } + @GuardedBy("mPersistentDataLock") + private NetworkStats filterStatsByUidTagSets(NetworkStats stats) { + final NetworkStats filteredStats = + new NetworkStats(stats.getElapsedRealtime(), stats.size()); + + final NetworkStats.Entry entry = new NetworkStats.Entry(); + final Set tooManyTagsUidSet = new ArraySet<>(); + for (int i = 0; i < stats.size(); i++) { + stats.getValues(i, entry); + final int uid = entry.uid; + final int tag = entry.tag; + + if (tag == NetworkStats.TAG_NONE) { + filteredStats.combineValues(entry); + continue; + } + + SparseBooleanArray tagSet = mUidTagSets.get(uid); + if (tagSet == null) { + tagSet = new SparseBooleanArray(); + } + if (tagSet.size() < mPerUidTagThrottlingThreshold || tagSet.get(tag)) { + filteredStats.combineValues(entry); + tagSet.put(tag, true); + mUidTagSets.put(uid, tagSet); + } else { + tooManyTagsUidSet.add(uid); + } + } + if (tooManyTagsUidSet.size() > 0) { + Log.wtf(TAG, "Too many tags detected for uids: " + tooManyTagsUidSet); + } + return filteredStats; + } + @GuardedBy("mPersistentDataLock") private NetworkStats adjustForTunAnd464Xlat(NetworkStats uidDetailStats, NetworkStats previousStats, UnderlyingNetworkInfo[] vpnArray) { @@ -307,4 +393,34 @@ public class NetworkStatsFactory { pe.initCause(cause); return pe; } + + /** + * Dump the contents of NetworkStatsFactory. + */ + public void dump(IndentingPrintWriter pw) { + dumpUidTagSets(pw); + } + + private void dumpUidTagSets(IndentingPrintWriter pw) { + pw.println("Top distinct tag counts in UidTagSets:"); + pw.increaseIndent(); + final List> countForUidList = new ArrayList<>(); + synchronized (mPersistentDataLock) { + for (int i = 0; i < mUidTagSets.size(); i++) { + final Pair countForUid = + new Pair<>(mUidTagSets.keyAt(i), mUidTagSets.valueAt(i).size()); + countForUidList.add(countForUid); + } + } + Collections.sort(countForUidList, + (entry1, entry2) -> Integer.compare(entry2.second, entry1.second)); + final int dumpSize = Math.min(countForUidList.size(), DUMP_TAGS_PER_UID_COUNT); + for (int j = 0; j < dumpSize; j++) { + final Pair entry = countForUidList.get(j); + pw.print(entry.first); + pw.print("="); + pw.println(entry.second); + } + pw.decreaseIndent(); + } } diff --git a/service-t/src/com/android/server/net/NetworkStatsService.java b/service-t/src/com/android/server/net/NetworkStatsService.java index 5c5f4ca086..75d30a91e6 100644 --- a/service-t/src/com/android/server/net/NetworkStatsService.java +++ b/service-t/src/com/android/server/net/NetworkStatsService.java @@ -3228,6 +3228,12 @@ public class NetworkStatsService extends INetworkStatsService.Stub { pw.increaseIndent(); mSkDestroyListener.dump(pw); pw.decreaseIndent(); + + pw.println(); + pw.println("NetworkStatsFactory logs:"); + pw.increaseIndent(); + mStatsFactory.dump(pw); + pw.decreaseIndent(); } } diff --git a/tests/unit/java/com/android/server/net/NetworkStatsFactoryTest.java b/tests/unit/java/com/android/server/net/NetworkStatsFactoryTest.java index 63daebc281..89acf69c4f 100644 --- a/tests/unit/java/com/android/server/net/NetworkStatsFactoryTest.java +++ b/tests/unit/java/com/android/server/net/NetworkStatsFactoryTest.java @@ -20,6 +20,7 @@ import static android.net.NetworkStats.DEFAULT_NETWORK_ALL; import static android.net.NetworkStats.DEFAULT_NETWORK_NO; import static android.net.NetworkStats.METERED_ALL; import static android.net.NetworkStats.METERED_NO; +import static android.net.NetworkStats.METERED_YES; import static android.net.NetworkStats.ROAMING_ALL; import static android.net.NetworkStats.ROAMING_NO; import static android.net.NetworkStats.SET_ALL; @@ -29,6 +30,8 @@ import static android.net.NetworkStats.TAG_ALL; import static android.net.NetworkStats.TAG_NONE; import static android.net.NetworkStats.UID_ALL; +import static com.android.server.net.NetworkStatsFactory.CONFIG_PER_UID_TAG_THROTTLING; +import static com.android.server.net.NetworkStatsFactory.CONFIG_PER_UID_TAG_THROTTLING_THRESHOLD; import static com.android.server.net.NetworkStatsFactory.kernelToTag; import static com.android.testutils.DevSdkIgnoreRuleKt.SC_V2; @@ -36,6 +39,9 @@ import static org.junit.Assert.assertEquals; import static org.junit.Assert.fail; import static org.mockito.ArgumentMatchers.any; import static org.mockito.ArgumentMatchers.anyInt; +import static org.mockito.ArgumentMatchers.anyString; +import static org.mockito.ArgumentMatchers.eq; +import static org.mockito.Mockito.doAnswer; import static org.mockito.Mockito.doReturn; import android.content.Context; @@ -52,12 +58,15 @@ import com.android.internal.util.ProcFileReader; import com.android.server.BpfNetMaps; import com.android.testutils.DevSdkIgnoreRule; import com.android.testutils.DevSdkIgnoreRunner; +import com.android.testutils.com.android.testutils.SetFeatureFlagsRule; +import com.android.testutils.com.android.testutils.SetFeatureFlagsRule.FeatureFlag; import libcore.io.IoUtils; import libcore.testing.io.TestIoUtils; import org.junit.After; import org.junit.Before; +import org.junit.Rule; import org.junit.Test; import org.junit.runner.RunWith; import org.mockito.Mock; @@ -66,6 +75,7 @@ import org.mockito.MockitoAnnotations; import java.io.File; import java.io.IOException; import java.net.ProtocolException; +import java.util.HashMap; /** Tests for {@link NetworkStatsFactory}. */ @RunWith(DevSdkIgnoreRunner.class) @@ -73,6 +83,7 @@ import java.net.ProtocolException; @DevSdkIgnoreRule.IgnoreUpTo(SC_V2) public class NetworkStatsFactoryTest extends NetworkStatsBaseTest { private static final String CLAT_PREFIX = "v4-"; + private static final int TEST_TAGS_PER_UID_THRESHOLD = 10; private File mTestProc; private NetworkStatsFactory mFactory; @@ -80,6 +91,16 @@ public class NetworkStatsFactoryTest extends NetworkStatsBaseTest { @Mock private NetworkStatsFactory.Dependencies mDeps; @Mock private BpfNetMaps mBpfNetMaps; + final HashMap mFeatureFlags = new HashMap<>(); + // This will set feature flags from @FeatureFlag annotations + // into the map before setUp() runs. + @Rule + public final SetFeatureFlagsRule mSetFeatureFlagsRule = + new SetFeatureFlagsRule((name, enabled) -> { + mFeatureFlags.put(name, enabled); + return null; + }, (name) -> mFeatureFlags.getOrDefault(name, false)); + @Before public void setUp() throws Exception { MockitoAnnotations.initMocks(this); @@ -90,6 +111,10 @@ public class NetworkStatsFactoryTest extends NetworkStatsBaseTest { // related to networkStatsFactory is compiled to a minimal native library and loaded here. System.loadLibrary("networkstatsfactorytestjni"); doReturn(mBpfNetMaps).when(mDeps).createBpfNetMaps(any()); + doAnswer(invocation -> mFeatureFlags.getOrDefault((String) invocation.getArgument(1), true)) + .when(mDeps).isFeatureNotChickenedOut(any(), anyString()); + doReturn(TEST_TAGS_PER_UID_THRESHOLD).when(mDeps) + .getDeviceConfigPropertyInt(eq(CONFIG_PER_UID_TAG_THROTTLING_THRESHOLD), anyInt()); mFactory = new NetworkStatsFactory(mContext, mDeps); mFactory.updateUnderlyingNetworkInfos(new UnderlyingNetworkInfo[0]); @@ -498,6 +523,71 @@ public class NetworkStatsFactoryTest extends NetworkStatsBaseTest { assertValues(removedUidsStats, TEST_IFACE, UID_GREEN, 64L, 3L, 1024L, 8L); } + @FeatureFlag(name = CONFIG_PER_UID_TAG_THROTTLING) + @Test + public void testFilterTooManyTags_featureEnabled() throws Exception { + doTestFilterTooManyTags(true); + } + + @FeatureFlag(name = CONFIG_PER_UID_TAG_THROTTLING, enabled = false) + @Test + public void testFilterTooManyTags_featureDisabled() throws Exception { + doTestFilterTooManyTags(false); + } + + private void doTestFilterTooManyTags(boolean supportPerUidTagThrottling) throws Exception { + // Add entries for UID_RED which reaches the threshold. + final NetworkStats statsWithManyTags = new NetworkStats(0L, TEST_TAGS_PER_UID_THRESHOLD); + for (int tag = 1; tag <= TEST_TAGS_PER_UID_THRESHOLD; tag++) { + statsWithManyTags.combineValues( + new NetworkStats.Entry(TEST_IFACE, UID_RED, SET_DEFAULT, tag, + METERED_NO, ROAMING_NO, DEFAULT_NETWORK_NO, 12L, 18L, 14L, 1L, 0L)); + } + doReturn(statsWithManyTags).when(mDeps).getNetworkStatsDetail(); + final NetworkStats stats1 = mFactory.readNetworkStatsDetail(); + assertEquals(stats1.size(), TEST_TAGS_PER_UID_THRESHOLD); + + // Add 2 new entries with pre-existing tag, verify they can be added no matter what. + final NetworkStats newDiffWithExistingTag = new NetworkStats(0L, 2); + // This one should be added as a new entry, as the metered data doesn't exist yet. + newDiffWithExistingTag.combineValues( + new NetworkStats.Entry(TEST_IFACE, UID_RED, SET_DEFAULT, + TEST_TAGS_PER_UID_THRESHOLD, + METERED_YES, ROAMING_NO, DEFAULT_NETWORK_NO, 3L, 5L, 8L, 1L, 1L)); + // This one should be combined into existing entry. + newDiffWithExistingTag.combineValues( + new NetworkStats.Entry(TEST_IFACE, UID_RED, SET_DEFAULT, + TEST_TAGS_PER_UID_THRESHOLD, + METERED_NO, ROAMING_NO, DEFAULT_NETWORK_NO, 1L, 2L, 3L, 4L, 5L)); + + doReturn(newDiffWithExistingTag).when(mDeps).getNetworkStatsDetail(); + final NetworkStats stats2 = mFactory.readNetworkStatsDetail(); + assertEquals(stats2.size(), TEST_TAGS_PER_UID_THRESHOLD + 1); + assertValues(stats2, TEST_IFACE, UID_RED, SET_DEFAULT, TEST_TAGS_PER_UID_THRESHOLD, + METERED_YES, ROAMING_NO, DEFAULT_NETWORK_NO, 3L, 5L, 8L, 1L, 1L); + assertValues(stats2, TEST_IFACE, UID_RED, SET_DEFAULT, TEST_TAGS_PER_UID_THRESHOLD, + METERED_NO, ROAMING_NO, DEFAULT_NETWORK_NO, 13L, 20L, 17L, 5L, 5L); + + // Add an entry which exceeds the threshold, verify the entry is filtered out. + final NetworkStats newDiffWithNonExistingTag = new NetworkStats(0L, 1); + newDiffWithNonExistingTag.combineValues( + new NetworkStats.Entry(TEST_IFACE, UID_RED, SET_DEFAULT, + TEST_TAGS_PER_UID_THRESHOLD + 1, + METERED_NO, ROAMING_NO, DEFAULT_NETWORK_NO, 12L, 18L, 14L, 1L, 0L)); + doReturn(newDiffWithNonExistingTag).when(mDeps).getNetworkStatsDetail(); + final NetworkStats stats3 = mFactory.readNetworkStatsDetail(); + if (supportPerUidTagThrottling) { + assertEquals(stats3.size(), TEST_TAGS_PER_UID_THRESHOLD + 1); + assertNoStatsEntry(stats3, TEST_IFACE, UID_RED, SET_DEFAULT, + TEST_TAGS_PER_UID_THRESHOLD + 1); + } else { + assertEquals(stats3.size(), TEST_TAGS_PER_UID_THRESHOLD + 2); + assertValues(stats3, TEST_IFACE, UID_RED, SET_DEFAULT, + TEST_TAGS_PER_UID_THRESHOLD + 1, + METERED_NO, ROAMING_NO, DEFAULT_NETWORK_NO, 12L, 18L, 14L, 1L, 0L); + } + } + private NetworkStats buildEmptyStats() { return new NetworkStats(SystemClock.elapsedRealtime(), 0); }