summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
-rw-r--r--core/java/com/android/internal/os/KernelCpuThreadReader.java145
-rw-r--r--core/tests/coretests/src/com/android/internal/os/KernelCpuThreadReaderTest.java146
-rw-r--r--services/core/java/com/android/server/stats/StatsCompanionService.java55
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);
+ }
}
}
}