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.util.Log; 21 import androidx.annotation.Nullable; 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 class CpuMonitor { 77 private static final String TAG = "CpuMonitor"; 78 private static final int MOVING_AVERAGE_SAMPLES = 5; 79 80 private static final int CPU_STAT_SAMPLE_PERIOD_MS = 2000; 81 private static final int CPU_STAT_LOG_PERIOD_MS = 6000; 82 83 private final Context appContext; 84 // User CPU usage at current frequency. 85 private final MovingAverage userCpuUsage; 86 // System CPU usage at current frequency. 87 private final MovingAverage systemCpuUsage; 88 // Total CPU usage relative to maximum frequency. 89 private final MovingAverage totalCpuUsage; 90 // CPU frequency in percentage from maximum. 91 private final MovingAverage frequencyScale; 92 93 @Nullable 94 private ScheduledExecutorService executor; 95 private long lastStatLogTimeMs; 96 private long[] cpuFreqMax; 97 private int cpusPresent; 98 private int actualCpusPresent; 99 private boolean initialized; 100 private boolean cpuOveruse; 101 private String[] maxPath; 102 private String[] curPath; 103 private double[] curFreqScales; 104 @Nullable 105 private ProcStat lastProcStat; 106 107 private static class ProcStat { 108 final long userTime; 109 final long systemTime; 110 final long idleTime; 111 ProcStat(long userTime, long systemTime, long idleTime)112 ProcStat(long userTime, long systemTime, long idleTime) { 113 this.userTime = userTime; 114 this.systemTime = systemTime; 115 this.idleTime = idleTime; 116 } 117 } 118 119 private static class MovingAverage { 120 private final int size; 121 private double sum; 122 private double currentValue; 123 private double[] circBuffer; 124 private int circBufferIndex; 125 MovingAverage(int size)126 public MovingAverage(int size) { 127 if (size <= 0) { 128 throw new AssertionError("Size value in MovingAverage ctor should be positive."); 129 } 130 this.size = size; 131 circBuffer = new double[size]; 132 } 133 reset()134 public void reset() { 135 Arrays.fill(circBuffer, 0); 136 circBufferIndex = 0; 137 sum = 0; 138 currentValue = 0; 139 } 140 addValue(double value)141 public void addValue(double value) { 142 sum -= circBuffer[circBufferIndex]; 143 circBuffer[circBufferIndex++] = value; 144 currentValue = value; 145 sum += value; 146 if (circBufferIndex >= size) { 147 circBufferIndex = 0; 148 } 149 } 150 getCurrent()151 public double getCurrent() { 152 return currentValue; 153 } 154 getAverage()155 public double getAverage() { 156 return sum / (double) size; 157 } 158 } 159 isSupported()160 public static boolean isSupported() { 161 return Build.VERSION.SDK_INT < Build.VERSION_CODES.N; 162 } 163 CpuMonitor(Context context)164 public CpuMonitor(Context context) { 165 if (!isSupported()) { 166 throw new RuntimeException("CpuMonitor is not supported on this Android version."); 167 } 168 169 Log.d(TAG, "CpuMonitor ctor."); 170 appContext = context.getApplicationContext(); 171 userCpuUsage = new MovingAverage(MOVING_AVERAGE_SAMPLES); 172 systemCpuUsage = new MovingAverage(MOVING_AVERAGE_SAMPLES); 173 totalCpuUsage = new MovingAverage(MOVING_AVERAGE_SAMPLES); 174 frequencyScale = new MovingAverage(MOVING_AVERAGE_SAMPLES); 175 lastStatLogTimeMs = SystemClock.elapsedRealtime(); 176 177 scheduleCpuUtilizationTask(); 178 } 179 pause()180 public void pause() { 181 if (executor != null) { 182 Log.d(TAG, "pause"); 183 executor.shutdownNow(); 184 executor = null; 185 } 186 } 187 resume()188 public void resume() { 189 Log.d(TAG, "resume"); 190 resetStat(); 191 scheduleCpuUtilizationTask(); 192 } 193 194 // TODO(bugs.webrtc.org/8491): Remove NoSynchronizedMethodCheck suppression. 195 @SuppressWarnings("NoSynchronizedMethodCheck") reset()196 public synchronized void reset() { 197 if (executor != null) { 198 Log.d(TAG, "reset"); 199 resetStat(); 200 cpuOveruse = false; 201 } 202 } 203 204 // TODO(bugs.webrtc.org/8491): Remove NoSynchronizedMethodCheck suppression. 205 @SuppressWarnings("NoSynchronizedMethodCheck") getCpuUsageCurrent()206 public synchronized int getCpuUsageCurrent() { 207 return doubleToPercent(userCpuUsage.getCurrent() + systemCpuUsage.getCurrent()); 208 } 209 210 // TODO(bugs.webrtc.org/8491): Remove NoSynchronizedMethodCheck suppression. 211 @SuppressWarnings("NoSynchronizedMethodCheck") getCpuUsageAverage()212 public synchronized int getCpuUsageAverage() { 213 return doubleToPercent(userCpuUsage.getAverage() + systemCpuUsage.getAverage()); 214 } 215 216 // TODO(bugs.webrtc.org/8491): Remove NoSynchronizedMethodCheck suppression. 217 @SuppressWarnings("NoSynchronizedMethodCheck") getFrequencyScaleAverage()218 public synchronized int getFrequencyScaleAverage() { 219 return doubleToPercent(frequencyScale.getAverage()); 220 } 221 scheduleCpuUtilizationTask()222 private void scheduleCpuUtilizationTask() { 223 if (executor != null) { 224 executor.shutdownNow(); 225 executor = null; 226 } 227 228 executor = Executors.newSingleThreadScheduledExecutor(); 229 @SuppressWarnings("unused") // Prevent downstream linter warnings. 230 Future<?> possiblyIgnoredError = executor.scheduleAtFixedRate(new Runnable() { 231 @Override 232 public void run() { 233 cpuUtilizationTask(); 234 } 235 }, 0, CPU_STAT_SAMPLE_PERIOD_MS, TimeUnit.MILLISECONDS); 236 } 237 cpuUtilizationTask()238 private void cpuUtilizationTask() { 239 boolean cpuMonitorAvailable = sampleCpuUtilization(); 240 if (cpuMonitorAvailable 241 && SystemClock.elapsedRealtime() - lastStatLogTimeMs >= CPU_STAT_LOG_PERIOD_MS) { 242 lastStatLogTimeMs = SystemClock.elapsedRealtime(); 243 String statString = getStatString(); 244 Log.d(TAG, statString); 245 } 246 } 247 init()248 private void init() { 249 try (FileInputStream fin = new FileInputStream("/sys/devices/system/cpu/present"); 250 InputStreamReader streamReader = new InputStreamReader(fin, Charset.forName("UTF-8")); 251 BufferedReader reader = new BufferedReader(streamReader); 252 Scanner scanner = new Scanner(reader).useDelimiter("[-\n]");) { 253 scanner.nextInt(); // Skip leading number 0. 254 cpusPresent = 1 + scanner.nextInt(); 255 scanner.close(); 256 } catch (FileNotFoundException e) { 257 Log.e(TAG, "Cannot do CPU stats since /sys/devices/system/cpu/present is missing"); 258 } catch (IOException e) { 259 Log.e(TAG, "Error closing file"); 260 } catch (Exception e) { 261 Log.e(TAG, "Cannot do CPU stats due to /sys/devices/system/cpu/present parsing problem"); 262 } 263 264 cpuFreqMax = new long[cpusPresent]; 265 maxPath = new String[cpusPresent]; 266 curPath = new String[cpusPresent]; 267 curFreqScales = new double[cpusPresent]; 268 for (int i = 0; i < cpusPresent; i++) { 269 cpuFreqMax[i] = 0; // Frequency "not yet determined". 270 curFreqScales[i] = 0; 271 maxPath[i] = "/sys/devices/system/cpu/cpu" + i + "/cpufreq/cpuinfo_max_freq"; 272 curPath[i] = "/sys/devices/system/cpu/cpu" + i + "/cpufreq/scaling_cur_freq"; 273 } 274 275 lastProcStat = new ProcStat(0, 0, 0); 276 resetStat(); 277 278 initialized = true; 279 } 280 resetStat()281 private synchronized void resetStat() { 282 userCpuUsage.reset(); 283 systemCpuUsage.reset(); 284 totalCpuUsage.reset(); 285 frequencyScale.reset(); 286 lastStatLogTimeMs = SystemClock.elapsedRealtime(); 287 } 288 getBatteryLevel()289 private int getBatteryLevel() { 290 // Use sticky broadcast with null receiver to read battery level once only. 291 Intent intent = appContext.registerReceiver( 292 null /* receiver */, new IntentFilter(Intent.ACTION_BATTERY_CHANGED)); 293 294 int batteryLevel = 0; 295 int batteryScale = intent.getIntExtra(BatteryManager.EXTRA_SCALE, 100); 296 if (batteryScale > 0) { 297 batteryLevel = 298 (int) (100f * intent.getIntExtra(BatteryManager.EXTRA_LEVEL, 0) / batteryScale); 299 } 300 return batteryLevel; 301 } 302 303 /** 304 * Re-measure CPU use. Call this method at an interval of around 1/s. 305 * This method returns true on success. The fields 306 * cpuCurrent, cpuAvg3, and cpuAvgAll are updated on success, and represents: 307 * cpuCurrent: The CPU use since the last sampleCpuUtilization call. 308 * cpuAvg3: The average CPU over the last 3 calls. 309 * cpuAvgAll: The average CPU over the last SAMPLE_SAVE_NUMBER calls. 310 */ sampleCpuUtilization()311 private synchronized boolean sampleCpuUtilization() { 312 long lastSeenMaxFreq = 0; 313 long cpuFreqCurSum = 0; 314 long cpuFreqMaxSum = 0; 315 316 if (!initialized) { 317 init(); 318 } 319 if (cpusPresent == 0) { 320 return false; 321 } 322 323 actualCpusPresent = 0; 324 for (int i = 0; i < cpusPresent; i++) { 325 /* 326 * For each CPU, attempt to first read its max frequency, then its 327 * current frequency. Once as the max frequency for a CPU is found, 328 * save it in cpuFreqMax[]. 329 */ 330 331 curFreqScales[i] = 0; 332 if (cpuFreqMax[i] == 0) { 333 // We have never found this CPU's max frequency. Attempt to read it. 334 long cpufreqMax = readFreqFromFile(maxPath[i]); 335 if (cpufreqMax > 0) { 336 Log.d(TAG, "Core " + i + ". Max frequency: " + cpufreqMax); 337 lastSeenMaxFreq = cpufreqMax; 338 cpuFreqMax[i] = cpufreqMax; 339 maxPath[i] = null; // Kill path to free its memory. 340 } 341 } else { 342 lastSeenMaxFreq = cpuFreqMax[i]; // A valid, previously read value. 343 } 344 345 long cpuFreqCur = readFreqFromFile(curPath[i]); 346 if (cpuFreqCur == 0 && lastSeenMaxFreq == 0) { 347 // No current frequency information for this CPU core - ignore it. 348 continue; 349 } 350 if (cpuFreqCur > 0) { 351 actualCpusPresent++; 352 } 353 cpuFreqCurSum += cpuFreqCur; 354 355 /* Here, lastSeenMaxFreq might come from 356 * 1. cpuFreq[i], or 357 * 2. a previous iteration, or 358 * 3. a newly read value, or 359 * 4. hypothetically from the pre-loop dummy. 360 */ 361 cpuFreqMaxSum += lastSeenMaxFreq; 362 if (lastSeenMaxFreq > 0) { 363 curFreqScales[i] = (double) cpuFreqCur / lastSeenMaxFreq; 364 } 365 } 366 367 if (cpuFreqCurSum == 0 || cpuFreqMaxSum == 0) { 368 Log.e(TAG, "Could not read max or current frequency for any CPU"); 369 return false; 370 } 371 372 /* 373 * Since the cycle counts are for the period between the last invocation 374 * and this present one, we average the percentual CPU frequencies between 375 * now and the beginning of the measurement period. This is significantly 376 * incorrect only if the frequencies have peeked or dropped in between the 377 * invocations. 378 */ 379 double currentFrequencyScale = cpuFreqCurSum / (double) cpuFreqMaxSum; 380 if (frequencyScale.getCurrent() > 0) { 381 currentFrequencyScale = (frequencyScale.getCurrent() + currentFrequencyScale) * 0.5; 382 } 383 384 ProcStat procStat = readProcStat(); 385 if (procStat == null) { 386 return false; 387 } 388 389 long diffUserTime = procStat.userTime - lastProcStat.userTime; 390 long diffSystemTime = procStat.systemTime - lastProcStat.systemTime; 391 long diffIdleTime = procStat.idleTime - lastProcStat.idleTime; 392 long allTime = diffUserTime + diffSystemTime + diffIdleTime; 393 394 if (currentFrequencyScale == 0 || allTime == 0) { 395 return false; 396 } 397 398 // Update statistics. 399 frequencyScale.addValue(currentFrequencyScale); 400 401 double currentUserCpuUsage = diffUserTime / (double) allTime; 402 userCpuUsage.addValue(currentUserCpuUsage); 403 404 double currentSystemCpuUsage = diffSystemTime / (double) allTime; 405 systemCpuUsage.addValue(currentSystemCpuUsage); 406 407 double currentTotalCpuUsage = 408 (currentUserCpuUsage + currentSystemCpuUsage) * currentFrequencyScale; 409 totalCpuUsage.addValue(currentTotalCpuUsage); 410 411 // Save new measurements for next round's deltas. 412 lastProcStat = procStat; 413 414 return true; 415 } 416 doubleToPercent(double d)417 private int doubleToPercent(double d) { 418 return (int) (d * 100 + 0.5); 419 } 420 getStatString()421 private synchronized String getStatString() { 422 StringBuilder stat = new StringBuilder(); 423 stat.append("CPU User: ") 424 .append(doubleToPercent(userCpuUsage.getCurrent())) 425 .append("/") 426 .append(doubleToPercent(userCpuUsage.getAverage())) 427 .append(". System: ") 428 .append(doubleToPercent(systemCpuUsage.getCurrent())) 429 .append("/") 430 .append(doubleToPercent(systemCpuUsage.getAverage())) 431 .append(". Freq: ") 432 .append(doubleToPercent(frequencyScale.getCurrent())) 433 .append("/") 434 .append(doubleToPercent(frequencyScale.getAverage())) 435 .append(". Total usage: ") 436 .append(doubleToPercent(totalCpuUsage.getCurrent())) 437 .append("/") 438 .append(doubleToPercent(totalCpuUsage.getAverage())) 439 .append(". Cores: ") 440 .append(actualCpusPresent); 441 stat.append("( "); 442 for (int i = 0; i < cpusPresent; i++) { 443 stat.append(doubleToPercent(curFreqScales[i])).append(" "); 444 } 445 stat.append("). Battery: ").append(getBatteryLevel()); 446 if (cpuOveruse) { 447 stat.append(". Overuse."); 448 } 449 return stat.toString(); 450 } 451 452 /** 453 * Read a single integer value from the named file. Return the read value 454 * or if an error occurs return 0. 455 */ readFreqFromFile(String fileName)456 private long readFreqFromFile(String fileName) { 457 long number = 0; 458 try (FileInputStream stream = new FileInputStream(fileName); 459 InputStreamReader streamReader = new InputStreamReader(stream, Charset.forName("UTF-8")); 460 BufferedReader reader = new BufferedReader(streamReader)) { 461 String line = reader.readLine(); 462 number = parseLong(line); 463 } catch (FileNotFoundException e) { 464 // CPU core is off, so file with its scaling frequency .../cpufreq/scaling_cur_freq 465 // is not present. This is not an error. 466 } catch (IOException e) { 467 // CPU core is off, so file with its scaling frequency .../cpufreq/scaling_cur_freq 468 // is empty. This is not an error. 469 } 470 return number; 471 } 472 parseLong(String value)473 private static long parseLong(String value) { 474 long number = 0; 475 try { 476 number = Long.parseLong(value); 477 } catch (NumberFormatException e) { 478 Log.e(TAG, "parseLong error.", e); 479 } 480 return number; 481 } 482 483 /* 484 * Read the current utilization of all CPUs using the cumulative first line 485 * of /proc/stat. 486 */ 487 @SuppressWarnings("StringSplitter") readProcStat()488 private @Nullable ProcStat readProcStat() { 489 long userTime = 0; 490 long systemTime = 0; 491 long idleTime = 0; 492 try (FileInputStream stream = new FileInputStream("/proc/stat"); 493 InputStreamReader streamReader = new InputStreamReader(stream, Charset.forName("UTF-8")); 494 BufferedReader reader = new BufferedReader(streamReader)) { 495 // line should contain something like this: 496 // cpu 5093818 271838 3512830 165934119 101374 447076 272086 0 0 0 497 // user nice system idle iowait irq softirq 498 String line = reader.readLine(); 499 String[] lines = line.split("\\s+"); 500 int length = lines.length; 501 if (length >= 5) { 502 userTime = parseLong(lines[1]); // user 503 userTime += parseLong(lines[2]); // nice 504 systemTime = parseLong(lines[3]); // system 505 idleTime = parseLong(lines[4]); // idle 506 } 507 if (length >= 8) { 508 userTime += parseLong(lines[5]); // iowait 509 systemTime += parseLong(lines[6]); // irq 510 systemTime += parseLong(lines[7]); // softirq 511 } 512 } catch (FileNotFoundException e) { 513 Log.e(TAG, "Cannot open /proc/stat for reading", e); 514 return null; 515 } catch (Exception e) { 516 Log.e(TAG, "Problems parsing /proc/stat", e); 517 return null; 518 } 519 return new ProcStat(userTime, systemTime, idleTime); 520 } 521 } 522