summaryrefslogtreecommitdiff
path: root/core/java/com/android/internal/os/KernelCpuThreadReaderDiff.java
blob: 69ca9922e81f6f36c7498864512433cce55b55fc (plain)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
/*
 * 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()}.
 *
 * <p>Some notes on the diff calculation:
 *
 * <ul>
 *   <li>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.
 *   <li>The first call of {@link #getProcessCpuUsageDiffed()} will return no processes ("first
 *       call" is the first call in the lifetime of a {@link KernelCpuThreadReaderDiff} object).
 *   <li>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.
 *   <li>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.
 *   <li>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.
 * </ul>
 *
 * <p>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.
 *
 * <p>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:
 *
 * <ul>
 *   <li>We would threshold less and less threads as thread uptime increases.
 *   <li>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.
 * </ul>
 *
 * @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
     *
     * <p>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<ThreadKey, int[]> 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<KernelCpuThreadReader.ProcessCpuUsage> getProcessCpuUsageDiffed() {
        Map<ThreadKey, int[]> newCpuUsage = null;
        try {
            // Get the thread CPU usage and index them by ThreadKey
            final ArrayList<KernelCpuThreadReader.ProcessCpuUsage> 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<ThreadKey, int[]> createCpuUsageMap(
            List<KernelCpuThreadReader.ProcessCpuUsage> processCpuUsages) {
        final Map<ThreadKey, int[]> 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<ThreadKey, int[]> 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
     *
     * <p>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<KernelCpuThreadReader.ThreadCpuUsage> 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
     *
     * <p>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;
        }
    }
}