diff options
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); + } +} |