blob: b1328e86227330a5d8dcf99aa8a51e2e970d29b6 [file] [log] [blame]
/*
* Copyright (C) 2018 The Android Open Source Project
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package com.android.internal.os;
import android.annotation.Nullable;
import android.os.Process;
import android.util.Slog;
import com.android.internal.annotations.VisibleForTesting;
import com.android.internal.util.Preconditions;
import java.io.IOException;
import java.nio.file.DirectoryStream;
import java.nio.file.Files;
import java.nio.file.Path;
import java.nio.file.Paths;
import java.util.ArrayList;
import java.util.function.Predicate;
/**
* Given a process, will iterate over the child threads of the process, and return the CPU usage
* statistics for each child thread. The CPU usage statistics contain the amount of time spent in a
* frequency band.
*
* <p>Frequencies are bucketed together to reduce the amount of data created. This means that we
* return less frequencies than provided by {@link ProcTimeInStateReader}. The number of
* frequencies is configurable by {@link #setNumBuckets}. Frequencies are reported as the lowest
* frequency in that range. Frequencies are spread as evenly as possible across the buckets. The
* buckets do not cross over the little/big frequencies reported.
*
* <p>N.B.: In order to bucket across little/big frequencies correctly, we assume that the {@code
* time_in_state} file contains every little core frequency in ascending order, followed by every
* big core frequency in ascending order. This assumption might not hold for devices with different
* kernel implementations of the {@code time_in_state} file generation.
*/
public class KernelCpuThreadReader {
private static final String TAG = "KernelCpuThreadReader";
private static final boolean DEBUG = false;
/**
* The name of the file to read CPU statistics from, must be found in {@code
* /proc/$PID/task/$TID}
*/
private static final String CPU_STATISTICS_FILENAME = "time_in_state";
/**
* The name of the file to read process command line invocation from, must be found in
* {@code /proc/$PID/}
*/
private static final String PROCESS_NAME_FILENAME = "cmdline";
/**
* The name of the file to read thread name from, must be found in
* {@code /proc/$PID/task/$TID}
*/
private static final String THREAD_NAME_FILENAME = "comm";
/**
* Glob pattern for the process directory names under {@code proc}
*/
private static final String PROCESS_DIRECTORY_FILTER = "[0-9]*";
/**
* Default process name when the name can't be read
*/
private static final String DEFAULT_PROCESS_NAME = "unknown_process";
/**
* Default thread name when the name can't be read
*/
private static final String DEFAULT_THREAD_NAME = "unknown_thread";
/**
* Default mount location of the {@code proc} filesystem
*/
private static final Path DEFAULT_PROC_PATH = Paths.get("/proc");
/**
* The initial {@code time_in_state} file for {@link ProcTimeInStateReader}
*/
private static final Path DEFAULT_INITIAL_TIME_IN_STATE_PATH =
DEFAULT_PROC_PATH.resolve("self/time_in_state");
/**
* Value returned when there was an error getting an integer ID value (e.g. PID, UID)
*/
private static final int ID_ERROR = -1;
/**
* When checking whether to report data for a thread, we check the UID of the thread's owner
* against this predicate
*/
private Predicate<Integer> mUidPredicate;
/**
* Where the proc filesystem is mounted
*/
private final Path mProcPath;
/**
* Frequencies read from the {@code time_in_state} file. Read from {@link
* #mProcTimeInStateReader#getCpuFrequenciesKhz()} and cast to {@code int[]}
*/
private int[] mFrequenciesKhz;
/**
* Used to read and parse {@code time_in_state} files
*/
private final ProcTimeInStateReader mProcTimeInStateReader;
/**
* Used to sort frequencies and usage times into buckets
*/
private FrequencyBucketCreator mFrequencyBucketCreator;
private final Injector mInjector;
/**
* Create with a path where `proc` is mounted. Used primarily for testing
*
* @param procPath where `proc` is mounted (to find, see {@code mount | grep ^proc})
* @param initialTimeInStatePath where the initial {@code time_in_state} file exists to define
* format
*/
@VisibleForTesting
public KernelCpuThreadReader(
int numBuckets,
Predicate<Integer> uidPredicate,
Path procPath,
Path initialTimeInStatePath,
Injector injector) throws IOException {
mUidPredicate = uidPredicate;
mProcPath = procPath;
mProcTimeInStateReader = new ProcTimeInStateReader(initialTimeInStatePath);
mInjector = injector;
setNumBuckets(numBuckets);
}
/**
* Create the reader and handle exceptions during creation
*
* @return the reader, null if an exception was thrown during creation
*/
@Nullable
public static KernelCpuThreadReader create(int numBuckets, Predicate<Integer> uidPredicate) {
try {
return new KernelCpuThreadReader(
numBuckets,
uidPredicate,
DEFAULT_PROC_PATH,
DEFAULT_INITIAL_TIME_IN_STATE_PATH,
new Injector());
} catch (IOException e) {
Slog.e(TAG, "Failed to initialize KernelCpuThreadReader", e);
return null;
}
}
/**
* Get the per-thread CPU usage of all processes belonging to a set of UIDs
*
* <p>This function will crawl through all process {@code proc} directories found by the pattern
* {@code /proc/[0-9]*}, and then check the UID using {@code /proc/$PID/status}. This takes
* approximately 500ms on a Pixel 2. Therefore, this method can be computationally expensive,
* and should not be called more than once an hour.
*
* <p>Data is only collected for UIDs passing the predicate supplied in {@link
* #setUidPredicate}.
*/
@Nullable
public ArrayList<ProcessCpuUsage> getProcessCpuUsageByUids() {
if (DEBUG) {
Slog.d(TAG, "Reading CPU thread usages for processes owned by UIDs");
}
final ArrayList<ProcessCpuUsage> processCpuUsages = new ArrayList<>();
try (DirectoryStream<Path> processPaths =
Files.newDirectoryStream(mProcPath, PROCESS_DIRECTORY_FILTER)) {
for (Path processPath : processPaths) {
final int processId = getProcessId(processPath);
final int uid = mInjector.getUidForPid(processId);
if (uid == ID_ERROR || processId == ID_ERROR) {
continue;
}
if (!mUidPredicate.test(uid)) {
continue;
}
final ProcessCpuUsage processCpuUsage =
getProcessCpuUsage(processPath, processId, uid);
if (processCpuUsage != null) {
processCpuUsages.add(processCpuUsage);
}
}
} catch (IOException e) {
Slog.w("Failed to iterate over process paths", e);
return null;
}
if (processCpuUsages.isEmpty()) {
Slog.w(TAG, "Didn't successfully get any process CPU information for UIDs specified");
return null;
}
if (DEBUG) {
Slog.d(TAG, "Read usage for " + processCpuUsages.size() + " processes");
}
return processCpuUsages;
}
/**
* Read all of the CPU usage statistics for each child thread of the current process
*
* @return process CPU usage containing usage of all child threads
*/
@Nullable
public ProcessCpuUsage getCurrentProcessCpuUsage() {
return getProcessCpuUsage(mProcPath.resolve("self"), mInjector.myPid(), mInjector.myUid());
}
/**
* Read all of the CPU usage statistics for each child thread of a process
*
* @param processPath the {@code /proc} path of the thread
* @param processId the ID of the process
* @param uid the ID of the user who owns the process
* @return process CPU usage containing usage of all child threads. Null if the process exited
* and its {@code proc} directory was removed while collecting information
*/
@Nullable
private ProcessCpuUsage getProcessCpuUsage(Path processPath, int processId, int uid) {
if (DEBUG) {
Slog.d(TAG, "Reading CPU thread usages with directory " + processPath
+ " process ID " + processId
+ " and user ID " + uid);
}
final Path allThreadsPath = processPath.resolve("task");
final ArrayList<ThreadCpuUsage> threadCpuUsages = new ArrayList<>();
try (DirectoryStream<Path> threadPaths = Files.newDirectoryStream(allThreadsPath)) {
for (Path threadDirectory : threadPaths) {
ThreadCpuUsage threadCpuUsage = getThreadCpuUsage(threadDirectory);
if (threadCpuUsage != null) {
threadCpuUsages.add(threadCpuUsage);
}
}
} catch (IOException e) {
// Expected when a process finishes
return null;
}
// If we found no threads, then the process has exited while we were reading from it
if (threadCpuUsages.isEmpty()) {
return null;
}
if (DEBUG) {
Slog.d(TAG, "Read CPU usage of " + threadCpuUsages.size() + " threads");
}
return new ProcessCpuUsage(
processId,
getProcessName(processPath),
uid,
threadCpuUsages);
}
/**
* Set the number of frequency buckets to use
*/
void setNumBuckets(int numBuckets) {
if (numBuckets < 1) {
Slog.w(TAG, "Number of buckets must be at least 1, but was " + numBuckets);
return;
}
// If `numBuckets` hasn't changed since the last set, do nothing
if (mFrequenciesKhz != null && mFrequenciesKhz.length == numBuckets) {
return;
}
mFrequencyBucketCreator = new FrequencyBucketCreator(
mProcTimeInStateReader.getFrequenciesKhz(), numBuckets);
mFrequenciesKhz = mFrequencyBucketCreator.getBucketMinFrequencies(
mProcTimeInStateReader.getFrequenciesKhz());
}
/**
* Set the UID predicate for {@link #getProcessCpuUsageByUids}
*/
void setUidPredicate(Predicate<Integer> uidPredicate) {
mUidPredicate = uidPredicate;
}
/**
* Get the CPU frequencies that correspond to the times reported in
* {@link ThreadCpuUsage#usageTimesMillis}
*/
@Nullable
public int[] getCpuFrequenciesKhz() {
return mFrequenciesKhz;
}
/**
* Get a thread's CPU usage
*
* @param threadDirectory the {@code /proc} directory of the thread
* @return thread CPU usage. Null if the thread exited and its {@code proc} directory was
* removed while collecting information
*/
@Nullable
private ThreadCpuUsage getThreadCpuUsage(Path threadDirectory) {
// Get the thread ID from the directory name
final int threadId;
try {
final String directoryName = threadDirectory.getFileName().toString();
threadId = Integer.parseInt(directoryName);
} catch (NumberFormatException e) {
Slog.w(TAG, "Failed to parse thread ID when iterating over /proc/*/task", e);
return null;
}
// Get the thread name from the thread directory
final String threadName = getThreadName(threadDirectory);
// Get the CPU statistics from the directory
final Path threadCpuStatPath = threadDirectory.resolve(CPU_STATISTICS_FILENAME);
final long[] cpuUsagesLong = mProcTimeInStateReader.getUsageTimesMillis(threadCpuStatPath);
if (cpuUsagesLong == null) {
return null;
}
int[] cpuUsages = mFrequencyBucketCreator.getBucketedValues(cpuUsagesLong);
return new ThreadCpuUsage(threadId, threadName, cpuUsages);
}
/**
* Get the command used to start a process
*/
private String getProcessName(Path processPath) {
final Path processNamePath = processPath.resolve(PROCESS_NAME_FILENAME);
final String processName =
ProcStatsUtil.readSingleLineProcFile(processNamePath.toString());
if (processName != null) {
return processName;
}
return DEFAULT_PROCESS_NAME;
}
/**
* Get the name of a thread, given the {@code /proc} path of the thread
*/
private String getThreadName(Path threadPath) {
final Path threadNamePath = threadPath.resolve(THREAD_NAME_FILENAME);
final String threadName =
ProcStatsUtil.readNullSeparatedFile(threadNamePath.toString());
if (threadName == null) {
return DEFAULT_THREAD_NAME;
}
return threadName;
}
/**
* Get the ID of a process from its path
*
* @param processPath {@code proc} path of the process
* @return the ID, {@link #ID_ERROR} if the path could not be parsed
*/
private int getProcessId(Path processPath) {
String fileName = processPath.getFileName().toString();
try {
return Integer.parseInt(fileName);
} catch (NumberFormatException e) {
Slog.w(TAG, "Failed to parse " + fileName + " as process ID", e);
return ID_ERROR;
}
}
/**
* Puts frequencies and usage times into buckets
*/
@VisibleForTesting
public static class FrequencyBucketCreator {
private final int mNumBuckets;
private final int mNumFrequencies;
private final int mBigFrequenciesStartIndex;
private final int mLittleNumBuckets;
private final int mBigNumBuckets;
private final int mLittleBucketSize;
private final int mBigBucketSize;
/**
* Buckets based of a list of frequencies
*
* @param frequencies the frequencies to base buckets off
* @param numBuckets how many buckets to create
*/
@VisibleForTesting
public FrequencyBucketCreator(long[] frequencies, int numBuckets) {
Preconditions.checkArgument(numBuckets > 0);
mNumFrequencies = frequencies.length;
mBigFrequenciesStartIndex = getBigFrequenciesStartIndex(frequencies);
final int littleNumBuckets;
final int bigNumBuckets;
if (mBigFrequenciesStartIndex < frequencies.length) {
littleNumBuckets = numBuckets / 2;
bigNumBuckets = numBuckets - littleNumBuckets;
} else {
// If we've got no big frequencies, set all buckets to little frequencies
littleNumBuckets = numBuckets;
bigNumBuckets = 0;
}
// Ensure that we don't have more buckets than frequencies
mLittleNumBuckets = Math.min(littleNumBuckets, mBigFrequenciesStartIndex);
mBigNumBuckets = Math.min(
bigNumBuckets, frequencies.length - mBigFrequenciesStartIndex);
mNumBuckets = mLittleNumBuckets + mBigNumBuckets;
// Set the size of each little and big bucket. If they have no buckets, the size is zero
mLittleBucketSize = mLittleNumBuckets == 0 ? 0 :
mBigFrequenciesStartIndex / mLittleNumBuckets;
mBigBucketSize = mBigNumBuckets == 0 ? 0 :
(frequencies.length - mBigFrequenciesStartIndex) / mBigNumBuckets;
}
/**
* Find the index where frequencies change from little core to big core
*/
@VisibleForTesting
public static int getBigFrequenciesStartIndex(long[] frequenciesKhz) {
for (int i = 0; i < frequenciesKhz.length - 1; i++) {
if (frequenciesKhz[i] > frequenciesKhz[i + 1]) {
return i + 1;
}
}
return frequenciesKhz.length;
}
/**
* Get the minimum frequency in each bucket
*/
@VisibleForTesting
public int[] getBucketMinFrequencies(long[] frequenciesKhz) {
Preconditions.checkArgument(frequenciesKhz.length == mNumFrequencies);
// If there's only one bucket, we bucket everything together so the first bucket is the
// min frequency
if (mNumBuckets == 1) {
return new int[]{(int) frequenciesKhz[0]};
}
final int[] bucketMinFrequencies = new int[mNumBuckets];
// Initialize little buckets min frequencies
for (int i = 0; i < mLittleNumBuckets; i++) {
bucketMinFrequencies[i] = (int) frequenciesKhz[i * mLittleBucketSize];
}
// Initialize big buckets min frequencies
for (int i = 0; i < mBigNumBuckets; i++) {
final int frequencyIndex = mBigFrequenciesStartIndex + i * mBigBucketSize;
bucketMinFrequencies[mLittleNumBuckets + i] = (int) frequenciesKhz[frequencyIndex];
}
return bucketMinFrequencies;
}
/**
* Put an array of values into buckets. This takes a {@code long[]} and returns {@code
* int[]} as everywhere this method is used will have to do the conversion anyway, so we
* save time by doing it here instead
*
* @param values the values to bucket
* @return the bucketed usage times
*/
@VisibleForTesting
public int[] getBucketedValues(long[] values) {
Preconditions.checkArgument(values.length == mNumFrequencies);
final int[] bucketed = new int[mNumBuckets];
// If there's only one bucket, add all frequencies in
if (mNumBuckets == 1) {
for (int i = 0; i < values.length; i++) {
bucketed[0] += values[i];
}
return bucketed;
}
// Initialize the little buckets
for (int i = 0; i < mBigFrequenciesStartIndex; i++) {
final int bucketIndex = Math.min(i / mLittleBucketSize, mLittleNumBuckets - 1);
bucketed[bucketIndex] += values[i];
}
// Initialize the big buckets
for (int i = mBigFrequenciesStartIndex; i < values.length; i++) {
final int bucketIndex = Math.min(
mLittleNumBuckets + (i - mBigFrequenciesStartIndex) / mBigBucketSize,
mNumBuckets - 1);
bucketed[bucketIndex] += values[i];
}
return bucketed;
}
}
/**
* CPU usage of a process
*/
public static class ProcessCpuUsage {
public final int processId;
public final String processName;
public final int uid;
public final ArrayList<ThreadCpuUsage> threadCpuUsages;
ProcessCpuUsage(
int processId,
String processName,
int uid,
ArrayList<ThreadCpuUsage> threadCpuUsages) {
this.processId = processId;
this.processName = processName;
this.uid = uid;
this.threadCpuUsages = threadCpuUsages;
}
}
/**
* CPU usage of a thread
*/
public static class ThreadCpuUsage {
public final int threadId;
public final String threadName;
public final int[] usageTimesMillis;
ThreadCpuUsage(
int threadId,
String threadName,
int[] usageTimesMillis) {
this.threadId = threadId;
this.threadName = threadName;
this.usageTimesMillis = usageTimesMillis;
}
}
/**
* Used to inject static methods from {@link Process}
*/
@VisibleForTesting
public static class Injector {
/**
* Get the PID of the current process
*/
public int myPid() {
return Process.myPid();
}
/**
* Get the UID that owns the current process
*/
public int myUid() {
return Process.myUid();
}
/**
* Get the UID for the process with ID {@code pid}
*/
public int getUidForPid(int pid) {
return Process.getUidForPid(pid);
}
}
}