296 lines
14 KiB
Java
296 lines
14 KiB
Java
|
/*
|
||
|
* Copyright 2019 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.wifi;
|
||
|
|
||
|
import static android.net.wifi.WifiManager.DEVICE_MOBILITY_STATE_STATIONARY;
|
||
|
import static android.net.wifi.WifiManager.DEVICE_MOBILITY_STATE_UNKNOWN;
|
||
|
|
||
|
import android.content.Context;
|
||
|
import android.net.wifi.ScanResult;
|
||
|
import android.net.wifi.WifiManager.DeviceMobilityState;
|
||
|
import android.util.Log;
|
||
|
import android.util.SparseArray;
|
||
|
import android.util.SparseIntArray;
|
||
|
|
||
|
import com.android.internal.annotations.VisibleForTesting;
|
||
|
import com.android.server.wifi.WifiLinkLayerStats.ChannelStats;
|
||
|
import com.android.server.wifi.util.InformationElementUtil.BssLoad;
|
||
|
import com.android.wifi.resources.R;
|
||
|
|
||
|
import java.util.ArrayDeque;
|
||
|
import java.util.Iterator;
|
||
|
|
||
|
/**
|
||
|
* This class collects channel stats over a Wifi Interface
|
||
|
* and calculates channel utilization using the latest and cached channel stats.
|
||
|
* Cache saves previous readings of channel stats in a FIFO.
|
||
|
* The cache is updated when a new stats arrives and it has been a long while since the last update.
|
||
|
* To get more statistically sound channel utilization, for these devices which support
|
||
|
* mobility state report, the cache update is stopped when the device stays in the stationary state.
|
||
|
* TODO(b/159052883): This may need to be reworked for STA + STA.
|
||
|
*/
|
||
|
public class WifiChannelUtilization {
|
||
|
private static final String TAG = "WifiChannelUtilization";
|
||
|
private static boolean sVerboseLoggingEnabled = false;
|
||
|
public static final int UNKNOWN_FREQ = -1;
|
||
|
// Invalidate the utilization value if it is larger than the following value.
|
||
|
// This is to detect and mitigate the incorrect HW reports of ccaBusy/OnTime.
|
||
|
// It is reasonable to assume that utilization ratio in the real life is never beyond this value
|
||
|
// given by all the inter-frame-spacings (IFS)
|
||
|
static final int UTILIZATION_RATIO_MAX = BssLoad.MAX_CHANNEL_UTILIZATION * 94 / 100;
|
||
|
// Minimum time interval in ms between two cache updates.
|
||
|
@VisibleForTesting
|
||
|
static final int DEFAULT_CACHE_UPDATE_INTERVAL_MIN_MS = 10 * 60 * 1000;
|
||
|
// To get valid channel utilization, the time difference between the reference chanStat's
|
||
|
// radioOnTime and current chanStat's radioOntime should be no less than the following value
|
||
|
@VisibleForTesting
|
||
|
static final int RADIO_ON_TIME_DIFF_MIN_MS = 250;
|
||
|
// The number of chanStatsMap readings saved in cache
|
||
|
// where each reading corresponds to one link layer stats update.
|
||
|
@VisibleForTesting
|
||
|
static final int CHANNEL_STATS_CACHE_SIZE = 5;
|
||
|
private final Clock mClock;
|
||
|
private final Context mContext;
|
||
|
private @DeviceMobilityState int mDeviceMobilityState = DEVICE_MOBILITY_STATE_UNKNOWN;
|
||
|
private int mCacheUpdateIntervalMinMs = DEFAULT_CACHE_UPDATE_INTERVAL_MIN_MS;
|
||
|
|
||
|
// Map frequency (key) to utilization ratio (value) with the valid range of
|
||
|
// [BssLoad.MIN_CHANNEL_UTILIZATION, BssLoad.MAX_CHANNEL_UTILIZATION],
|
||
|
// where MIN_CHANNEL_UTILIZATION corresponds to ratio 0%
|
||
|
// and MAX_CHANNEL_UTILIZATION corresponds to ratio 100%
|
||
|
private SparseIntArray mChannelUtilizationMap = new SparseIntArray();
|
||
|
private ArrayDeque<SparseArray<ChannelStats>> mChannelStatsMapCache = new ArrayDeque<>();
|
||
|
private long mLastChannelStatsMapTimeStamp;
|
||
|
private int mLastChannelStatsMapMobilityState;
|
||
|
|
||
|
WifiChannelUtilization(Clock clock, Context context) {
|
||
|
mContext = context;
|
||
|
mClock = clock;
|
||
|
}
|
||
|
|
||
|
/**
|
||
|
* Enable/Disable verbose logging.
|
||
|
* @param verbose true to enable and false to disable.
|
||
|
*/
|
||
|
public void enableVerboseLogging(boolean verbose) {
|
||
|
sVerboseLoggingEnabled = verbose;
|
||
|
}
|
||
|
|
||
|
/**
|
||
|
* (Re)initialize internal variables and status
|
||
|
* @param wifiLinkLayerStats The latest wifi link layer stats
|
||
|
*/
|
||
|
public void init(WifiLinkLayerStats wifiLinkLayerStats) {
|
||
|
mChannelUtilizationMap.clear();
|
||
|
mChannelStatsMapCache.clear();
|
||
|
mDeviceMobilityState = DEVICE_MOBILITY_STATE_UNKNOWN;
|
||
|
mLastChannelStatsMapMobilityState = DEVICE_MOBILITY_STATE_UNKNOWN;
|
||
|
for (int i = 0; i < (CHANNEL_STATS_CACHE_SIZE - 1); ++i) {
|
||
|
mChannelStatsMapCache.addFirst(new SparseArray<>());
|
||
|
}
|
||
|
if (wifiLinkLayerStats != null) {
|
||
|
mChannelStatsMapCache.addFirst(wifiLinkLayerStats.channelStatsMap);
|
||
|
} else {
|
||
|
mChannelStatsMapCache.addFirst(new SparseArray<>());
|
||
|
}
|
||
|
mLastChannelStatsMapTimeStamp = mClock.getElapsedSinceBootMillis();
|
||
|
if (sVerboseLoggingEnabled) {
|
||
|
Log.d(TAG, "initializing");
|
||
|
}
|
||
|
}
|
||
|
|
||
|
/**
|
||
|
* Set channel stats cache update minimum interval
|
||
|
*/
|
||
|
public void setCacheUpdateIntervalMs(int cacheUpdateIntervalMinMs) {
|
||
|
mCacheUpdateIntervalMinMs = cacheUpdateIntervalMinMs;
|
||
|
}
|
||
|
|
||
|
/**
|
||
|
* Get channel utilization ratio for a given frequency
|
||
|
* @param frequency The center frequency of 20MHz WLAN channel
|
||
|
* @return Utilization ratio value if it is available; BssLoad.INVALID otherwise
|
||
|
*/
|
||
|
public int getUtilizationRatio(int frequency) {
|
||
|
if (mContext.getResources().getBoolean(
|
||
|
R.bool.config_wifiChannelUtilizationOverrideEnabled)) {
|
||
|
if (ScanResult.is24GHz(frequency)) {
|
||
|
return mContext.getResources().getInteger(
|
||
|
R.integer.config_wifiChannelUtilizationOverride2g);
|
||
|
}
|
||
|
if (ScanResult.is5GHz(frequency)) {
|
||
|
return mContext.getResources().getInteger(
|
||
|
R.integer.config_wifiChannelUtilizationOverride5g);
|
||
|
}
|
||
|
return mContext.getResources().getInteger(
|
||
|
R.integer.config_wifiChannelUtilizationOverride6g);
|
||
|
}
|
||
|
return mChannelUtilizationMap.get(frequency, BssLoad.INVALID);
|
||
|
}
|
||
|
|
||
|
/**
|
||
|
* Update device mobility state
|
||
|
* @param newState the new device mobility state
|
||
|
*/
|
||
|
public void setDeviceMobilityState(@DeviceMobilityState int newState) {
|
||
|
mDeviceMobilityState = newState;
|
||
|
if (sVerboseLoggingEnabled) {
|
||
|
Log.d(TAG, " update device mobility state to " + newState);
|
||
|
}
|
||
|
}
|
||
|
|
||
|
/**
|
||
|
* Set channel utilization ratio for a given frequency
|
||
|
* @param frequency The center frequency of 20MHz channel
|
||
|
* @param utilizationRatio The utilization ratio of 20MHz channel
|
||
|
*/
|
||
|
public void setUtilizationRatio(int frequency, int utilizationRatio) {
|
||
|
mChannelUtilizationMap.put(frequency, utilizationRatio);
|
||
|
}
|
||
|
|
||
|
/**
|
||
|
* Update channel utilization with the latest link layer stats and the cached channel stats
|
||
|
* and then update channel stats cache
|
||
|
* If the given frequency is UNKNOWN_FREQ, calculate channel utilization of all frequencies
|
||
|
* Otherwise, calculate the channel utilization of the given frequency
|
||
|
* @param wifiLinkLayerStats The latest wifi link layer stats
|
||
|
* @param frequency Current frequency of network.
|
||
|
*/
|
||
|
public void refreshChannelStatsAndChannelUtilization(WifiLinkLayerStats wifiLinkLayerStats,
|
||
|
int frequency) {
|
||
|
if (mContext.getResources().getBoolean(
|
||
|
R.bool.config_wifiChannelUtilizationOverrideEnabled)) {
|
||
|
return;
|
||
|
}
|
||
|
|
||
|
if (wifiLinkLayerStats == null) {
|
||
|
return;
|
||
|
}
|
||
|
SparseArray<ChannelStats> channelStatsMap = wifiLinkLayerStats.channelStatsMap;
|
||
|
if (channelStatsMap == null) {
|
||
|
return;
|
||
|
}
|
||
|
if (frequency != UNKNOWN_FREQ) {
|
||
|
ChannelStats channelStats = channelStatsMap.get(frequency, null);
|
||
|
if (channelStats != null) calculateChannelUtilization(channelStats);
|
||
|
} else {
|
||
|
for (int i = 0; i < channelStatsMap.size(); i++) {
|
||
|
ChannelStats channelStats = channelStatsMap.valueAt(i);
|
||
|
calculateChannelUtilization(channelStats);
|
||
|
}
|
||
|
}
|
||
|
updateChannelStatsCache(channelStatsMap, frequency);
|
||
|
}
|
||
|
|
||
|
private void calculateChannelUtilization(ChannelStats channelStats) {
|
||
|
int freq = channelStats.frequency;
|
||
|
int ccaBusyTimeMs = channelStats.ccaBusyTimeMs;
|
||
|
int radioOnTimeMs = channelStats.radioOnTimeMs;
|
||
|
|
||
|
ChannelStats channelStatsRef = findChanStatsReference(freq, radioOnTimeMs);
|
||
|
int busyTimeDiff = ccaBusyTimeMs - channelStatsRef.ccaBusyTimeMs;
|
||
|
int radioOnTimeDiff = radioOnTimeMs - channelStatsRef.radioOnTimeMs;
|
||
|
int utilizationRatio = BssLoad.INVALID;
|
||
|
if (radioOnTimeDiff >= RADIO_ON_TIME_DIFF_MIN_MS && busyTimeDiff >= 0) {
|
||
|
utilizationRatio = calculateUtilizationRatio(radioOnTimeDiff, busyTimeDiff);
|
||
|
}
|
||
|
mChannelUtilizationMap.put(freq, utilizationRatio);
|
||
|
|
||
|
if (sVerboseLoggingEnabled) {
|
||
|
int utilizationRatioT0 = calculateUtilizationRatio(radioOnTimeMs, ccaBusyTimeMs);
|
||
|
StringBuilder sb = new StringBuilder();
|
||
|
Log.d(TAG, sb.append(" freq: ").append(freq)
|
||
|
.append(" onTime: ").append(radioOnTimeMs)
|
||
|
.append(" busyTime: ").append(ccaBusyTimeMs)
|
||
|
.append(" onTimeDiff: ").append(radioOnTimeDiff)
|
||
|
.append(" busyTimeDiff: ").append(busyTimeDiff)
|
||
|
.append(" utilization: ").append(utilizationRatio)
|
||
|
.append(" utilization t0: ").append(utilizationRatioT0)
|
||
|
.toString());
|
||
|
}
|
||
|
}
|
||
|
/**
|
||
|
* Find a proper channelStats reference from channelStatsMap cache.
|
||
|
* The search continues until it finds a channelStat at the given frequency with radioOnTime
|
||
|
* sufficiently smaller than current radioOnTime, or there is no channelStats for the given
|
||
|
* frequency or it reaches the end of cache.
|
||
|
* @param freq Frequency of current channel
|
||
|
* @param radioOnTimeMs The latest radioOnTime of current channel
|
||
|
* @return the found channelStat reference if search succeeds,
|
||
|
* or a placeholder channelStats with time zero if channelStats is not found
|
||
|
* for the given frequency,
|
||
|
* or a placeholder channelStats with the latest radioOnTimeMs if it reaches
|
||
|
* the end of cache.
|
||
|
*/
|
||
|
private ChannelStats findChanStatsReference(int freq, int radioOnTimeMs) {
|
||
|
// A placeholder channelStats with the latest radioOnTimeMs.
|
||
|
ChannelStats channelStatsCurrRadioOnTime = new ChannelStats();
|
||
|
channelStatsCurrRadioOnTime.radioOnTimeMs = radioOnTimeMs;
|
||
|
Iterator iterator = mChannelStatsMapCache.iterator();
|
||
|
while (iterator.hasNext()) {
|
||
|
SparseArray<ChannelStats> channelStatsMap = (SparseArray<ChannelStats>) iterator.next();
|
||
|
// If the freq can't be found in current channelStatsMap, stop search because it won't
|
||
|
// appear in older ones either due to the fact that channelStatsMap are accumulated
|
||
|
// in HW and thus a recent reading should have channels no less than old readings.
|
||
|
// Return a placeholder channelStats with zero radioOnTimeMs
|
||
|
if (channelStatsMap == null || channelStatsMap.get(freq) == null) {
|
||
|
return new ChannelStats();
|
||
|
}
|
||
|
ChannelStats channelStats = channelStatsMap.get(freq);
|
||
|
int radioOnTimeDiff = radioOnTimeMs - channelStats.radioOnTimeMs;
|
||
|
if (radioOnTimeDiff >= RADIO_ON_TIME_DIFF_MIN_MS) {
|
||
|
return channelStats;
|
||
|
}
|
||
|
}
|
||
|
return channelStatsCurrRadioOnTime;
|
||
|
}
|
||
|
|
||
|
private int calculateUtilizationRatio(int radioOnTimeDiff, int busyTimeDiff) {
|
||
|
if (radioOnTimeDiff > 0) {
|
||
|
int utilizationRatio = busyTimeDiff * BssLoad.MAX_CHANNEL_UTILIZATION / radioOnTimeDiff;
|
||
|
return (utilizationRatio > UTILIZATION_RATIO_MAX) ? BssLoad.INVALID : utilizationRatio;
|
||
|
} else {
|
||
|
return BssLoad.INVALID;
|
||
|
}
|
||
|
}
|
||
|
|
||
|
private void updateChannelStatsCache(SparseArray<ChannelStats> channelStatsMap, int freq) {
|
||
|
// Update cache if it hits one of following conditions
|
||
|
// 1) it has been a long while since the last update and device doesn't remain stationary
|
||
|
// 2) cache is empty
|
||
|
boolean remainStationary =
|
||
|
mLastChannelStatsMapMobilityState == DEVICE_MOBILITY_STATE_STATIONARY
|
||
|
&& mDeviceMobilityState == DEVICE_MOBILITY_STATE_STATIONARY;
|
||
|
long currTimeStamp = mClock.getElapsedSinceBootMillis();
|
||
|
boolean isLongTimeSinceLastUpdate =
|
||
|
(currTimeStamp - mLastChannelStatsMapTimeStamp) >= mCacheUpdateIntervalMinMs;
|
||
|
if ((isLongTimeSinceLastUpdate && !remainStationary) || isChannelStatsMapCacheEmpty(freq)) {
|
||
|
mChannelStatsMapCache.addFirst(channelStatsMap);
|
||
|
mChannelStatsMapCache.removeLast();
|
||
|
mLastChannelStatsMapTimeStamp = currTimeStamp;
|
||
|
mLastChannelStatsMapMobilityState = mDeviceMobilityState;
|
||
|
}
|
||
|
}
|
||
|
|
||
|
private boolean isChannelStatsMapCacheEmpty(int freq) {
|
||
|
SparseArray<ChannelStats> channelStatsMap = mChannelStatsMapCache.peekFirst();
|
||
|
if (channelStatsMap == null || channelStatsMap.size() == 0) return true;
|
||
|
if (freq != UNKNOWN_FREQ && channelStatsMap.get(freq) == null) return true;
|
||
|
return false;
|
||
|
}
|
||
|
}
|