diff --git a/apct-tests/perftests/core/src/android/os/KernelCpuThreadReaderPerfTest.java b/apct-tests/perftests/core/src/android/os/KernelCpuThreadReaderPerfTest.java index 1f261882b2d3a..da9ed6e490432 100644 --- a/apct-tests/perftests/core/src/android/os/KernelCpuThreadReaderPerfTest.java +++ b/apct-tests/perftests/core/src/android/os/KernelCpuThreadReaderPerfTest.java @@ -40,7 +40,7 @@ public class KernelCpuThreadReaderPerfTest { public final PerfStatusReporter mPerfStatusReporter = new PerfStatusReporter(); private final KernelCpuThreadReader mKernelCpuThreadReader = - KernelCpuThreadReader.create(8, uid -> 1000 <= uid && uid < 2000, 0); + KernelCpuThreadReader.create(8, uid -> 1000 <= uid && uid < 2000); @Test public void timeReadCurrentProcessCpuUsage() { diff --git a/core/java/com/android/internal/os/KernelCpuThreadReader.java b/core/java/com/android/internal/os/KernelCpuThreadReader.java index e4de1586bc514..3686048cee158 100644 --- a/core/java/com/android/internal/os/KernelCpuThreadReader.java +++ b/core/java/com/android/internal/os/KernelCpuThreadReader.java @@ -92,24 +92,12 @@ public class KernelCpuThreadReader { /** Value returned when there was an error getting an integer ID value (e.g. PID, UID) */ private static final int ID_ERROR = -1; - /** Thread ID used when reporting CPU used by other threads */ - private static final int OTHER_THREADS_ID = -1; - - /** Thread name used when reporting CPU used by other threads */ - private static final String OTHER_THREADS_NAME = "__OTHER_THREADS"; - /** * When checking whether to report data for a thread, we check the UID of the thread's owner * against this predicate */ private Predicate mUidPredicate; - /** - * If a thread has strictly less than {@code minimumTotalCpuUsageMillis} total CPU usage, it - * will not be reported - */ - private int mMinimumTotalCpuUsageMillis; - /** Where the proc filesystem is mounted */ private final Path mProcPath; @@ -138,13 +126,11 @@ public class KernelCpuThreadReader { public KernelCpuThreadReader( int numBuckets, Predicate uidPredicate, - int minimumTotalCpuUsageMillis, Path procPath, Path initialTimeInStatePath, Injector injector) throws IOException { mUidPredicate = uidPredicate; - mMinimumTotalCpuUsageMillis = minimumTotalCpuUsageMillis; mProcPath = procPath; mProcTimeInStateReader = new ProcTimeInStateReader(initialTimeInStatePath); mInjector = injector; @@ -157,13 +143,11 @@ public class KernelCpuThreadReader { * @return the reader, null if an exception was thrown during creation */ @Nullable - public static KernelCpuThreadReader create( - int numBuckets, Predicate uidPredicate, int minimumTotalCpuUsageMillis) { + public static KernelCpuThreadReader create(int numBuckets, Predicate uidPredicate) { try { return new KernelCpuThreadReader( numBuckets, uidPredicate, - minimumTotalCpuUsageMillis, DEFAULT_PROC_PATH, DEFAULT_INITIAL_TIME_IN_STATE_PATH, new Injector()); @@ -258,18 +242,6 @@ public class KernelCpuThreadReader { mUidPredicate = uidPredicate; } - /** - * If a thread has strictly less than {@code minimumTotalCpuUsageMillis} total CPU usage, it - * will not be reported - */ - void setMinimumTotalCpuUsageMillis(int minimumTotalCpuUsageMillis) { - if (minimumTotalCpuUsageMillis < 0) { - Slog.w(TAG, "Negative minimumTotalCpuUsageMillis: " + minimumTotalCpuUsageMillis); - return; - } - mMinimumTotalCpuUsageMillis = minimumTotalCpuUsageMillis; - } - /** * Read all of the CPU usage statistics for each child thread of a process * @@ -292,7 +264,6 @@ public class KernelCpuThreadReader { + uid); } - int[] filteredThreadsCpuUsage = null; final Path allThreadsPath = processPath.resolve("task"); final ArrayList threadCpuUsages = new ArrayList<>(); try (DirectoryStream threadPaths = Files.newDirectoryStream(allThreadsPath)) { @@ -301,14 +272,6 @@ public class KernelCpuThreadReader { if (threadCpuUsage == null) { continue; } - if (mMinimumTotalCpuUsageMillis > totalCpuUsage(threadCpuUsage.usageTimesMillis)) { - if (filteredThreadsCpuUsage == null) { - filteredThreadsCpuUsage = new int[mFrequenciesKhz.length]; - } - filteredThreadsCpuUsage = - sumCpuUsage(filteredThreadsCpuUsage, threadCpuUsage.usageTimesMillis); - continue; - } threadCpuUsages.add(threadCpuUsage); } } catch (IOException e) { @@ -320,14 +283,6 @@ public class KernelCpuThreadReader { if (threadCpuUsages.isEmpty()) { return null; } - - // Add the filtered out thread CPU usage under an "other threads" ThreadCpuUsage - if (filteredThreadsCpuUsage != null) { - threadCpuUsages.add( - new ThreadCpuUsage( - OTHER_THREADS_ID, OTHER_THREADS_NAME, filteredThreadsCpuUsage)); - } - if (DEBUG) { Slog.d(TAG, "Read CPU usage of " + threadCpuUsages.size() + " threads"); } @@ -404,25 +359,6 @@ public class KernelCpuThreadReader { } } - /** Get the sum of all CPU usage across all frequencies */ - @SuppressWarnings("ForLoopReplaceableByForEach") - private static int totalCpuUsage(int[] cpuUsage) { - int total = 0; - for (int i = 0; i < cpuUsage.length; i++) { - total += cpuUsage[i]; - } - return total; - } - - /** Add two CPU frequency usages together */ - private static int[] sumCpuUsage(int[] a, int[] b) { - int[] summed = new int[a.length]; - for (int i = 0; i < a.length; i++) { - summed[i] = a[i] + b[i]; - } - return summed; - } - /** Puts frequencies and usage times into buckets */ @VisibleForTesting public static class FrequencyBucketCreator { @@ -553,9 +489,10 @@ public class KernelCpuThreadReader { public final int processId; public final String processName; public final int uid; - public final ArrayList threadCpuUsages; + public ArrayList threadCpuUsages; - ProcessCpuUsage( + @VisibleForTesting + public ProcessCpuUsage( int processId, String processName, int uid, @@ -571,9 +508,10 @@ public class KernelCpuThreadReader { public static class ThreadCpuUsage { public final int threadId; public final String threadName; - public final int[] usageTimesMillis; + public int[] usageTimesMillis; - ThreadCpuUsage(int threadId, String threadName, int[] usageTimesMillis) { + @VisibleForTesting + public 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/KernelCpuThreadReaderDiff.java b/core/java/com/android/internal/os/KernelCpuThreadReaderDiff.java new file mode 100644 index 0000000000000..ffdc33c64f69d --- /dev/null +++ b/core/java/com/android/internal/os/KernelCpuThreadReaderDiff.java @@ -0,0 +1,305 @@ +/* + * 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.util.ArrayMap; +import android.util.Slog; + +import com.android.internal.annotations.VisibleForTesting; + +import java.util.ArrayList; +import java.util.List; +import java.util.Map; +import java.util.Objects; + +/** + * Delegates per-thread CPU collection to {@link KernelCpuThreadReader}, and calculates the + * difference between CPU usage at each call of {@link #getProcessCpuUsageDiffed()}. + * + *

Some notes on the diff calculation: + * + *

    + *
  • The diffing is done between each call of {@link #getProcessCpuUsageDiffed()}, i.e. call N + * of this method will return CPU used by threads between call N-1 and N. + *
  • The first call of {@link #getProcessCpuUsageDiffed()} will return no processes ("first + * call" is the first call in the lifetime of a {@link KernelCpuThreadReaderDiff} object). + *
  • If a thread does not exist at call N, but does exist at call N+1, the diff will assume that + * the CPU usage at call N was zero. Thus, the diff reported will be equivalent to the value + * returned by {@link KernelCpuThreadReader#getProcessCpuUsage()} at call N+1. + *
  • If an error occurs in {@link KernelCpuThreadReader} at call N, we will return no + * information for CPU usage between call N-1 and N (as we don't know the start value) and + * between N and N+1 (as we don't know the end value). Assuming all other calls are + * successful, the next call to return data will be N+2, for the period between N+1 and N+2. + *
  • If an error occurs in this class (but not in {@link KernelCpuThreadReader}) at call N, the + * data will only be dropped for call N, as we can still use the CPU data for the surrounding + * calls. + *
+ * + *

Additionally to diffing, this class also contains logic for thresholding reported threads. A + * thread will not be reported unless its total CPU usage is at least equal to the value set in + * {@link #setMinimumTotalCpuUsageMillis}. Filtered thread CPU usage is summed and reported under + * one "other threads" thread. This reduces the cardinality of the {@link + * #getProcessCpuUsageDiffed()} result. + * + *

Thresholding is done in this class, instead of {@link KernelCpuThreadReader}, and instead of + * WestWorld, because the thresholding should be done after diffing, not before. This is because of + * two issues with thresholding before diffing: + * + *

    + *
  • We would threshold less and less threads as thread uptime increases. + *
  • We would encounter errors as the filtered threads become unfiltered, as the "other threads" + * result could have negative diffs, and the newly unfiltered threads would have incorrect + * diffs that include CPU usage from when they were filtered. + *
+ * + * @hide Only for use within the system server + */ +@SuppressWarnings("ForLoopReplaceableByForEach") +public class KernelCpuThreadReaderDiff { + private static final String TAG = "KernelCpuThreadReaderDiff"; + + /** Thread ID used when reporting CPU used by other threads */ + private static final int OTHER_THREADS_ID = -1; + + /** Thread name used when reporting CPU used by other threads */ + private static final String OTHER_THREADS_NAME = "__OTHER_THREADS"; + + private final KernelCpuThreadReader mReader; + + /** + * CPU usage from the previous call of {@link #getProcessCpuUsageDiffed()}. Null if there was no + * previous call, or if the previous call failed + * + *

Maps the thread's identifier to the per-frequency CPU usage for that thread. The + * identifier contains the minimal amount of information to identify a thread (see {@link + * ThreadKey} for more information), thus reducing memory consumption. + */ + @Nullable private Map mPreviousCpuUsage; + + /** + * If a thread has strictly less than {@code minimumTotalCpuUsageMillis} total CPU usage, it + * will not be reported + */ + private int mMinimumTotalCpuUsageMillis; + + @VisibleForTesting + public KernelCpuThreadReaderDiff(KernelCpuThreadReader reader, int minimumTotalCpuUsageMillis) { + mReader = reader; + mMinimumTotalCpuUsageMillis = minimumTotalCpuUsageMillis; + mPreviousCpuUsage = null; + } + + /** + * Returns the difference in CPU usage since the last time this method was called. + * + * @see KernelCpuThreadReader#getProcessCpuUsage() + */ + @Nullable + public ArrayList getProcessCpuUsageDiffed() { + Map newCpuUsage = null; + try { + // Get the thread CPU usage and index them by ThreadKey + final ArrayList processCpuUsages = + mReader.getProcessCpuUsage(); + newCpuUsage = createCpuUsageMap(processCpuUsages); + // If there is no previous CPU usage, return nothing + if (mPreviousCpuUsage == null) { + return null; + } + + // Do diffing and thresholding for each process + for (int i = 0; i < processCpuUsages.size(); i++) { + KernelCpuThreadReader.ProcessCpuUsage processCpuUsage = processCpuUsages.get(i); + changeToDiffs(mPreviousCpuUsage, processCpuUsage); + applyThresholding(processCpuUsage); + } + return processCpuUsages; + } finally { + // Always update the previous CPU usage. If we haven't got an update, it will be set to + // null, so the next call knows there no previous values + mPreviousCpuUsage = newCpuUsage; + } + } + + /** @see KernelCpuThreadReader#getCpuFrequenciesKhz() */ + @Nullable + public int[] getCpuFrequenciesKhz() { + return mReader.getCpuFrequenciesKhz(); + } + + /** + * If a thread has strictly less than {@code minimumTotalCpuUsageMillis} total CPU usage, it + * will not be reported + */ + void setMinimumTotalCpuUsageMillis(int minimumTotalCpuUsageMillis) { + if (minimumTotalCpuUsageMillis < 0) { + Slog.w(TAG, "Negative minimumTotalCpuUsageMillis: " + minimumTotalCpuUsageMillis); + return; + } + mMinimumTotalCpuUsageMillis = minimumTotalCpuUsageMillis; + } + + /** + * Create a map of a thread's identifier to a thread's CPU usage. Used for fast indexing when + * calculating diffs + */ + private static Map createCpuUsageMap( + List processCpuUsages) { + final Map cpuUsageMap = new ArrayMap<>(); + for (int i = 0; i < processCpuUsages.size(); i++) { + KernelCpuThreadReader.ProcessCpuUsage processCpuUsage = processCpuUsages.get(i); + for (int j = 0; j < processCpuUsage.threadCpuUsages.size(); j++) { + KernelCpuThreadReader.ThreadCpuUsage threadCpuUsage = + processCpuUsage.threadCpuUsages.get(j); + cpuUsageMap.put( + new ThreadKey( + processCpuUsage.processId, + threadCpuUsage.threadId, + processCpuUsage.processName, + threadCpuUsage.threadName), + threadCpuUsage.usageTimesMillis); + } + } + return cpuUsageMap; + } + + /** + * Calculate the difference in per-frequency CPU usage for all threads in a process + * + * @param previousCpuUsage CPU usage from the last call, the base of the diff + * @param processCpuUsage CPU usage from the current call, this value is modified to contain the + * diffed values + */ + private static void changeToDiffs( + Map previousCpuUsage, + KernelCpuThreadReader.ProcessCpuUsage processCpuUsage) { + for (int i = 0; i < processCpuUsage.threadCpuUsages.size(); i++) { + KernelCpuThreadReader.ThreadCpuUsage threadCpuUsage = + processCpuUsage.threadCpuUsages.get(i); + final ThreadKey key = + new ThreadKey( + processCpuUsage.processId, + threadCpuUsage.threadId, + processCpuUsage.processName, + threadCpuUsage.threadName); + int[] previous = previousCpuUsage.get(key); + if (previous == null) { + // If there's no previous CPU usage, assume that it's zero + previous = new int[threadCpuUsage.usageTimesMillis.length]; + } + threadCpuUsage.usageTimesMillis = + cpuTimeDiff(threadCpuUsage.usageTimesMillis, previous); + } + } + + /** + * Filter out any threads with less than {@link #mMinimumTotalCpuUsageMillis} total CPU usage + * + *

The sum of the CPU usage of filtered threads is added under a single thread, labeled with + * {@link #OTHER_THREADS_ID} and {@link #OTHER_THREADS_NAME}. + * + * @param processCpuUsage CPU usage to apply thresholding to, this value is modified to change + * the threads it contains + */ + private void applyThresholding(KernelCpuThreadReader.ProcessCpuUsage processCpuUsage) { + int[] filteredThreadsCpuUsage = null; + final ArrayList thresholded = new ArrayList<>(); + for (int i = 0; i < processCpuUsage.threadCpuUsages.size(); i++) { + KernelCpuThreadReader.ThreadCpuUsage threadCpuUsage = + processCpuUsage.threadCpuUsages.get(i); + if (mMinimumTotalCpuUsageMillis > totalCpuUsage(threadCpuUsage.usageTimesMillis)) { + if (filteredThreadsCpuUsage == null) { + filteredThreadsCpuUsage = new int[threadCpuUsage.usageTimesMillis.length]; + } + addToCpuUsage(filteredThreadsCpuUsage, threadCpuUsage.usageTimesMillis); + continue; + } + thresholded.add(threadCpuUsage); + } + if (filteredThreadsCpuUsage != null) { + thresholded.add( + new KernelCpuThreadReader.ThreadCpuUsage( + OTHER_THREADS_ID, OTHER_THREADS_NAME, filteredThreadsCpuUsage)); + } + processCpuUsage.threadCpuUsages = thresholded; + } + + /** Get the sum of all CPU usage across all frequencies */ + private static int totalCpuUsage(int[] cpuUsage) { + int total = 0; + for (int i = 0; i < cpuUsage.length; i++) { + total += cpuUsage[i]; + } + return total; + } + + /** Add two CPU frequency usages together */ + private static void addToCpuUsage(int[] a, int[] b) { + for (int i = 0; i < a.length; i++) { + a[i] += b[i]; + } + } + + /** Subtract two CPU frequency usages from each other */ + private static int[] cpuTimeDiff(int[] a, int[] b) { + int[] difference = new int[a.length]; + for (int i = 0; i < a.length; i++) { + difference[i] = a[i] - b[i]; + } + return difference; + } + + /** + * Identifies a thread + * + *

Only stores the minimum amount of information to identify a thread. This includes the + * PID/TID, but as both are recycled as processes/threads end and begin, we also store the hash + * of the name of the process/thread. + */ + private static class ThreadKey { + private final int mProcessId; + private final int mThreadId; + private final int mProcessNameHash; + private final int mThreadNameHash; + + ThreadKey(int processId, int threadId, String processName, String threadName) { + this.mProcessId = processId; + this.mThreadId = threadId; + // Only store the hash to reduce memory consumption + this.mProcessNameHash = Objects.hash(processName); + this.mThreadNameHash = Objects.hash(threadName); + } + + @Override + public int hashCode() { + return Objects.hash(mProcessId, mThreadId, mProcessNameHash, mThreadNameHash); + } + + @Override + public boolean equals(Object obj) { + if (!(obj instanceof ThreadKey)) { + return false; + } + ThreadKey other = (ThreadKey) obj; + return mProcessId == other.mProcessId + && mThreadId == other.mThreadId + && mProcessNameHash == other.mProcessNameHash + && mThreadNameHash == other.mThreadNameHash; + } + } +} diff --git a/core/java/com/android/internal/os/KernelCpuThreadReaderSettingsObserver.java b/core/java/com/android/internal/os/KernelCpuThreadReaderSettingsObserver.java index 3851ce6d9cbdc..f8c0d9e4a27ee 100644 --- a/core/java/com/android/internal/os/KernelCpuThreadReaderSettingsObserver.java +++ b/core/java/com/android/internal/os/KernelCpuThreadReaderSettingsObserver.java @@ -67,12 +67,14 @@ public class KernelCpuThreadReaderSettingsObserver extends ContentObserver { @Nullable private final KernelCpuThreadReader mKernelCpuThreadReader; + @Nullable private final KernelCpuThreadReaderDiff mKernelCpuThreadReaderDiff; + /** * @return returns a created {@link KernelCpuThreadReader} that will be modified by any change * in settings, returns null if creation failed */ @Nullable - public static KernelCpuThreadReader getSettingsModifiedReader(Context context) { + public static KernelCpuThreadReaderDiff getSettingsModifiedReader(Context context) { // Create the observer KernelCpuThreadReaderSettingsObserver settingsObserver = new KernelCpuThreadReaderSettingsObserver(context); @@ -82,7 +84,7 @@ public class KernelCpuThreadReaderSettingsObserver extends ContentObserver { .registerContentObserver( settingsUri, false, settingsObserver, UserHandle.USER_SYSTEM); // Return the observer's reader - return settingsObserver.mKernelCpuThreadReader; + return settingsObserver.mKernelCpuThreadReaderDiff; } private KernelCpuThreadReaderSettingsObserver(Context context) { @@ -90,9 +92,10 @@ public class KernelCpuThreadReaderSettingsObserver extends ContentObserver { mContext = context; mKernelCpuThreadReader = KernelCpuThreadReader.create( - NUM_BUCKETS_DEFAULT, - UidPredicate.fromString(COLLECTED_UIDS_DEFAULT), - MINIMUM_TOTAL_CPU_USAGE_MILLIS_DEFAULT); + NUM_BUCKETS_DEFAULT, UidPredicate.fromString(COLLECTED_UIDS_DEFAULT)); + mKernelCpuThreadReaderDiff = + new KernelCpuThreadReaderDiff( + mKernelCpuThreadReader, MINIMUM_TOTAL_CPU_USAGE_MILLIS_DEFAULT); } @Override @@ -130,7 +133,7 @@ public class KernelCpuThreadReaderSettingsObserver extends ContentObserver { mKernelCpuThreadReader.setNumBuckets( parser.getInt(NUM_BUCKETS_SETTINGS_KEY, NUM_BUCKETS_DEFAULT)); mKernelCpuThreadReader.setUidPredicate(uidPredicate); - mKernelCpuThreadReader.setMinimumTotalCpuUsageMillis( + mKernelCpuThreadReaderDiff.setMinimumTotalCpuUsageMillis( parser.getInt( MINIMUM_TOTAL_CPU_USAGE_MILLIS_SETTINGS_KEY, MINIMUM_TOTAL_CPU_USAGE_MILLIS_DEFAULT)); diff --git a/core/tests/coretests/src/com/android/internal/os/KernelCpuThreadReaderDiffTest.java b/core/tests/coretests/src/com/android/internal/os/KernelCpuThreadReaderDiffTest.java new file mode 100644 index 0000000000000..460fe47966ead --- /dev/null +++ b/core/tests/coretests/src/com/android/internal/os/KernelCpuThreadReaderDiffTest.java @@ -0,0 +1,242 @@ +/* + * 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 com.google.common.truth.Truth.assertThat; + +import static org.mockito.Mockito.mockitoSession; +import static org.mockito.Mockito.when; +import static org.testng.Assert.assertThrows; + +import static java.util.stream.Collectors.toList; + +import android.platform.test.annotations.Presubmit; + +import androidx.test.ext.junit.runners.AndroidJUnit4; +import androidx.test.filters.SmallTest; + +import org.junit.After; +import org.junit.Before; +import org.junit.Test; +import org.junit.runner.RunWith; +import org.mockito.Mock; +import org.mockito.MockitoSession; + +import java.util.ArrayList; +import java.util.Arrays; +import java.util.Collection; +import java.util.Collections; + +@Presubmit +@SmallTest +@RunWith(AndroidJUnit4.class) +public class KernelCpuThreadReaderDiffTest { + + private MockitoSession mMockingSessions; + @Mock KernelCpuThreadReader mMockReader; + + @Before + public void setUp() { + mMockingSessions = mockitoSession().initMocks(this).startMocking(); + } + + @After + public void tearDown() { + if (mMockingSessions != null) { + mMockingSessions.finishMocking(); + } + } + + @Test + public void test_empty() { + KernelCpuThreadReaderDiff kernelCpuThreadReaderDiff = + new KernelCpuThreadReaderDiff(mMockReader, 0); + assertThat(kernelCpuThreadReaderDiff.getProcessCpuUsageDiffed()).isNull(); + assertThat(kernelCpuThreadReaderDiff.getProcessCpuUsageDiffed()).isEmpty(); + } + + @Test + public void test_simple() { + when(mMockReader.getProcessCpuUsage()) + .thenReturn(createProcess(new int[] {100, 100, 100})) + .thenReturn(createProcess(new int[] {150, 160, 170})); + KernelCpuThreadReaderDiff kernelCpuThreadReaderDiff = + new KernelCpuThreadReaderDiff(mMockReader, 0); + assertThat(kernelCpuThreadReaderDiff.getProcessCpuUsageDiffed()).isNull(); + assertThat(cpuUsages(kernelCpuThreadReaderDiff.getProcessCpuUsageDiffed())) + .containsExactly(Arrays.asList(50, 60, 70)); + } + + @Test + public void test_failure() { + when(mMockReader.getProcessCpuUsage()) + .thenReturn(createProcess(new int[] {1})) + .thenReturn(createProcess(new int[] {2})) + .thenThrow(new RuntimeException()) + .thenReturn(createProcess(new int[] {4})) + .thenReturn(createProcess(new int[] {6})); + KernelCpuThreadReaderDiff kernelCpuThreadReaderDiff = + new KernelCpuThreadReaderDiff(mMockReader, 0); + assertThat(kernelCpuThreadReaderDiff.getProcessCpuUsageDiffed()).isNull(); + assertThat(cpuUsages(kernelCpuThreadReaderDiff.getProcessCpuUsageDiffed())) + .containsExactly(Collections.singletonList(1)); + assertThrows( + RuntimeException.class, + () -> cpuUsages(kernelCpuThreadReaderDiff.getProcessCpuUsageDiffed())); + assertThat(kernelCpuThreadReaderDiff.getProcessCpuUsageDiffed()).isNull(); + assertThat(cpuUsages(kernelCpuThreadReaderDiff.getProcessCpuUsageDiffed())) + .containsExactly(Collections.singletonList(2)); + } + + @Test + public void test_twoFailures() { + when(mMockReader.getProcessCpuUsage()) + .thenReturn(createProcess(new int[] {1})) + .thenReturn(createProcess(new int[] {2})) + .thenThrow(new RuntimeException()) + .thenThrow(new RuntimeException()) + .thenReturn(createProcess(new int[] {4})) + .thenReturn(createProcess(new int[] {6})); + KernelCpuThreadReaderDiff kernelCpuThreadReaderDiff = + new KernelCpuThreadReaderDiff(mMockReader, 0); + assertThat(kernelCpuThreadReaderDiff.getProcessCpuUsageDiffed()).isNull(); + assertThat(cpuUsages(kernelCpuThreadReaderDiff.getProcessCpuUsageDiffed())) + .containsExactly(Collections.singletonList(1)); + assertThrows( + RuntimeException.class, + () -> cpuUsages(kernelCpuThreadReaderDiff.getProcessCpuUsageDiffed())); + assertThrows( + RuntimeException.class, + () -> cpuUsages(kernelCpuThreadReaderDiff.getProcessCpuUsageDiffed())); + assertThat(kernelCpuThreadReaderDiff.getProcessCpuUsageDiffed()).isNull(); + assertThat(cpuUsages(kernelCpuThreadReaderDiff.getProcessCpuUsageDiffed())) + .containsExactly(Collections.singletonList(2)); + } + + @Test + public void test_negativeDiff() { + when(mMockReader.getProcessCpuUsage()) + .thenReturn(createProcess(new int[] {2})) + .thenReturn(createProcess(new int[] {1})); + KernelCpuThreadReaderDiff kernelCpuThreadReaderDiff = + new KernelCpuThreadReaderDiff(mMockReader, 0); + assertThat(kernelCpuThreadReaderDiff.getProcessCpuUsageDiffed()).isNull(); + assertThat(cpuUsages(kernelCpuThreadReaderDiff.getProcessCpuUsageDiffed())) + .containsExactly(Collections.singletonList(-1)); + } + + @Test + public void test_threshold() { + when(mMockReader.getProcessCpuUsage()) + .thenReturn(createProcess(new int[] {1})) + .thenReturn(createProcess(new int[] {10})) + .thenReturn(createProcess(new int[] {12})) + .thenReturn(createProcess(new int[] {20})); + KernelCpuThreadReaderDiff kernelCpuThreadReaderDiff = + new KernelCpuThreadReaderDiff(mMockReader, 5); + assertThat(kernelCpuThreadReaderDiff.getProcessCpuUsageDiffed()).isNull(); + + ArrayList processes1 = + kernelCpuThreadReaderDiff.getProcessCpuUsageDiffed(); + assertThat(cpuUsages(processes1)).containsExactly(Collections.singletonList(9)); + assertThat(threadNames(processes1)).containsExactly("thread0"); + + ArrayList processes2 = + kernelCpuThreadReaderDiff.getProcessCpuUsageDiffed(); + assertThat(cpuUsages(processes2)).containsExactly(Collections.singletonList(2)); + assertThat(threadNames(processes2)).containsExactly("__OTHER_THREADS"); + + ArrayList processes3 = + kernelCpuThreadReaderDiff.getProcessCpuUsageDiffed(); + assertThat(cpuUsages(processes3)).containsExactly(Collections.singletonList(8)); + assertThat(threadNames(processes3)).containsExactly("thread0"); + } + + @Test + public void test_newThread() { + when(mMockReader.getProcessCpuUsage()) + .thenReturn(createProcess(new int[] {1})) + .thenReturn(createProcess(new int[] {2})) + .thenReturn(createProcess(new int[] {4}, new int[] {5})); + KernelCpuThreadReaderDiff kernelCpuThreadReaderDiff = + new KernelCpuThreadReaderDiff(mMockReader, 0); + assertThat(kernelCpuThreadReaderDiff.getProcessCpuUsageDiffed()).isNull(); + + ArrayList processes1 = + kernelCpuThreadReaderDiff.getProcessCpuUsageDiffed(); + assertThat(cpuUsages(processes1)).containsExactly(Collections.singletonList(1)); + assertThat(threadNames(processes1)).containsExactly("thread0"); + + ArrayList processes2 = + kernelCpuThreadReaderDiff.getProcessCpuUsageDiffed(); + assertThat(cpuUsages(processes2)) + .containsExactly(Collections.singletonList(2), Collections.singletonList(5)); + assertThat(threadNames(processes2)).containsExactly("thread0", "thread1"); + } + + @Test + public void test_stoppedThread() { + when(mMockReader.getProcessCpuUsage()) + .thenReturn(createProcess(new int[] {1}, new int[] {1})) + .thenReturn(createProcess(new int[] {2}, new int[] {3})) + .thenReturn(createProcess(new int[] {4})); + KernelCpuThreadReaderDiff kernelCpuThreadReaderDiff = + new KernelCpuThreadReaderDiff(mMockReader, 0); + assertThat(kernelCpuThreadReaderDiff.getProcessCpuUsageDiffed()).isNull(); + + ArrayList processes1 = + kernelCpuThreadReaderDiff.getProcessCpuUsageDiffed(); + assertThat(cpuUsages(processes1)) + .containsExactly(Collections.singletonList(1), Collections.singletonList(2)); + assertThat(threadNames(processes1)).containsExactly("thread0", "thread1"); + + ArrayList processes2 = + kernelCpuThreadReaderDiff.getProcessCpuUsageDiffed(); + assertThat(cpuUsages(processes2)).containsExactly(Collections.singletonList(2)); + assertThat(threadNames(processes2)).containsExactly("thread0"); + } + + private ArrayList createProcess( + int[]... cpuUsageMillis) { + ArrayList threadCpuUsages = new ArrayList<>(); + for (int i = 0; i < cpuUsageMillis.length; i++) { + int[] cpuUsage = cpuUsageMillis[i]; + threadCpuUsages.add( + new KernelCpuThreadReader.ThreadCpuUsage(0, "thread" + i, cpuUsage)); + } + return new ArrayList<>( + Collections.singletonList( + new KernelCpuThreadReader.ProcessCpuUsage( + 0, "process", 0, threadCpuUsages))); + } + + private Collection> cpuUsages( + Collection processCpuUsages) { + return processCpuUsages.stream() + .flatMap(p -> p.threadCpuUsages.stream()) + .map(t -> Arrays.stream(t.usageTimesMillis).boxed().collect(toList())) + .collect(toList()); + } + + private Collection threadNames( + Collection processCpuUsages) { + return processCpuUsages.stream() + .flatMap(p -> p.threadCpuUsages.stream()) + .map(t -> t.threadName) + .collect(toList()); + } +} diff --git a/core/tests/coretests/src/com/android/internal/os/KernelCpuThreadReaderEndToEndTest.java b/core/tests/coretests/src/com/android/internal/os/KernelCpuThreadReaderEndToEndTest.java index e9cad0acfcc6c..d43989c06a72c 100644 --- a/core/tests/coretests/src/com/android/internal/os/KernelCpuThreadReaderEndToEndTest.java +++ b/core/tests/coretests/src/com/android/internal/os/KernelCpuThreadReaderEndToEndTest.java @@ -125,7 +125,7 @@ public class KernelCpuThreadReaderEndToEndTest { // Get thread data from KernelCpuThreadReader final KernelCpuThreadReader kernelCpuThreadReader = - KernelCpuThreadReader.create(8, uid -> uid == Process.myUid(), 0); + KernelCpuThreadReader.create(8, uid -> uid == Process.myUid()); assertNotNull(kernelCpuThreadReader); kernelCpuThreadReader.setUidPredicate(uid -> uid == Process.myUid()); final Optional currentProcessCpuUsage = 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 61209e21dda8e..ae847c1256338 100644 --- a/core/tests/coretests/src/com/android/internal/os/KernelCpuThreadReaderTest.java +++ b/core/tests/coretests/src/com/android/internal/os/KernelCpuThreadReaderTest.java @@ -84,7 +84,6 @@ public class KernelCpuThreadReaderTest { final KernelCpuThreadReader kernelCpuThreadReader = new KernelCpuThreadReader( 8, uidPredicate, - 0, mProcDirectory.toPath(), mProcDirectory.toPath().resolve(uids[0] + "/task/" + uids[0] + "/time_in_state"), processUtils); @@ -103,90 +102,6 @@ public class KernelCpuThreadReaderTest { } } - @Test - public void testReader_filtersLowUsage() throws IOException { - int[] uids = new int[]{0, 1, 2, 3, 4}; - int[] cpuUsage = new int[]{10, 0, 2, 100, 3}; - int[] expectedUids = new int[]{0, 3, 4}; - Predicate uidPredicate = uid -> true; - KernelCpuThreadReader.Injector processUtils = - new KernelCpuThreadReader.Injector() { - @Override - public int getUidForPid(int pid) { - return pid; - } - }; - - for (int i = 0; i < uids.length; i++) { - int uid = uids[i]; - setupDirectory( - mProcDirectory.toPath().resolve(String.valueOf(uid)), - new int[]{uid * 10}, - "process" + uid, - new String[]{"thread" + uid}, - new int[]{1000}, - new int[][]{{cpuUsage[i]}}); - } - final KernelCpuThreadReader kernelCpuThreadReader = new KernelCpuThreadReader( - 8, - uidPredicate, - 30, - mProcDirectory.toPath(), - mProcDirectory.toPath().resolve(uids[0] + "/task/" + uids[0] + "/time_in_state"), - processUtils); - ArrayList processCpuUsageByUids = - kernelCpuThreadReader.getProcessCpuUsage(); - processCpuUsageByUids.sort(Comparator.comparing(usage -> usage.uid)); - - assertEquals(expectedUids.length, processCpuUsageByUids.size()); - for (int i = 0; i < expectedUids.length; i++) { - KernelCpuThreadReader.ProcessCpuUsage processCpuUsage = - processCpuUsageByUids.get(i); - assertEquals(expectedUids[i], processCpuUsage.uid); - } - - } - - @Test - public void testReader_otherThreads() throws IOException { - final Path processPath = mProcDirectory.toPath().resolve("1000"); - setupDirectory( - processPath, - new int[]{1, 2, 3}, - "process", - new String[]{"thread1", "thread2", "thread3"}, - new int[]{1000, 2000}, - new int[][]{{0, 100}, {10, 0}, {0, 300}}); - KernelCpuThreadReader.Injector injector = - new KernelCpuThreadReader.Injector() { - @Override - public int getUidForPid(int pid) { - return 0; - } - }; - final KernelCpuThreadReader kernelCpuThreadReader = - new KernelCpuThreadReader( - 8, - uid -> true, - 2000, - mProcDirectory.toPath(), - processPath.resolve("task/1/time_in_state"), - injector); - ArrayList processCpuUsages = - kernelCpuThreadReader.getProcessCpuUsage(); - assertEquals(1, processCpuUsages.size()); - checkResults( - processCpuUsages.get(0), - kernelCpuThreadReader.getCpuFrequenciesKhz(), - 0, - 1000, - new int[] {-1, 3}, - "process", - new String[] {"__OTHER_THREADS", "thread3"}, - new int[] {1000, 2000}, - new int[][] {{10, 100}, {0, 300}}); - } - private void setupDirectory(Path processPath, int[] threadIds, String processName, String[] threadNames, int[] cpuFrequencies, int[][] cpuTimes) throws IOException { // Make /proc/$PID diff --git a/services/core/java/com/android/server/stats/StatsCompanionService.java b/services/core/java/com/android/server/stats/StatsCompanionService.java index 15148f3af1948..f83b3ea10e050 100644 --- a/services/core/java/com/android/server/stats/StatsCompanionService.java +++ b/services/core/java/com/android/server/stats/StatsCompanionService.java @@ -110,6 +110,7 @@ import com.android.internal.os.BatteryStatsHelper; import com.android.internal.os.BinderCallsStats.ExportedCallStat; import com.android.internal.os.KernelCpuSpeedReader; import com.android.internal.os.KernelCpuThreadReader; +import com.android.internal.os.KernelCpuThreadReaderDiff; import com.android.internal.os.KernelCpuThreadReaderSettingsObserver; import com.android.internal.os.KernelCpuUidTimeReader.KernelCpuUidActiveTimeReader; import com.android.internal.os.KernelCpuUidTimeReader.KernelCpuUidClusterTimeReader; @@ -262,7 +263,7 @@ public class StatsCompanionService extends IStatsCompanionService.Stub { private StoragedUidIoStatsReader mStoragedUidIoStatsReader = new StoragedUidIoStatsReader(); @Nullable - private final KernelCpuThreadReader mKernelCpuThreadReader; + private final KernelCpuThreadReaderDiff mKernelCpuThreadReader; private long mDebugElapsedClockPreviousValue = 0; private long mDebugElapsedClockPullCount = 0; @@ -1726,7 +1727,7 @@ public class StatsCompanionService extends IStatsCompanionService.Stub { throw new IllegalStateException("mKernelCpuThreadReader is null"); } ArrayList processCpuUsages = - this.mKernelCpuThreadReader.getProcessCpuUsage(); + this.mKernelCpuThreadReader.getProcessCpuUsageDiffed(); if (processCpuUsages == null) { throw new IllegalStateException("processCpuUsages is null"); }