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

Commit 492ed242 authored by Dmitri Plotnikov's avatar Dmitri Plotnikov
Browse files

Make KernelSingleProcessCpuThreadReader aggregate CPU times

This is done to
- prepare to switch to a native implementation
- reduce memory allocations

Test: atest FrameworksCoreTests:com.android.internal.os.KernelSingleProcessCpuThreadReaderTest
Test: atest FrameworksCoreTests:com.android.internal.os.SystemServerCpuThreadReaderTest
Test: atest FrameworksCoreTests:com.android.internal.os.BatteryStatsBinderCallStatsTest
Test: atest FrameworksCoreTests:com.android.internal.os.SystemServicePowerCalculatorTest

Bug: 169279846

Change-Id: I4ff41ad7110120836518d86a8c74cd18631e96c7
parent e86bb695
Loading
Loading
Loading
Loading
+2 −29
Original line number Diff line number Diff line
@@ -1019,8 +1019,6 @@ public class BatteryStatsImpl extends BatteryStats {
     */
    private LongSamplingCounterArray mSystemServerCpuTimesUs;
    private long[] mTmpSystemServerCpuTimesUs;
    /**
     * Times spent by the system server threads grouped by cluster and CPU speed.
     */
@@ -12432,41 +12430,15 @@ public class BatteryStatsImpl extends BatteryStats {
            return;
        }
        int numCpuClusters = mPowerProfile.getNumCpuClusters();
        if (mSystemServerCpuTimesUs == null) {
            mSystemServerCpuTimesUs = new LongSamplingCounterArray(mOnBatteryTimeBase);
            mSystemServerThreadCpuTimesUs = new LongSamplingCounterArray(mOnBatteryTimeBase);
            mBinderThreadCpuTimesUs = new LongSamplingCounterArray(mOnBatteryTimeBase);
        }
        mSystemServerCpuTimesUs.addCountLocked(systemServiceCpuThreadTimes.processCpuTimesUs);
        mSystemServerThreadCpuTimesUs.addCountLocked(systemServiceCpuThreadTimes.threadCpuTimesUs);
        mBinderThreadCpuTimesUs.addCountLocked(systemServiceCpuThreadTimes.binderThreadCpuTimesUs);
        long totalCpuTimeAllThreads = 0;
        for (int index = systemServiceCpuThreadTimes.threadCpuTimesUs.length - 1; index >= 0;
                index--) {
            totalCpuTimeAllThreads += systemServiceCpuThreadTimes.threadCpuTimesUs[index];
        }
        // Estimate per cluster per frequency CPU time for the entire process
        // by distributing the total process CPU time proportionately to how much
        // CPU time its threads took on those clusters/frequencies.  This algorithm
        // works more accurately when when we have equally distributed concurrency.
        // TODO(b/169279846): obtain actual process CPU times from the kernel
        long processCpuTime = systemServiceCpuThreadTimes.processCpuTimeUs;
        if (mTmpSystemServerCpuTimesUs == null) {
            mTmpSystemServerCpuTimesUs =
                    new long[systemServiceCpuThreadTimes.threadCpuTimesUs.length];
        }
        for (int index = systemServiceCpuThreadTimes.threadCpuTimesUs.length - 1; index >= 0;
                index--) {
            mTmpSystemServerCpuTimesUs[index] =
                    processCpuTime * systemServiceCpuThreadTimes.threadCpuTimesUs[index]
                            / totalCpuTimeAllThreads;
        }
        mSystemServerCpuTimesUs.addCountLocked(mTmpSystemServerCpuTimesUs);
        if (DEBUG_BINDER_STATS) {
            Slog.d(TAG, "System server threads per CPU cluster (binder threads/total threads/%)");
            long totalCpuTimeMs = 0;
@@ -12480,6 +12452,7 @@ public class BatteryStatsImpl extends BatteryStats {
            final long[] binderThreadCpuTimesUs =
                    mBinderThreadCpuTimesUs.getCountsLocked(0);
            int index = 0;
            int numCpuClusters = mPowerProfile.getNumCpuClusters();
            for (int cluster = 0; cluster < numCpuClusters; cluster++) {
                StringBuilder sb = new StringBuilder();
                sb.append("cpu").append(cpuIndex).append(": [");
+57 −39
Original line number Diff line number Diff line
@@ -33,8 +33,7 @@ import java.nio.file.DirectoryStream;
import java.nio.file.Files;
import java.nio.file.Path;
import java.nio.file.Paths;
import java.util.ArrayList;
import java.util.List;
import java.util.Arrays;

/**
 * Iterates over all threads owned by a given process, and return the CPU usage for
@@ -151,7 +150,7 @@ public class KernelSingleProcessCpuThreadReader {

    /**
     * Get the CPU frequencies that correspond to the times reported in {@link
     * ThreadCpuUsage#usageTimesMillis}
     * ProcessCpuUsage#processCpuTimesMillis} etc.
     */
    public int getCpuFrequencyCount() {
        if (mFrequencyCount == 0) {
@@ -163,14 +162,22 @@ public class KernelSingleProcessCpuThreadReader {
    /**
     * Get the total and per-thread CPU usage of the process with the PID specified in the
     * constructor.
     * @param selectedThreadIds a SORTED array of native Thread IDs whose CPU times should
     *                          be aggregated as a group.  This is expected to be a subset
     *                          of all thread IDs owned by the process.
     */
    @Nullable
    public ProcessCpuUsage getProcessCpuUsage() {
    public ProcessCpuUsage getProcessCpuUsage(int[] selectedThreadIds) {
        if (DEBUG) {
            Slog.d(TAG, "Reading CPU thread usages with directory " + mProcPath + " process ID "
                    + mPid);
        }

        if (!isSorted(selectedThreadIds)) {
            throw new IllegalArgumentException("selectedThreadIds is not sorted: "
                    + Arrays.toString(selectedThreadIds));
        }

        if (!Process.readProcFile(mProcessStatFilePath, PROCESS_FULL_STATS_FORMAT, null,
                mProcessFullStatsData, null)) {
            Slog.e(TAG, "Failed to read process stat file " + mProcessStatFilePath);
@@ -182,39 +189,43 @@ public class KernelSingleProcessCpuThreadReader {

        long processCpuTimeMillis = (utime + stime) * mJiffyMillis;

        final ArrayList<ThreadCpuUsage> threadCpuUsages = new ArrayList<>();
        int cpuFrequencyCount = getCpuFrequencyCount();
        ProcessCpuUsage processCpuUsage = new ProcessCpuUsage(cpuFrequencyCount);
        try (DirectoryStream<Path> threadPaths = Files.newDirectoryStream(mThreadsDirectoryPath)) {
            for (Path threadDirectory : threadPaths) {
                ThreadCpuUsage threadCpuUsage = getThreadCpuUsage(threadDirectory);
                if (threadCpuUsage == null) {
                    continue;
                }
                threadCpuUsages.add(threadCpuUsage);
                readThreadCpuUsage(processCpuUsage, selectedThreadIds, threadDirectory);
            }
        } catch (IOException | DirectoryIteratorException e) {
            // Expected when a process finishes
            return null;
        }

        // If we found no threads, then the process has exited while we were reading from it
        if (threadCpuUsages.isEmpty()) {
            return null;
        // Estimate per cluster per frequency CPU time for the entire process
        // by distributing the total process CPU time proportionately to how much
        // CPU time its threads took on those clusters/frequencies.  This algorithm
        // works more accurately when when we have equally distributed concurrency.
        // TODO(b/169279846): obtain actual process CPU times from the kernel
        long totalCpuTimeAllThreads = 0;
        for (int i = cpuFrequencyCount - 1; i >= 0; i--) {
            totalCpuTimeAllThreads += processCpuUsage.threadCpuTimesMillis[i];
        }
        if (DEBUG) {
            Slog.d(TAG, "Read CPU usage of " + threadCpuUsages.size() + " threads");

        for (int i = cpuFrequencyCount - 1; i >= 0; i--) {
            processCpuUsage.processCpuTimesMillis[i] =
                    processCpuTimeMillis * processCpuUsage.threadCpuTimesMillis[i]
                            / totalCpuTimeAllThreads;
        }
        return new ProcessCpuUsage(processCpuTimeMillis, threadCpuUsages);

        return processCpuUsage;
    }

    /**
     * Get a thread's CPU usage
     * Reads a thread's CPU usage and aggregates the per-cluster per-frequency CPU times.
     *
     * @param threadDirectory the {@code /proc} directory of the thread
     * @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) {
    private void readThreadCpuUsage(ProcessCpuUsage processCpuUsage, int[] selectedThreadIds,
            Path threadDirectory) {
        // Get the thread ID from the directory name
        final int threadId;
        try {
@@ -222,38 +233,45 @@ public class KernelSingleProcessCpuThreadReader {
            threadId = Integer.parseInt(directoryName);
        } catch (NumberFormatException e) {
            Slog.w(TAG, "Failed to parse thread ID when iterating over /proc/*/task", e);
            return null;
            return;
        }

        // Get the CPU statistics from the directory
        final Path threadCpuStatPath = threadDirectory.resolve(CPU_STATISTICS_FILENAME);
        final long[] cpuUsages = mProcTimeInStateReader.getUsageTimesMillis(threadCpuStatPath);
        if (cpuUsages == null) {
            return null;
            return;
        }

        return new ThreadCpuUsage(threadId, cpuUsages);
        final int cpuFrequencyCount = getCpuFrequencyCount();
        final boolean isSelectedThread = Arrays.binarySearch(selectedThreadIds, threadId) >= 0;
        for (int i = cpuFrequencyCount - 1; i >= 0; i--) {
            processCpuUsage.threadCpuTimesMillis[i] += cpuUsages[i];
            if (isSelectedThread) {
                processCpuUsage.selectedThreadCpuTimesMillis[i] += cpuUsages[i];
            }
        }
    }

    /** CPU usage of a process and all of its threads */
    /** CPU usage of a process, all of its threads and a selected subset of its threads */
    public static class ProcessCpuUsage {
        public final long cpuTimeMillis;
        public final List<ThreadCpuUsage> threadCpuUsages;

        ProcessCpuUsage(long cpuTimeMillis, List<ThreadCpuUsage> threadCpuUsages) {
            this.cpuTimeMillis = cpuTimeMillis;
            this.threadCpuUsages = threadCpuUsages;
        public long[] processCpuTimesMillis;
        public long[] threadCpuTimesMillis;
        public long[] selectedThreadCpuTimesMillis;

        public ProcessCpuUsage(int cpuFrequencyCount) {
            processCpuTimesMillis = new long[cpuFrequencyCount];
            threadCpuTimesMillis = new long[cpuFrequencyCount];
            selectedThreadCpuTimesMillis = new long[cpuFrequencyCount];
        }
    }

    /** CPU usage of a thread */
    public static class ThreadCpuUsage {
        public final int threadId;
        public final long[] usageTimesMillis;

        ThreadCpuUsage(int threadId, long[] usageTimesMillis) {
            this.threadId = threadId;
            this.usageTimesMillis = usageTimesMillis;
    private static boolean isSorted(int[] array) {
        for (int i = 0; i < array.length - 1; i++) {
            if (array[i] > array[i + 1]) {
                return false;
            }
        }
        return true;
    }
}
+21 −45
Original line number Diff line number Diff line
@@ -24,7 +24,6 @@ import com.android.internal.annotations.VisibleForTesting;
import java.io.IOException;
import java.nio.file.Path;
import java.util.Arrays;
import java.util.List;

/**
 * Reads /proc/UID/task/TID/time_in_state files to obtain statistics on CPU usage
@@ -34,10 +33,7 @@ public class SystemServerCpuThreadReader {
    private final KernelSingleProcessCpuThreadReader mKernelCpuThreadReader;
    private int[] mBinderThreadNativeTids = new int[0];  // Sorted

    private long mProcessCpuTimeUs;
    private long[] mThreadCpuTimesUs;
    private long[] mBinderThreadCpuTimesUs;
    private long mLastProcessCpuTimeUs;
    private long[] mLastProcessCpuTimeUs;
    private long[] mLastThreadCpuTimesUs;
    private long[] mLastBinderThreadCpuTimesUs;

@@ -46,14 +42,15 @@ public class SystemServerCpuThreadReader {
     */
    public static class SystemServiceCpuThreadTimes {
        // The entire process
        public long processCpuTimeUs;
        public long[] processCpuTimesUs;
        // All threads
        public long[] threadCpuTimesUs;
        // Just the threads handling incoming binder calls
        public long[] binderThreadCpuTimesUs;
    }

    private SystemServiceCpuThreadTimes mDeltaCpuThreadTimes = new SystemServiceCpuThreadTimes();
    private final SystemServiceCpuThreadTimes mDeltaCpuThreadTimes =
            new SystemServiceCpuThreadTimes();

    /**
     * Creates a configured instance of SystemServerCpuThreadReader.
@@ -83,59 +80,38 @@ public class SystemServerCpuThreadReader {
     */
    @Nullable
    public SystemServiceCpuThreadTimes readDelta() {
        int numCpuFrequencies = mKernelCpuThreadReader.getCpuFrequencyCount();
        if (mBinderThreadCpuTimesUs == null) {
            mThreadCpuTimesUs = new long[numCpuFrequencies];
            mBinderThreadCpuTimesUs = new long[numCpuFrequencies];

        final int numCpuFrequencies = mKernelCpuThreadReader.getCpuFrequencyCount();
        if (mLastProcessCpuTimeUs == null) {
            mLastProcessCpuTimeUs = new long[numCpuFrequencies];
            mLastThreadCpuTimesUs = new long[numCpuFrequencies];
            mLastBinderThreadCpuTimesUs = new long[numCpuFrequencies];

            mDeltaCpuThreadTimes.processCpuTimesUs = new long[numCpuFrequencies];
            mDeltaCpuThreadTimes.threadCpuTimesUs = new long[numCpuFrequencies];
            mDeltaCpuThreadTimes.binderThreadCpuTimesUs = new long[numCpuFrequencies];
        }

        mProcessCpuTimeUs = 0;
        Arrays.fill(mThreadCpuTimesUs, 0);
        Arrays.fill(mBinderThreadCpuTimesUs, 0);

        KernelSingleProcessCpuThreadReader.ProcessCpuUsage processCpuUsage =
                mKernelCpuThreadReader.getProcessCpuUsage();
        final KernelSingleProcessCpuThreadReader.ProcessCpuUsage processCpuUsage =
                mKernelCpuThreadReader.getProcessCpuUsage(mBinderThreadNativeTids);
        if (processCpuUsage == null) {
            return null;
        }

        mProcessCpuTimeUs = processCpuUsage.cpuTimeMillis * 1000;

        List<KernelSingleProcessCpuThreadReader.ThreadCpuUsage> threadCpuUsages =
                processCpuUsage.threadCpuUsages;
        int threadCpuUsagesSize = threadCpuUsages.size();
        for (int i = 0; i < threadCpuUsagesSize; i++) {
            KernelSingleProcessCpuThreadReader.ThreadCpuUsage tcu = threadCpuUsages.get(i);
            boolean isBinderThread =
                    Arrays.binarySearch(mBinderThreadNativeTids, tcu.threadId) >= 0;
            for (int k = 0; k < numCpuFrequencies; k++) {
                long usageTimeUs = tcu.usageTimesMillis[k] * 1000;
                mThreadCpuTimesUs[k] += usageTimeUs;
                if (isBinderThread) {
                    mBinderThreadCpuTimesUs[k] += usageTimeUs;
                }
            }
        }

        for (int i = 0; i < mThreadCpuTimesUs.length; i++) {
            mDeltaCpuThreadTimes.processCpuTimeUs =
                    Math.max(0, mProcessCpuTimeUs - mLastProcessCpuTimeUs);
        for (int i = numCpuFrequencies - 1; i >= 0; i--) {
            long processCpuTimesUs = processCpuUsage.processCpuTimesMillis[i] * 1000;
            long threadCpuTimesUs = processCpuUsage.threadCpuTimesMillis[i] * 1000;
            long binderThreadCpuTimesUs = processCpuUsage.selectedThreadCpuTimesMillis[i] * 1000;
            mDeltaCpuThreadTimes.processCpuTimesUs[i] =
                    Math.max(0, processCpuTimesUs - mLastProcessCpuTimeUs[i]);
            mDeltaCpuThreadTimes.threadCpuTimesUs[i] =
                    Math.max(0, mThreadCpuTimesUs[i] - mLastThreadCpuTimesUs[i]);
                    Math.max(0, threadCpuTimesUs - mLastThreadCpuTimesUs[i]);
            mDeltaCpuThreadTimes.binderThreadCpuTimesUs[i] =
                    Math.max(0, mBinderThreadCpuTimesUs[i] - mLastBinderThreadCpuTimesUs[i]);
            mLastThreadCpuTimesUs[i] = mThreadCpuTimesUs[i];
            mLastBinderThreadCpuTimesUs[i] = mBinderThreadCpuTimesUs[i];
                    Math.max(0, binderThreadCpuTimesUs - mLastBinderThreadCpuTimesUs[i]);
            mLastProcessCpuTimeUs[i] = processCpuTimesUs;
            mLastThreadCpuTimesUs[i] = threadCpuTimesUs;
            mLastBinderThreadCpuTimesUs[i] = binderThreadCpuTimesUs;
        }

        mLastProcessCpuTimeUs = mProcessCpuTimeUs;

        return mDeltaCpuThreadTimes;
    }
}
+7 −19
Original line number Diff line number Diff line
@@ -38,8 +38,6 @@ import java.io.IOException;
import java.io.OutputStream;
import java.nio.file.Files;
import java.nio.file.Path;
import java.util.Comparator;
import java.util.List;

@SmallTest
@RunWith(AndroidJUnit4.class)
@@ -59,31 +57,21 @@ public class KernelSingleProcessCpuThreadReaderTest {
    }

    @Test
    public void getThreadCpuUsage() throws IOException {
    public void getProcessCpuUsage() throws IOException {
        setupDirectory(42,
                new int[] {42, 1, 2, 3},
                new int[] {1000, 2000},
                // Units are 10ms aka 10000Us
                new int[][] {{100, 200}, {0, 200}, {100, 300}, {0, 400}},
                new int[] {1400, 1500});
                new int[][] {{100, 200}, {0, 200}, {100, 300}, {0, 600}},
                new int[] {4500, 500});

        KernelSingleProcessCpuThreadReader reader = new KernelSingleProcessCpuThreadReader(42,
                mProcDirectory.toPath());
        KernelSingleProcessCpuThreadReader.ProcessCpuUsage processCpuUsage =
                reader.getProcessCpuUsage();
        assertThat(processCpuUsage.cpuTimeMillis).isEqualTo(29000);
        List<KernelSingleProcessCpuThreadReader.ThreadCpuUsage> threadCpuUsage =
                processCpuUsage.threadCpuUsages;
        threadCpuUsage.sort(Comparator.comparingInt(o -> o.threadId));
        assertThat(threadCpuUsage).hasSize(4);
        assertThat(threadCpuUsage.get(0).threadId).isEqualTo(1);
        assertThat(threadCpuUsage.get(0).usageTimesMillis).isEqualTo(new long[]{0, 2000});
        assertThat(threadCpuUsage.get(1).threadId).isEqualTo(2);
        assertThat(threadCpuUsage.get(1).usageTimesMillis).isEqualTo(new long[]{1000, 3000});
        assertThat(threadCpuUsage.get(2).threadId).isEqualTo(3);
        assertThat(threadCpuUsage.get(2).usageTimesMillis).isEqualTo(new long[]{0, 4000});
        assertThat(threadCpuUsage.get(3).threadId).isEqualTo(42);
        assertThat(threadCpuUsage.get(3).usageTimesMillis).isEqualTo(new long[]{1000, 2000});
                reader.getProcessCpuUsage(new int[] {2, 3});
        assertThat(processCpuUsage.threadCpuTimesMillis).isEqualTo(new long[] {2000, 13000});
        assertThat(processCpuUsage.selectedThreadCpuTimesMillis).isEqualTo(new long[] {1000, 9000});
        assertThat(processCpuUsage.processCpuTimesMillis).isEqualTo(new long[] {6666, 43333});
    }

    @Test
+5 −5
Original line number Diff line number Diff line
@@ -66,7 +66,7 @@ public class SystemServicePowerCalculatorTest {
    public void testCalculateApp() {
        // Test Power Profile has two CPU clusters with 3 and 4 speeds, thus 7 freq times total
        mMockSystemServerCpuThreadReader.setCpuTimes(
                210000,
                new long[] {10000, 15000, 20000, 25000, 30000, 35000, 40000},
                new long[] {30000, 40000, 50000, 60000, 70000, 80000, 90000},
                new long[] {20000, 30000, 40000, 50000, 60000, 70000, 80000});

@@ -107,13 +107,13 @@ public class SystemServicePowerCalculatorTest {
                mMockBatteryStats.getUidStatsLocked(workSourceUid1), 0);
        mSystemServicePowerCalculator.calculateApp(app1, app1.uidObj, 0, 0,
                BatteryStats.STATS_SINCE_CHARGED);
        assertEquals(0.00018958, app1.systemServiceCpuPowerMah, 0.0000001);
        assertEquals(0.00016269, app1.systemServiceCpuPowerMah, 0.0000001);

        BatterySipper app2 = new BatterySipper(BatterySipper.DrainType.APP,
                mMockBatteryStats.getUidStatsLocked(workSourceUid2), 0);
        mSystemServicePowerCalculator.calculateApp(app2, app2.uidObj, 0, 0,
                BatteryStats.STATS_SINCE_CHARGED);
        assertEquals(0.00170625, app2.systemServiceCpuPowerMah, 0.0000001);
        assertEquals(0.00146426, app2.systemServiceCpuPowerMah, 0.0000001);
    }

    private static class MockKernelCpuUidFreqTimeReader extends
@@ -148,9 +148,9 @@ public class SystemServicePowerCalculatorTest {
            super(null);
        }

        public void setCpuTimes(long processCpuTimeUs, long[] threadCpuTimesUs,
        public void setCpuTimes(long[] processCpuTimesUs, long[] threadCpuTimesUs,
                long[] binderThreadCpuTimesUs) {
            mThreadTimes.processCpuTimeUs = processCpuTimeUs;
            mThreadTimes.processCpuTimesUs = processCpuTimesUs;
            mThreadTimes.threadCpuTimesUs = threadCpuTimesUs;
            mThreadTimes.binderThreadCpuTimesUs = binderThreadCpuTimesUs;
        }