summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
-rw-r--r--apct-tests/perftests/core/src/android/os/KernelCpuThreadReaderPerfTest.java52
-rw-r--r--core/java/android/os/Process.java5
-rw-r--r--core/java/com/android/internal/os/KernelCpuThreadReader.java306
-rw-r--r--core/java/com/android/internal/os/ProcStatsUtil.java145
-rw-r--r--core/java/com/android/internal/os/ProcTimeInStateReader.java186
-rw-r--r--core/java/com/android/internal/os/ProcessCpuTracker.java37
-rw-r--r--core/tests/coretests/src/com/android/internal/os/KernelCpuThreadReaderTest.java142
7 files changed, 836 insertions, 37 deletions
diff --git a/apct-tests/perftests/core/src/android/os/KernelCpuThreadReaderPerfTest.java b/apct-tests/perftests/core/src/android/os/KernelCpuThreadReaderPerfTest.java
new file mode 100644
index 000000000000..9034034539e9
--- /dev/null
+++ b/apct-tests/perftests/core/src/android/os/KernelCpuThreadReaderPerfTest.java
@@ -0,0 +1,52 @@
+/*
+ * 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 android.os;
+
+import static org.junit.Assert.assertNotNull;
+
+import android.perftests.utils.BenchmarkState;
+import android.perftests.utils.PerfStatusReporter;
+import android.support.test.filters.LargeTest;
+import android.support.test.runner.AndroidJUnit4;
+
+import com.android.internal.os.KernelCpuThreadReader;
+
+import org.junit.Rule;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+
+
+/**
+ * Performance tests collecting per-thread CPU data.
+ */
+@RunWith(AndroidJUnit4.class)
+@LargeTest
+public class KernelCpuThreadReaderPerfTest {
+ @Rule
+ public final PerfStatusReporter mPerfStatusReporter = new PerfStatusReporter();
+
+ private final KernelCpuThreadReader mKernelCpuThreadReader = KernelCpuThreadReader.create();
+
+ @Test
+ public void timeReadCurrentProcessCpuUsage() {
+ final BenchmarkState state = mPerfStatusReporter.getBenchmarkState();
+ assertNotNull(mKernelCpuThreadReader);
+ while (state.keepRunning()) {
+ this.mKernelCpuThreadReader.getCurrentProcessCpuUsage();
+ }
+ }
+}
diff --git a/core/java/android/os/Process.java b/core/java/android/os/Process.java
index 379d28cf10a0..651caece01f9 100644
--- a/core/java/android/os/Process.java
+++ b/core/java/android/os/Process.java
@@ -990,6 +990,8 @@ public class Process {
/** @hide */
public static final int PROC_TAB_TERM = (int)'\t';
/** @hide */
+ public static final int PROC_NEWLINE_TERM = (int) '\n';
+ /** @hide */
public static final int PROC_COMBINE = 0x100;
/** @hide */
public static final int PROC_PARENS = 0x200;
@@ -1009,7 +1011,8 @@ public class Process {
*
* <p>The format is a list of integers, where every integer describes a variable in the file. It
* specifies how the variable is syntactically terminated (e.g. {@link Process#PROC_SPACE_TERM},
- * {@link Process#PROC_TAB_TERM}, {@link Process#PROC_ZERO_TERM}).
+ * {@link Process#PROC_TAB_TERM}, {@link Process#PROC_ZERO_TERM}, {@link
+ * Process#PROC_NEWLINE_TERM}).
*
* <p>If the variable should be parsed and returned to the caller, the termination type should
* be binary OR'd with the type of output (e.g. {@link Process#PROC_OUT_STRING}, {@link
diff --git a/core/java/com/android/internal/os/KernelCpuThreadReader.java b/core/java/com/android/internal/os/KernelCpuThreadReader.java
new file mode 100644
index 000000000000..6b277a0bd512
--- /dev/null
+++ b/core/java/com/android/internal/os/KernelCpuThreadReader.java
@@ -0,0 +1,306 @@
+/*
+ * 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;
+ }
+ }
+}
diff --git a/core/java/com/android/internal/os/ProcStatsUtil.java b/core/java/com/android/internal/os/ProcStatsUtil.java
new file mode 100644
index 000000000000..06519758a698
--- /dev/null
+++ b/core/java/com/android/internal/os/ProcStatsUtil.java
@@ -0,0 +1,145 @@
+/*
+ * 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.StrictMode;
+import android.util.Slog;
+
+import java.io.ByteArrayOutputStream;
+import java.io.FileInputStream;
+import java.io.IOException;
+
+/**
+ * Utility functions for reading {@code proc} files
+ */
+final class ProcStatsUtil {
+
+ private static final String TAG = "ProcStatsUtil";
+
+ /**
+ * How much to read into a buffer when reading a proc file
+ */
+ private static final int READ_SIZE = 1024;
+
+ /**
+ * Class only contains static utility functions, and should not be instantiated
+ */
+ private ProcStatsUtil() {
+ }
+
+ /**
+ * Read a {@code proc} file where the contents are separated by null bytes. Replaces the null
+ * bytes with spaces, and removes any trailing null bytes
+ *
+ * @param path path of the file to read
+ */
+ @Nullable
+ static String readNullSeparatedFile(String path) {
+ String contents = readSingleLineProcFile(path);
+ if (contents == null) {
+ return null;
+ }
+
+ // Content is either double-null terminated, or terminates at end of line. Remove anything
+ // after the double-null
+ final int endIndex = contents.indexOf("\0\0");
+ if (endIndex != -1) {
+ contents = contents.substring(0, endIndex);
+ }
+
+ // Change the null-separated contents into space-seperated
+ return contents.replace("\0", " ");
+ }
+
+ /**
+ * Read a {@code proc} file that contains a single line (e.g. {@code /proc/$PID/cmdline}, {@code
+ * /proc/$PID/comm})
+ *
+ * @param path path of the file to read
+ */
+ @Nullable
+ static String readSingleLineProcFile(String path) {
+ return readTerminatedProcFile(path, (byte) '\n');
+ }
+
+ /**
+ * Read a {@code proc} file that terminates with a specific byte
+ *
+ * @param path path of the file to read
+ * @param terminator byte that terminates the file. We stop reading once this character is
+ * seen, or at the end of the file
+ */
+ @Nullable
+ public static String readTerminatedProcFile(String path, byte terminator) {
+ // Permit disk reads here, as /proc isn't really "on disk" and should be fast.
+ // TODO: make BlockGuard ignore /proc/ and /sys/ files perhaps?
+ final StrictMode.ThreadPolicy savedPolicy = StrictMode.allowThreadDiskReads();
+ try (FileInputStream is = new FileInputStream(path)) {
+ ByteArrayOutputStream byteStream = null;
+ final byte[] buffer = new byte[READ_SIZE];
+ while (true) {
+ // Read file into buffer
+ final int len = is.read(buffer);
+ if (len <= 0) {
+ // If we've read nothing, we're done
+ break;
+ }
+
+ // Find the terminating character
+ int terminatingIndex = -1;
+ for (int i = 0; i < len; i++) {
+ if (buffer[i] == terminator) {
+ terminatingIndex = i;
+ break;
+ }
+ }
+ final boolean foundTerminator = terminatingIndex != -1;
+
+ // If we have found it and the byte stream isn't initialized, we don't need to
+ // initialize it and can return the string here
+ if (foundTerminator && byteStream == null) {
+ return new String(buffer, 0, terminatingIndex);
+ }
+
+ // Initialize the byte stream
+ if (byteStream == null) {
+ byteStream = new ByteArrayOutputStream(READ_SIZE);
+ }
+
+ // Write the whole buffer if terminator not found, or up to the terminator if found
+ byteStream.write(buffer, 0, foundTerminator ? terminatingIndex : len);
+
+ // If we've found the terminator, we can finish
+ if (foundTerminator) {
+ break;
+ }
+ }
+
+ // If the byte stream is null at the end, this means that we have read an empty file
+ if (byteStream == null) {
+ return "";
+ }
+ return byteStream.toString();
+ } catch (IOException e) {
+ Slog.w(TAG, "Failed to open proc file", e);
+ return null;
+ } finally {
+ StrictMode.setThreadPolicy(savedPolicy);
+ }
+ }
+}
diff --git a/core/java/com/android/internal/os/ProcTimeInStateReader.java b/core/java/com/android/internal/os/ProcTimeInStateReader.java
new file mode 100644
index 000000000000..3a634984a4ec
--- /dev/null
+++ b/core/java/com/android/internal/os/ProcTimeInStateReader.java
@@ -0,0 +1,186 @@
+/*
+ * 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 java.io.IOException;
+import java.nio.file.Files;
+import java.nio.file.Path;
+
+/**
+ * Reads and parses {@code time_in_state} files in the {@code proc} filesystem.
+ *
+ * Every line in a {@code time_in_state} file contains two numbers, separated by a single space
+ * character. The first number is the frequency of the CPU used in kilohertz. The second number is
+ * the time spent in this frequency. In the {@code time_in_state} file, this is given in 10s of
+ * milliseconds, but this class returns in milliseconds. This can be per user, process, or thread
+ * depending on which {@code time_in_state} file is used.
+ *
+ * For example, a {@code time_in_state} file would look like this:
+ * <pre>
+ * 300000 3
+ * 364800 0
+ * ...
+ * 1824000 0
+ * 1900800 1
+ * </pre>
+ *
+ * This file would indicate that the CPU has spent 30 milliseconds at frequency 300,000KHz (300Mhz)
+ * and 10 milliseconds at frequency 1,900,800KHz (1.9GHz).
+ */
+public class ProcTimeInStateReader {
+ private static final String TAG = "ProcTimeInStateReader";
+
+ /**
+ * The format of a single line of the {@code time_in_state} file that exports the frequency
+ * values
+ */
+ private static final int[] TIME_IN_STATE_LINE_FREQUENCY_FORMAT = {
+ Process.PROC_OUT_LONG | Process.PROC_SPACE_TERM,
+ Process.PROC_NEWLINE_TERM,
+ };
+
+ /**
+ * The format of a single line of the {@code time_in_state} file that exports the time values
+ */
+ private static final int[] TIME_IN_STATE_LINE_TIME_FORMAT = {
+ Process.PROC_SPACE_TERM,
+ Process.PROC_OUT_LONG | Process.PROC_NEWLINE_TERM,
+ };
+
+ /**
+ * The format of the {@code time_in_state} file, defined using {@link Process}'s {@code
+ * PROC_OUT_LONG} and related variables
+ *
+ * Defined on first successful read of {@code time_in_state} file.
+ */
+ private int[] mTimeInStateTimeFormat;
+
+ /**
+ * The frequencies reported in each {@code time_in_state} file
+ *
+ * Defined on first successful read of {@code time_in_state} file.
+ */
+ private long[] mFrequenciesKhz;
+
+ /**
+ * @param initialTimeInStateFile the file to base the format of the frequency files on, and to
+ * read frequencies from. Expected to be in the same format as all other {@code time_in_state}
+ * files, and contain the same frequencies.
+ * @throws IOException if reading the initial {@code time_in_state} file failed
+ */
+ public ProcTimeInStateReader(Path initialTimeInStateFile) throws IOException {
+ initializeTimeInStateFormat(initialTimeInStateFile);
+ }
+
+ /**
+ * Read the CPU usages from a file
+ *
+ * @param timeInStatePath path where the CPU usages are read from
+ * @return list of CPU usage times from the file. These correspond to the CPU frequencies given
+ * by {@link ProcTimeInStateReader#getFrequenciesKhz}
+ */
+ @Nullable
+ public long[] getUsageTimesMillis(final Path timeInStatePath) {
+ // Read in the time_in_state file
+ final long[] readLongs = new long[mFrequenciesKhz.length];
+ final boolean readSuccess = Process.readProcFile(
+ timeInStatePath.toString(),
+ mTimeInStateTimeFormat,
+ null, readLongs, null);
+ if (!readSuccess) {
+ return null;
+ }
+ // Usage time is given in 10ms, so convert to ms
+ for (int i = 0; i < readLongs.length; i++) {
+ readLongs[i] *= 10;
+ }
+ return readLongs;
+ }
+
+ /**
+ * Get the frequencies found in each {@code time_in_state} file
+ *
+ * @return list of CPU frequencies. These correspond to the CPU times given by {@link
+ * ProcTimeInStateReader#getUsageTimesMillis(Path)}()}.
+ */
+ @Nullable
+ public long[] getFrequenciesKhz() {
+ return mFrequenciesKhz;
+ }
+
+ /**
+ * Set the {@link #mTimeInStateTimeFormat} and {@link #mFrequenciesKhz} variables based on the
+ * an input file. If the file is empty, these variables aren't set
+ *
+ * This needs to be run once on the first invocation of {@link #getUsageTimesMillis(Path)}. This
+ * is because we need to know how many frequencies are available in order to parse time
+ * {@code time_in_state} file using {@link Process#readProcFile}, which only accepts
+ * fixed-length formats. Also, as the frequencies do not change between {@code time_in_state}
+ * files, we read and store them here.
+ *
+ * @param timeInStatePath the input file to base the format off of
+ */
+ private void initializeTimeInStateFormat(final Path timeInStatePath) throws IOException {
+ // Read the bytes of the `time_in_state` file
+ byte[] timeInStateBytes = Files.readAllBytes(timeInStatePath);
+
+ // The number of lines in the `time_in_state` file is the number of frequencies available
+ int numFrequencies = 0;
+ for (int i = 0; i < timeInStateBytes.length; i++) {
+ if (timeInStateBytes[i] == '\n') {
+ numFrequencies++;
+ }
+ }
+ if (numFrequencies == 0) {
+ throw new IOException("Empty time_in_state file");
+ }
+
+ // Set `mTimeInStateTimeFormat` and `timeInStateFrequencyFormat` to the correct length, and
+ // then copy in the `TIME_IN_STATE_{FREQUENCY,TIME}_LINE_FORMAT` until it's full. As we only
+ // use the frequency format in this method, it is not an member variable.
+ final int[] timeInStateTimeFormat =
+ new int[numFrequencies * TIME_IN_STATE_LINE_TIME_FORMAT.length];
+ final int[] timeInStateFrequencyFormat =
+ new int[numFrequencies * TIME_IN_STATE_LINE_FREQUENCY_FORMAT.length];
+ for (int i = 0; i < numFrequencies; i++) {
+ System.arraycopy(
+ TIME_IN_STATE_LINE_FREQUENCY_FORMAT, 0, timeInStateFrequencyFormat,
+ i * TIME_IN_STATE_LINE_FREQUENCY_FORMAT.length,
+ TIME_IN_STATE_LINE_FREQUENCY_FORMAT.length);
+ System.arraycopy(
+ TIME_IN_STATE_LINE_TIME_FORMAT, 0, timeInStateTimeFormat,
+ i * TIME_IN_STATE_LINE_TIME_FORMAT.length,
+ TIME_IN_STATE_LINE_TIME_FORMAT.length);
+ }
+
+ // Read the frequencies from the `time_in_state` file and store them, as they will be the
+ // same for every `time_in_state` file
+ final long[] readLongs = new long[numFrequencies];
+ final boolean readSuccess = Process.parseProcLine(
+ timeInStateBytes, 0, timeInStateBytes.length, timeInStateFrequencyFormat,
+ null, readLongs, null);
+ if (!readSuccess) {
+ throw new IOException("Failed to parse time_in_state file");
+ }
+
+ mTimeInStateTimeFormat = timeInStateTimeFormat;
+ mFrequenciesKhz = readLongs;
+ }
+}
diff --git a/core/java/com/android/internal/os/ProcessCpuTracker.java b/core/java/com/android/internal/os/ProcessCpuTracker.java
index 1ee4269d974b..4b878c7c4808 100644
--- a/core/java/com/android/internal/os/ProcessCpuTracker.java
+++ b/core/java/com/android/internal/os/ProcessCpuTracker.java
@@ -28,10 +28,7 @@ import android.util.Slog;
import com.android.internal.util.FastPrintWriter;
-import libcore.io.IoUtils;
-
import java.io.File;
-import java.io.FileInputStream;
import java.io.PrintWriter;
import java.io.StringWriter;
import java.text.SimpleDateFormat;
@@ -40,7 +37,6 @@ import java.util.Collections;
import java.util.Comparator;
import java.util.Date;
import java.util.List;
-import java.util.StringTokenizer;
public class ProcessCpuTracker {
private static final String TAG = "ProcessCpuTracker";
@@ -176,8 +172,6 @@ public class ProcessCpuTracker {
private boolean mFirst = true;
- private byte[] mBuffer = new byte[4096];
-
public interface FilterStats {
/** Which stats to pick when filtering */
boolean needed(Stats stats);
@@ -863,40 +857,11 @@ public class ProcessCpuTracker {
pw.println();
}
- private String readFile(String file, char endChar) {
- // Permit disk reads here, as /proc/meminfo isn't really "on
- // disk" and should be fast. TODO: make BlockGuard ignore
- // /proc/ and /sys/ files perhaps?
- StrictMode.ThreadPolicy savedPolicy = StrictMode.allowThreadDiskReads();
- FileInputStream is = null;
- try {
- is = new FileInputStream(file);
- int len = is.read(mBuffer);
- is.close();
-
- if (len > 0) {
- int i;
- for (i=0; i<len; i++) {
- if (mBuffer[i] == endChar) {
- break;
- }
- }
- return new String(mBuffer, 0, i);
- }
- } catch (java.io.FileNotFoundException e) {
- } catch (java.io.IOException e) {
- } finally {
- IoUtils.closeQuietly(is);
- StrictMode.setThreadPolicy(savedPolicy);
- }
- return null;
- }
-
private void getName(Stats st, String cmdlineFile) {
String newName = st.name;
if (st.name == null || st.name.equals("app_process")
|| st.name.equals("<pre-initialized>")) {
- String cmdName = readFile(cmdlineFile, '\0');
+ String cmdName = ProcStatsUtil.readTerminatedProcFile(cmdlineFile, (byte) '\0');
if (cmdName != null && cmdName.length() > 1) {
newName = cmdName;
int i = newName.lastIndexOf("/");
diff --git a/core/tests/coretests/src/com/android/internal/os/KernelCpuThreadReaderTest.java b/core/tests/coretests/src/com/android/internal/os/KernelCpuThreadReaderTest.java
new file mode 100644
index 000000000000..b9ef4349e414
--- /dev/null
+++ b/core/tests/coretests/src/com/android/internal/os/KernelCpuThreadReaderTest.java
@@ -0,0 +1,142 @@
+/*
+ * 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 static org.junit.Assert.assertEquals;
+import static org.junit.Assert.assertNotNull;
+import static org.junit.Assert.assertTrue;
+
+import android.content.Context;
+import android.os.FileUtils;
+import android.support.test.InstrumentationRegistry;
+import android.support.test.filters.SmallTest;
+import android.support.test.runner.AndroidJUnit4;
+
+import org.junit.After;
+import org.junit.Before;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+
+import java.io.File;
+import java.io.IOException;
+import java.io.OutputStream;
+import java.nio.file.Files;
+import java.nio.file.Path;
+import java.util.ArrayList;
+import java.util.Comparator;
+
+@SmallTest
+@RunWith(AndroidJUnit4.class)
+public class KernelCpuThreadReaderTest {
+
+ private static final String PROCESS_NAME = "test_process";
+ private static final int[] THREAD_IDS = {0, 1000, 1235, 4321};
+ private static final String[] THREAD_NAMES = {
+ "test_thread_1", "test_thread_2", "test_thread_3", "test_thread_4"
+ };
+ private static final int[] THREAD_CPU_FREQUENCIES = {
+ 1000, 2000, 3000, 4000,
+ };
+ private static final int[][] THREAD_CPU_TIMES = {
+ {1, 0, 0, 1},
+ {0, 0, 0, 0},
+ {1000, 1000, 1000, 1000},
+ {0, 1, 2, 3},
+ };
+
+ private File mProcDirectory;
+
+ @Before
+ public void setUp() {
+ Context context = InstrumentationRegistry.getContext();
+ mProcDirectory = context.getDir("proc", Context.MODE_PRIVATE);
+ }
+
+ @After
+ public void tearDown() throws Exception {
+ FileUtils.deleteContents(mProcDirectory);
+ }
+
+ @Test
+ public void testSimple() throws IOException {
+ // Make /proc/self
+ final Path selfPath = mProcDirectory.toPath().resolve("self");
+ assertTrue(selfPath.toFile().mkdirs());
+
+ // Make /proc/self/task
+ final Path selfThreadsPath = selfPath.resolve("task");
+ assertTrue(selfThreadsPath.toFile().mkdirs());
+
+ // Make /proc/self/cmdline
+ Files.write(selfPath.resolve("cmdline"), PROCESS_NAME.getBytes());
+
+ // Make thread directories in reverse order, as they are read in order of creation by
+ // CpuThreadProcReader
+ for (int i = 0; i < THREAD_IDS.length; i++) {
+ // Make /proc/self/task/$TID
+ final Path threadPath = selfThreadsPath.resolve(String.valueOf(THREAD_IDS[i]));
+ assertTrue(threadPath.toFile().mkdirs());
+
+ // Make /proc/self/task/$TID/comm
+ Files.write(threadPath.resolve("comm"), THREAD_NAMES[i].getBytes());
+
+ // Make /proc/self/task/$TID/time_in_state
+ final OutputStream timeInStateStream =
+ Files.newOutputStream(threadPath.resolve("time_in_state"));
+ for (int j = 0; j < THREAD_CPU_FREQUENCIES.length; j++) {
+ final String line = String.valueOf(THREAD_CPU_FREQUENCIES[j]) + " "
+ + String.valueOf(THREAD_CPU_TIMES[i][j]) + "\n";
+ timeInStateStream.write(line.getBytes());
+ }
+ timeInStateStream.close();
+ }
+
+ final KernelCpuThreadReader kernelCpuThreadReader = new KernelCpuThreadReader(
+ mProcDirectory.toPath(),
+ mProcDirectory.toPath().resolve("self/task/" + THREAD_IDS[0] + "/time_in_state"));
+ final KernelCpuThreadReader.ProcessCpuUsage processCpuUsage =
+ kernelCpuThreadReader.getCurrentProcessCpuUsage();
+
+ assertNotNull(processCpuUsage);
+ assertEquals(android.os.Process.myPid(), processCpuUsage.processId);
+ assertEquals(android.os.Process.myUid(), processCpuUsage.uid);
+ assertEquals(PROCESS_NAME, processCpuUsage.processName);
+
+ // Sort the thread CPU usages to compare with test case
+ final ArrayList<KernelCpuThreadReader.ThreadCpuUsage> threadCpuUsages =
+ new ArrayList<>(processCpuUsage.threadCpuUsages);
+ threadCpuUsages.sort(Comparator.comparingInt(a -> a.threadId));
+
+ int threadCount = 0;
+ for (KernelCpuThreadReader.ThreadCpuUsage threadCpuUsage : threadCpuUsages) {
+ assertEquals(THREAD_IDS[threadCount], threadCpuUsage.threadId);
+ assertEquals(THREAD_NAMES[threadCount], threadCpuUsage.threadName);
+
+ for (int i = 0; i < threadCpuUsage.usageTimesMillis.length; i++) {
+ assertEquals(
+ THREAD_CPU_TIMES[threadCount][i] * 10,
+ threadCpuUsage.usageTimesMillis[i]);
+ assertEquals(
+ THREAD_CPU_FREQUENCIES[i],
+ kernelCpuThreadReader.getCpuFrequenciesKhz()[i]);
+ }
+ threadCount++;
+ }
+
+ assertEquals(threadCount, THREAD_IDS.length);
+ }
+}