1 /* 2 * Copyright (C) 2012 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.ddmlib.MultiLineReceiver; 20 import com.android.tradefed.log.LogUtil.CLog; 21 import com.android.tradefed.util.SimpleStats; 22 23 import java.io.BufferedWriter; 24 import java.io.File; 25 import java.io.FileWriter; 26 import java.io.IOException; 27 import java.util.ArrayList; 28 import java.util.HashMap; 29 import java.util.LinkedList; 30 import java.util.List; 31 import java.util.Map; 32 33 /** 34 * Helper class which runs {@code cpustats} continuously on an {@link ITestDevice} and parses the 35 * output. 36 * <p> 37 * Provides a method to record the output of {@code cpustats} and get all the cpu usage measurements 38 * as well methods for performing calculations on that data to find the mean of the cpu workload, 39 * the approximate cpu frequency, and the percentage of used cpu frequency. 40 * </p><p> 41 * This is meant to be a replacement for {@link TopHelper}, which does not provide stats about cpu 42 * frequency and has a higher overhead due to measuring processes/threads. 43 * </p><p> 44 * The {@code cpustats} command was added in the Jellybean release, so this collector should only be 45 * used for new tests. 46 * </p> 47 * @see TopHelper 48 */ 49 public class CpuStatsCollector extends Thread { 50 private static final String CPU_STATS_CMD = "cpustats -m -d %s"; 51 52 private final ITestDevice mTestDevice; 53 private long mDelay; 54 55 /** 56 * Used to distinguish between the different CPU time categories. 57 */ 58 enum TimeCategory { 59 USER, 60 NICE, 61 SYS, 62 IDLE, 63 IOW, 64 IRQ, 65 SIRQ 66 } 67 68 /** 69 * Class for holding parsed output data for a single {@code cpustats} output. 70 * <p> 71 * This class holds parsed output, and also performs simple calculations on that data. The 72 * methods which perform these calucations should only be called after the object has been 73 * populated. 74 * </p> 75 */ 76 public static class CpuStats { 77 public Map<TimeCategory, Integer> mTimeStats = new HashMap<TimeCategory, Integer>(); 78 public Map<Integer, Integer> mFreqStats = new HashMap<Integer, Integer>(); 79 private Map<TimeCategory, Double> mPercentageStats = new HashMap<TimeCategory, Double>(); 80 private Integer mTotalTime = null; 81 private Double mAverageMhz = null; 82 83 /** 84 * Get the percentage of cycles used on a given category. 85 */ 86 public Double getPercentage(TimeCategory category) { 87 if (!mPercentageStats.containsKey(category)) { 88 mPercentageStats.put(category, 100.0 * mTimeStats.get(category) / getTotalTime()); 89 } 90 return mPercentageStats.get(category); 91 } 92 93 /** 94 * Estimate the MHz used by the cpu during the duration. 95 * <p> 96 * This is calculated by: 97 * </p><code> 98 * ((sum(c_time) - idle) / sum(c_time)) * (sum(freq * f_time) / sum(f_time)) 99 * </code><p> 100 * where {@code c_time} is the time for a given category, {@code idle} is the time in the 101 * idle state, {@code freq} is a frequency and {@code f_time} is the time spent in that 102 * frequency. 103 * </p> 104 */ 105 public Double getEstimatedMhz() { 106 if (mFreqStats.isEmpty()) { 107 return null; 108 } 109 return getTotalUsage() * getAverageMhz(); 110 } 111 112 /** 113 * Get the amount of MHz as a percentage of available MHz used by the cpu during the 114 * duration. 115 * <p> 116 * This is calculated by: 117 * </p><code> 118 * 100 * sum(freq * f_time) / (max_freq * sum(f_time)) 119 * </code><p> 120 * where {@code freq} is a frequency, {@code f_time} is the time spent in that frequency, 121 * and {@code max_freq} is the maximum frequency the cpu is capable of. 122 * </p> 123 */ 124 public Double getUsedMhzPercentage() { 125 if (mFreqStats.isEmpty()) { 126 return null; 127 } 128 return 100.0 * getAverageMhz() / getMaxMhz(); 129 } 130 131 /** 132 * Get the total usage, or the sum of all the times except idle over the sum of all the 133 * times. 134 */ 135 private Double getTotalUsage() { 136 return (double) (getTotalTime() - mTimeStats.get(TimeCategory.IDLE)) / getTotalTime(); 137 } 138 139 /** 140 * Get the average MHz. 141 * <p> 142 * This is calculated by: 143 * </p><code> 144 * sum(freq * f_time) / sum(f_time)) 145 * </code><p> 146 * where {@code freq} is a frequency and {@code f_time} is the time spent in that frequency. 147 * </p> 148 */ 149 private Double getAverageMhz() { 150 if (mFreqStats.isEmpty()) { 151 return null; 152 } 153 if (mAverageMhz == null) { 154 double sumFreqTime = 0.0; 155 long sumTime = 0; 156 for (Map.Entry<Integer, Integer> e : mFreqStats.entrySet()) { 157 sumFreqTime += e.getKey() * e.getValue() / 1000.0; 158 sumTime += e.getValue(); 159 } 160 mAverageMhz = sumFreqTime / sumTime; 161 } 162 return mAverageMhz; 163 } 164 165 /** 166 * Get the maximum possible MHz. 167 */ 168 private Double getMaxMhz() { 169 if (mFreqStats.isEmpty()) { 170 return null; 171 } 172 int max = 0; 173 for (int freq : mFreqStats.keySet()) { 174 max = Math.max(max, freq); 175 } 176 return max / 1000.0; 177 } 178 179 /** 180 * Get the total amount of time cycles. 181 */ 182 private Integer getTotalTime() { 183 if (mTotalTime == null) { 184 int sum = 0; 185 for (int time : mTimeStats.values()) { 186 sum += time; 187 } 188 mTotalTime = sum; 189 } 190 return mTotalTime; 191 } 192 } 193 194 /** 195 * Receiver which parses the output from {@code cpustats} and optionally logs to a file. 196 */ 197 public static class CpuStatsReceiver extends MultiLineReceiver { 198 private Map<String, List<CpuStats>> mCpuStats = new HashMap<String, List<CpuStats>>(4); 199 200 private boolean mIsCancelled = false; 201 private File mLogFile = null; 202 private BufferedWriter mLogWriter = null; 203 204 public CpuStatsReceiver() { 205 setTrimLine(false); 206 } 207 208 /** 209 * Specify a file to log the output to. 210 * <p> 211 * This can be called at any time in the receivers life cycle, but only new output will be 212 * logged to the file. 213 * </p> 214 */ 215 public synchronized void logToFile(File logFile) { 216 try { 217 mLogFile = logFile; 218 mLogWriter = new BufferedWriter(new FileWriter(mLogFile)); 219 } catch (IOException e) { 220 CLog.e("IOException when creating a fileWriter:"); 221 CLog.e(e); 222 mLogWriter = null; 223 } 224 } 225 226 /** 227 * {@inheritDoc} 228 */ 229 @Override 230 public void processNewLines(String[] lines) { 231 if (mIsCancelled) { 232 return; 233 } 234 synchronized (this) { 235 if (mLogWriter != null) { 236 try { 237 for (String line : lines) { 238 mLogWriter.write(line + "\n"); 239 } 240 } catch (IOException e) { 241 CLog.e("Error writing to file"); 242 CLog.e(e); 243 } 244 } 245 } 246 for (String line : lines) { 247 String[] args = line.trim().split(","); 248 if (args.length >= 8) { 249 try { 250 CpuStats s = new CpuStats(); 251 s.mTimeStats.put(TimeCategory.USER, Integer.parseInt(args[1])); 252 s.mTimeStats.put(TimeCategory.NICE, Integer.parseInt(args[2])); 253 s.mTimeStats.put(TimeCategory.SYS, Integer.parseInt(args[3])); 254 s.mTimeStats.put(TimeCategory.IDLE, Integer.parseInt(args[4])); 255 s.mTimeStats.put(TimeCategory.IOW, Integer.parseInt(args[5])); 256 s.mTimeStats.put(TimeCategory.IRQ, Integer.parseInt(args[6])); 257 s.mTimeStats.put(TimeCategory.SIRQ, Integer.parseInt(args[7])); 258 for (int i = 0; i + 8 < args.length; i += 2) { 259 s.mFreqStats.put(Integer.parseInt(args[8 + i]), 260 Integer.parseInt(args[9 + i])); 261 } 262 synchronized(this) { 263 if (!mCpuStats.containsKey(args[0])) { 264 mCpuStats.put(args[0], new LinkedList<CpuStats>()); 265 } 266 mCpuStats.get(args[0]).add(s); 267 } 268 } catch (NumberFormatException e) { 269 CLog.w("Unexpected input: %s", line.trim()); 270 } catch (IndexOutOfBoundsException e) { 271 CLog.w("Unexpected input: %s", line.trim()); 272 } 273 } else if (args.length > 1 || !"".equals(args[0])) { 274 CLog.w("Unexpected input: %s", line.trim()); 275 } 276 } 277 } 278 279 /** 280 * Cancels the {@code cpustats} command. 281 */ 282 public synchronized void cancel() { 283 if (mIsCancelled) { 284 return; 285 } 286 mIsCancelled = true; 287 if (mLogWriter != null) { 288 try { 289 mLogWriter.flush(); 290 mLogWriter.close(); 291 } catch (IOException e) { 292 CLog.e("Error closing writer"); 293 CLog.e(e); 294 } finally { 295 mLogWriter = null; 296 } 297 } 298 } 299 300 /** 301 * {@inheritDoc} 302 */ 303 @Override 304 public synchronized boolean isCancelled() { 305 return mIsCancelled; 306 } 307 308 /** 309 * Get all the parsed data as a map from label to lists of {@link CpuStats} objects. 310 */ 311 public synchronized Map<String, List<CpuStats>> getCpuStats() { 312 Map<String, List<CpuStats>> copy = new HashMap<String, List<CpuStats>>( 313 mCpuStats.size()); 314 for (String k : mCpuStats.keySet()) { 315 copy.put(k, new ArrayList<CpuStats>(mCpuStats.get(k))); 316 } 317 return copy; 318 } 319 } 320 321 private CpuStatsReceiver mReceiver = new CpuStatsReceiver(); 322 323 /** 324 * Create a {@link CpuStatsCollector}. 325 * 326 * @param testDevice The test device 327 */ 328 public CpuStatsCollector(ITestDevice testDevice) { 329 this(testDevice, 1); 330 } 331 332 /** 333 * Create a {@link CpuStatsCollector} with a delay specified. 334 * 335 * @param testDevice The test device 336 * @param delay The delay time in seconds 337 */ 338 public CpuStatsCollector(ITestDevice testDevice, int delay) { 339 super("CpuStatsCollector"); 340 mTestDevice = testDevice; 341 mDelay = delay; 342 } 343 344 /** 345 * Specify a file to log output to. 346 * 347 * @param logFile the file to log output to. 348 */ 349 public void logToFile(File logFile) { 350 mReceiver.logToFile(logFile); 351 } 352 353 /** 354 * Cancels the {@code cpustats} command. 355 */ 356 public synchronized void cancel() { 357 mReceiver.cancel(); 358 } 359 360 /** 361 * Gets whether the {@code cpustats} command is canceled. 362 * 363 * @return if the {@code cpustats} command is canceled. 364 */ 365 public synchronized boolean isCancelled() { 366 return mReceiver.isCancelled(); 367 } 368 369 /** 370 * {@inheritDoc} 371 */ 372 @Override 373 public void run() { 374 try { 375 mTestDevice.executeShellCommand(String.format(CPU_STATS_CMD, mDelay), mReceiver); 376 } catch (DeviceNotAvailableException e) { 377 CLog.e("Device %s not available:", mTestDevice.getSerialNumber()); 378 CLog.e(e); 379 } 380 } 381 382 /** 383 * Get the mapping of labels to lists of {@link CpuStats} instances. 384 * 385 * @return a mapping of labels to lists of {@link CpuStats} instances. The labels will include 386 * "Total" and "cpu0"..."cpuN" for each CPU on the device. 387 */ 388 public Map<String, List<CpuStats>> getCpuStats() { 389 return mReceiver.getCpuStats(); 390 } 391 392 /** 393 * Get the mean of the total CPU usage for a list of {@link CpuStats}. 394 * 395 * @param cpuStats the list of {@link CpuStats} 396 * @return The average usage as a percentage (0 to 100). 397 */ 398 public static Double getTotalPercentageMean(List<CpuStats> cpuStats) { 399 SimpleStats stats = new SimpleStats(); 400 for (CpuStats s : cpuStats) { 401 if (s.getTotalUsage() != null) { 402 stats.add(s.getTotalUsage()); 403 } 404 } 405 return 100 * stats.mean(); 406 } 407 408 /** 409 * Get the mean of the user and nice CPU usage for a list of {@link CpuStats}. 410 * 411 * @param cpuStats the list of {@link CpuStats} 412 * @return The average usage as a percentage (0 to 100). 413 */ 414 public static Double getUserPercentageMean(List<CpuStats> cpuStats) { 415 return (getPercentageMean(cpuStats, TimeCategory.USER) + 416 getPercentageMean(cpuStats, TimeCategory.NICE)); 417 } 418 419 /** 420 * Get the mean of the system CPU usage for a list of {@link CpuStats}. 421 * 422 * @param cpuStats the list of {@link CpuStats} 423 * @return The average usage as a percentage (0 to 100). 424 */ 425 public static Double getSystemPercentageMean(List<CpuStats> cpuStats) { 426 return getPercentageMean(cpuStats, TimeCategory.SYS); 427 } 428 429 /** 430 * Get the mean of the iow CPU usage for a list of {@link CpuStats}. 431 * 432 * @param cpuStats the list of {@link CpuStats} 433 * @return The average usage as a percentage (0 to 100). 434 */ 435 public static Double getIowPercentageMean(List<CpuStats> cpuStats) { 436 return getPercentageMean(cpuStats, TimeCategory.IOW); 437 } 438 439 /** 440 * Get the mean of the IRQ and SIRQ CPU usage for a list of {@link CpuStats}. 441 * 442 * @param cpuStats the list of {@link CpuStats} 443 * @return The average usage as a percentage (0 to 100). 444 */ 445 public static Double getIrqPercentageMean(List<CpuStats> cpuStats) { 446 return (getPercentageMean(cpuStats, TimeCategory.IRQ) + 447 getPercentageMean(cpuStats, TimeCategory.SIRQ)); 448 } 449 450 /** 451 * Get the mean of the estimated MHz for a list of {@link CpuStats}. 452 * 453 * @param cpuStats the list of {@link CpuStats} 454 * @return The average estimated MHz in MHz. 455 * @see CpuStats#getEstimatedMhz() 456 */ 457 public static Double getEstimatedMhzMean(List<CpuStats> cpuStats) { 458 SimpleStats stats = new SimpleStats(); 459 for (CpuStats s : cpuStats) { 460 if (!s.mFreqStats.isEmpty()) { 461 stats.add(s.getEstimatedMhz()); 462 } 463 } 464 return stats.mean(); 465 } 466 467 /** 468 * Get the mean of the used MHz for a list of {@link CpuStats}. 469 * 470 * @param cpuStats the list of {@link CpuStats} 471 * @return The average used MHz as a percentage (0 to 100). 472 * @see CpuStats#getUsedMhzPercentage() 473 */ 474 public static Double getUsedMhzPercentageMean(List<CpuStats> cpuStats) { 475 SimpleStats stats = new SimpleStats(); 476 for (CpuStats s : cpuStats) { 477 if (!s.mFreqStats.isEmpty()) { 478 stats.add(s.getUsedMhzPercentage()); 479 } 480 } 481 return stats.mean(); 482 } 483 484 /** 485 * Helper method for calculating the percentage mean for a {@link TimeCategory}. 486 */ 487 private static Double getPercentageMean(List<CpuStats> cpuStats, TimeCategory category) { 488 SimpleStats stats = new SimpleStats(); 489 for (CpuStats s : cpuStats) { 490 stats.add(s.getPercentage(category)); 491 } 492 return stats.mean(); 493 } 494 495 /** 496 * Method to access the receiver used to parse the cpu stats. Used for unit testing. 497 */ 498 CpuStatsReceiver getReceiver() { 499 return mReceiver; 500 } 501 } 502