blob: 752fd7620a1c7d420919ece00dcba6a0b916f676 [file]
/*
* 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.content.Context;
import android.content.Intent;
import android.content.IntentFilter;
import android.os.BatteryManager;
import android.os.Environment;
import android.os.SystemClock;
import android.util.Log;
import java.io.BufferedReader;
import java.io.File;
import java.io.FileNotFoundException;
import java.io.FileOutputStream;
import java.io.FileReader;
import java.io.IOException;
import java.text.SimpleDateFormat;
import java.util.Calendar;
import java.util.Date;
import java.util.Scanner;
import org.appspot.apprtc.util.LooperExecutor;
/**
* 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 through 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 String DUMP_FILE = "cpu_log.txt";
private static final int CPU_STAT_SAMPLE_PERIOD = 2000;
private static final int CPU_STAT_LOG_PERIOD = 6000;
private final Context appContext;
private LooperExecutor executor;
private long lastStatLogTimeMs;
private int iterations;
private double currentUserCpuUsage;
private double currentSystemCpuUsage;
private double currentTotalCpuUsage;
private double currentFrequencyScale = -1;
private double sumUserCpuUsage;
private double sumSystemCpuUsage;
private double sumFrequencyScale;
private double sumTotalCpuUsage;
private long[] cpuFreqMax;
private int cpusPresent;
private int actualCpusPresent;
private boolean initialized = false;
private String[] maxPath;
private String[] curPath;
private double[] curFreqScales;
private ProcStat lastProcStat;
private static boolean dumpEnabled = false;
private static FileOutputStream fileWriter;
private 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;
}
}
public CpuMonitor(Context context) {
Log.d(TAG, "CpuMonitor ctor.");
appContext = context.getApplicationContext();
lastStatLogTimeMs = 0;
executor = new LooperExecutor();
executor.requestStart();
scheduleCpuUtilizationTask();
}
public void release() {
if (executor != null) {
Log.d(TAG, "release");
executor.cancelScheduledTasks();
executor.requestStop();
executor = null;
}
}
public void pause() {
if (executor != null) {
Log.d(TAG, "pause");
executor.cancelScheduledTasks();
}
}
public void resume() {
if (executor != null) {
Log.d(TAG, "resume");
resetStat();
scheduleCpuUtilizationTask();
}
}
public synchronized int getCpuUsageCurrent() {
return doubleToPercent(currentTotalCpuUsage);
}
public synchronized int getCpuUsageAverage() {
return sumDoubleToPercent(sumTotalCpuUsage, iterations);
}
public synchronized int getCpuFrequencyScaleCurrent() {
return doubleToPercent(currentFrequencyScale);
}
private void scheduleCpuUtilizationTask() {
executor.scheduleAtFixedRate(new Runnable() {
@Override
public void run() {
logCpuUtilization();
}
}, CPU_STAT_SAMPLE_PERIOD);
}
private void checkDump(String statString) {
if (!dumpEnabled) {
return;
}
if (fileWriter == null) {
Log.d(TAG, "Start log dump");
String fileName = Environment.getExternalStorageDirectory().getAbsolutePath()
+ File.separator + DUMP_FILE;
try {
fileWriter = new FileOutputStream(fileName, false /* append */);
} catch (FileNotFoundException e) {
Log.e(TAG, "Can not open file.", e);
dumpEnabled = false;
return;
}
}
Date date = Calendar.getInstance().getTime();
SimpleDateFormat df = new SimpleDateFormat("MM-dd HH:mm:ss.SSS");
String msg = df.format(date) + " " + TAG + ":" + statString + "\n";
try {
fileWriter.write(msg.getBytes());
} catch (IOException e) {
Log.e(TAG, "Can not write to file.", e);
dumpEnabled = false;
}
}
private void logCpuUtilization() {
boolean logStatistics = false;
if (SystemClock.elapsedRealtime() - lastStatLogTimeMs >= CPU_STAT_LOG_PERIOD) {
lastStatLogTimeMs = SystemClock.elapsedRealtime();
logStatistics = true;
}
boolean cpuMonitorAvailable = sampleCpuUtilization();
if (logStatistics && cpuMonitorAvailable) {
String statString = getStatString();
checkDump(statString);
Log.d(TAG, statString);
resetStat();
}
}
private void init() {
try {
FileReader fin = new FileReader("/sys/devices/system/cpu/present");
try {
BufferedReader reader = new BufferedReader(fin);
Scanner scanner = new Scanner(reader).useDelimiter("[-\n]");
scanner.nextInt(); // Skip leading number 0.
cpusPresent = 1 + scanner.nextInt();
scanner.close();
} catch (Exception e) {
Log.e(TAG, "Cannot do CPU stats due to /sys/devices/system/cpu/present parsing problem");
} finally {
fin.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");
}
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() {
sumUserCpuUsage = 0;
sumSystemCpuUsage = 0;
sumFrequencyScale = 0;
sumTotalCpuUsage = 0;
iterations = 0;
}
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) {
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 newFrequencyScale = (double) cpuFreqCurSum / cpuFreqMaxSum;
double frequencyScale;
if (currentFrequencyScale > 0) {
frequencyScale = (currentFrequencyScale + newFrequencyScale) * 0.5;
} else {
frequencyScale = newFrequencyScale;
}
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 (frequencyScale == 0 || allTime == 0) {
return false;
}
// Update statistics.
currentFrequencyScale = frequencyScale;
sumFrequencyScale += frequencyScale;
currentUserCpuUsage = (double) diffUserTime / allTime;
sumUserCpuUsage += currentUserCpuUsage;
currentSystemCpuUsage = (double) diffSystemTime / allTime;
sumSystemCpuUsage += currentSystemCpuUsage;
currentTotalCpuUsage = (currentUserCpuUsage + currentSystemCpuUsage) * currentFrequencyScale;
sumTotalCpuUsage += currentTotalCpuUsage;
iterations++;
// Save new measurements for next round's deltas.
lastProcStat = procStat;
return true;
}
private int doubleToPercent(double d) {
return (int) (d * 100 + 0.5);
}
private int sumDoubleToPercent(double d, int iterations) {
if (iterations > 0) {
return (int) (d * 100.0 / (double) iterations + 0.5);
} else {
return 0;
}
}
private String getStatString() {
StringBuilder stat = new StringBuilder();
stat.append("CPU User: ")
.append(doubleToPercent(currentUserCpuUsage)).append("/")
.append(sumDoubleToPercent(sumUserCpuUsage, iterations)).append(" (")
.append(doubleToPercent(currentUserCpuUsage * currentFrequencyScale)).append(")")
.append(". System: ")
.append(doubleToPercent(currentSystemCpuUsage)).append("/")
.append(sumDoubleToPercent(sumSystemCpuUsage, iterations)).append(" (")
.append(doubleToPercent(currentSystemCpuUsage * currentFrequencyScale)).append(")")
.append(". CPU freq %: ")
.append(doubleToPercent(currentFrequencyScale)).append("/")
.append(sumDoubleToPercent(sumFrequencyScale, iterations))
.append(". Total CPU usage: ")
.append(doubleToPercent(currentTotalCpuUsage)).append("/")
.append(sumDoubleToPercent(sumTotalCpuUsage, iterations))
.append(". Cores: ")
.append(actualCpusPresent);
stat.append("( ");
for (int i = 0; i < cpusPresent; i++) {
stat.append(doubleToPercent(curFreqScales[i])).append(" ");
}
stat.append("). Battery %: ")
.append(getBatteryLevel());
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 {
FileReader fin = new FileReader(fileName);
try {
BufferedReader rdr = new BufferedReader(fin);
Scanner scannerC = new Scanner(rdr);
number = scannerC.nextLong();
scannerC.close();
} catch (Exception e) {
// CPU presumably got offline just after we opened file.
} finally {
fin.close();
}
} catch (FileNotFoundException e) {
// CPU is offline, not an error.
} catch (IOException e) {
Log.e(TAG, "Error closing file");
}
return number;
}
/*
* Read the current utilization of all CPUs using the cumulative first line
* of /proc/stat.
*/
private ProcStat readProcStat() {
long userTime = 0;
long systemTime = 0;
long idleTime = 0;
try {
FileReader fin = new FileReader("/proc/stat");
try {
BufferedReader rdr = new BufferedReader(fin);
Scanner scanner = new Scanner(rdr);
scanner.next();
userTime = scanner.nextLong();
long nice = scanner.nextLong();
userTime += nice;
systemTime = scanner.nextLong();
idleTime = scanner.nextLong();
long ioWaitTime = scanner.nextLong();
userTime += ioWaitTime;
scanner.close();
} catch (Exception e) {
Log.e(TAG, "Problems parsing /proc/stat");
return null;
} finally {
fin.close();
}
} catch (FileNotFoundException e) {
Log.e(TAG, "Cannot open /proc/stat for reading");
return null;
} catch (IOException e) {
Log.e(TAG, "Problems reading /proc/stat");
return null;
}
return new ProcStat(userTime, systemTime, idleTime);
}
}