1 /* 2 * Copyright 2015 The WebRTC Project Authors. All rights reserved. 3 * 4 * Use of this source code is governed by a BSD-style license 5 * that can be found in the LICENSE file in the root of the source 6 * tree. An additional intellectual property rights grant can be found 7 * in the file PATENTS. All contributing project authors may 8 * be found in the AUTHORS file in the root of the source tree. 9 */ 10 11 package org.appspot.apprtc; 12 13 import android.annotation.TargetApi; 14 import android.content.Context; 15 import android.content.Intent; 16 import android.content.IntentFilter; 17 import android.os.BatteryManager; 18 import android.os.Build; 19 import android.os.SystemClock; 20 import android.support.annotation.Nullable; 21 import android.util.Log; 22 import java.io.BufferedReader; 23 import java.io.FileInputStream; 24 import java.io.FileNotFoundException; 25 import java.io.IOException; 26 import java.io.InputStreamReader; 27 import java.nio.charset.Charset; 28 import java.util.Arrays; 29 import java.util.Scanner; 30 import java.util.concurrent.Executors; 31 import java.util.concurrent.Future; 32 import java.util.concurrent.ScheduledExecutorService; 33 import java.util.concurrent.TimeUnit; 34 35 /** 36 * Simple CPU monitor. The caller creates a CpuMonitor object which can then 37 * be used via sampleCpuUtilization() to collect the percentual use of the 38 * cumulative CPU capacity for all CPUs running at their nominal frequency. 3 39 * values are generated: (1) getCpuCurrent() returns the use since the last 40 * sampleCpuUtilization(), (2) getCpuAvg3() returns the use since 3 prior 41 * calls, and (3) getCpuAvgAll() returns the use over all SAMPLE_SAVE_NUMBER 42 * calls. 43 * 44 * <p>CPUs in Android are often "offline", and while this of course means 0 Hz 45 * as current frequency, in this state we cannot even get their nominal 46 * frequency. We therefore tread carefully, and allow any CPU to be missing. 47 * Missing CPUs are assumed to have the same nominal frequency as any close 48 * lower-numbered CPU, but as soon as it is online, we'll get their proper 49 * frequency and remember it. (Since CPU 0 in practice always seem to be 50 * online, this unidirectional frequency inheritance should be no problem in 51 * practice.) 52 * 53 * <p>Caveats: 54 * o No provision made for zany "turbo" mode, common in the x86 world. 55 * o No provision made for ARM big.LITTLE; if CPU n can switch behind our 56 * back, we might get incorrect estimates. 57 * o This is not thread-safe. To call asynchronously, create different 58 * CpuMonitor objects. 59 * 60 * <p>If we can gather enough info to generate a sensible result, 61 * sampleCpuUtilization returns true. It is designed to never throw an 62 * exception. 63 * 64 * <p>sampleCpuUtilization should not be called too often in its present form, 65 * since then deltas would be small and the percent values would fluctuate and 66 * be unreadable. If it is desirable to call it more often than say once per 67 * second, one would need to increase SAMPLE_SAVE_NUMBER and probably use 68 * Queue<Integer> to avoid copying overhead. 69 * 70 * <p>Known problems: 71 * 1. Nexus 7 devices running Kitkat have a kernel which often output an 72 * incorrect 'idle' field in /proc/stat. The value is close to twice the 73 * correct value, and then returns to back to correct reading. Both when 74 * jumping up and back down we might create faulty CPU load readings. 75 */ 76 @TargetApi(Build.VERSION_CODES.KITKAT) 77 class CpuMonitor { 78 private static final String TAG = "CpuMonitor"; 79 private static final int MOVING_AVERAGE_SAMPLES = 5; 80 81 private static final int CPU_STAT_SAMPLE_PERIOD_MS = 2000; 82 private static final int CPU_STAT_LOG_PERIOD_MS = 6000; 83 84 private final Context appContext; 85 // User CPU usage at current frequency. 86 private final MovingAverage userCpuUsage; 87 // System CPU usage at current frequency. 88 private final MovingAverage systemCpuUsage; 89 // Total CPU usage relative to maximum frequency. 90 private final MovingAverage totalCpuUsage; 91 // CPU frequency in percentage from maximum. 92 private final MovingAverage frequencyScale; 93 94 @Nullable 95 private ScheduledExecutorService executor; 96 private long lastStatLogTimeMs; 97 private long[] cpuFreqMax; 98 private int cpusPresent; 99 private int actualCpusPresent; 100 private boolean initialized; 101 private boolean cpuOveruse; 102 private String[] maxPath; 103 private String[] curPath; 104 private double[] curFreqScales; 105 @Nullable 106 private ProcStat lastProcStat; 107 108 private static class ProcStat { 109 final long userTime; 110 final long systemTime; 111 final long idleTime; 112 ProcStat(long userTime, long systemTime, long idleTime)113 ProcStat(long userTime, long systemTime, long idleTime) { 114 this.userTime = userTime; 115 this.systemTime = systemTime; 116 this.idleTime = idleTime; 117 } 118 } 119 120 private static class MovingAverage { 121 private final int size; 122 private double sum; 123 private double currentValue; 124 private double[] circBuffer; 125 private int circBufferIndex; 126 MovingAverage(int size)127 public MovingAverage(int size) { 128 if (size <= 0) { 129 throw new AssertionError("Size value in MovingAverage ctor should be positive."); 130 } 131 this.size = size; 132 circBuffer = new double[size]; 133 } 134 reset()135 public void reset() { 136 Arrays.fill(circBuffer, 0); 137 circBufferIndex = 0; 138 sum = 0; 139 currentValue = 0; 140 } 141 addValue(double value)142 public void addValue(double value) { 143 sum -= circBuffer[circBufferIndex]; 144 circBuffer[circBufferIndex++] = value; 145 currentValue = value; 146 sum += value; 147 if (circBufferIndex >= size) { 148 circBufferIndex = 0; 149 } 150 } 151 getCurrent()152 public double getCurrent() { 153 return currentValue; 154 } 155 getAverage()156 public double getAverage() { 157 return sum / (double) size; 158 } 159 } 160 isSupported()161 public static boolean isSupported() { 162 return Build.VERSION.SDK_INT >= Build.VERSION_CODES.KITKAT 163 && Build.VERSION.SDK_INT < Build.VERSION_CODES.N; 164 } 165 CpuMonitor(Context context)166 public CpuMonitor(Context context) { 167 if (!isSupported()) { 168 throw new RuntimeException("CpuMonitor is not supported on this Android version."); 169 } 170 171 Log.d(TAG, "CpuMonitor ctor."); 172 appContext = context.getApplicationContext(); 173 userCpuUsage = new MovingAverage(MOVING_AVERAGE_SAMPLES); 174 systemCpuUsage = new MovingAverage(MOVING_AVERAGE_SAMPLES); 175 totalCpuUsage = new MovingAverage(MOVING_AVERAGE_SAMPLES); 176 frequencyScale = new MovingAverage(MOVING_AVERAGE_SAMPLES); 177 lastStatLogTimeMs = SystemClock.elapsedRealtime(); 178 179 scheduleCpuUtilizationTask(); 180 } 181 pause()182 public void pause() { 183 if (executor != null) { 184 Log.d(TAG, "pause"); 185 executor.shutdownNow(); 186 executor = null; 187 } 188 } 189 resume()190 public void resume() { 191 Log.d(TAG, "resume"); 192 resetStat(); 193 scheduleCpuUtilizationTask(); 194 } 195 196 // TODO(bugs.webrtc.org/8491): Remove NoSynchronizedMethodCheck suppression. 197 @SuppressWarnings("NoSynchronizedMethodCheck") reset()198 public synchronized void reset() { 199 if (executor != null) { 200 Log.d(TAG, "reset"); 201 resetStat(); 202 cpuOveruse = false; 203 } 204 } 205 206 // TODO(bugs.webrtc.org/8491): Remove NoSynchronizedMethodCheck suppression. 207 @SuppressWarnings("NoSynchronizedMethodCheck") getCpuUsageCurrent()208 public synchronized int getCpuUsageCurrent() { 209 return doubleToPercent(userCpuUsage.getCurrent() + systemCpuUsage.getCurrent()); 210 } 211 212 // TODO(bugs.webrtc.org/8491): Remove NoSynchronizedMethodCheck suppression. 213 @SuppressWarnings("NoSynchronizedMethodCheck") getCpuUsageAverage()214 public synchronized int getCpuUsageAverage() { 215 return doubleToPercent(userCpuUsage.getAverage() + systemCpuUsage.getAverage()); 216 } 217 218 // TODO(bugs.webrtc.org/8491): Remove NoSynchronizedMethodCheck suppression. 219 @SuppressWarnings("NoSynchronizedMethodCheck") getFrequencyScaleAverage()220 public synchronized int getFrequencyScaleAverage() { 221 return doubleToPercent(frequencyScale.getAverage()); 222 } 223 scheduleCpuUtilizationTask()224 private void scheduleCpuUtilizationTask() { 225 if (executor != null) { 226 executor.shutdownNow(); 227 executor = null; 228 } 229 230 executor = Executors.newSingleThreadScheduledExecutor(); 231 @SuppressWarnings("unused") // Prevent downstream linter warnings. 232 Future<?> possiblyIgnoredError = executor.scheduleAtFixedRate(new Runnable() { 233 @Override 234 public void run() { 235 cpuUtilizationTask(); 236 } 237 }, 0, CPU_STAT_SAMPLE_PERIOD_MS, TimeUnit.MILLISECONDS); 238 } 239 cpuUtilizationTask()240 private void cpuUtilizationTask() { 241 boolean cpuMonitorAvailable = sampleCpuUtilization(); 242 if (cpuMonitorAvailable 243 && SystemClock.elapsedRealtime() - lastStatLogTimeMs >= CPU_STAT_LOG_PERIOD_MS) { 244 lastStatLogTimeMs = SystemClock.elapsedRealtime(); 245 String statString = getStatString(); 246 Log.d(TAG, statString); 247 } 248 } 249 init()250 private void init() { 251 try (FileInputStream fin = new FileInputStream("/sys/devices/system/cpu/present"); 252 InputStreamReader streamReader = new InputStreamReader(fin, Charset.forName("UTF-8")); 253 BufferedReader reader = new BufferedReader(streamReader); 254 Scanner scanner = new Scanner(reader).useDelimiter("[-\n]");) { 255 scanner.nextInt(); // Skip leading number 0. 256 cpusPresent = 1 + scanner.nextInt(); 257 scanner.close(); 258 } catch (FileNotFoundException e) { 259 Log.e(TAG, "Cannot do CPU stats since /sys/devices/system/cpu/present is missing"); 260 } catch (IOException e) { 261 Log.e(TAG, "Error closing file"); 262 } catch (Exception e) { 263 Log.e(TAG, "Cannot do CPU stats due to /sys/devices/system/cpu/present parsing problem"); 264 } 265 266 cpuFreqMax = new long[cpusPresent]; 267 maxPath = new String[cpusPresent]; 268 curPath = new String[cpusPresent]; 269 curFreqScales = new double[cpusPresent]; 270 for (int i = 0; i < cpusPresent; i++) { 271 cpuFreqMax[i] = 0; // Frequency "not yet determined". 272 curFreqScales[i] = 0; 273 maxPath[i] = "/sys/devices/system/cpu/cpu" + i + "/cpufreq/cpuinfo_max_freq"; 274 curPath[i] = "/sys/devices/system/cpu/cpu" + i + "/cpufreq/scaling_cur_freq"; 275 } 276 277 lastProcStat = new ProcStat(0, 0, 0); 278 resetStat(); 279 280 initialized = true; 281 } 282 resetStat()283 private synchronized void resetStat() { 284 userCpuUsage.reset(); 285 systemCpuUsage.reset(); 286 totalCpuUsage.reset(); 287 frequencyScale.reset(); 288 lastStatLogTimeMs = SystemClock.elapsedRealtime(); 289 } 290 getBatteryLevel()291 private int getBatteryLevel() { 292 // Use sticky broadcast with null receiver to read battery level once only. 293 Intent intent = appContext.registerReceiver( 294 null /* receiver */, new IntentFilter(Intent.ACTION_BATTERY_CHANGED)); 295 296 int batteryLevel = 0; 297 int batteryScale = intent.getIntExtra(BatteryManager.EXTRA_SCALE, 100); 298 if (batteryScale > 0) { 299 batteryLevel = 300 (int) (100f * intent.getIntExtra(BatteryManager.EXTRA_LEVEL, 0) / batteryScale); 301 } 302 return batteryLevel; 303 } 304 305 /** 306 * Re-measure CPU use. Call this method at an interval of around 1/s. 307 * This method returns true on success. The fields 308 * cpuCurrent, cpuAvg3, and cpuAvgAll are updated on success, and represents: 309 * cpuCurrent: The CPU use since the last sampleCpuUtilization call. 310 * cpuAvg3: The average CPU over the last 3 calls. 311 * cpuAvgAll: The average CPU over the last SAMPLE_SAVE_NUMBER calls. 312 */ sampleCpuUtilization()313 private synchronized boolean sampleCpuUtilization() { 314 long lastSeenMaxFreq = 0; 315 long cpuFreqCurSum = 0; 316 long cpuFreqMaxSum = 0; 317 318 if (!initialized) { 319 init(); 320 } 321 if (cpusPresent == 0) { 322 return false; 323 } 324 325 actualCpusPresent = 0; 326 for (int i = 0; i < cpusPresent; i++) { 327 /* 328 * For each CPU, attempt to first read its max frequency, then its 329 * current frequency. Once as the max frequency for a CPU is found, 330 * save it in cpuFreqMax[]. 331 */ 332 333 curFreqScales[i] = 0; 334 if (cpuFreqMax[i] == 0) { 335 // We have never found this CPU's max frequency. Attempt to read it. 336 long cpufreqMax = readFreqFromFile(maxPath[i]); 337 if (cpufreqMax > 0) { 338 Log.d(TAG, "Core " + i + ". Max frequency: " + cpufreqMax); 339 lastSeenMaxFreq = cpufreqMax; 340 cpuFreqMax[i] = cpufreqMax; 341 maxPath[i] = null; // Kill path to free its memory. 342 } 343 } else { 344 lastSeenMaxFreq = cpuFreqMax[i]; // A valid, previously read value. 345 } 346 347 long cpuFreqCur = readFreqFromFile(curPath[i]); 348 if (cpuFreqCur == 0 && lastSeenMaxFreq == 0) { 349 // No current frequency information for this CPU core - ignore it. 350 continue; 351 } 352 if (cpuFreqCur > 0) { 353 actualCpusPresent++; 354 } 355 cpuFreqCurSum += cpuFreqCur; 356 357 /* Here, lastSeenMaxFreq might come from 358 * 1. cpuFreq[i], or 359 * 2. a previous iteration, or 360 * 3. a newly read value, or 361 * 4. hypothetically from the pre-loop dummy. 362 */ 363 cpuFreqMaxSum += lastSeenMaxFreq; 364 if (lastSeenMaxFreq > 0) { 365 curFreqScales[i] = (double) cpuFreqCur / lastSeenMaxFreq; 366 } 367 } 368 369 if (cpuFreqCurSum == 0 || cpuFreqMaxSum == 0) { 370 Log.e(TAG, "Could not read max or current frequency for any CPU"); 371 return false; 372 } 373 374 /* 375 * Since the cycle counts are for the period between the last invocation 376 * and this present one, we average the percentual CPU frequencies between 377 * now and the beginning of the measurement period. This is significantly 378 * incorrect only if the frequencies have peeked or dropped in between the 379 * invocations. 380 */ 381 double currentFrequencyScale = cpuFreqCurSum / (double) cpuFreqMaxSum; 382 if (frequencyScale.getCurrent() > 0) { 383 currentFrequencyScale = (frequencyScale.getCurrent() + currentFrequencyScale) * 0.5; 384 } 385 386 ProcStat procStat = readProcStat(); 387 if (procStat == null) { 388 return false; 389 } 390 391 long diffUserTime = procStat.userTime - lastProcStat.userTime; 392 long diffSystemTime = procStat.systemTime - lastProcStat.systemTime; 393 long diffIdleTime = procStat.idleTime - lastProcStat.idleTime; 394 long allTime = diffUserTime + diffSystemTime + diffIdleTime; 395 396 if (currentFrequencyScale == 0 || allTime == 0) { 397 return false; 398 } 399 400 // Update statistics. 401 frequencyScale.addValue(currentFrequencyScale); 402 403 double currentUserCpuUsage = diffUserTime / (double) allTime; 404 userCpuUsage.addValue(currentUserCpuUsage); 405 406 double currentSystemCpuUsage = diffSystemTime / (double) allTime; 407 systemCpuUsage.addValue(currentSystemCpuUsage); 408 409 double currentTotalCpuUsage = 410 (currentUserCpuUsage + currentSystemCpuUsage) * currentFrequencyScale; 411 totalCpuUsage.addValue(currentTotalCpuUsage); 412 413 // Save new measurements for next round's deltas. 414 lastProcStat = procStat; 415 416 return true; 417 } 418 doubleToPercent(double d)419 private int doubleToPercent(double d) { 420 return (int) (d * 100 + 0.5); 421 } 422 getStatString()423 private synchronized String getStatString() { 424 StringBuilder stat = new StringBuilder(); 425 stat.append("CPU User: ") 426 .append(doubleToPercent(userCpuUsage.getCurrent())) 427 .append("/") 428 .append(doubleToPercent(userCpuUsage.getAverage())) 429 .append(". System: ") 430 .append(doubleToPercent(systemCpuUsage.getCurrent())) 431 .append("/") 432 .append(doubleToPercent(systemCpuUsage.getAverage())) 433 .append(". Freq: ") 434 .append(doubleToPercent(frequencyScale.getCurrent())) 435 .append("/") 436 .append(doubleToPercent(frequencyScale.getAverage())) 437 .append(". Total usage: ") 438 .append(doubleToPercent(totalCpuUsage.getCurrent())) 439 .append("/") 440 .append(doubleToPercent(totalCpuUsage.getAverage())) 441 .append(". Cores: ") 442 .append(actualCpusPresent); 443 stat.append("( "); 444 for (int i = 0; i < cpusPresent; i++) { 445 stat.append(doubleToPercent(curFreqScales[i])).append(" "); 446 } 447 stat.append("). Battery: ").append(getBatteryLevel()); 448 if (cpuOveruse) { 449 stat.append(". Overuse."); 450 } 451 return stat.toString(); 452 } 453 454 /** 455 * Read a single integer value from the named file. Return the read value 456 * or if an error occurs return 0. 457 */ readFreqFromFile(String fileName)458 private long readFreqFromFile(String fileName) { 459 long number = 0; 460 try (FileInputStream stream = new FileInputStream(fileName); 461 InputStreamReader streamReader = new InputStreamReader(stream, Charset.forName("UTF-8")); 462 BufferedReader reader = new BufferedReader(streamReader)) { 463 String line = reader.readLine(); 464 number = parseLong(line); 465 } catch (FileNotFoundException e) { 466 // CPU core is off, so file with its scaling frequency .../cpufreq/scaling_cur_freq 467 // is not present. This is not an error. 468 } catch (IOException e) { 469 // CPU core is off, so file with its scaling frequency .../cpufreq/scaling_cur_freq 470 // is empty. This is not an error. 471 } 472 return number; 473 } 474 parseLong(String value)475 private static long parseLong(String value) { 476 long number = 0; 477 try { 478 number = Long.parseLong(value); 479 } catch (NumberFormatException e) { 480 Log.e(TAG, "parseLong error.", e); 481 } 482 return number; 483 } 484 485 /* 486 * Read the current utilization of all CPUs using the cumulative first line 487 * of /proc/stat. 488 */ 489 @SuppressWarnings("StringSplitter") readProcStat()490 private @Nullable ProcStat readProcStat() { 491 long userTime = 0; 492 long systemTime = 0; 493 long idleTime = 0; 494 try (FileInputStream stream = new FileInputStream("/proc/stat"); 495 InputStreamReader streamReader = new InputStreamReader(stream, Charset.forName("UTF-8")); 496 BufferedReader reader = new BufferedReader(streamReader)) { 497 // line should contain something like this: 498 // cpu 5093818 271838 3512830 165934119 101374 447076 272086 0 0 0 499 // user nice system idle iowait irq softirq 500 String line = reader.readLine(); 501 String[] lines = line.split("\\s+"); 502 int length = lines.length; 503 if (length >= 5) { 504 userTime = parseLong(lines[1]); // user 505 userTime += parseLong(lines[2]); // nice 506 systemTime = parseLong(lines[3]); // system 507 idleTime = parseLong(lines[4]); // idle 508 } 509 if (length >= 8) { 510 userTime += parseLong(lines[5]); // iowait 511 systemTime += parseLong(lines[6]); // irq 512 systemTime += parseLong(lines[7]); // softirq 513 } 514 } catch (FileNotFoundException e) { 515 Log.e(TAG, "Cannot open /proc/stat for reading", e); 516 return null; 517 } catch (Exception e) { 518 Log.e(TAG, "Problems parsing /proc/stat", e); 519 return null; 520 } 521 return new ProcStat(userTime, systemTime, idleTime); 522 } 523 } 524