• 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.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