1 /*
2  * Copyright 2019 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.wifi;
18 
19 import static android.net.wifi.WifiManager.DEVICE_MOBILITY_STATE_STATIONARY;
20 import static android.net.wifi.WifiManager.DEVICE_MOBILITY_STATE_UNKNOWN;
21 
22 import android.content.Context;
23 import android.net.wifi.ScanResult;
24 import android.net.wifi.WifiManager.DeviceMobilityState;
25 import android.util.Log;
26 import android.util.SparseArray;
27 import android.util.SparseIntArray;
28 
29 import com.android.internal.annotations.VisibleForTesting;
30 import com.android.server.wifi.WifiLinkLayerStats.ChannelStats;
31 import com.android.server.wifi.util.InformationElementUtil.BssLoad;
32 import com.android.wifi.resources.R;
33 
34 import java.util.ArrayDeque;
35 import java.util.Iterator;
36 
37 /**
38  * This class collects channel stats over a Wifi Interface
39  * and calculates channel utilization using the latest and cached channel stats.
40  * Cache saves previous readings of channel stats in a FIFO.
41  * The cache is updated when a new stats arrives and it has been a long while since the last update.
42  * To get more statistically sound channel utilization, for these devices which support
43  * mobility state report, the cache update is stopped when the device stays in the stationary state.
44  */
45 public class WifiChannelUtilization {
46     private static final String TAG = "WifiChannelUtilization";
47     private static boolean sVerboseLoggingEnabled = false;
48     public static final int UNKNOWN_FREQ = -1;
49     // Invalidate the utilization value if it is larger than the following value.
50     // This is to detect and mitigate the incorrect HW reports of ccaBusy/OnTime.
51     // It is reasonable to assume that utilization ratio in the real life is never beyond this value
52     // given by all the inter-frame-spacings (IFS)
53     static final int UTILIZATION_RATIO_MAX = BssLoad.MAX_CHANNEL_UTILIZATION * 94 / 100;
54     // Minimum time interval in ms between two cache updates.
55     @VisibleForTesting
56     static final int DEFAULT_CACHE_UPDATE_INTERVAL_MIN_MS = 10 * 60 * 1000;
57     // To get valid channel utilization, the time difference between the reference chanStat's
58     // radioOnTime and current chanStat's radioOntime should be no less than the following value
59     @VisibleForTesting
60     static final int RADIO_ON_TIME_DIFF_MIN_MS = 250;
61     // The number of chanStatsMap readings saved in cache
62     // where each reading corresponds to one link layer stats update.
63     @VisibleForTesting
64     static final int CHANNEL_STATS_CACHE_SIZE = 5;
65     private final Clock mClock;
66     private final Context mContext;
67     private @DeviceMobilityState int mDeviceMobilityState = DEVICE_MOBILITY_STATE_UNKNOWN;
68     private int mCacheUpdateIntervalMinMs = DEFAULT_CACHE_UPDATE_INTERVAL_MIN_MS;
69 
70     // Map frequency (key) to utilization ratio (value) with the valid range of
71     // [BssLoad.MIN_CHANNEL_UTILIZATION, BssLoad.MAX_CHANNEL_UTILIZATION],
72     // where MIN_CHANNEL_UTILIZATION corresponds to ratio 0%
73     // and MAX_CHANNEL_UTILIZATION corresponds to ratio 100%
74     private SparseIntArray mChannelUtilizationMap = new SparseIntArray();
75     private ArrayDeque<SparseArray<ChannelStats>> mChannelStatsMapCache = new ArrayDeque<>();
76     private long mLastChannelStatsMapTimeStamp;
77     private int mLastChannelStatsMapMobilityState;
78 
WifiChannelUtilization(Clock clock, Context context)79     WifiChannelUtilization(Clock clock, Context context) {
80         mContext = context;
81         mClock = clock;
82     }
83 
84     /**
85      * Enable/Disable verbose logging.
86      * @param verbose true to enable and false to disable.
87      */
enableVerboseLogging(boolean verbose)88     public void enableVerboseLogging(boolean verbose) {
89         sVerboseLoggingEnabled = verbose;
90     }
91 
92     /**
93      * (Re)initialize internal variables and status
94      * @param wifiLinkLayerStats The latest wifi link layer stats
95      */
init(WifiLinkLayerStats wifiLinkLayerStats)96     public void init(WifiLinkLayerStats wifiLinkLayerStats) {
97         mChannelUtilizationMap.clear();
98         mChannelStatsMapCache.clear();
99         mDeviceMobilityState = DEVICE_MOBILITY_STATE_UNKNOWN;
100         mLastChannelStatsMapMobilityState = DEVICE_MOBILITY_STATE_UNKNOWN;
101         for (int i = 0; i < (CHANNEL_STATS_CACHE_SIZE - 1); ++i) {
102             mChannelStatsMapCache.addFirst(new SparseArray<>());
103         }
104         if (wifiLinkLayerStats != null) {
105             mChannelStatsMapCache.addFirst(wifiLinkLayerStats.channelStatsMap);
106         } else {
107             mChannelStatsMapCache.addFirst(new SparseArray<>());
108         }
109         mLastChannelStatsMapTimeStamp = mClock.getElapsedSinceBootMillis();
110         if (sVerboseLoggingEnabled) {
111             Log.d(TAG, "initializing");
112         }
113     }
114 
115     /**
116      * Set channel stats cache update minimum interval
117      */
setCacheUpdateIntervalMs(int cacheUpdateIntervalMinMs)118     public void setCacheUpdateIntervalMs(int cacheUpdateIntervalMinMs) {
119         mCacheUpdateIntervalMinMs = cacheUpdateIntervalMinMs;
120     }
121 
122     /**
123      * Get channel utilization ratio for a given frequency
124      * @param frequency The center frequency of 20MHz WLAN channel
125      * @return Utilization ratio value if it is available; BssLoad.INVALID otherwise
126      */
getUtilizationRatio(int frequency)127     public int getUtilizationRatio(int frequency) {
128         if (mContext.getResources().getBoolean(
129                 R.bool.config_wifiChannelUtilizationOverrideEnabled)) {
130             if (ScanResult.is24GHz(frequency)) {
131                 return mContext.getResources().getInteger(
132                         R.integer.config_wifiChannelUtilizationOverride2g);
133             }
134             if (ScanResult.is5GHz(frequency)) {
135                 return mContext.getResources().getInteger(
136                         R.integer.config_wifiChannelUtilizationOverride5g);
137             }
138             return mContext.getResources().getInteger(
139                         R.integer.config_wifiChannelUtilizationOverride6g);
140         }
141         return mChannelUtilizationMap.get(frequency, BssLoad.INVALID);
142     }
143 
144     /**
145      * Update device mobility state
146      * @param newState the new device mobility state
147      */
setDeviceMobilityState(@eviceMobilityState int newState)148     public void setDeviceMobilityState(@DeviceMobilityState int newState) {
149         mDeviceMobilityState = newState;
150         if (sVerboseLoggingEnabled) {
151             Log.d(TAG, " update device mobility state to " + newState);
152         }
153     }
154 
155     /**
156      * Set channel utilization ratio for a given frequency
157      * @param frequency The center frequency of 20MHz channel
158      * @param utilizationRatio The utilization ratio of 20MHz channel
159      */
setUtilizationRatio(int frequency, int utilizationRatio)160     public void setUtilizationRatio(int frequency, int utilizationRatio) {
161         mChannelUtilizationMap.put(frequency, utilizationRatio);
162     }
163 
164     /**
165      * Update channel utilization with the latest link layer stats and the cached channel stats
166      * and then update channel stats cache
167      * If the given frequency is UNKNOWN_FREQ, calculate channel utilization of all frequencies
168      * Otherwise, calculate the channel utilization of the given frequency
169      * @param wifiLinkLayerStats The latest wifi link layer stats
170      * @param frequency Current frequency of network.
171      */
refreshChannelStatsAndChannelUtilization(WifiLinkLayerStats wifiLinkLayerStats, int frequency)172     public void refreshChannelStatsAndChannelUtilization(WifiLinkLayerStats wifiLinkLayerStats,
173             int frequency) {
174         if (mContext.getResources().getBoolean(
175                 R.bool.config_wifiChannelUtilizationOverrideEnabled)) {
176             return;
177         }
178 
179         if (wifiLinkLayerStats == null) {
180             return;
181         }
182         SparseArray<ChannelStats>  channelStatsMap = wifiLinkLayerStats.channelStatsMap;
183         if (channelStatsMap == null) {
184             return;
185         }
186         if (frequency != UNKNOWN_FREQ) {
187             ChannelStats channelStats = channelStatsMap.get(frequency, null);
188             if (channelStats != null) calculateChannelUtilization(channelStats);
189         } else {
190             for (int i = 0; i < channelStatsMap.size(); i++) {
191                 ChannelStats channelStats = channelStatsMap.valueAt(i);
192                 calculateChannelUtilization(channelStats);
193             }
194         }
195         updateChannelStatsCache(channelStatsMap, frequency);
196     }
197 
calculateChannelUtilization(ChannelStats channelStats)198     private void calculateChannelUtilization(ChannelStats channelStats) {
199         int freq = channelStats.frequency;
200         int ccaBusyTimeMs = channelStats.ccaBusyTimeMs;
201         int radioOnTimeMs = channelStats.radioOnTimeMs;
202 
203         ChannelStats channelStatsRef = findChanStatsReference(freq, radioOnTimeMs);
204         int busyTimeDiff = ccaBusyTimeMs - channelStatsRef.ccaBusyTimeMs;
205         int radioOnTimeDiff = radioOnTimeMs - channelStatsRef.radioOnTimeMs;
206         int utilizationRatio = BssLoad.INVALID;
207         if (radioOnTimeDiff >= RADIO_ON_TIME_DIFF_MIN_MS && busyTimeDiff >= 0) {
208             utilizationRatio = calculateUtilizationRatio(radioOnTimeDiff, busyTimeDiff);
209         }
210         mChannelUtilizationMap.put(freq, utilizationRatio);
211 
212         if (sVerboseLoggingEnabled) {
213             int utilizationRatioT0 = calculateUtilizationRatio(radioOnTimeMs, ccaBusyTimeMs);
214             StringBuilder sb = new StringBuilder();
215             Log.d(TAG, sb.append(" freq: ").append(freq)
216                     .append(" onTime: ").append(radioOnTimeMs)
217                     .append(" busyTime: ").append(ccaBusyTimeMs)
218                     .append(" onTimeDiff: ").append(radioOnTimeDiff)
219                     .append(" busyTimeDiff: ").append(busyTimeDiff)
220                     .append(" utilization: ").append(utilizationRatio)
221                     .append(" utilization t0: ").append(utilizationRatioT0)
222                     .toString());
223         }
224     }
225     /**
226      * Find a proper channelStats reference from channelStatsMap cache.
227      * The search continues until it finds a channelStat at the given frequency with radioOnTime
228      * sufficiently smaller than current radioOnTime, or there is no channelStats for the given
229      * frequency or it reaches the end of cache.
230      * @param freq Frequency of current channel
231      * @param radioOnTimeMs The latest radioOnTime of current channel
232      * @return the found channelStat reference if search succeeds, or a dummy channelStats with time
233      * zero if channelStats is not found for the given frequency, or a dummy channelStats with the
234      * latest radioOnTimeMs if it reaches the end of cache.
235      */
findChanStatsReference(int freq, int radioOnTimeMs)236     private ChannelStats findChanStatsReference(int freq, int radioOnTimeMs) {
237         // A dummy channelStats with the latest radioOnTimeMs.
238         ChannelStats channelStatsCurrRadioOnTime = new ChannelStats();
239         channelStatsCurrRadioOnTime.radioOnTimeMs = radioOnTimeMs;
240         Iterator iterator = mChannelStatsMapCache.iterator();
241         while (iterator.hasNext()) {
242             SparseArray<ChannelStats> channelStatsMap = (SparseArray<ChannelStats>) iterator.next();
243             // If the freq can't be found in current channelStatsMap, stop search because it won't
244             // appear in older ones either due to the fact that channelStatsMap are accumulated
245             // in HW and thus a recent reading should have channels no less than old readings.
246             // Return a dummy channelStats with zero radioOnTimeMs
247             if (channelStatsMap == null || channelStatsMap.get(freq) == null) {
248                 return new ChannelStats();
249             }
250             ChannelStats channelStats = channelStatsMap.get(freq);
251             int radioOnTimeDiff = radioOnTimeMs - channelStats.radioOnTimeMs;
252             if (radioOnTimeDiff >= RADIO_ON_TIME_DIFF_MIN_MS) {
253                 return channelStats;
254             }
255         }
256         return channelStatsCurrRadioOnTime;
257     }
258 
calculateUtilizationRatio(int radioOnTimeDiff, int busyTimeDiff)259     private int calculateUtilizationRatio(int radioOnTimeDiff, int busyTimeDiff) {
260         if (radioOnTimeDiff > 0) {
261             int utilizationRatio = busyTimeDiff * BssLoad.MAX_CHANNEL_UTILIZATION / radioOnTimeDiff;
262             return (utilizationRatio > UTILIZATION_RATIO_MAX) ? BssLoad.INVALID : utilizationRatio;
263         } else {
264             return BssLoad.INVALID;
265         }
266     }
267 
updateChannelStatsCache(SparseArray<ChannelStats> channelStatsMap, int freq)268     private void updateChannelStatsCache(SparseArray<ChannelStats> channelStatsMap, int freq) {
269         // Update cache if it hits one of following conditions
270         // 1) it has been a long while since the last update and device doesn't remain stationary
271         // 2) cache is empty
272         boolean remainStationary =
273                 mLastChannelStatsMapMobilityState == DEVICE_MOBILITY_STATE_STATIONARY
274                 && mDeviceMobilityState == DEVICE_MOBILITY_STATE_STATIONARY;
275         long currTimeStamp = mClock.getElapsedSinceBootMillis();
276         boolean isLongTimeSinceLastUpdate =
277                 (currTimeStamp - mLastChannelStatsMapTimeStamp) >= mCacheUpdateIntervalMinMs;
278         if ((isLongTimeSinceLastUpdate && !remainStationary) || isChannelStatsMapCacheEmpty(freq)) {
279             mChannelStatsMapCache.addFirst(channelStatsMap);
280             mChannelStatsMapCache.removeLast();
281             mLastChannelStatsMapTimeStamp = currTimeStamp;
282             mLastChannelStatsMapMobilityState = mDeviceMobilityState;
283         }
284     }
285 
isChannelStatsMapCacheEmpty(int freq)286     private boolean isChannelStatsMapCacheEmpty(int freq) {
287         SparseArray<ChannelStats> channelStatsMap = mChannelStatsMapCache.peekFirst();
288         if (channelStatsMap == null || channelStatsMap.size() == 0) return true;
289         if (freq != UNKNOWN_FREQ && channelStatsMap.get(freq) == null) return true;
290         return false;
291     }
292 }
293