Donate to e Foundation | Murena handsets with /e/OS | Own a part of Murena! Learn more

Commit dc797543 authored by Misha Wagner's avatar Misha Wagner
Browse files

Add retrieval of thread CPU data for processes owned by specified UIDs

By default, the UIDs collected are all system users, i.e. UIDs in the range
[1000, 2000).

Bug: 119089294
Test: KernelCpuThreadReaderTest#testReader_byUids

Change-Id: I162916f2238aad975b657c9299cb9035718768bb
parent 960bde36
Loading
Loading
Loading
Loading
+139 −6
Original line number Diff line number Diff line
@@ -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
@@ -69,6 +70,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
     */
@@ -95,6 +101,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
     */
@@ -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();
@@ -153,6 +180,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
     *
@@ -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) {
@@ -279,6 +369,22 @@ public class KernelCpuThreadReader {
        return threadName;
    }

    /**
     * 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
     */
@@ -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);
        }
    }
}
+113 −33
Original line number Diff line number Diff line
@@ -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;
                    }

        // Make /proc/self/task
        final Path selfThreadsPath = selfPath.resolve("task");
                    @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/$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
+30 −25
Original line number Diff line number Diff line
@@ -1630,14 +1630,18 @@ 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) {
        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
@@ -1645,9 +1649,9 @@ public class StatsCompanionService extends IStatsCompanionService.Stub {
                    continue;
                }

            for (int i = 0; i < threadCpuUsage.usageTimesMillis.length; i++) {
                for (int k = 0; k < threadCpuUsage.usageTimesMillis.length; k++) {
                    // Do not report CPU usage at a frequency when it's zero
                if (threadCpuUsage.usageTimesMillis[i] == 0) {
                    if (threadCpuUsage.usageTimesMillis[k] == 0) {
                        continue;
                    }

@@ -1658,12 +1662,13 @@ public class StatsCompanionService extends IStatsCompanionService.Stub {
                    e.writeInt(threadCpuUsage.threadId);
                    e.writeString(processCpuUsage.processName);
                    e.writeString(threadCpuUsage.threadName);
                e.writeInt(cpuFrequencies[i]);
                e.writeInt(threadCpuUsage.usageTimesMillis[i]);
                    e.writeInt(cpuFrequencies[k]);
                    e.writeInt(threadCpuUsage.usageTimesMillis[k]);
                    pulledData.add(e);
                }
            }
        }
    }

    /**
     * Pulls various data.