diff options
3 files changed, 282 insertions, 64 deletions
diff --git a/core/java/com/android/internal/os/KernelCpuThreadReader.java b/core/java/com/android/internal/os/KernelCpuThreadReader.java index ade5d05b592e..3861739228a3 100644 --- a/core/java/com/android/internal/os/KernelCpuThreadReader.java +++ b/core/java/com/android/internal/os/KernelCpuThreadReader.java @@ -29,6 +29,7 @@ import java.nio.file.Files; import java.nio.file.Path; import java.nio.file.Paths; import java.util.ArrayList; +import java.util.function.Predicate; /** * Given a process, will iterate over the child threads of the process, and return the CPU usage @@ -70,6 +71,11 @@ public class KernelCpuThreadReader { 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"; @@ -96,6 +102,18 @@ public class KernelCpuThreadReader { private static final int NUM_BUCKETS = 8; /** + * Default predicate for what UIDs to check for when getting processes. This filters to only + * select system UIDs (1000-1999) + */ + private static final Predicate<Integer> DEFAULT_UID_PREDICATE = + uid -> 1000 <= uid && uid < 2000; + + /** + * Value returned when there was an error getting an integer ID value (e.g. PID, UID) + */ + private static final int ID_ERROR = -1; + + /** * Where the proc filesystem is mounted */ private final Path mProcPath; @@ -116,8 +134,13 @@ public class KernelCpuThreadReader { */ private final FrequencyBucketCreator mFrequencyBucketCreator; + private final Injector mInjector; + private KernelCpuThreadReader() throws IOException { - this(DEFAULT_PROC_PATH, DEFAULT_INITIAL_TIME_IN_STATE_PATH); + this( + DEFAULT_PROC_PATH, + DEFAULT_INITIAL_TIME_IN_STATE_PATH, + new Injector()); } /** @@ -128,9 +151,13 @@ public class KernelCpuThreadReader { * format */ @VisibleForTesting - public KernelCpuThreadReader(Path procPath, Path initialTimeInStatePath) throws IOException { + public KernelCpuThreadReader( + Path procPath, + Path initialTimeInStatePath, + Injector injector) throws IOException { mProcPath = procPath; mProcTimeInStateReader = new ProcTimeInStateReader(initialTimeInStatePath); + mInjector = injector; // Copy mProcTimeInState's frequencies and initialize bucketing final long[] frequenciesKhz = mProcTimeInStateReader.getFrequenciesKhz(); @@ -154,6 +181,67 @@ public class KernelCpuThreadReader { } /** + * Get the per-thread CPU usage of all processes belonging to UIDs between {@code [1000, 2000)} + */ + @Nullable + public ArrayList<ProcessCpuUsage> getProcessCpuUsageByUids() { + return getProcessCpuUsageByUids(DEFAULT_UID_PREDICATE); + } + + /** + * Get the per-thread CPU usage of all processes belonging to a set of UIDs + * + * <p>This function will crawl through all process {@code proc} directories found by the pattern + * {@code /proc/[0-9]*}, and then check the UID using {@code /proc/$PID/status}. This takes + * approximately 500ms on a Pixel 2. Therefore, this method can be computationally expensive, + * and should not be called more than once an hour. + * + * @param uidPredicate only get usage from processes owned by UIDs that match this predicate + */ + @Nullable + public ArrayList<ProcessCpuUsage> getProcessCpuUsageByUids(Predicate<Integer> uidPredicate) { + 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 (!uidPredicate.test(uid)) { + continue; + } + + final ProcessCpuUsage processCpuUsage = + getProcessCpuUsage(processPath, processId, uid); + if (processCpuUsage != null) { + processCpuUsages.add(processCpuUsage); + } + } + } catch (IOException e) { + Slog.w("Failed to iterate over process paths", e); + return null; + } + + if (processCpuUsages.isEmpty()) { + Slog.w(TAG, "Didn't successfully get any process CPU information for UIDs specified"); + return null; + } + + if (DEBUG) { + Slog.d(TAG, "Read usage for " + processCpuUsages.size() + " processes"); + } + + return processCpuUsages; + } + + /** * Read all of the CPU usage statistics for each child thread of the current process * * @return process CPU usage containing usage of all child threads @@ -162,8 +250,8 @@ public class KernelCpuThreadReader { public ProcessCpuUsage getCurrentProcessCpuUsage() { return getProcessCpuUsage( mProcPath.resolve("self"), - Process.myPid(), - Process.myUid()); + mInjector.myPid(), + mInjector.myUid()); } /** @@ -172,7 +260,8 @@ public class KernelCpuThreadReader { * @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 + * @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) { @@ -224,7 +313,8 @@ public class KernelCpuThreadReader { * 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 + * @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) { @@ -280,6 +370,22 @@ public class KernelCpuThreadReader { } /** + * Get the ID of a process from its path + * + * @param processPath {@code proc} path of the process + * @return the ID, {@link #ID_ERROR} if the path could not be parsed + */ + private int getProcessId(Path processPath) { + String fileName = processPath.getFileName().toString(); + try { + return Integer.parseInt(fileName); + } catch (NumberFormatException e) { + Slog.w(TAG, "Failed to parse " + fileName + " as process ID", e); + return ID_ERROR; + } + } + + /** * Puts frequencies and usage times into buckets */ @VisibleForTesting @@ -443,4 +549,31 @@ public class KernelCpuThreadReader { this.usageTimesMillis = usageTimesMillis; } } + + /** + * Used to inject static methods from {@link Process} + */ + @VisibleForTesting + public static class Injector { + /** + * Get the PID of the current process + */ + public int myPid() { + return Process.myPid(); + } + + /** + * Get the UID that owns the current process + */ + public int myUid() { + return Process.myUid(); + } + + /** + * Get the UID for the process with ID {@code pid} + */ + public int getUidForPid(int pid) { + return Process.getUidForPid(pid); + } + } } diff --git a/core/tests/coretests/src/com/android/internal/os/KernelCpuThreadReaderTest.java b/core/tests/coretests/src/com/android/internal/os/KernelCpuThreadReaderTest.java index c866bc4c3e00..b242a34cc703 100644 --- a/core/tests/coretests/src/com/android/internal/os/KernelCpuThreadReaderTest.java +++ b/core/tests/coretests/src/com/android/internal/os/KernelCpuThreadReaderTest.java @@ -39,13 +39,16 @@ import java.nio.file.Files; import java.nio.file.Path; import java.util.ArrayList; import java.util.Comparator; +import java.util.function.Predicate; @SmallTest @RunWith(AndroidJUnit4.class) public class KernelCpuThreadReaderTest { - private static final String PROCESS_NAME = "test_process"; + private static final int UID = 1000; + private static final int PROCESS_ID = 1234; private static final int[] THREAD_IDS = {0, 1000, 1235, 4321}; + private static final String PROCESS_NAME = "test_process"; private static final String[] THREAD_NAMES = { "test_thread_1", "test_thread_2", "test_thread_3", "test_thread_4" }; @@ -73,49 +76,126 @@ public class KernelCpuThreadReaderTest { } @Test - public void testSimple() throws IOException { - // Make /proc/self - final Path selfPath = mProcDirectory.toPath().resolve("self"); - assertTrue(selfPath.toFile().mkdirs()); + public void testReader_currentProcess() throws IOException { + KernelCpuThreadReader.Injector processUtils = + new KernelCpuThreadReader.Injector() { + @Override + public int myPid() { + return PROCESS_ID; + } + + @Override + public int myUid() { + return UID; + } + + @Override + public int getUidForPid(int pid) { + return 0; + } + }; + setupDirectory(mProcDirectory.toPath().resolve("self"), THREAD_IDS, PROCESS_NAME, + THREAD_NAMES, THREAD_CPU_FREQUENCIES, THREAD_CPU_TIMES); + + final KernelCpuThreadReader kernelCpuThreadReader = new KernelCpuThreadReader( + mProcDirectory.toPath(), + mProcDirectory.toPath().resolve("self/task/" + THREAD_IDS[0] + "/time_in_state"), + processUtils); + final KernelCpuThreadReader.ProcessCpuUsage processCpuUsage = + kernelCpuThreadReader.getCurrentProcessCpuUsage(); + checkResults(processCpuUsage, kernelCpuThreadReader.getCpuFrequenciesKhz(), UID, PROCESS_ID, + THREAD_IDS, PROCESS_NAME, THREAD_NAMES, THREAD_CPU_FREQUENCIES, THREAD_CPU_TIMES); + } + + @Test + public void testReader_byUids() throws IOException { + int[] uids = new int[]{0, 2, 3, 4, 5, 6000}; + Predicate<Integer> uidPredicate = uid -> uid == 0 || uid >= 4; + int[] expectedUids = new int[]{0, 4, 5, 6000}; + KernelCpuThreadReader.Injector processUtils = + new KernelCpuThreadReader.Injector() { + @Override + public int myPid() { + return 0; + } + + @Override + public int myUid() { + return 0; + } + + @Override + public int getUidForPid(int pid) { + return pid; + } + }; + + for (int uid : uids) { + setupDirectory(mProcDirectory.toPath().resolve(String.valueOf(uid)), + new int[]{uid * 10}, + "process" + uid, new String[]{"thread" + uid}, new int[]{1000}, + new int[][]{{uid}}); + } + final KernelCpuThreadReader kernelCpuThreadReader = new KernelCpuThreadReader( + mProcDirectory.toPath(), + mProcDirectory.toPath().resolve(uids[0] + "/task/" + uids[0] + "/time_in_state"), + processUtils); + ArrayList<KernelCpuThreadReader.ProcessCpuUsage> processCpuUsageByUids = + kernelCpuThreadReader.getProcessCpuUsageByUids(uidPredicate); + processCpuUsageByUids.sort(Comparator.comparing(usage -> usage.processId)); + + assertEquals(expectedUids.length, processCpuUsageByUids.size()); + for (int i = 0; i < expectedUids.length; i++) { + KernelCpuThreadReader.ProcessCpuUsage processCpuUsage = + processCpuUsageByUids.get(i); + int uid = expectedUids[i]; + checkResults(processCpuUsage, kernelCpuThreadReader.getCpuFrequenciesKhz(), + uid, uid, new int[]{uid * 10}, "process" + uid, new String[]{"thread" + uid}, + new int[]{1000}, new int[][]{{uid}}); + } + } + + private void setupDirectory(Path processPath, int[] threadIds, String processName, + String[] threadNames, int[] cpuFrequencies, int[][] cpuTimes) throws IOException { + // Make /proc/$PID + assertTrue(processPath.toFile().mkdirs()); - // Make /proc/self/task - final Path selfThreadsPath = selfPath.resolve("task"); + // Make /proc/$PID/task + final Path selfThreadsPath = processPath.resolve("task"); assertTrue(selfThreadsPath.toFile().mkdirs()); - // Make /proc/self/cmdline - Files.write(selfPath.resolve("cmdline"), PROCESS_NAME.getBytes()); + // Make /proc/$PID/cmdline + Files.write(processPath.resolve("cmdline"), processName.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])); + for (int i = 0; i < threadIds.length; i++) { + // Make /proc/$PID/task/$TID + final Path threadPath = selfThreadsPath.resolve(String.valueOf(threadIds[i])); assertTrue(threadPath.toFile().mkdirs()); - // Make /proc/self/task/$TID/comm - Files.write(threadPath.resolve("comm"), THREAD_NAMES[i].getBytes()); + // Make /proc/$PID/task/$TID/comm + Files.write(threadPath.resolve("comm"), threadNames[i].getBytes()); - // Make /proc/self/task/$TID/time_in_state + // Make /proc/$PID/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"; + for (int j = 0; j < cpuFrequencies.length; j++) { + final String line = String.valueOf(cpuFrequencies[j]) + " " + + String.valueOf(cpuTimes[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(); - + private void checkResults(KernelCpuThreadReader.ProcessCpuUsage processCpuUsage, + int[] readerCpuFrequencies, int uid, int processId, int[] threadIds, String processName, + String[] threadNames, int[] cpuFrequencies, int[][] cpuTimes) { assertNotNull(processCpuUsage); - assertEquals(android.os.Process.myPid(), processCpuUsage.processId); - assertEquals(android.os.Process.myUid(), processCpuUsage.uid); - assertEquals(PROCESS_NAME, processCpuUsage.processName); + assertEquals(processId, processCpuUsage.processId); + assertEquals(uid, processCpuUsage.uid); + assertEquals(processName, processCpuUsage.processName); // Sort the thread CPU usages to compare with test case final ArrayList<KernelCpuThreadReader.ThreadCpuUsage> threadCpuUsages = @@ -124,21 +204,21 @@ public class KernelCpuThreadReaderTest { int threadCount = 0; for (KernelCpuThreadReader.ThreadCpuUsage threadCpuUsage : threadCpuUsages) { - assertEquals(THREAD_IDS[threadCount], threadCpuUsage.threadId); - assertEquals(THREAD_NAMES[threadCount], threadCpuUsage.threadName); + assertEquals(threadIds[threadCount], threadCpuUsage.threadId); + assertEquals(threadNames[threadCount], threadCpuUsage.threadName); for (int i = 0; i < threadCpuUsage.usageTimesMillis.length; i++) { assertEquals( - THREAD_CPU_TIMES[threadCount][i] * 10, + cpuTimes[threadCount][i] * 10, threadCpuUsage.usageTimesMillis[i]); assertEquals( - THREAD_CPU_FREQUENCIES[i], - kernelCpuThreadReader.getCpuFrequenciesKhz()[i]); + cpuFrequencies[i], + readerCpuFrequencies[i]); } threadCount++; } - assertEquals(threadCount, THREAD_IDS.length); + assertEquals(threadCount, threadIds.length); } @Test diff --git a/services/core/java/com/android/server/stats/StatsCompanionService.java b/services/core/java/com/android/server/stats/StatsCompanionService.java index d2ca85023fe8..6f6846dc0363 100644 --- a/services/core/java/com/android/server/stats/StatsCompanionService.java +++ b/services/core/java/com/android/server/stats/StatsCompanionService.java @@ -1630,37 +1630,42 @@ public class StatsCompanionService extends IStatsCompanionService.Stub { if (this.mKernelCpuThreadReader == null) { return; } - KernelCpuThreadReader.ProcessCpuUsage processCpuUsage = this.mKernelCpuThreadReader - .getCurrentProcessCpuUsage(); - if (processCpuUsage == null) { + ArrayList<KernelCpuThreadReader.ProcessCpuUsage> processCpuUsages = + this.mKernelCpuThreadReader.getProcessCpuUsageByUids(); + if (processCpuUsages == null) { return; } int[] cpuFrequencies = mKernelCpuThreadReader.getCpuFrequenciesKhz(); - for (KernelCpuThreadReader.ThreadCpuUsage threadCpuUsage - : processCpuUsage.threadCpuUsages) { - if (threadCpuUsage.usageTimesMillis.length != cpuFrequencies.length) { - Slog.w(TAG, "Unexpected number of usage times," - + " expected " + cpuFrequencies.length - + " but got " + threadCpuUsage.usageTimesMillis.length); - continue; - } - - for (int i = 0; i < threadCpuUsage.usageTimesMillis.length; i++) { - // Do not report CPU usage at a frequency when it's zero - if (threadCpuUsage.usageTimesMillis[i] == 0) { + for (int i = 0; i < processCpuUsages.size(); i++) { + KernelCpuThreadReader.ProcessCpuUsage processCpuUsage = processCpuUsages.get(i); + ArrayList<KernelCpuThreadReader.ThreadCpuUsage> threadCpuUsages = + processCpuUsage.threadCpuUsages; + for (int j = 0; j < threadCpuUsages.size(); j++) { + KernelCpuThreadReader.ThreadCpuUsage threadCpuUsage = threadCpuUsages.get(j); + if (threadCpuUsage.usageTimesMillis.length != cpuFrequencies.length) { + Slog.w(TAG, "Unexpected number of usage times," + + " expected " + cpuFrequencies.length + + " but got " + threadCpuUsage.usageTimesMillis.length); continue; } - StatsLogEventWrapper e = - new StatsLogEventWrapper(tagId, elapsedNanos, wallClockNanos); - e.writeInt(processCpuUsage.uid); - e.writeInt(processCpuUsage.processId); - e.writeInt(threadCpuUsage.threadId); - e.writeString(processCpuUsage.processName); - e.writeString(threadCpuUsage.threadName); - e.writeInt(cpuFrequencies[i]); - e.writeInt(threadCpuUsage.usageTimesMillis[i]); - pulledData.add(e); + for (int k = 0; k < threadCpuUsage.usageTimesMillis.length; k++) { + // Do not report CPU usage at a frequency when it's zero + if (threadCpuUsage.usageTimesMillis[k] == 0) { + continue; + } + + StatsLogEventWrapper e = + new StatsLogEventWrapper(tagId, elapsedNanos, wallClockNanos); + e.writeInt(processCpuUsage.uid); + e.writeInt(processCpuUsage.processId); + e.writeInt(threadCpuUsage.threadId); + e.writeString(processCpuUsage.processName); + e.writeString(threadCpuUsage.threadName); + e.writeInt(cpuFrequencies[k]); + e.writeInt(threadCpuUsage.usageTimesMillis[k]); + pulledData.add(e); + } } } } |