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
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
551
552
553
554
555
556
557
558
559
560
561
562
563
564
565
566
567
568
569
570
|
/*
* 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.os.Process;
import android.util.IntArray;
import android.util.Slog;
import com.android.internal.annotations.VisibleForTesting;
import com.android.internal.util.Preconditions;
import java.io.IOException;
import java.nio.file.DirectoryIteratorException;
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.Arrays;
import java.util.function.Predicate;
/**
* Iterates over processes, and all threads owned by those processes, and return the CPU usage for
* each thread. The CPU usage statistics contain the amount of time spent in a frequency band. CPU
* usage is collected using {@link ProcTimeInStateReader}.
*
* <p>We only collect CPU data for processes and threads that are owned by certain UIDs. These UIDs
* are configured via {@link #setUidPredicate}.
*
* <p>Frequencies are bucketed together to reduce the amount of data created. This means that we
* return less frequencies than provided by {@link ProcTimeInStateReader}. The number of frequencies
* is configurable by {@link #setNumBuckets}. Frequencies are reported as the lowest frequency in
* that range. Frequencies are spread as evenly as possible across the buckets. The buckets do not
* cross over the little/big frequencies reported.
*
* <p>N.B.: In order to bucket across little/big frequencies correctly, we assume that the {@code
* time_in_state} file contains every little core frequency in ascending order, followed by every
* big core frequency in ascending order. This assumption might not hold for devices with different
* kernel implementations of the {@code time_in_state} file generation.
*/
public class KernelCpuThreadReader {
private static final String TAG = "KernelCpuThreadReader";
private static final boolean DEBUG = false;
/**
* The name of the file to read CPU statistics from, must be found in {@code
* /proc/$PID/task/$TID}
*/
private static final String CPU_STATISTICS_FILENAME = "time_in_state";
/**
* The name of the file to read process command line invocation from, must be found in {@code
* /proc/$PID/}
*/
private static final String PROCESS_NAME_FILENAME = "cmdline";
/**
* The name of the file to read thread name from, must be found in {@code /proc/$PID/task/$TID}
*/
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 */
private static final String DEFAULT_PROCESS_NAME = "unknown_process";
/** Default thread name when the name can't be read */
private static final String DEFAULT_THREAD_NAME = "unknown_thread";
/** Default mount location of the {@code proc} filesystem */
private static final Path DEFAULT_PROC_PATH = Paths.get("/proc");
/** The initial {@code time_in_state} file for {@link ProcTimeInStateReader} */
private static final Path DEFAULT_INITIAL_TIME_IN_STATE_PATH =
DEFAULT_PROC_PATH.resolve("self/time_in_state");
/** Value returned when there was an error getting an integer ID value (e.g. PID, UID) */
private static final int ID_ERROR = -1;
/**
* When checking whether to report data for a thread, we check the UID of the thread's owner
* against this predicate
*/
private Predicate<Integer> mUidPredicate;
/** Where the proc filesystem is mounted */
private final Path mProcPath;
/**
* Frequencies read from the {@code time_in_state} file. Read from {@link
* #mProcTimeInStateReader#getCpuFrequenciesKhz()} and cast to {@code int[]}
*/
private int[] mFrequenciesKhz;
/** Used to read and parse {@code time_in_state} files */
private final ProcTimeInStateReader mProcTimeInStateReader;
/** Used to sort frequencies and usage times into buckets */
private FrequencyBucketCreator mFrequencyBucketCreator;
private final Injector mInjector;
/**
* Create with a path where `proc` is mounted. Used primarily for testing
*
* @param procPath where `proc` is mounted (to find, see {@code mount | grep ^proc})
* @param initialTimeInStatePath where the initial {@code time_in_state} file exists to define
* format
*/
@VisibleForTesting
public KernelCpuThreadReader(
int numBuckets,
Predicate<Integer> uidPredicate,
Path procPath,
Path initialTimeInStatePath,
Injector injector)
throws IOException {
mUidPredicate = uidPredicate;
mProcPath = procPath;
mProcTimeInStateReader = new ProcTimeInStateReader(initialTimeInStatePath);
mInjector = injector;
setNumBuckets(numBuckets);
}
/**
* Create the reader and handle exceptions during creation
*
* @return the reader, null if an exception was thrown during creation
*/
@Nullable
public static KernelCpuThreadReader create(int numBuckets, Predicate<Integer> uidPredicate) {
try {
return new KernelCpuThreadReader(
numBuckets,
uidPredicate,
DEFAULT_PROC_PATH,
DEFAULT_INITIAL_TIME_IN_STATE_PATH,
new Injector());
} catch (IOException e) {
Slog.e(TAG, "Failed to initialize KernelCpuThreadReader", e);
return null;
}
}
/**
* 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 2017 device. Therefore, this method can be computationally
* expensive, and should not be called more than once an hour.
*
* <p>Data is only collected for UIDs passing the predicate supplied in {@link
* #setUidPredicate}.
*/
@Nullable
public ArrayList<ProcessCpuUsage> getProcessCpuUsage() {
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 (!mUidPredicate.test(uid)) {
continue;
}
final ProcessCpuUsage processCpuUsage =
getProcessCpuUsage(processPath, processId, uid);
if (processCpuUsage != null) {
processCpuUsages.add(processCpuUsage);
}
}
} catch (IOException e) {
Slog.w(TAG, "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;
}
/**
* Get the CPU frequencies that correspond to the times reported in {@link
* ThreadCpuUsage#usageTimesMillis}
*/
@Nullable
public int[] getCpuFrequenciesKhz() {
return mFrequenciesKhz;
}
/** Set the number of frequency buckets to use */
void setNumBuckets(int numBuckets) {
// If `numBuckets` hasn't changed since the last set, do nothing
if (mFrequenciesKhz != null && mFrequenciesKhz.length == numBuckets) {
return;
}
final long[] frequenciesKhz = mProcTimeInStateReader.getFrequenciesKhz();
if (numBuckets != 0) {
mFrequencyBucketCreator = new FrequencyBucketCreator(frequenciesKhz, numBuckets);
mFrequenciesKhz = mFrequencyBucketCreator.bucketFrequencies(frequenciesKhz);
} else {
mFrequencyBucketCreator = null;
mFrequenciesKhz = new int[frequenciesKhz.length];
for (int i = 0; i < frequenciesKhz.length; i++) {
mFrequenciesKhz[i] = (int) frequenciesKhz[i];
}
}
}
/** Set the UID predicate for {@link #getProcessCpuUsage} */
void setUidPredicate(Predicate<Integer> uidPredicate) {
mUidPredicate = uidPredicate;
}
/**
* Read all of the CPU usage statistics for each child thread of a process
*
* @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. 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) {
if (DEBUG) {
Slog.d(
TAG,
"Reading CPU thread usages with directory "
+ processPath
+ " process ID "
+ processId
+ " and user ID "
+ uid);
}
final Path allThreadsPath = processPath.resolve("task");
final ArrayList<ThreadCpuUsage> threadCpuUsages = new ArrayList<>();
try (DirectoryStream<Path> threadPaths = Files.newDirectoryStream(allThreadsPath)) {
for (Path threadDirectory : threadPaths) {
ThreadCpuUsage threadCpuUsage = getThreadCpuUsage(threadDirectory);
if (threadCpuUsage == null) {
continue;
}
threadCpuUsages.add(threadCpuUsage);
}
} 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;
}
if (DEBUG) {
Slog.d(TAG, "Read CPU usage of " + threadCpuUsages.size() + " threads");
}
return new ProcessCpuUsage(processId, getProcessName(processPath), uid, threadCpuUsages);
}
/**
* Get a thread's CPU usage
*
* @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) {
// Get the thread ID from the directory name
final int threadId;
try {
final String directoryName = threadDirectory.getFileName().toString();
threadId = Integer.parseInt(directoryName);
} catch (NumberFormatException e) {
Slog.w(TAG, "Failed to parse thread ID when iterating over /proc/*/task", e);
return null;
}
// Get the thread name from the thread directory
final String threadName = getThreadName(threadDirectory);
// Get the CPU statistics from the directory
final Path threadCpuStatPath = threadDirectory.resolve(CPU_STATISTICS_FILENAME);
final long[] cpuUsagesLong = mProcTimeInStateReader.getUsageTimesMillis(threadCpuStatPath);
if (cpuUsagesLong == null) {
return null;
}
final int[] cpuUsages;
if (mFrequencyBucketCreator != null) {
cpuUsages = mFrequencyBucketCreator.bucketValues(cpuUsagesLong);
} else {
cpuUsages = new int[cpuUsagesLong.length];
for (int i = 0; i < cpuUsagesLong.length; i++) {
cpuUsages[i] = (int) cpuUsagesLong[i];
}
}
return new ThreadCpuUsage(threadId, threadName, cpuUsages);
}
/** Get the command used to start a process */
private String getProcessName(Path processPath) {
final Path processNamePath = processPath.resolve(PROCESS_NAME_FILENAME);
final String processName = ProcStatsUtil.readSingleLineProcFile(processNamePath.toString());
if (processName != null) {
return processName;
}
return DEFAULT_PROCESS_NAME;
}
/** Get the name of a thread, given the {@code /proc} path of the thread */
private String getThreadName(Path threadPath) {
final Path threadNamePath = threadPath.resolve(THREAD_NAME_FILENAME);
final String threadName = ProcStatsUtil.readNullSeparatedFile(threadNamePath.toString());
if (threadName == null) {
return DEFAULT_THREAD_NAME;
}
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;
}
}
/**
* Quantizes a list of N frequencies into a list of M frequencies (where M<=N)
*
* <p>In order to reduce data sent from the device, we discard precise frequency information for
* an approximation. This is done by putting groups of adjacent frequencies into the same
* bucket, and then reporting that bucket under the minimum frequency in that bucket.
*
* <p>Many devices have multiple core clusters. We do not want to report frequencies from
* different clusters under the same bucket, so some complication arises.
*
* <p>Buckets are allocated evenly across all core clusters, i.e. they all have the same number
* of buckets regardless of how many frequencies they contain. This is done to reduce code
* complexity, and in practice the number of frequencies doesn't vary too much between core
* clusters.
*
* <p>If the number of buckets is not a factor of the number of frequencies, the remainder of
* the frequencies are placed into the last bucket.
*
* <p>It is possible to have less buckets than asked for, so any calling code can't assume that
* initializing with N buckets will use return N values. This happens in two scenarios:
*
* <ul>
* <li>There are less frequencies available than buckets asked for.
* <li>There are less frequencies in a core cluster than buckets allocated to that core
* cluster.
* </ul>
*/
@VisibleForTesting
public static class FrequencyBucketCreator {
private final int mNumFrequencies;
private final int mNumBuckets;
private final int[] mBucketStartIndices;
@VisibleForTesting
public FrequencyBucketCreator(long[] frequencies, int targetNumBuckets) {
mNumFrequencies = frequencies.length;
int[] clusterStartIndices = getClusterStartIndices(frequencies);
mBucketStartIndices =
getBucketStartIndices(clusterStartIndices, targetNumBuckets, mNumFrequencies);
mNumBuckets = mBucketStartIndices.length;
}
/**
* Put an array of values into buckets. This takes a {@code long[]} and returns {@code
* int[]} as everywhere this method is used will have to do the conversion anyway, so we
* save time by doing it here instead
*
* @param values the values to bucket
* @return the bucketed usage times
*/
@VisibleForTesting
public int[] bucketValues(long[] values) {
Preconditions.checkArgument(values.length == mNumFrequencies);
int[] buckets = new int[mNumBuckets];
for (int bucketIdx = 0; bucketIdx < mNumBuckets; bucketIdx++) {
final int bucketStartIdx = getLowerBound(bucketIdx, mBucketStartIndices);
final int bucketEndIdx =
getUpperBound(bucketIdx, mBucketStartIndices, values.length);
for (int valuesIdx = bucketStartIdx; valuesIdx < bucketEndIdx; valuesIdx++) {
buckets[bucketIdx] += values[valuesIdx];
}
}
return buckets;
}
/** Get the minimum frequency in each bucket */
@VisibleForTesting
public int[] bucketFrequencies(long[] frequencies) {
Preconditions.checkArgument(frequencies.length == mNumFrequencies);
int[] buckets = new int[mNumBuckets];
for (int i = 0; i < buckets.length; i++) {
buckets[i] = (int) frequencies[mBucketStartIndices[i]];
}
return buckets;
}
/**
* Get the index in frequencies where each core cluster starts
*
* <p>The frequencies for each cluster are given in ascending order, appended to each other.
* This means that every time there is a decrease in frequencies (instead of increase) a new
* cluster has started.
*/
private static int[] getClusterStartIndices(long[] frequencies) {
IntArray indices = new IntArray();
indices.add(0);
for (int i = 0; i < frequencies.length - 1; i++) {
if (frequencies[i] >= frequencies[i + 1]) {
indices.add(i + 1);
}
}
return indices.toArray();
}
/** Get the index in frequencies where each bucket starts */
private static int[] getBucketStartIndices(
int[] clusterStartIndices, int targetNumBuckets, int numFrequencies) {
int numClusters = clusterStartIndices.length;
// If we haven't got enough buckets for every cluster, we instead have one bucket per
// cluster, with the last bucket containing the remaining clusters
if (numClusters > targetNumBuckets) {
return Arrays.copyOfRange(clusterStartIndices, 0, targetNumBuckets);
}
IntArray bucketStartIndices = new IntArray();
for (int clusterIdx = 0; clusterIdx < numClusters; clusterIdx++) {
final int clusterStartIdx = getLowerBound(clusterIdx, clusterStartIndices);
final int clusterEndIdx =
getUpperBound(clusterIdx, clusterStartIndices, numFrequencies);
final int numBucketsInCluster;
if (clusterIdx != numClusters - 1) {
numBucketsInCluster = targetNumBuckets / numClusters;
} else {
// If we're in the last cluster, the bucket will contain the remainder of the
// frequencies
int previousBucketsInCluster = targetNumBuckets / numClusters;
numBucketsInCluster =
targetNumBuckets - (previousBucketsInCluster * (numClusters - 1));
}
final int numFrequenciesInCluster = clusterEndIdx - clusterStartIdx;
// If there are less frequencies than buckets in a cluster, we have one bucket per
// frequency, and do not use the remaining buckets
final int numFrequenciesInBucket =
Math.max(1, numFrequenciesInCluster / numBucketsInCluster);
for (int bucketIdx = 0; bucketIdx < numBucketsInCluster; bucketIdx++) {
int bucketStartIdx = clusterStartIdx + bucketIdx * numFrequenciesInBucket;
// If we've gone over the end index, ignore the rest of the buckets for this
// cluster
if (bucketStartIdx >= clusterEndIdx) {
break;
}
bucketStartIndices.add(bucketStartIdx);
}
}
return bucketStartIndices.toArray();
}
private static int getLowerBound(int index, int[] startIndices) {
return startIndices[index];
}
private static int getUpperBound(int index, int[] startIndices, int max) {
if (index != startIndices.length - 1) {
return startIndices[index + 1];
} else {
return max;
}
}
}
/** CPU usage of a process */
public static class ProcessCpuUsage {
public final int processId;
public final String processName;
public final int uid;
public ArrayList<ThreadCpuUsage> threadCpuUsages;
@VisibleForTesting
public ProcessCpuUsage(
int processId,
String processName,
int uid,
ArrayList<ThreadCpuUsage> threadCpuUsages) {
this.processId = processId;
this.processName = processName;
this.uid = uid;
this.threadCpuUsages = threadCpuUsages;
}
}
/** CPU usage of a thread */
public static class ThreadCpuUsage {
public final int threadId;
public final String threadName;
public int[] usageTimesMillis;
@VisibleForTesting
public ThreadCpuUsage(int threadId, String threadName, int[] usageTimesMillis) {
this.threadId = threadId;
this.threadName = threadName;
this.usageTimesMillis = usageTimesMillis;
}
}
/** Used to inject static methods from {@link Process} */
@VisibleForTesting
public static class Injector {
/** Get the UID for the process with ID {@code pid} */
public int getUidForPid(int pid) {
return Process.getUidForPid(pid);
}
}
}
|