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