572 lines
23 KiB
Java
572 lines
23 KiB
Java
/*
|
|
* 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.IntArray;
|
|
import android.util.Slog;
|
|
|
|
import com.android.internal.annotations.VisibleForTesting;
|
|
import com.android.internal.util.Preconditions;
|
|
|
|
import java.io.IOException;
|
|
import java.nio.file.DirectoryIteratorException;
|
|
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.Arrays;
|
|
import java.util.function.Predicate;
|
|
|
|
/**
|
|
* Iterates over processes, and all threads owned by those processes, and return the CPU usage for
|
|
* each thread. The CPU usage statistics contain the amount of time spent in a frequency band. CPU
|
|
* usage is collected using {@link ProcTimeInStateReader}.
|
|
*
|
|
* <p>We only collect CPU data for processes and threads that are owned by certain UIDs. These UIDs
|
|
* are configured via {@link #setUidPredicate}.
|
|
*
|
|
* <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 2017 device. 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> getProcessCpuUsage() {
|
|
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(TAG, "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;
|
|
}
|
|
|
|
/**
|
|
* Get the CPU frequencies that correspond to the times reported in {@link
|
|
* ThreadCpuUsage#usageTimesMillis}
|
|
*/
|
|
@Nullable
|
|
public int[] getCpuFrequenciesKhz() {
|
|
return mFrequenciesKhz;
|
|
}
|
|
|
|
/** Set the number of frequency buckets to use */
|
|
void setNumBuckets(int numBuckets) {
|
|
// If `numBuckets` hasn't changed since the last set, do nothing
|
|
if (mFrequenciesKhz != null && mFrequenciesKhz.length == numBuckets) {
|
|
return;
|
|
}
|
|
|
|
final long[] frequenciesKhz = mProcTimeInStateReader.getFrequenciesKhz();
|
|
if (numBuckets != 0) {
|
|
mFrequencyBucketCreator = new FrequencyBucketCreator(frequenciesKhz, numBuckets);
|
|
mFrequenciesKhz = mFrequencyBucketCreator.bucketFrequencies(frequenciesKhz);
|
|
} else {
|
|
mFrequencyBucketCreator = null;
|
|
mFrequenciesKhz = new int[frequenciesKhz.length];
|
|
for (int i = 0; i < frequenciesKhz.length; i++) {
|
|
mFrequenciesKhz[i] = (int) frequenciesKhz[i];
|
|
}
|
|
}
|
|
}
|
|
|
|
/** Set the UID predicate for {@link #getProcessCpuUsage} */
|
|
@VisibleForTesting
|
|
public void setUidPredicate(Predicate<Integer> uidPredicate) {
|
|
mUidPredicate = uidPredicate;
|
|
}
|
|
|
|
/**
|
|
* 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) {
|
|
continue;
|
|
}
|
|
threadCpuUsages.add(threadCpuUsage);
|
|
}
|
|
} catch (IOException | DirectoryIteratorException 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);
|
|
}
|
|
|
|
/**
|
|
* 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;
|
|
}
|
|
final int[] cpuUsages;
|
|
if (mFrequencyBucketCreator != null) {
|
|
cpuUsages = mFrequencyBucketCreator.bucketValues(cpuUsagesLong);
|
|
} else {
|
|
cpuUsages = new int[cpuUsagesLong.length];
|
|
for (int i = 0; i < cpuUsagesLong.length; i++) {
|
|
cpuUsages[i] = (int) cpuUsagesLong[i];
|
|
}
|
|
}
|
|
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;
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Quantizes a list of N frequencies into a list of M frequencies (where M<=N)
|
|
*
|
|
* <p>In order to reduce data sent from the device, we discard precise frequency information for
|
|
* an approximation. This is done by putting groups of adjacent frequencies into the same
|
|
* bucket, and then reporting that bucket under the minimum frequency in that bucket.
|
|
*
|
|
* <p>Many devices have multiple core clusters. We do not want to report frequencies from
|
|
* different clusters under the same bucket, so some complication arises.
|
|
*
|
|
* <p>Buckets are allocated evenly across all core clusters, i.e. they all have the same number
|
|
* of buckets regardless of how many frequencies they contain. This is done to reduce code
|
|
* complexity, and in practice the number of frequencies doesn't vary too much between core
|
|
* clusters.
|
|
*
|
|
* <p>If the number of buckets is not a factor of the number of frequencies, the remainder of
|
|
* the frequencies are placed into the last bucket.
|
|
*
|
|
* <p>It is possible to have less buckets than asked for, so any calling code can't assume that
|
|
* initializing with N buckets will use return N values. This happens in two scenarios:
|
|
*
|
|
* <ul>
|
|
* <li>There are less frequencies available than buckets asked for.
|
|
* <li>There are less frequencies in a core cluster than buckets allocated to that core
|
|
* cluster.
|
|
* </ul>
|
|
*/
|
|
@VisibleForTesting
|
|
public static class FrequencyBucketCreator {
|
|
private final int mNumFrequencies;
|
|
private final int mNumBuckets;
|
|
private final int[] mBucketStartIndices;
|
|
|
|
@VisibleForTesting
|
|
public FrequencyBucketCreator(long[] frequencies, int targetNumBuckets) {
|
|
mNumFrequencies = frequencies.length;
|
|
int[] clusterStartIndices = getClusterStartIndices(frequencies);
|
|
mBucketStartIndices =
|
|
getBucketStartIndices(clusterStartIndices, targetNumBuckets, mNumFrequencies);
|
|
mNumBuckets = mBucketStartIndices.length;
|
|
}
|
|
|
|
/**
|
|
* 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[] bucketValues(long[] values) {
|
|
Preconditions.checkArgument(values.length == mNumFrequencies);
|
|
int[] buckets = new int[mNumBuckets];
|
|
for (int bucketIdx = 0; bucketIdx < mNumBuckets; bucketIdx++) {
|
|
final int bucketStartIdx = getLowerBound(bucketIdx, mBucketStartIndices);
|
|
final int bucketEndIdx =
|
|
getUpperBound(bucketIdx, mBucketStartIndices, values.length);
|
|
for (int valuesIdx = bucketStartIdx; valuesIdx < bucketEndIdx; valuesIdx++) {
|
|
buckets[bucketIdx] += values[valuesIdx];
|
|
}
|
|
}
|
|
return buckets;
|
|
}
|
|
|
|
/** Get the minimum frequency in each bucket */
|
|
@VisibleForTesting
|
|
public int[] bucketFrequencies(long[] frequencies) {
|
|
Preconditions.checkArgument(frequencies.length == mNumFrequencies);
|
|
int[] buckets = new int[mNumBuckets];
|
|
for (int i = 0; i < buckets.length; i++) {
|
|
buckets[i] = (int) frequencies[mBucketStartIndices[i]];
|
|
}
|
|
return buckets;
|
|
}
|
|
|
|
/**
|
|
* Get the index in frequencies where each core cluster starts
|
|
*
|
|
* <p>The frequencies for each cluster are given in ascending order, appended to each other.
|
|
* This means that every time there is a decrease in frequencies (instead of increase) a new
|
|
* cluster has started.
|
|
*/
|
|
private static int[] getClusterStartIndices(long[] frequencies) {
|
|
IntArray indices = new IntArray();
|
|
indices.add(0);
|
|
for (int i = 0; i < frequencies.length - 1; i++) {
|
|
if (frequencies[i] >= frequencies[i + 1]) {
|
|
indices.add(i + 1);
|
|
}
|
|
}
|
|
return indices.toArray();
|
|
}
|
|
|
|
/** Get the index in frequencies where each bucket starts */
|
|
private static int[] getBucketStartIndices(
|
|
int[] clusterStartIndices, int targetNumBuckets, int numFrequencies) {
|
|
int numClusters = clusterStartIndices.length;
|
|
|
|
// If we haven't got enough buckets for every cluster, we instead have one bucket per
|
|
// cluster, with the last bucket containing the remaining clusters
|
|
if (numClusters > targetNumBuckets) {
|
|
return Arrays.copyOfRange(clusterStartIndices, 0, targetNumBuckets);
|
|
}
|
|
|
|
IntArray bucketStartIndices = new IntArray();
|
|
for (int clusterIdx = 0; clusterIdx < numClusters; clusterIdx++) {
|
|
final int clusterStartIdx = getLowerBound(clusterIdx, clusterStartIndices);
|
|
final int clusterEndIdx =
|
|
getUpperBound(clusterIdx, clusterStartIndices, numFrequencies);
|
|
|
|
final int numBucketsInCluster;
|
|
if (clusterIdx != numClusters - 1) {
|
|
numBucketsInCluster = targetNumBuckets / numClusters;
|
|
} else {
|
|
// If we're in the last cluster, the bucket will contain the remainder of the
|
|
// frequencies
|
|
int previousBucketsInCluster = targetNumBuckets / numClusters;
|
|
numBucketsInCluster =
|
|
targetNumBuckets - (previousBucketsInCluster * (numClusters - 1));
|
|
}
|
|
|
|
final int numFrequenciesInCluster = clusterEndIdx - clusterStartIdx;
|
|
// If there are less frequencies than buckets in a cluster, we have one bucket per
|
|
// frequency, and do not use the remaining buckets
|
|
final int numFrequenciesInBucket =
|
|
Math.max(1, numFrequenciesInCluster / numBucketsInCluster);
|
|
for (int bucketIdx = 0; bucketIdx < numBucketsInCluster; bucketIdx++) {
|
|
int bucketStartIdx = clusterStartIdx + bucketIdx * numFrequenciesInBucket;
|
|
// If we've gone over the end index, ignore the rest of the buckets for this
|
|
// cluster
|
|
if (bucketStartIdx >= clusterEndIdx) {
|
|
break;
|
|
}
|
|
bucketStartIndices.add(bucketStartIdx);
|
|
}
|
|
}
|
|
return bucketStartIndices.toArray();
|
|
}
|
|
|
|
private static int getLowerBound(int index, int[] startIndices) {
|
|
return startIndices[index];
|
|
}
|
|
|
|
private static int getUpperBound(int index, int[] startIndices, int max) {
|
|
if (index != startIndices.length - 1) {
|
|
return startIndices[index + 1];
|
|
} else {
|
|
return max;
|
|
}
|
|
}
|
|
}
|
|
|
|
/** CPU usage of a process */
|
|
public static class ProcessCpuUsage {
|
|
public final int processId;
|
|
public final String processName;
|
|
public final int uid;
|
|
public ArrayList<ThreadCpuUsage> threadCpuUsages;
|
|
|
|
@VisibleForTesting
|
|
public 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 int[] usageTimesMillis;
|
|
|
|
@VisibleForTesting
|
|
public 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 UID for the process with ID {@code pid} */
|
|
public int getUidForPid(int pid) {
|
|
return Process.getUidForPid(pid);
|
|
}
|
|
}
|
|
}
|