diff --git a/service/src/com/android/server/ConnectivityService.java b/service/src/com/android/server/ConnectivityService.java index 7339d083ba..3391323275 100755 --- a/service/src/com/android/server/ConnectivityService.java +++ b/service/src/com/android/server/ConnectivityService.java @@ -328,6 +328,7 @@ import com.android.server.connectivity.NetworkNotificationManager.NotificationTy import com.android.server.connectivity.NetworkOffer; import com.android.server.connectivity.NetworkPreferenceList; import com.android.server.connectivity.NetworkRanker; +import com.android.server.connectivity.NetworkRequestStateStatsMetrics; import com.android.server.connectivity.PermissionMonitor; import com.android.server.connectivity.ProfileNetworkPreferenceInfo; import com.android.server.connectivity.ProxyTracker; @@ -941,6 +942,8 @@ public class ConnectivityService extends IConnectivityManager.Stub private final IpConnectivityLog mMetricsLog; + @Nullable private final NetworkRequestStateStatsMetrics mNetworkRequestStateStatsMetrics; + @GuardedBy("mBandwidthRequests") private final SparseArray mBandwidthRequests = new SparseArray<>(10); @@ -1421,6 +1424,19 @@ public class ConnectivityService extends IConnectivityManager.Stub return new AutomaticOnOffKeepaliveTracker(c, h); } + /** + * @see NetworkRequestStateStatsMetrics + */ + public NetworkRequestStateStatsMetrics makeNetworkRequestStateStatsMetrics( + Context context) { + // We currently have network requests metric for Watch devices only + if (context.getPackageManager().hasSystemFeature(FEATURE_WATCH)) { + return new NetworkRequestStateStatsMetrics(); + } else { + return null; + } + } + /** * @see BatteryStatsManager */ @@ -1654,6 +1670,7 @@ public class ConnectivityService extends IConnectivityManager.Stub new RequestInfoPerUidCounter(MAX_NETWORK_REQUESTS_PER_SYSTEM_UID - 1); mMetricsLog = logger; + mNetworkRequestStateStatsMetrics = mDeps.makeNetworkRequestStateStatsMetrics(mContext); final NetworkRequest defaultInternetRequest = createDefaultRequest(); mDefaultRequest = new NetworkRequestInfo( Process.myUid(), defaultInternetRequest, null, @@ -5324,6 +5341,8 @@ public class ConnectivityService extends IConnectivityManager.Stub updateSignalStrengthThresholds(network, "REGISTER", req); } } + } else if (req.isRequest() && mNetworkRequestStateStatsMetrics != null) { + mNetworkRequestStateStatsMetrics.onNetworkRequestReceived(req); } } @@ -5541,6 +5560,8 @@ public class ConnectivityService extends IConnectivityManager.Stub } if (req.isListen()) { removeListenRequestFromNetworks(req); + } else if (req.isRequest() && mNetworkRequestStateStatsMetrics != null) { + mNetworkRequestStateStatsMetrics.onNetworkRequestRemoved(req); } } nri.unlinkDeathRecipient(); diff --git a/service/src/com/android/server/connectivity/NetworkRequestStateInfo.java b/service/src/com/android/server/connectivity/NetworkRequestStateInfo.java new file mode 100644 index 0000000000..ab3d31502c --- /dev/null +++ b/service/src/com/android/server/connectivity/NetworkRequestStateInfo.java @@ -0,0 +1,108 @@ +/* + * Copyright (C) 2024 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. + */ + +package com.android.server.connectivity; + +import static com.android.server.ConnectivityStatsLog.NETWORK_REQUEST_STATE_CHANGED__STATE__NETWORK_REQUEST_STATE_RECEIVED; +import static com.android.server.ConnectivityStatsLog.NETWORK_REQUEST_STATE_CHANGED__STATE__NETWORK_REQUEST_STATE_REMOVED; +import static com.android.server.ConnectivityStatsLog.NETWORK_REQUEST_STATE_CHANGED__STATE__NETWORK_REQUEST_STATE_UNKNOWN; + +import android.net.NetworkCapabilities; +import android.net.NetworkRequest; +import android.os.SystemClock; + +import com.android.net.module.util.BitUtils; + + +class NetworkRequestStateInfo { + private final NetworkRequest mNetworkRequest; + private final long mNetworkRequestReceivedTime; + + private enum NetworkRequestState { + RECEIVED, + REMOVED + } + private NetworkRequestState mNetworkRequestState; + private int mNetworkRequestDurationMillis; + private final Dependencies mDependencies; + + NetworkRequestStateInfo(NetworkRequest networkRequest, + Dependencies deps) { + mDependencies = deps; + mNetworkRequest = networkRequest; + mNetworkRequestReceivedTime = mDependencies.getElapsedRealtime(); + mNetworkRequestDurationMillis = 0; + mNetworkRequestState = NetworkRequestState.RECEIVED; + } + + NetworkRequestStateInfo(NetworkRequestStateInfo anotherNetworkRequestStateInfo) { + mDependencies = anotherNetworkRequestStateInfo.mDependencies; + mNetworkRequest = new NetworkRequest(anotherNetworkRequestStateInfo.mNetworkRequest); + mNetworkRequestReceivedTime = anotherNetworkRequestStateInfo.mNetworkRequestReceivedTime; + mNetworkRequestDurationMillis = + anotherNetworkRequestStateInfo.mNetworkRequestDurationMillis; + mNetworkRequestState = anotherNetworkRequestStateInfo.mNetworkRequestState; + } + + public void setNetworkRequestRemoved() { + mNetworkRequestState = NetworkRequestState.REMOVED; + mNetworkRequestDurationMillis = (int) ( + mDependencies.getElapsedRealtime() - mNetworkRequestReceivedTime); + } + + public int getNetworkRequestStateStatsType() { + if (mNetworkRequestState == NetworkRequestState.RECEIVED) { + return NETWORK_REQUEST_STATE_CHANGED__STATE__NETWORK_REQUEST_STATE_RECEIVED; + } else if (mNetworkRequestState == NetworkRequestState.REMOVED) { + return NETWORK_REQUEST_STATE_CHANGED__STATE__NETWORK_REQUEST_STATE_REMOVED; + } else { + return NETWORK_REQUEST_STATE_CHANGED__STATE__NETWORK_REQUEST_STATE_UNKNOWN; + } + } + + public int getRequestId() { + return mNetworkRequest.requestId; + } + + public int getPackageUid() { + return mNetworkRequest.networkCapabilities.getRequestorUid(); + } + + public int getTransportTypes() { + return (int) BitUtils.packBits(mNetworkRequest.networkCapabilities.getTransportTypes()); + } + + public boolean getNetCapabilityNotMetered() { + return mNetworkRequest.hasCapability(NetworkCapabilities.NET_CAPABILITY_NOT_METERED); + } + + public boolean getNetCapabilityInternet() { + return mNetworkRequest.hasCapability(NetworkCapabilities.NET_CAPABILITY_INTERNET); + } + + public int getNetworkRequestDurationMillis() { + return mNetworkRequestDurationMillis; + } + + /** Dependency class */ + public static class Dependencies { + // Returns a timestamp with the time base of SystemClock.elapsedRealtime to keep durations + // relative to start time and avoid timezone change, including time spent in deep sleep. + public long getElapsedRealtime() { + return SystemClock.elapsedRealtime(); + } + } +} diff --git a/service/src/com/android/server/connectivity/NetworkRequestStateStatsMetrics.java b/service/src/com/android/server/connectivity/NetworkRequestStateStatsMetrics.java new file mode 100644 index 0000000000..1bc654ab63 --- /dev/null +++ b/service/src/com/android/server/connectivity/NetworkRequestStateStatsMetrics.java @@ -0,0 +1,228 @@ +/* + * Copyright (C) 2024 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. + */ + +package com.android.server.connectivity; + +import static com.android.server.ConnectivityStatsLog.NETWORK_REQUEST_STATE_CHANGED; + +import android.annotation.NonNull; +import android.net.NetworkRequest; +import android.os.Handler; +import android.os.HandlerThread; +import android.os.Looper; +import android.os.Message; +import android.os.SystemClock; +import android.util.Log; +import android.util.SparseArray; + +import com.android.internal.annotations.VisibleForTesting; +import com.android.server.ConnectivityStatsLog; + +import java.util.ArrayDeque; + +/** + * A Connectivity Service helper class to push atoms capturing network requests have been received + * and removed and its metadata. + * + * Atom events are logged in the ConnectivityStatsLog. Network request id: network request metadata + * hashmap is stored to calculate network request duration when it is removed. + * + * Note that this class is not thread-safe. The instance of the class needs to be + * synchronized in the callers when being used in multiple threads. + */ +public class NetworkRequestStateStatsMetrics { + + private static final String TAG = "NetworkRequestStateStatsMetrics"; + private static final int CMD_SEND_PENDING_NETWORK_REQUEST_STATE_METRIC = 0; + private static final int CMD_SEND_MAYBE_ENQUEUE_NETWORK_REQUEST_STATE_METRIC = 1; + + @VisibleForTesting + static final int MAX_QUEUED_REQUESTS = 20; + + // Stats logging frequency is limited to 10 ms at least, 500ms are taken as a safely margin + // for cases of longer periods of frequent network requests. + private static final int ATOM_INTERVAL_MS = 500; + private final StatsLoggingHandler mStatsLoggingHandler; + + private final Dependencies mDependencies; + + private final NetworkRequestStateInfo.Dependencies mNRStateInfoDeps; + private final SparseArray mNetworkRequestsActive; + + public NetworkRequestStateStatsMetrics() { + this(new Dependencies(), new NetworkRequestStateInfo.Dependencies()); + } + + @VisibleForTesting + NetworkRequestStateStatsMetrics(Dependencies deps, + NetworkRequestStateInfo.Dependencies nrStateInfoDeps) { + mNetworkRequestsActive = new SparseArray<>(); + mDependencies = deps; + mNRStateInfoDeps = nrStateInfoDeps; + HandlerThread handlerThread = mDependencies.makeHandlerThread(TAG); + handlerThread.start(); + mStatsLoggingHandler = new StatsLoggingHandler(handlerThread.getLooper()); + } + + /** + * Register network request receive event, push RECEIVE atom + * + * @param networkRequest network request received + */ + public void onNetworkRequestReceived(NetworkRequest networkRequest) { + if (mNetworkRequestsActive.contains(networkRequest.requestId)) { + Log.w(TAG, "Received already registered network request, id = " + + networkRequest.requestId); + } else { + Log.d(TAG, "Registered nr with ID = " + networkRequest.requestId + + ", package_uid = " + networkRequest.networkCapabilities.getRequestorUid()); + NetworkRequestStateInfo networkRequestStateInfo = new NetworkRequestStateInfo( + networkRequest, mNRStateInfoDeps); + mNetworkRequestsActive.put(networkRequest.requestId, networkRequestStateInfo); + mStatsLoggingHandler.sendMessage(Message.obtain( + mStatsLoggingHandler, + CMD_SEND_MAYBE_ENQUEUE_NETWORK_REQUEST_STATE_METRIC, + networkRequestStateInfo)); + } + } + + /** + * Register network request remove event, push REMOVE atom + * + * @param networkRequest network request removed + */ + public void onNetworkRequestRemoved(NetworkRequest networkRequest) { + NetworkRequestStateInfo networkRequestStateInfo = mNetworkRequestsActive.get( + networkRequest.requestId); + if (networkRequestStateInfo == null) { + Log.w(TAG, "This NR hasn't been registered. NR id = " + networkRequest.requestId); + } else { + Log.d(TAG, "Removed nr with ID = " + networkRequest.requestId); + mNetworkRequestsActive.remove(networkRequest.requestId); + networkRequestStateInfo = new NetworkRequestStateInfo(networkRequestStateInfo); + networkRequestStateInfo.setNetworkRequestRemoved(); + mStatsLoggingHandler.sendMessage(Message.obtain( + mStatsLoggingHandler, + CMD_SEND_MAYBE_ENQUEUE_NETWORK_REQUEST_STATE_METRIC, + networkRequestStateInfo)); + } + } + + /** Dependency class */ + public static class Dependencies { + /** + * Creates a thread with provided tag. + * + * @param tag for the thread. + */ + public HandlerThread makeHandlerThread(@NonNull final String tag) { + return new HandlerThread(tag); + } + + /** + * @see Handler#sendMessageDelayed(Message, long) + */ + public void sendMessageDelayed(@NonNull Handler handler, int what, long delayMillis) { + handler.sendMessageDelayed(Message.obtain(handler, what), delayMillis); + } + + /** + * Gets number of millis since event. + * + * @param eventTimeMillis long timestamp in millis when the event occurred. + */ + public long getMillisSinceEvent(long eventTimeMillis) { + return SystemClock.elapsedRealtime() - eventTimeMillis; + } + + /** + * Writes a NETWORK_REQUEST_STATE_CHANGED event to ConnectivityStatsLog. + * + * @param networkRequestStateInfo NetworkRequestStateInfo containing network request info. + */ + public void writeStats(NetworkRequestStateInfo networkRequestStateInfo) { + ConnectivityStatsLog.write( + NETWORK_REQUEST_STATE_CHANGED, + networkRequestStateInfo.getPackageUid(), + networkRequestStateInfo.getTransportTypes(), + networkRequestStateInfo.getNetCapabilityNotMetered(), + networkRequestStateInfo.getNetCapabilityInternet(), + networkRequestStateInfo.getNetworkRequestStateStatsType(), + networkRequestStateInfo.getNetworkRequestDurationMillis()); + } + } + + private class StatsLoggingHandler extends Handler { + private static final String TAG = "NetworkRequestsStateStatsLoggingHandler"; + + private final ArrayDeque mPendingState = new ArrayDeque<>(); + + private long mLastLogTime = 0; + + StatsLoggingHandler(Looper looper) { + super(looper); + } + + private void maybeEnqueueStatsMessage(NetworkRequestStateInfo networkRequestStateInfo) { + if (mPendingState.size() < MAX_QUEUED_REQUESTS) { + mPendingState.add(networkRequestStateInfo); + } else { + Log.w(TAG, "Too many network requests received within last " + ATOM_INTERVAL_MS + + " ms, dropping the last network request (id = " + + networkRequestStateInfo.getRequestId() + ") event"); + return; + } + if (hasMessages(CMD_SEND_PENDING_NETWORK_REQUEST_STATE_METRIC)) { + return; + } + long millisSinceLastLog = mDependencies.getMillisSinceEvent(mLastLogTime); + + if (millisSinceLastLog >= ATOM_INTERVAL_MS) { + sendMessage( + Message.obtain(this, CMD_SEND_PENDING_NETWORK_REQUEST_STATE_METRIC)); + } else { + mDependencies.sendMessageDelayed( + this, + CMD_SEND_PENDING_NETWORK_REQUEST_STATE_METRIC, + ATOM_INTERVAL_MS - millisSinceLastLog); + } + } + + @Override + public void handleMessage(Message msg) { + NetworkRequestStateInfo loggingInfo; + switch (msg.what) { + case CMD_SEND_MAYBE_ENQUEUE_NETWORK_REQUEST_STATE_METRIC: + maybeEnqueueStatsMessage((NetworkRequestStateInfo) msg.obj); + break; + case CMD_SEND_PENDING_NETWORK_REQUEST_STATE_METRIC: + mLastLogTime = SystemClock.elapsedRealtime(); + if (!mPendingState.isEmpty()) { + loggingInfo = mPendingState.remove(); + mDependencies.writeStats(loggingInfo); + if (!mPendingState.isEmpty()) { + mDependencies.sendMessageDelayed( + this, + CMD_SEND_PENDING_NETWORK_REQUEST_STATE_METRIC, + ATOM_INTERVAL_MS); + } + } + break; + default: // fall out + } + } + } +} diff --git a/tests/unit/java/com/android/server/connectivity/NetworkRequestStateInfoTest.java b/tests/unit/java/com/android/server/connectivity/NetworkRequestStateInfoTest.java new file mode 100644 index 0000000000..44a645a93a --- /dev/null +++ b/tests/unit/java/com/android/server/connectivity/NetworkRequestStateInfoTest.java @@ -0,0 +1,84 @@ +/* + * Copyright (C) 2023 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. + */ + +package com.android.server.connectivity; + +import static com.android.server.ConnectivityStatsLog.NETWORK_REQUEST_STATE_CHANGED__STATE__NETWORK_REQUEST_STATE_RECEIVED; +import static com.android.server.ConnectivityStatsLog.NETWORK_REQUEST_STATE_CHANGED__STATE__NETWORK_REQUEST_STATE_REMOVED; + +import static org.junit.Assert.assertEquals; + +import android.net.NetworkCapabilities; +import android.net.NetworkRequest; +import android.os.Build; + +import com.android.testutils.DevSdkIgnoreRule; +import com.android.testutils.DevSdkIgnoreRunner; + +import org.junit.Before; +import org.junit.Test; +import org.junit.runner.RunWith; +import org.mockito.Mock; +import org.mockito.Mockito; +import org.mockito.MockitoAnnotations; + +@RunWith(DevSdkIgnoreRunner.class) +@DevSdkIgnoreRule.IgnoreUpTo(Build.VERSION_CODES.R) +public class NetworkRequestStateInfoTest { + + @Mock + private NetworkRequestStateInfo.Dependencies mDependencies; + + @Before + public void setup() { + MockitoAnnotations.initMocks(this); + } + @Test + public void testSetNetworkRequestRemoved() { + final long nrStartTime = 1L; + final long nrEndTime = 101L; + + NetworkRequest notMeteredWifiNetworkRequest = new NetworkRequest( + new NetworkCapabilities() + .addTransportType(NetworkCapabilities.TRANSPORT_WIFI) + .setCapability(NetworkCapabilities.NET_CAPABILITY_NOT_METERED, true), + 0, 1, NetworkRequest.Type.REQUEST + ); + + // This call will be used to calculate NR received time + Mockito.when(mDependencies.getElapsedRealtime()).thenReturn(nrStartTime); + NetworkRequestStateInfo networkRequestStateInfo = new NetworkRequestStateInfo( + notMeteredWifiNetworkRequest, mDependencies); + + // This call will be used to calculate NR removed time + Mockito.when(mDependencies.getElapsedRealtime()).thenReturn(nrEndTime); + networkRequestStateInfo.setNetworkRequestRemoved(); + assertEquals( + nrEndTime - nrStartTime, + networkRequestStateInfo.getNetworkRequestDurationMillis()); + assertEquals(networkRequestStateInfo.getNetworkRequestStateStatsType(), + NETWORK_REQUEST_STATE_CHANGED__STATE__NETWORK_REQUEST_STATE_REMOVED); + } + + @Test + public void testCheckInitialState() { + NetworkRequestStateInfo networkRequestStateInfo = new NetworkRequestStateInfo( + new NetworkRequest(new NetworkCapabilities(), 0, 1, NetworkRequest.Type.REQUEST), + mDependencies); + assertEquals(networkRequestStateInfo.getNetworkRequestStateStatsType(), + NETWORK_REQUEST_STATE_CHANGED__STATE__NETWORK_REQUEST_STATE_RECEIVED); + } +} diff --git a/tests/unit/java/com/android/server/connectivity/NetworkRequestStateStatsMetricsTest.java b/tests/unit/java/com/android/server/connectivity/NetworkRequestStateStatsMetricsTest.java new file mode 100644 index 0000000000..e8b62f8f66 --- /dev/null +++ b/tests/unit/java/com/android/server/connectivity/NetworkRequestStateStatsMetricsTest.java @@ -0,0 +1,201 @@ +/* + * Copyright (C) 2024 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. + */ + +package com.android.server.connectivity; + +import static com.android.server.ConnectivityStatsLog.NETWORK_REQUEST_STATE_CHANGED__STATE__NETWORK_REQUEST_STATE_RECEIVED; +import static com.android.server.ConnectivityStatsLog.NETWORK_REQUEST_STATE_CHANGED__STATE__NETWORK_REQUEST_STATE_REMOVED; + +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertFalse; +import static org.junit.Assert.assertTrue; +import static org.mockito.Mockito.any; +import static org.mockito.Mockito.anyLong; +import static org.mockito.Mockito.clearInvocations; +import static org.mockito.Mockito.never; +import static org.mockito.Mockito.timeout; +import static org.mockito.Mockito.times; +import static org.mockito.Mockito.verify; + +import android.net.NetworkCapabilities; +import android.net.NetworkRequest; +import android.os.ConditionVariable; +import android.os.Handler; +import android.os.HandlerThread; +import android.os.Message; + +import androidx.test.ext.junit.runners.AndroidJUnit4; + +import com.android.testutils.HandlerUtils; + +import org.junit.Before; +import org.junit.Test; +import org.junit.runner.RunWith; +import org.mockito.ArgumentCaptor; +import org.mockito.Captor; +import org.mockito.Mock; +import org.mockito.Mockito; +import org.mockito.MockitoAnnotations; + +@RunWith(AndroidJUnit4.class) +public class NetworkRequestStateStatsMetricsTest { + @Mock + private NetworkRequestStateStatsMetrics.Dependencies mNRStateStatsDeps; + @Mock + private NetworkRequestStateInfo.Dependencies mNRStateInfoDeps; + @Captor + private ArgumentCaptor mHandlerCaptor; + @Captor + private ArgumentCaptor mMessageWhatCaptor; + + private NetworkRequestStateStatsMetrics mNetworkRequestStateStatsMetrics; + private HandlerThread mHandlerThread; + private static final int TEST_REQUEST_ID = 10; + private static final int TEST_PACKAGE_UID = 20; + private static final int TIMEOUT_MS = 30_000; + private static final NetworkRequest NOT_METERED_WIFI_NETWORK_REQUEST = new NetworkRequest( + new NetworkCapabilities() + .addTransportType(NetworkCapabilities.TRANSPORT_WIFI) + .setCapability(NetworkCapabilities.NET_CAPABILITY_NOT_METERED, true) + .setCapability(NetworkCapabilities.NET_CAPABILITY_INTERNET, false) + .setRequestorUid(TEST_PACKAGE_UID), + 0, TEST_REQUEST_ID, NetworkRequest.Type.REQUEST + ); + + @Before + public void setup() { + MockitoAnnotations.initMocks(this); + mHandlerThread = new HandlerThread("NetworkRequestStateStatsMetrics"); + Mockito.when(mNRStateStatsDeps.makeHandlerThread("NetworkRequestStateStatsMetrics")) + .thenReturn(mHandlerThread); + Mockito.when(mNRStateStatsDeps.getMillisSinceEvent(anyLong())).thenReturn(0L); + Mockito.doAnswer(invocation -> { + mHandlerCaptor.getValue().sendMessage( + Message.obtain(mHandlerCaptor.getValue(), mMessageWhatCaptor.getValue())); + return null; + }).when(mNRStateStatsDeps).sendMessageDelayed( + mHandlerCaptor.capture(), mMessageWhatCaptor.capture(), anyLong()); + mNetworkRequestStateStatsMetrics = new NetworkRequestStateStatsMetrics( + mNRStateStatsDeps, mNRStateInfoDeps); + } + + @Test + public void testNetworkRequestReceivedRemoved() { + final long nrStartTime = 1L; + final long nrEndTime = 101L; + // This call will be used to calculate NR received time + Mockito.when(mNRStateInfoDeps.getElapsedRealtime()).thenReturn(nrStartTime); + mNetworkRequestStateStatsMetrics.onNetworkRequestReceived(NOT_METERED_WIFI_NETWORK_REQUEST); + HandlerUtils.waitForIdle(mHandlerThread, TIMEOUT_MS); + + ArgumentCaptor networkRequestStateInfoCaptor = + ArgumentCaptor.forClass(NetworkRequestStateInfo.class); + verify(mNRStateStatsDeps, times(1)) + .writeStats(networkRequestStateInfoCaptor.capture()); + + NetworkRequestStateInfo nrStateInfoSent = networkRequestStateInfoCaptor.getValue(); + assertEquals(NETWORK_REQUEST_STATE_CHANGED__STATE__NETWORK_REQUEST_STATE_RECEIVED, + nrStateInfoSent.getNetworkRequestStateStatsType()); + assertEquals(NOT_METERED_WIFI_NETWORK_REQUEST.requestId, nrStateInfoSent.getRequestId()); + assertEquals(TEST_PACKAGE_UID, nrStateInfoSent.getPackageUid()); + assertEquals(1 << NetworkCapabilities.TRANSPORT_WIFI, nrStateInfoSent.getTransportTypes()); + assertTrue(nrStateInfoSent.getNetCapabilityNotMetered()); + assertFalse(nrStateInfoSent.getNetCapabilityInternet()); + assertEquals(0, nrStateInfoSent.getNetworkRequestDurationMillis()); + + clearInvocations(mNRStateStatsDeps); + // This call will be used to calculate NR removed time + Mockito.when(mNRStateInfoDeps.getElapsedRealtime()).thenReturn(nrEndTime); + mNetworkRequestStateStatsMetrics.onNetworkRequestRemoved(NOT_METERED_WIFI_NETWORK_REQUEST); + HandlerUtils.waitForIdle(mHandlerThread, TIMEOUT_MS); + + verify(mNRStateStatsDeps, times(1)) + .writeStats(networkRequestStateInfoCaptor.capture()); + + nrStateInfoSent = networkRequestStateInfoCaptor.getValue(); + assertEquals(NETWORK_REQUEST_STATE_CHANGED__STATE__NETWORK_REQUEST_STATE_REMOVED, + nrStateInfoSent.getNetworkRequestStateStatsType()); + assertEquals(NOT_METERED_WIFI_NETWORK_REQUEST.requestId, nrStateInfoSent.getRequestId()); + assertEquals(TEST_PACKAGE_UID, nrStateInfoSent.getPackageUid()); + assertEquals(1 << NetworkCapabilities.TRANSPORT_WIFI, nrStateInfoSent.getTransportTypes()); + assertTrue(nrStateInfoSent.getNetCapabilityNotMetered()); + assertFalse(nrStateInfoSent.getNetCapabilityInternet()); + assertEquals(nrEndTime - nrStartTime, nrStateInfoSent.getNetworkRequestDurationMillis()); + } + + @Test + public void testUnreceivedNetworkRequestRemoved() { + mNetworkRequestStateStatsMetrics.onNetworkRequestRemoved(NOT_METERED_WIFI_NETWORK_REQUEST); + HandlerUtils.waitForIdle(mHandlerThread, TIMEOUT_MS); + verify(mNRStateStatsDeps, never()) + .writeStats(any(NetworkRequestStateInfo.class)); + } + + @Test + public void testNoMessagesWhenNetworkRequestReceived() { + mNetworkRequestStateStatsMetrics.onNetworkRequestReceived(NOT_METERED_WIFI_NETWORK_REQUEST); + HandlerUtils.waitForIdle(mHandlerThread, TIMEOUT_MS); + verify(mNRStateStatsDeps, times(1)) + .writeStats(any(NetworkRequestStateInfo.class)); + + clearInvocations(mNRStateStatsDeps); + mNetworkRequestStateStatsMetrics.onNetworkRequestReceived(NOT_METERED_WIFI_NETWORK_REQUEST); + HandlerUtils.waitForIdle(mHandlerThread, TIMEOUT_MS); + verify(mNRStateStatsDeps, never()) + .writeStats(any(NetworkRequestStateInfo.class)); + } + + @Test + public void testMessageQueueSizeLimitNotExceeded() { + // Imitate many events (MAX_QUEUED_REQUESTS) are coming together at once while + // the other event is being processed. + final ConditionVariable cv = new ConditionVariable(); + mHandlerThread.getThreadHandler().post(() -> cv.block()); + for (int i = 0; i < NetworkRequestStateStatsMetrics.MAX_QUEUED_REQUESTS / 2; i++) { + mNetworkRequestStateStatsMetrics.onNetworkRequestReceived(new NetworkRequest( + new NetworkCapabilities().setRequestorUid(TEST_PACKAGE_UID), + 0, i + 1, NetworkRequest.Type.REQUEST)); + mNetworkRequestStateStatsMetrics.onNetworkRequestRemoved(new NetworkRequest( + new NetworkCapabilities().setRequestorUid(TEST_PACKAGE_UID), + 0, i + 1, NetworkRequest.Type.REQUEST)); + } + + // When event queue is full, all other events should be dropped. + mNetworkRequestStateStatsMetrics.onNetworkRequestReceived(new NetworkRequest( + new NetworkCapabilities().setRequestorUid(TEST_PACKAGE_UID), + 0, 2 * NetworkRequestStateStatsMetrics.MAX_QUEUED_REQUESTS + 1, + NetworkRequest.Type.REQUEST)); + + cv.open(); + + // Check only first MAX_QUEUED_REQUESTS events are logged. + ArgumentCaptor networkRequestStateInfoCaptor = + ArgumentCaptor.forClass(NetworkRequestStateInfo.class); + verify(mNRStateStatsDeps, timeout(100).times( + NetworkRequestStateStatsMetrics.MAX_QUEUED_REQUESTS)) + .writeStats(networkRequestStateInfoCaptor.capture()); + for (int i = 0; i < NetworkRequestStateStatsMetrics.MAX_QUEUED_REQUESTS; i++) { + NetworkRequestStateInfo nrStateInfoSent = + networkRequestStateInfoCaptor.getAllValues().get(i); + assertEquals(i / 2 + 1, nrStateInfoSent.getRequestId()); + assertEquals( + (i % 2 == 0) + ? NETWORK_REQUEST_STATE_CHANGED__STATE__NETWORK_REQUEST_STATE_RECEIVED + : NETWORK_REQUEST_STATE_CHANGED__STATE__NETWORK_REQUEST_STATE_REMOVED, + nrStateInfoSent.getNetworkRequestStateStatsType()); + } + } +} diff --git a/tests/unit/java/com/android/server/connectivityservice/CSNetworkRequestStateStatsMetricsTests.kt b/tests/unit/java/com/android/server/connectivityservice/CSNetworkRequestStateStatsMetricsTests.kt new file mode 100644 index 0000000000..35f8ae515c --- /dev/null +++ b/tests/unit/java/com/android/server/connectivityservice/CSNetworkRequestStateStatsMetricsTests.kt @@ -0,0 +1,83 @@ +/* + * Copyright (C) 2023 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. + */ + +package com.android.server + +import android.net.NetworkCapabilities +import android.net.NetworkRequest +import android.os.Build +import android.os.Process +import com.android.testutils.DevSdkIgnoreRule +import com.android.testutils.DevSdkIgnoreRunner +import com.android.testutils.TestableNetworkCallback +import org.junit.Before +import org.junit.Test +import org.junit.runner.RunWith +import org.mockito.Mockito.argThat +import org.mockito.Mockito.clearInvocations +import org.mockito.Mockito.never +import org.mockito.Mockito.verify + +@RunWith(DevSdkIgnoreRunner::class) +@DevSdkIgnoreRule.IgnoreUpTo(Build.VERSION_CODES.R) +class CSNetworkRequestStateStatsMetricsTests : CSTest() { + private val CELL_INTERNET_NOT_METERED_NC = NetworkCapabilities.Builder() + .addTransportType(NetworkCapabilities.TRANSPORT_CELLULAR) + .addCapability(NetworkCapabilities.NET_CAPABILITY_INTERNET) + .addCapability(NetworkCapabilities.NET_CAPABILITY_NOT_METERED) + .build().setRequestorUidAndPackageName(Process.myUid(), context.getPackageName()) + + private val CELL_INTERNET_NOT_METERED_NR = NetworkRequest.Builder() + .setCapabilities(CELL_INTERNET_NOT_METERED_NC).build() + + @Before + fun setup() { + waitForIdle() + clearInvocations(networkRequestStateStatsMetrics) + } + + @Test + fun testRequestTypeNRProduceMetrics() { + cm.requestNetwork(CELL_INTERNET_NOT_METERED_NR, TestableNetworkCallback()) + waitForIdle() + + verify(networkRequestStateStatsMetrics).onNetworkRequestReceived( + argThat{req -> req.networkCapabilities.equals( + CELL_INTERNET_NOT_METERED_NR.networkCapabilities)}) + } + + @Test + fun testListenTypeNRProduceNoMetrics() { + cm.registerNetworkCallback(CELL_INTERNET_NOT_METERED_NR, TestableNetworkCallback()) + waitForIdle() + verify(networkRequestStateStatsMetrics, never()).onNetworkRequestReceived(any()) + } + + @Test + fun testRemoveRequestTypeNRProduceMetrics() { + val cb = TestableNetworkCallback() + cm.requestNetwork(CELL_INTERNET_NOT_METERED_NR, cb) + + waitForIdle() + clearInvocations(networkRequestStateStatsMetrics) + + cm.unregisterNetworkCallback(cb) + waitForIdle() + verify(networkRequestStateStatsMetrics).onNetworkRequestRemoved( + argThat{req -> req.networkCapabilities.equals( + CELL_INTERNET_NOT_METERED_NR.networkCapabilities)}) + } +} diff --git a/tests/unit/java/com/android/server/connectivityservice/base/CSTest.kt b/tests/unit/java/com/android/server/connectivityservice/base/CSTest.kt index 958c4f2961..21396f2757 100644 --- a/tests/unit/java/com/android/server/connectivityservice/base/CSTest.kt +++ b/tests/unit/java/com/android/server/connectivityservice/base/CSTest.kt @@ -63,6 +63,7 @@ import com.android.server.connectivity.ClatCoordinator import com.android.server.connectivity.ConnectivityFlags import com.android.server.connectivity.MultinetworkPolicyTracker import com.android.server.connectivity.MultinetworkPolicyTrackerTestDependencies +import com.android.server.connectivity.NetworkRequestStateStatsMetrics import com.android.server.connectivity.ProxyTracker import com.android.testutils.visibleOnHandlerThread import com.android.testutils.waitForIdle @@ -157,6 +158,7 @@ open class CSTest { val netd = mock() val bpfNetMaps = mock() val clatCoordinator = mock() + val networkRequestStateStatsMetrics = mock() val proxyTracker = ProxyTracker(context, mock(), 16 /* EVENT_PROXY_HAS_CHANGED */) val alarmManager = makeMockAlarmManager() val systemConfigManager = makeMockSystemConfigManager() @@ -197,6 +199,9 @@ open class CSTest { MultinetworkPolicyTracker(c, h, r, MultinetworkPolicyTrackerTestDependencies(connResources.get())) + override fun makeNetworkRequestStateStatsMetrics(c: Context) = + this@CSTest.networkRequestStateStatsMetrics + // All queried features must be mocked, because the test cannot hold the // READ_DEVICE_CONFIG permission and device config utils use static methods for // checking permissions.