1 /* 2 * Copyright (C) 2014 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.tradefed.device; 18 19 import com.android.tradefed.command.remote.DeviceDescriptor; 20 import com.android.tradefed.config.GlobalConfiguration; 21 import com.android.tradefed.config.Option; 22 import com.android.tradefed.log.LogUtil.CLog; 23 import com.android.tradefed.util.CircularByteArray; 24 25 import java.util.HashMap; 26 import java.util.Hashtable; 27 import java.util.Map; 28 import java.util.Timer; 29 import java.util.TimerTask; 30 31 /** 32 * A {@link IDeviceMonitor} that calculates device utilization stats. 33 * <p/> 34 * Currently measures simple moving average of allocation time % over a 24 hour window. 35 */ 36 public class DeviceUtilStatsMonitor implements IDeviceMonitor { 37 38 private static final int INITIAL_DELAY_MS = 5000; 39 40 /** 41 * Enum for configuring treatment of stub devices when calculating average host utilization 42 */ 43 public enum StubDeviceUtil { 44 /** never include stub device data */ 45 IGNORE, 46 /** 47 * include stub device data only if any stub device of same type is allocated at least 48 * once 49 */ 50 INCLUDE_IF_USED, 51 /** always include stub device data */ 52 ALWAYS_INCLUDE 53 } 54 55 @Option(name = "collect-null-device", description = 56 "controls if null device data should be used when calculating avg host utilization") 57 private StubDeviceUtil mCollectNullDevice = StubDeviceUtil.INCLUDE_IF_USED; 58 59 @Option(name = "collect-emulator", description = 60 "controls if emulator data should be used when calculating avg host utilization") 61 private StubDeviceUtil mCollectEmulator = StubDeviceUtil.INCLUDE_IF_USED; 62 63 @Option(name = "sample-window-hours", description = 64 "the moving average window size to use, in hours") 65 private int mSampleWindowHours = 8; 66 67 @Option(name = "sample-interval-secs", description = 68 "the time period between samples, in seconds") 69 private int mSamplingIntervalSec = 60; 70 71 private boolean mNullDeviceAllocated = false; 72 private boolean mEmulatorAllocated = false; 73 74 /** 75 * Container for utilization stats. 76 */ 77 public static class UtilizationDesc { 78 final int mTotalUtil; 79 final Map<String, Integer> mDeviceUtil; 80 81 public UtilizationDesc(int totalUtil, Map<String, Integer> deviceUtil) { 82 mTotalUtil = totalUtil; 83 mDeviceUtil = deviceUtil; 84 } 85 86 /** 87 * Return the total utilization for all devices in TF process, measured as total allocation 88 * time for all devices vs total available time. 89 * 90 * @return percentage utilization 91 */ 92 public int getTotalUtil() { 93 return mTotalUtil; 94 } 95 96 /** 97 * Helper method to return percent utilization for a device. Returns 0 if no utilization 98 * data exists for device 99 */ 100 public Integer getUtilForDevice(String serial) { 101 Integer util = mDeviceUtil.get(serial); 102 if (util == null) { 103 return 0; 104 } 105 return util; 106 } 107 } 108 109 private class DeviceUtilRecord { 110 // store samples of device util, where 0 = avail, 1 = allocated 111 // TODO: save memory by using CircularBitArray 112 private CircularByteArray mData; 113 private int mConsecutiveMissedSamples = 0; 114 115 DeviceUtilRecord() { 116 mData = new CircularByteArray(mMaxSamples); 117 } 118 119 public void addSample(DeviceAllocationState state) { 120 if (DeviceAllocationState.Allocated.equals(state)) { 121 mData.add((byte)1); 122 } else { 123 mData.add((byte)0); 124 } 125 mConsecutiveMissedSamples = 0; 126 } 127 128 public long getNumAllocations() { 129 return mData.getSum(); 130 } 131 132 public long getTotalSamples() { 133 return mData.size(); 134 } 135 136 /** 137 * Record sample for missing device. 138 * 139 * @param serial device serial number 140 * @return true if sample was added, false if device has been missing for more than max 141 * samples 142 */ 143 public boolean addMissingSample(String serial) { 144 mConsecutiveMissedSamples++; 145 if (mConsecutiveMissedSamples > mMaxSamples) { 146 return false; 147 } 148 mData.add((byte)0); 149 return true; 150 } 151 } 152 153 private class SamplingTask extends TimerTask { 154 @Override 155 public void run() { 156 CLog.d("Collecting utilization"); 157 // track devices that we have records for, but are not reported by device lister 158 Map<String, DeviceUtilRecord> goneDevices = new HashMap<>(mDeviceUtilMap); 159 160 for (DeviceDescriptor deviceDesc : mDeviceLister.listDevices()) { 161 DeviceUtilRecord record = getDeviceRecord(deviceDesc.getSerial()); 162 record.addSample(deviceDesc.getState()); 163 goneDevices.remove(deviceDesc.getSerial()); 164 } 165 166 // now record samples for gone devices 167 for (Map.Entry<String, DeviceUtilRecord> goneSerialEntry : goneDevices.entrySet()) { 168 String serial = goneSerialEntry.getKey(); 169 if (!goneSerialEntry.getValue().addMissingSample(serial)) { 170 CLog.d("Forgetting device %s", serial); 171 mDeviceUtilMap.remove(serial); 172 } 173 } 174 } 175 } 176 177 private int mMaxSamples; 178 179 /** a map of device serial to device records */ 180 private Map<String, DeviceUtilRecord> mDeviceUtilMap = new Hashtable<>(); 181 182 private DeviceLister mDeviceLister; 183 184 private Timer mTimer; 185 private SamplingTask mSamplingTask = new SamplingTask(); 186 187 /** 188 * Get the device utilization up to the last 24 hours 189 */ 190 public synchronized UtilizationDesc getUtilizationStats() { 191 CLog.d("Calculating device util"); 192 193 long totalAllocSamples = 0; 194 long totalSamples = 0; 195 Map<String, Integer> deviceUtilMap = new HashMap<>(); 196 for (Map.Entry<String, DeviceUtilRecord> deviceRecordEntry : mDeviceUtilMap.entrySet()) { 197 if (shouldTrackDevice(deviceRecordEntry.getKey())) { 198 long allocSamples = deviceRecordEntry.getValue().getNumAllocations(); 199 long numSamples = deviceRecordEntry.getValue().getTotalSamples(); 200 totalAllocSamples += allocSamples; 201 totalSamples += numSamples; 202 deviceUtilMap.put(deviceRecordEntry.getKey(), getUtil(allocSamples, numSamples)); 203 } 204 } 205 return new UtilizationDesc(getUtil(totalAllocSamples, totalSamples), deviceUtilMap); 206 } 207 208 /** 209 * Get device utilization as a percent 210 */ 211 private static int getUtil(long allocSamples, long numSamples) { 212 if (numSamples <= 0) { 213 return 0; 214 } 215 return (int)((allocSamples * 100) / numSamples); 216 } 217 218 @Override 219 public void run() { 220 calculateMaxSamples(); 221 mTimer = new Timer(); 222 mTimer.scheduleAtFixedRate(mSamplingTask, INITIAL_DELAY_MS, mSamplingIntervalSec * 1000); 223 } 224 225 @Override 226 public void stop() { 227 if (mTimer != null) { 228 mTimer.cancel(); 229 mTimer.purge(); 230 } 231 } 232 233 @Override 234 public void setDeviceLister(DeviceLister lister) { 235 mDeviceLister = lister; 236 } 237 238 /** 239 * Listens to device state changes and records time that device transitions from or to 240 * available or allocated state. 241 */ 242 @Override 243 public synchronized void notifyDeviceStateChange(String serial, DeviceAllocationState oldState, 244 DeviceAllocationState newState) { 245 if (mNullDeviceAllocated && mEmulatorAllocated) { 246 // optimization, don't enter calculation below unless needed 247 return; 248 } 249 if (DeviceAllocationState.Allocated.equals(newState)) { 250 IDeviceManager dvcMgr = getDeviceManager(); 251 if (dvcMgr.isNullDevice(serial)) { 252 mNullDeviceAllocated = true; 253 } else if (dvcMgr.isEmulator(serial)) { 254 mEmulatorAllocated = true; 255 } 256 } 257 } 258 259 /** 260 * Get the device util records for given serial, creating if necessary. 261 */ 262 private DeviceUtilRecord getDeviceRecord(String serial) { 263 DeviceUtilRecord r = mDeviceUtilMap.get(serial); 264 if (r == null) { 265 r = new DeviceUtilRecord(); 266 mDeviceUtilMap.put(serial, r); 267 } 268 return r; 269 } 270 271 private boolean shouldTrackDevice(String serial) { 272 IDeviceManager dvcMgr = getDeviceManager(); 273 if (dvcMgr.isNullDevice(serial)) { 274 switch (mCollectNullDevice) { 275 case ALWAYS_INCLUDE: 276 return true; 277 case IGNORE: 278 return false; 279 case INCLUDE_IF_USED: 280 return mNullDeviceAllocated; 281 } 282 } else if (dvcMgr.isEmulator(serial)) { 283 switch (mCollectEmulator) { 284 case ALWAYS_INCLUDE: 285 return true; 286 case IGNORE: 287 return false; 288 case INCLUDE_IF_USED: 289 return mEmulatorAllocated; 290 } 291 } 292 return true; 293 } 294 295 IDeviceManager getDeviceManager() { 296 return GlobalConfiguration.getDeviceManagerInstance(); 297 } 298 299 TimerTask getSamplingTask() { 300 return mSamplingTask; 301 } 302 303 // @VisibleForTesting 304 void calculateMaxSamples() { 305 // find max samples to collect by converting sample window to seconds then divide by 306 // sampling interval 307 mMaxSamples = mSampleWindowHours * 60 * 60 / mSamplingIntervalSec; 308 assert(mMaxSamples > 0); 309 } 310 311 // @VisibleForTesting 312 void setMaxSamples(int maxSamples) { 313 mMaxSamples = maxSamples; 314 } 315 316 // @VisibleForTesting 317 int getMaxSamples() { 318 return mMaxSamples; 319 } 320 } 321