• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
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