1 /*
2  * Copyright (C) 2024 The Android Open Source Project
3  *
4  * Licensed under the Apache License, Version 2.0 (the "License");
5  * you may not use this file except in compliance with the License.
6  * You may obtain a copy of the License at
7  *
8  *      http://www.apache.org/licenses/LICENSE-2.0
9  *
10  * Unless required by applicable law or agreed to in writing, software
11  * distributed under the License is distributed on an "AS IS" BASIS,
12  * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13  * See the License for the specific language governing permissions and
14  * limitations under the License.
15  */
16 
17 package com.android.server.connectivity;
18 
19 import static com.android.server.ConnectivityStatsLog.NETWORK_REQUEST_STATE_CHANGED;
20 
21 import android.annotation.NonNull;
22 import android.net.NetworkRequest;
23 import android.os.Handler;
24 import android.os.HandlerThread;
25 import android.os.Looper;
26 import android.os.Message;
27 import android.os.SystemClock;
28 import android.util.Log;
29 import android.util.SparseArray;
30 
31 import com.android.internal.annotations.VisibleForTesting;
32 import com.android.server.ConnectivityStatsLog;
33 
34 import java.util.ArrayDeque;
35 
36 /**
37  * A Connectivity Service helper class to push atoms capturing network requests have been received
38  * and removed and its metadata.
39  *
40  * Atom events are logged in the ConnectivityStatsLog. Network request id: network request metadata
41  * hashmap is stored to calculate network request duration when it is removed.
42  *
43  * Note that this class is not thread-safe. The instance of the class needs to be
44  * synchronized in the callers when being used in multiple threads.
45  */
46 public class NetworkRequestStateStatsMetrics {
47 
48     private static final String TAG = "NetworkRequestStateStatsMetrics";
49     private static final int CMD_SEND_PENDING_NETWORK_REQUEST_STATE_METRIC = 0;
50     private static final int CMD_SEND_MAYBE_ENQUEUE_NETWORK_REQUEST_STATE_METRIC = 1;
51 
52     @VisibleForTesting
53     static final int MAX_QUEUED_REQUESTS = 20;
54 
55     // Stats logging frequency is limited to 10 ms at least, 500ms are taken as a safely margin
56     // for cases of longer periods of frequent network requests.
57     private static final int ATOM_INTERVAL_MS = 500;
58     private final StatsLoggingHandler mStatsLoggingHandler;
59 
60     private final Dependencies mDependencies;
61 
62     private final NetworkRequestStateInfo.Dependencies mNRStateInfoDeps;
63     private final SparseArray<NetworkRequestStateInfo> mNetworkRequestsActive;
64 
NetworkRequestStateStatsMetrics()65     public NetworkRequestStateStatsMetrics() {
66         this(new Dependencies(), new NetworkRequestStateInfo.Dependencies());
67     }
68 
69     @VisibleForTesting
NetworkRequestStateStatsMetrics(Dependencies deps, NetworkRequestStateInfo.Dependencies nrStateInfoDeps)70     NetworkRequestStateStatsMetrics(Dependencies deps,
71             NetworkRequestStateInfo.Dependencies nrStateInfoDeps) {
72         mNetworkRequestsActive = new SparseArray<>();
73         mDependencies = deps;
74         mNRStateInfoDeps = nrStateInfoDeps;
75         HandlerThread handlerThread = mDependencies.makeHandlerThread(TAG);
76         handlerThread.start();
77         mStatsLoggingHandler = new StatsLoggingHandler(handlerThread.getLooper());
78     }
79 
80     /**
81      * Register network request receive event, push RECEIVE atom
82      *
83      * @param networkRequest network request received
84      */
onNetworkRequestReceived(NetworkRequest networkRequest)85     public void onNetworkRequestReceived(NetworkRequest networkRequest) {
86         if (mNetworkRequestsActive.contains(networkRequest.requestId)) {
87             Log.w(TAG, "Received already registered network request, id = "
88                     + networkRequest.requestId);
89         } else {
90             Log.d(TAG, "Registered nr with ID = " + networkRequest.requestId
91                     + ", package_uid = " + networkRequest.networkCapabilities.getRequestorUid());
92             NetworkRequestStateInfo networkRequestStateInfo = new NetworkRequestStateInfo(
93                     networkRequest, mNRStateInfoDeps);
94             mNetworkRequestsActive.put(networkRequest.requestId, networkRequestStateInfo);
95             mStatsLoggingHandler.sendMessage(Message.obtain(
96                     mStatsLoggingHandler,
97                     CMD_SEND_MAYBE_ENQUEUE_NETWORK_REQUEST_STATE_METRIC,
98                     networkRequestStateInfo));
99         }
100     }
101 
102     /**
103      * Register network request remove event, push REMOVE atom
104      *
105      * @param networkRequest network request removed
106      */
onNetworkRequestRemoved(NetworkRequest networkRequest)107     public void onNetworkRequestRemoved(NetworkRequest networkRequest) {
108         NetworkRequestStateInfo networkRequestStateInfo = mNetworkRequestsActive.get(
109                 networkRequest.requestId);
110         if (networkRequestStateInfo == null) {
111             Log.w(TAG, "This NR hasn't been registered. NR id = " + networkRequest.requestId);
112         } else {
113             Log.d(TAG, "Removed nr with ID = " + networkRequest.requestId);
114             mNetworkRequestsActive.remove(networkRequest.requestId);
115             networkRequestStateInfo = new NetworkRequestStateInfo(networkRequestStateInfo);
116             networkRequestStateInfo.setNetworkRequestRemoved();
117             mStatsLoggingHandler.sendMessage(Message.obtain(
118                     mStatsLoggingHandler,
119                     CMD_SEND_MAYBE_ENQUEUE_NETWORK_REQUEST_STATE_METRIC,
120                     networkRequestStateInfo));
121         }
122     }
123 
124     /** Dependency class */
125     public static class Dependencies {
126         /**
127          * Creates a thread with provided tag.
128          *
129          * @param tag for the thread.
130          */
makeHandlerThread(@onNull final String tag)131         public HandlerThread makeHandlerThread(@NonNull final String tag) {
132             return new HandlerThread(tag);
133         }
134 
135         /**
136          * @see Handler#sendMessageDelayed(Message, long)
137          */
sendMessageDelayed(@onNull Handler handler, int what, long delayMillis)138         public void sendMessageDelayed(@NonNull Handler handler, int what, long delayMillis) {
139             handler.sendMessageDelayed(Message.obtain(handler, what), delayMillis);
140         }
141 
142         /**
143          * Gets number of millis since event.
144          *
145          * @param eventTimeMillis long timestamp in millis when the event occurred.
146          */
getMillisSinceEvent(long eventTimeMillis)147         public long getMillisSinceEvent(long eventTimeMillis) {
148             return SystemClock.elapsedRealtime() - eventTimeMillis;
149         }
150 
151         /**
152          * Writes a NETWORK_REQUEST_STATE_CHANGED event to ConnectivityStatsLog.
153          *
154          * @param networkRequestStateInfo NetworkRequestStateInfo containing network request info.
155          */
writeStats(NetworkRequestStateInfo networkRequestStateInfo)156         public void writeStats(NetworkRequestStateInfo networkRequestStateInfo) {
157             ConnectivityStatsLog.write(
158                     NETWORK_REQUEST_STATE_CHANGED,
159                     networkRequestStateInfo.getPackageUid(),
160                     networkRequestStateInfo.getTransportTypes(),
161                     networkRequestStateInfo.getNetCapabilityNotMetered(),
162                     networkRequestStateInfo.getNetCapabilityInternet(),
163                     networkRequestStateInfo.getNetworkRequestStateStatsType(),
164                     networkRequestStateInfo.getNetworkRequestDurationMillis());
165         }
166     }
167 
168     private class StatsLoggingHandler extends Handler {
169         private static final String TAG = "NetworkRequestsStateStatsLoggingHandler";
170 
171         private final ArrayDeque<NetworkRequestStateInfo> mPendingState = new ArrayDeque<>();
172 
173         private long mLastLogTime = 0;
174 
StatsLoggingHandler(Looper looper)175         StatsLoggingHandler(Looper looper) {
176             super(looper);
177         }
178 
maybeEnqueueStatsMessage(NetworkRequestStateInfo networkRequestStateInfo)179         private void maybeEnqueueStatsMessage(NetworkRequestStateInfo networkRequestStateInfo) {
180             if (mPendingState.size() < MAX_QUEUED_REQUESTS) {
181                 mPendingState.add(networkRequestStateInfo);
182             } else {
183                 Log.w(TAG, "Too many network requests received within last " + ATOM_INTERVAL_MS
184                         + " ms, dropping the last network request (id = "
185                         + networkRequestStateInfo.getRequestId() + ") event");
186                 return;
187             }
188             if (hasMessages(CMD_SEND_PENDING_NETWORK_REQUEST_STATE_METRIC)) {
189                 return;
190             }
191             long millisSinceLastLog = mDependencies.getMillisSinceEvent(mLastLogTime);
192 
193             if (millisSinceLastLog >= ATOM_INTERVAL_MS) {
194                 sendMessage(
195                         Message.obtain(this, CMD_SEND_PENDING_NETWORK_REQUEST_STATE_METRIC));
196             } else {
197                 mDependencies.sendMessageDelayed(
198                         this,
199                         CMD_SEND_PENDING_NETWORK_REQUEST_STATE_METRIC,
200                         ATOM_INTERVAL_MS - millisSinceLastLog);
201             }
202         }
203 
204         @Override
handleMessage(Message msg)205         public void handleMessage(Message msg) {
206             NetworkRequestStateInfo loggingInfo;
207             switch (msg.what) {
208                 case CMD_SEND_MAYBE_ENQUEUE_NETWORK_REQUEST_STATE_METRIC:
209                     maybeEnqueueStatsMessage((NetworkRequestStateInfo) msg.obj);
210                     break;
211                 case CMD_SEND_PENDING_NETWORK_REQUEST_STATE_METRIC:
212                     mLastLogTime = SystemClock.elapsedRealtime();
213                     if (!mPendingState.isEmpty()) {
214                         loggingInfo = mPendingState.remove();
215                         mDependencies.writeStats(loggingInfo);
216                         if (!mPendingState.isEmpty()) {
217                             mDependencies.sendMessageDelayed(
218                                     this,
219                                     CMD_SEND_PENDING_NETWORK_REQUEST_STATE_METRIC,
220                                     ATOM_INTERVAL_MS);
221                         }
222                     }
223                     break;
224                 default: // fall out
225             }
226         }
227     }
228 }
229