blob: 6b277a0bd5124e503158d6f7fe4feac32a5c27eb [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 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;
/**
* 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.
*/
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";
/**
* 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");
/**
* 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 final int[] mFrequenciesKhz;
/**
* Used to read and parse {@code time_in_state} files
*/
private final ProcTimeInStateReader mProcTimeInStateReader;
private KernelCpuThreadReader() throws IOException {
this(DEFAULT_PROC_PATH, DEFAULT_INITIAL_TIME_IN_STATE_PATH);
}
/**
* 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(Path procPath, Path initialTimeInStatePath) throws IOException {
mProcPath = procPath;
mProcTimeInStateReader = new ProcTimeInStateReader(initialTimeInStatePath);
// Copy mProcTimeInState's frequencies, casting the longs to ints
long[] frequenciesKhz = mProcTimeInStateReader.getFrequenciesKhz();
mFrequenciesKhz = new int[frequenciesKhz.length];
for (int i = 0; i < frequenciesKhz.length; i++) {
mFrequenciesKhz[i] = (int) frequenciesKhz[i];
}
}
/**
* Create the reader and handle exceptions during creation
*
* @return the reader, null if an exception was thrown during creation
*/
@Nullable
public static KernelCpuThreadReader create() {
try {
return new KernelCpuThreadReader();
} catch (IOException e) {
Slog.e(TAG, "Failed to initialize KernelCpuThreadReader", e);
return null;
}
}
/**
* 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"),
Process.myPid(),
Process.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
*/
@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) {
Slog.w(TAG, "Failed to iterate over thread paths", e);
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 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 null in the case that the directory read failed
*/
@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;
}
// Convert long[] to int[]
final int[] 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;
}
/**
* 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;
}
}
}