/* * 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.android.internal.util.Preconditions.checkNotNull; 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: * *

* *

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 * statsd, because the thresholding should be done after diffing, not before. This is because of * two issues with thresholding before diffing: * *

* * @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 = checkNotNull(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; } } }