| /* | |
| * Copyright 2015 The WebRTC Project Authors. All rights reserved. | |
| * | |
| * Use of this source code is governed by a BSD-style license | |
| * that can be found in the LICENSE file in the root of the source | |
| * tree. An additional intellectual property rights grant can be found | |
| * in the file PATENTS. All contributing project authors may | |
| * be found in the AUTHORS file in the root of the source tree. | |
| */ | |
| package org.appspot.apprtc; | |
| import android.annotation.TargetApi; | |
| import android.content.Context; | |
| import android.content.Intent; | |
| import android.content.IntentFilter; | |
| import android.os.BatteryManager; | |
| import android.os.Build; | |
| import android.os.SystemClock; | |
| import android.util.Log; | |
| import androidx.annotation.Nullable; | |
| import java.io.BufferedReader; | |
| import java.io.FileInputStream; | |
| import java.io.FileNotFoundException; | |
| import java.io.IOException; | |
| import java.io.InputStreamReader; | |
| import java.nio.charset.Charset; | |
| import java.util.Arrays; | |
| import java.util.Scanner; | |
| import java.util.concurrent.Executors; | |
| import java.util.concurrent.Future; | |
| import java.util.concurrent.ScheduledExecutorService; | |
| import java.util.concurrent.TimeUnit; | |
| /** | |
| * Simple CPU monitor. The caller creates a CpuMonitor object which can then | |
| * be used via sampleCpuUtilization() to collect the percentual use of the | |
| * cumulative CPU capacity for all CPUs running at their nominal frequency. 3 | |
| * values are generated: (1) getCpuCurrent() returns the use since the last | |
| * sampleCpuUtilization(), (2) getCpuAvg3() returns the use since 3 prior | |
| * calls, and (3) getCpuAvgAll() returns the use over all SAMPLE_SAVE_NUMBER | |
| * calls. | |
| * | |
| * <p>CPUs in Android are often "offline", and while this of course means 0 Hz | |
| * as current frequency, in this state we cannot even get their nominal | |
| * frequency. We therefore tread carefully, and allow any CPU to be missing. | |
| * Missing CPUs are assumed to have the same nominal frequency as any close | |
| * lower-numbered CPU, but as soon as it is online, we'll get their proper | |
| * frequency and remember it. (Since CPU 0 in practice always seem to be | |
| * online, this unidirectional frequency inheritance should be no problem in | |
| * practice.) | |
| * | |
| * <p>Caveats: | |
| * o No provision made for zany "turbo" mode, common in the x86 world. | |
| * o No provision made for ARM big.LITTLE; if CPU n can switch behind our | |
| * back, we might get incorrect estimates. | |
| * o This is not thread-safe. To call asynchronously, create different | |
| * CpuMonitor objects. | |
| * | |
| * <p>If we can gather enough info to generate a sensible result, | |
| * sampleCpuUtilization returns true. It is designed to never throw an | |
| * exception. | |
| * | |
| * <p>sampleCpuUtilization should not be called too often in its present form, | |
| * since then deltas would be small and the percent values would fluctuate and | |
| * be unreadable. If it is desirable to call it more often than say once per | |
| * second, one would need to increase SAMPLE_SAVE_NUMBER and probably use | |
| * Queue<Integer> to avoid copying overhead. | |
| * | |
| * <p>Known problems: | |
| * 1. Nexus 7 devices running Kitkat have a kernel which often output an | |
| * incorrect 'idle' field in /proc/stat. The value is close to twice the | |
| * correct value, and then returns to back to correct reading. Both when | |
| * jumping up and back down we might create faulty CPU load readings. | |
| */ | |
| class CpuMonitor { | |
| private static final String TAG = "CpuMonitor"; | |
| private static final int MOVING_AVERAGE_SAMPLES = 5; | |
| private static final int CPU_STAT_SAMPLE_PERIOD_MS = 2000; | |
| private static final int CPU_STAT_LOG_PERIOD_MS = 6000; | |
| private final Context appContext; | |
| // User CPU usage at current frequency. | |
| private final MovingAverage userCpuUsage; | |
| // System CPU usage at current frequency. | |
| private final MovingAverage systemCpuUsage; | |
| // Total CPU usage relative to maximum frequency. | |
| private final MovingAverage totalCpuUsage; | |
| // CPU frequency in percentage from maximum. | |
| private final MovingAverage frequencyScale; | |
| @Nullable | |
| private ScheduledExecutorService executor; | |
| private long lastStatLogTimeMs; | |
| private long[] cpuFreqMax; | |
| private int cpusPresent; | |
| private int actualCpusPresent; | |
| private boolean initialized; | |
| private boolean cpuOveruse; | |
| private String[] maxPath; | |
| private String[] curPath; | |
| private double[] curFreqScales; | |
| @Nullable | |
| private ProcStat lastProcStat; | |
| private static class ProcStat { | |
| final long userTime; | |
| final long systemTime; | |
| final long idleTime; | |
| ProcStat(long userTime, long systemTime, long idleTime) { | |
| this.userTime = userTime; | |
| this.systemTime = systemTime; | |
| this.idleTime = idleTime; | |
| } | |
| } | |
| private static class MovingAverage { | |
| private final int size; | |
| private double sum; | |
| private double currentValue; | |
| private final double[] circBuffer; | |
| private int circBufferIndex; | |
| public MovingAverage(int size) { | |
| if (size <= 0) { | |
| throw new AssertionError("Size value in MovingAverage ctor should be positive."); | |
| } | |
| this.size = size; | |
| circBuffer = new double[size]; | |
| } | |
| public void reset() { | |
| Arrays.fill(circBuffer, 0); | |
| circBufferIndex = 0; | |
| sum = 0; | |
| currentValue = 0; | |
| } | |
| public void addValue(double value) { | |
| sum -= circBuffer[circBufferIndex]; | |
| circBuffer[circBufferIndex++] = value; | |
| currentValue = value; | |
| sum += value; | |
| if (circBufferIndex >= size) { | |
| circBufferIndex = 0; | |
| } | |
| } | |
| public double getCurrent() { | |
| return currentValue; | |
| } | |
| public double getAverage() { | |
| return sum / (double) size; | |
| } | |
| } | |
| public static boolean isSupported() { | |
| return Build.VERSION.SDK_INT < Build.VERSION_CODES.N; | |
| } | |
| public CpuMonitor(Context context) { | |
| if (!isSupported()) { | |
| throw new RuntimeException("CpuMonitor is not supported on this Android version."); | |
| } | |
| Log.d(TAG, "CpuMonitor ctor."); | |
| appContext = context.getApplicationContext(); | |
| userCpuUsage = new MovingAverage(MOVING_AVERAGE_SAMPLES); | |
| systemCpuUsage = new MovingAverage(MOVING_AVERAGE_SAMPLES); | |
| totalCpuUsage = new MovingAverage(MOVING_AVERAGE_SAMPLES); | |
| frequencyScale = new MovingAverage(MOVING_AVERAGE_SAMPLES); | |
| lastStatLogTimeMs = SystemClock.elapsedRealtime(); | |
| scheduleCpuUtilizationTask(); | |
| } | |
| public void pause() { | |
| if (executor != null) { | |
| Log.d(TAG, "pause"); | |
| executor.shutdownNow(); | |
| executor = null; | |
| } | |
| } | |
| public void resume() { | |
| Log.d(TAG, "resume"); | |
| resetStat(); | |
| scheduleCpuUtilizationTask(); | |
| } | |
| // TODO(bugs.webrtc.org/8491): Remove NoSynchronizedMethodCheck suppression. | |
| @SuppressWarnings("NoSynchronizedMethodCheck") | |
| public synchronized void reset() { | |
| if (executor != null) { | |
| Log.d(TAG, "reset"); | |
| resetStat(); | |
| cpuOveruse = false; | |
| } | |
| } | |
| // TODO(bugs.webrtc.org/8491): Remove NoSynchronizedMethodCheck suppression. | |
| @SuppressWarnings("NoSynchronizedMethodCheck") | |
| public synchronized int getCpuUsageCurrent() { | |
| return doubleToPercent(userCpuUsage.getCurrent() + systemCpuUsage.getCurrent()); | |
| } | |
| // TODO(bugs.webrtc.org/8491): Remove NoSynchronizedMethodCheck suppression. | |
| @SuppressWarnings("NoSynchronizedMethodCheck") | |
| public synchronized int getCpuUsageAverage() { | |
| return doubleToPercent(userCpuUsage.getAverage() + systemCpuUsage.getAverage()); | |
| } | |
| // TODO(bugs.webrtc.org/8491): Remove NoSynchronizedMethodCheck suppression. | |
| @SuppressWarnings("NoSynchronizedMethodCheck") | |
| public synchronized int getFrequencyScaleAverage() { | |
| return doubleToPercent(frequencyScale.getAverage()); | |
| } | |
| private void scheduleCpuUtilizationTask() { | |
| if (executor != null) { | |
| executor.shutdownNow(); | |
| executor = null; | |
| } | |
| executor = Executors.newSingleThreadScheduledExecutor(); | |
| @SuppressWarnings("unused") // Prevent downstream linter warnings. | |
| Future<?> possiblyIgnoredError = executor.scheduleAtFixedRate(new Runnable() { | |
| @Override | |
| public void run() { | |
| cpuUtilizationTask(); | |
| } | |
| }, 0, CPU_STAT_SAMPLE_PERIOD_MS, TimeUnit.MILLISECONDS); | |
| } | |
| private void cpuUtilizationTask() { | |
| boolean cpuMonitorAvailable = sampleCpuUtilization(); | |
| if (cpuMonitorAvailable | |
| && SystemClock.elapsedRealtime() - lastStatLogTimeMs >= CPU_STAT_LOG_PERIOD_MS) { | |
| lastStatLogTimeMs = SystemClock.elapsedRealtime(); | |
| String statString = getStatString(); | |
| Log.d(TAG, statString); | |
| } | |
| } | |
| private void init() { | |
| try (FileInputStream fin = new FileInputStream("/sys/devices/system/cpu/present"); | |
| InputStreamReader streamReader = new InputStreamReader(fin, Charset.forName("UTF-8")); | |
| BufferedReader reader = new BufferedReader(streamReader); | |
| Scanner scanner = new Scanner(reader).useDelimiter("[-\n]");) { | |
| scanner.nextInt(); // Skip leading number 0. | |
| cpusPresent = 1 + scanner.nextInt(); | |
| scanner.close(); | |
| } catch (FileNotFoundException e) { | |
| Log.e(TAG, "Cannot do CPU stats since /sys/devices/system/cpu/present is missing"); | |
| } catch (IOException e) { | |
| Log.e(TAG, "Error closing file"); | |
| } catch (Exception e) { | |
| Log.e(TAG, "Cannot do CPU stats due to /sys/devices/system/cpu/present parsing problem"); | |
| } | |
| cpuFreqMax = new long[cpusPresent]; | |
| maxPath = new String[cpusPresent]; | |
| curPath = new String[cpusPresent]; | |
| curFreqScales = new double[cpusPresent]; | |
| for (int i = 0; i < cpusPresent; i++) { | |
| cpuFreqMax[i] = 0; // Frequency "not yet determined". | |
| curFreqScales[i] = 0; | |
| maxPath[i] = "/sys/devices/system/cpu/cpu" + i + "/cpufreq/cpuinfo_max_freq"; | |
| curPath[i] = "/sys/devices/system/cpu/cpu" + i + "/cpufreq/scaling_cur_freq"; | |
| } | |
| lastProcStat = new ProcStat(0, 0, 0); | |
| resetStat(); | |
| initialized = true; | |
| } | |
| private synchronized void resetStat() { | |
| userCpuUsage.reset(); | |
| systemCpuUsage.reset(); | |
| totalCpuUsage.reset(); | |
| frequencyScale.reset(); | |
| lastStatLogTimeMs = SystemClock.elapsedRealtime(); | |
| } | |
| private int getBatteryLevel() { | |
| // Use sticky broadcast with null receiver to read battery level once only. | |
| Intent intent = appContext.registerReceiver( | |
| null /* receiver */, new IntentFilter(Intent.ACTION_BATTERY_CHANGED)); | |
| int batteryLevel = 0; | |
| int batteryScale = intent.getIntExtra(BatteryManager.EXTRA_SCALE, 100); | |
| if (batteryScale > 0) { | |
| batteryLevel = | |
| (int) (100f * intent.getIntExtra(BatteryManager.EXTRA_LEVEL, 0) / batteryScale); | |
| } | |
| return batteryLevel; | |
| } | |
| /** | |
| * Re-measure CPU use. Call this method at an interval of around 1/s. | |
| * This method returns true on success. The fields | |
| * cpuCurrent, cpuAvg3, and cpuAvgAll are updated on success, and represents: | |
| * cpuCurrent: The CPU use since the last sampleCpuUtilization call. | |
| * cpuAvg3: The average CPU over the last 3 calls. | |
| * cpuAvgAll: The average CPU over the last SAMPLE_SAVE_NUMBER calls. | |
| */ | |
| private synchronized boolean sampleCpuUtilization() { | |
| long lastSeenMaxFreq = 0; | |
| long cpuFreqCurSum = 0; | |
| long cpuFreqMaxSum = 0; | |
| if (!initialized) { | |
| init(); | |
| } | |
| if (cpusPresent == 0) { | |
| return false; | |
| } | |
| actualCpusPresent = 0; | |
| for (int i = 0; i < cpusPresent; i++) { | |
| /* | |
| * For each CPU, attempt to first read its max frequency, then its | |
| * current frequency. Once as the max frequency for a CPU is found, | |
| * save it in cpuFreqMax[]. | |
| */ | |
| curFreqScales[i] = 0; | |
| if (cpuFreqMax[i] == 0) { | |
| // We have never found this CPU's max frequency. Attempt to read it. | |
| long cpufreqMax = readFreqFromFile(maxPath[i]); | |
| if (cpufreqMax > 0) { | |
| Log.d(TAG, "Core " + i + ". Max frequency: " + cpufreqMax); | |
| lastSeenMaxFreq = cpufreqMax; | |
| cpuFreqMax[i] = cpufreqMax; | |
| maxPath[i] = null; // Kill path to free its memory. | |
| } | |
| } else { | |
| lastSeenMaxFreq = cpuFreqMax[i]; // A valid, previously read value. | |
| } | |
| long cpuFreqCur = readFreqFromFile(curPath[i]); | |
| if (cpuFreqCur == 0 && lastSeenMaxFreq == 0) { | |
| // No current frequency information for this CPU core - ignore it. | |
| continue; | |
| } | |
| if (cpuFreqCur > 0) { | |
| actualCpusPresent++; | |
| } | |
| cpuFreqCurSum += cpuFreqCur; | |
| /* Here, lastSeenMaxFreq might come from | |
| * 1. cpuFreq[i], or | |
| * 2. a previous iteration, or | |
| * 3. a newly read value, or | |
| * 4. hypothetically from the pre-loop dummy. | |
| */ | |
| cpuFreqMaxSum += lastSeenMaxFreq; | |
| if (lastSeenMaxFreq > 0) { | |
| curFreqScales[i] = (double) cpuFreqCur / lastSeenMaxFreq; | |
| } | |
| } | |
| if (cpuFreqCurSum == 0 || cpuFreqMaxSum == 0) { | |
| Log.e(TAG, "Could not read max or current frequency for any CPU"); | |
| return false; | |
| } | |
| /* | |
| * Since the cycle counts are for the period between the last invocation | |
| * and this present one, we average the percentual CPU frequencies between | |
| * now and the beginning of the measurement period. This is significantly | |
| * incorrect only if the frequencies have peeked or dropped in between the | |
| * invocations. | |
| */ | |
| double currentFrequencyScale = cpuFreqCurSum / (double) cpuFreqMaxSum; | |
| if (frequencyScale.getCurrent() > 0) { | |
| currentFrequencyScale = (frequencyScale.getCurrent() + currentFrequencyScale) * 0.5; | |
| } | |
| ProcStat procStat = readProcStat(); | |
| if (procStat == null) { | |
| return false; | |
| } | |
| long diffUserTime = procStat.userTime - lastProcStat.userTime; | |
| long diffSystemTime = procStat.systemTime - lastProcStat.systemTime; | |
| long diffIdleTime = procStat.idleTime - lastProcStat.idleTime; | |
| long allTime = diffUserTime + diffSystemTime + diffIdleTime; | |
| if (currentFrequencyScale == 0 || allTime == 0) { | |
| return false; | |
| } | |
| // Update statistics. | |
| frequencyScale.addValue(currentFrequencyScale); | |
| double currentUserCpuUsage = diffUserTime / (double) allTime; | |
| userCpuUsage.addValue(currentUserCpuUsage); | |
| double currentSystemCpuUsage = diffSystemTime / (double) allTime; | |
| systemCpuUsage.addValue(currentSystemCpuUsage); | |
| double currentTotalCpuUsage = | |
| (currentUserCpuUsage + currentSystemCpuUsage) * currentFrequencyScale; | |
| totalCpuUsage.addValue(currentTotalCpuUsage); | |
| // Save new measurements for next round's deltas. | |
| lastProcStat = procStat; | |
| return true; | |
| } | |
| private int doubleToPercent(double d) { | |
| return (int) (d * 100 + 0.5); | |
| } | |
| private synchronized String getStatString() { | |
| StringBuilder stat = new StringBuilder(); | |
| stat.append("CPU User: ") | |
| .append(doubleToPercent(userCpuUsage.getCurrent())) | |
| .append("/") | |
| .append(doubleToPercent(userCpuUsage.getAverage())) | |
| .append(". System: ") | |
| .append(doubleToPercent(systemCpuUsage.getCurrent())) | |
| .append("/") | |
| .append(doubleToPercent(systemCpuUsage.getAverage())) | |
| .append(". Freq: ") | |
| .append(doubleToPercent(frequencyScale.getCurrent())) | |
| .append("/") | |
| .append(doubleToPercent(frequencyScale.getAverage())) | |
| .append(". Total usage: ") | |
| .append(doubleToPercent(totalCpuUsage.getCurrent())) | |
| .append("/") | |
| .append(doubleToPercent(totalCpuUsage.getAverage())) | |
| .append(". Cores: ") | |
| .append(actualCpusPresent); | |
| stat.append("( "); | |
| for (int i = 0; i < cpusPresent; i++) { | |
| stat.append(doubleToPercent(curFreqScales[i])).append(" "); | |
| } | |
| stat.append("). Battery: ").append(getBatteryLevel()); | |
| if (cpuOveruse) { | |
| stat.append(". Overuse."); | |
| } | |
| return stat.toString(); | |
| } | |
| /** | |
| * Read a single integer value from the named file. Return the read value | |
| * or if an error occurs return 0. | |
| */ | |
| private long readFreqFromFile(String fileName) { | |
| long number = 0; | |
| try (FileInputStream stream = new FileInputStream(fileName); | |
| InputStreamReader streamReader = new InputStreamReader(stream, Charset.forName("UTF-8")); | |
| BufferedReader reader = new BufferedReader(streamReader)) { | |
| String line = reader.readLine(); | |
| number = parseLong(line); | |
| } catch (FileNotFoundException e) { | |
| // CPU core is off, so file with its scaling frequency .../cpufreq/scaling_cur_freq | |
| // is not present. This is not an error. | |
| } catch (IOException e) { | |
| // CPU core is off, so file with its scaling frequency .../cpufreq/scaling_cur_freq | |
| // is empty. This is not an error. | |
| } | |
| return number; | |
| } | |
| private static long parseLong(String value) { | |
| long number = 0; | |
| try { | |
| number = Long.parseLong(value); | |
| } catch (NumberFormatException e) { | |
| Log.e(TAG, "parseLong error.", e); | |
| } | |
| return number; | |
| } | |
| /* | |
| * Read the current utilization of all CPUs using the cumulative first line | |
| * of /proc/stat. | |
| */ | |
| @SuppressWarnings("StringSplitter") | |
| private @Nullable ProcStat readProcStat() { | |
| long userTime = 0; | |
| long systemTime = 0; | |
| long idleTime = 0; | |
| try (FileInputStream stream = new FileInputStream("/proc/stat"); | |
| InputStreamReader streamReader = new InputStreamReader(stream, Charset.forName("UTF-8")); | |
| BufferedReader reader = new BufferedReader(streamReader)) { | |
| // line should contain something like this: | |
| // cpu 5093818 271838 3512830 165934119 101374 447076 272086 0 0 0 | |
| // user nice system idle iowait irq softirq | |
| String line = reader.readLine(); | |
| String[] lines = line.split("\\s+"); | |
| int length = lines.length; | |
| if (length >= 5) { | |
| userTime = parseLong(lines[1]); // user | |
| userTime += parseLong(lines[2]); // nice | |
| systemTime = parseLong(lines[3]); // system | |
| idleTime = parseLong(lines[4]); // idle | |
| } | |
| if (length >= 8) { | |
| userTime += parseLong(lines[5]); // iowait | |
| systemTime += parseLong(lines[6]); // irq | |
| systemTime += parseLong(lines[7]); // softirq | |
| } | |
| } catch (FileNotFoundException e) { | |
| Log.e(TAG, "Cannot open /proc/stat for reading", e); | |
| return null; | |
| } catch (Exception e) { | |
| Log.e(TAG, "Problems parsing /proc/stat", e); | |
| return null; | |
| } | |
| return new ProcStat(userTime, systemTime, idleTime); | |
| } | |
| } |