1 /*
2  * Copyright (C) 2018 The Android Open Source Project
3  *
4  * Licensed under the Apache License, Version 2.0 (the "License");
5  * you may not use this file except in compliance with the License.
6  * You may obtain a copy of the License at
7  *
8  *      http://www.apache.org/licenses/LICENSE-2.0
9  *
10  * Unless required by applicable law or agreed to in writing, software
11  * distributed under the License is distributed on an "AS IS" BASIS,
12  * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13  * See the License for the specific language governing permissions and
14  * limitations under the License.
15  */
16 
17 package com.android.internal.os;
18 
19 import android.annotation.Nullable;
20 import android.os.Process;
21 import android.util.Slog;
22 
23 import com.android.internal.annotations.VisibleForTesting;
24 import com.android.internal.util.ArrayUtils;
25 import com.android.internal.util.Preconditions;
26 
27 import java.io.IOException;
28 import java.nio.file.DirectoryStream;
29 import java.nio.file.Files;
30 import java.nio.file.Path;
31 import java.nio.file.Paths;
32 import java.util.ArrayList;
33 import java.util.Arrays;
34 import java.util.function.Predicate;
35 
36 /**
37  * Iterates over processes, and all threads owned by those processes, and return the CPU usage for
38  * each thread. The CPU usage statistics contain the amount of time spent in a frequency band. CPU
39  * usage is collected using {@link ProcTimeInStateReader}.
40  *
41  * <p>We only collect CPU data for processes and threads that are owned by certain UIDs. These UIDs
42  * are configured via {@link #setUidPredicate}.
43  *
44  * <p>Frequencies are bucketed together to reduce the amount of data created. This means that we
45  * return less frequencies than provided by {@link ProcTimeInStateReader}. The number of frequencies
46  * is configurable by {@link #setNumBuckets}. Frequencies are reported as the lowest frequency in
47  * that range. Frequencies are spread as evenly as possible across the buckets. The buckets do not
48  * cross over the little/big frequencies reported.
49  *
50  * <p>N.B.: In order to bucket across little/big frequencies correctly, we assume that the {@code
51  * time_in_state} file contains every little core frequency in ascending order, followed by every
52  * big core frequency in ascending order. This assumption might not hold for devices with different
53  * kernel implementations of the {@code time_in_state} file generation.
54  */
55 public class KernelCpuThreadReader {
56 
57     private static final String TAG = "KernelCpuThreadReader";
58 
59     private static final boolean DEBUG = false;
60 
61     /**
62      * The name of the file to read CPU statistics from, must be found in {@code
63      * /proc/$PID/task/$TID}
64      */
65     private static final String CPU_STATISTICS_FILENAME = "time_in_state";
66 
67     /**
68      * The name of the file to read process command line invocation from, must be found in {@code
69      * /proc/$PID/}
70      */
71     private static final String PROCESS_NAME_FILENAME = "cmdline";
72 
73     /**
74      * The name of the file to read thread name from, must be found in {@code /proc/$PID/task/$TID}
75      */
76     private static final String THREAD_NAME_FILENAME = "comm";
77 
78     /** Glob pattern for the process directory names under {@code proc} */
79     private static final String PROCESS_DIRECTORY_FILTER = "[0-9]*";
80 
81     /** Default process name when the name can't be read */
82     private static final String DEFAULT_PROCESS_NAME = "unknown_process";
83 
84     /** Default thread name when the name can't be read */
85     private static final String DEFAULT_THREAD_NAME = "unknown_thread";
86 
87     /** Default mount location of the {@code proc} filesystem */
88     private static final Path DEFAULT_PROC_PATH = Paths.get("/proc");
89 
90     /** The initial {@code time_in_state} file for {@link ProcTimeInStateReader} */
91     private static final Path DEFAULT_INITIAL_TIME_IN_STATE_PATH =
92             DEFAULT_PROC_PATH.resolve("self/time_in_state");
93 
94     /** Value returned when there was an error getting an integer ID value (e.g. PID, UID) */
95     private static final int ID_ERROR = -1;
96 
97     /**
98      * When checking whether to report data for a thread, we check the UID of the thread's owner
99      * against this predicate
100      */
101     private Predicate<Integer> mUidPredicate;
102 
103     /** Where the proc filesystem is mounted */
104     private final Path mProcPath;
105 
106     /**
107      * Frequencies read from the {@code time_in_state} file. Read from {@link
108      * #mProcTimeInStateReader#getCpuFrequenciesKhz()} and cast to {@code int[]}
109      */
110     private int[] mFrequenciesKhz;
111 
112     /** Used to read and parse {@code time_in_state} files */
113     private final ProcTimeInStateReader mProcTimeInStateReader;
114 
115     /** Used to sort frequencies and usage times into buckets */
116     private FrequencyBucketCreator mFrequencyBucketCreator;
117 
118     private final Injector mInjector;
119 
120     /**
121      * Create with a path where `proc` is mounted. Used primarily for testing
122      *
123      * @param procPath where `proc` is mounted (to find, see {@code mount | grep ^proc})
124      * @param initialTimeInStatePath where the initial {@code time_in_state} file exists to define
125      *     format
126      */
127     @VisibleForTesting
KernelCpuThreadReader( int numBuckets, Predicate<Integer> uidPredicate, Path procPath, Path initialTimeInStatePath, Injector injector)128     public KernelCpuThreadReader(
129             int numBuckets,
130             Predicate<Integer> uidPredicate,
131             Path procPath,
132             Path initialTimeInStatePath,
133             Injector injector)
134             throws IOException {
135         mUidPredicate = uidPredicate;
136         mProcPath = procPath;
137         mProcTimeInStateReader = new ProcTimeInStateReader(initialTimeInStatePath);
138         mInjector = injector;
139         setNumBuckets(numBuckets);
140     }
141 
142     /**
143      * Create the reader and handle exceptions during creation
144      *
145      * @return the reader, null if an exception was thrown during creation
146      */
147     @Nullable
create(int numBuckets, Predicate<Integer> uidPredicate)148     public static KernelCpuThreadReader create(int numBuckets, Predicate<Integer> uidPredicate) {
149         try {
150             return new KernelCpuThreadReader(
151                     numBuckets,
152                     uidPredicate,
153                     DEFAULT_PROC_PATH,
154                     DEFAULT_INITIAL_TIME_IN_STATE_PATH,
155                     new Injector());
156         } catch (IOException e) {
157             Slog.e(TAG, "Failed to initialize KernelCpuThreadReader", e);
158             return null;
159         }
160     }
161 
162     /**
163      * Get the per-thread CPU usage of all processes belonging to a set of UIDs
164      *
165      * <p>This function will crawl through all process {@code proc} directories found by the pattern
166      * {@code /proc/[0-9]*}, and then check the UID using {@code /proc/$PID/status}. This takes
167      * approximately 500ms on a 2017 device. Therefore, this method can be computationally
168      * expensive, and should not be called more than once an hour.
169      *
170      * <p>Data is only collected for UIDs passing the predicate supplied in {@link
171      * #setUidPredicate}.
172      */
173     @Nullable
getProcessCpuUsage()174     public ArrayList<ProcessCpuUsage> getProcessCpuUsage() {
175         if (DEBUG) {
176             Slog.d(TAG, "Reading CPU thread usages for processes owned by UIDs");
177         }
178 
179         final ArrayList<ProcessCpuUsage> processCpuUsages = new ArrayList<>();
180 
181         try (DirectoryStream<Path> processPaths =
182                 Files.newDirectoryStream(mProcPath, PROCESS_DIRECTORY_FILTER)) {
183             for (Path processPath : processPaths) {
184                 final int processId = getProcessId(processPath);
185                 final int uid = mInjector.getUidForPid(processId);
186                 if (uid == ID_ERROR || processId == ID_ERROR) {
187                     continue;
188                 }
189                 if (!mUidPredicate.test(uid)) {
190                     continue;
191                 }
192 
193                 final ProcessCpuUsage processCpuUsage =
194                         getProcessCpuUsage(processPath, processId, uid);
195                 if (processCpuUsage != null) {
196                     processCpuUsages.add(processCpuUsage);
197                 }
198             }
199         } catch (IOException e) {
200             Slog.w(TAG, "Failed to iterate over process paths", e);
201             return null;
202         }
203 
204         if (processCpuUsages.isEmpty()) {
205             Slog.w(TAG, "Didn't successfully get any process CPU information for UIDs specified");
206             return null;
207         }
208 
209         if (DEBUG) {
210             Slog.d(TAG, "Read usage for " + processCpuUsages.size() + " processes");
211         }
212 
213         return processCpuUsages;
214     }
215 
216     /**
217      * Get the CPU frequencies that correspond to the times reported in {@link
218      * ThreadCpuUsage#usageTimesMillis}
219      */
220     @Nullable
getCpuFrequenciesKhz()221     public int[] getCpuFrequenciesKhz() {
222         return mFrequenciesKhz;
223     }
224 
225     /** Set the number of frequency buckets to use */
setNumBuckets(int numBuckets)226     void setNumBuckets(int numBuckets) {
227         if (numBuckets < 1) {
228             Slog.w(TAG, "Number of buckets must be at least 1, but was " + numBuckets);
229             return;
230         }
231         // If `numBuckets` hasn't changed since the last set, do nothing
232         if (mFrequenciesKhz != null && mFrequenciesKhz.length == numBuckets) {
233             return;
234         }
235         mFrequencyBucketCreator =
236                 new FrequencyBucketCreator(mProcTimeInStateReader.getFrequenciesKhz(), numBuckets);
237         mFrequenciesKhz =
238                 mFrequencyBucketCreator.bucketFrequencies(
239                         mProcTimeInStateReader.getFrequenciesKhz());
240     }
241 
242     /** Set the UID predicate for {@link #getProcessCpuUsage} */
setUidPredicate(Predicate<Integer> uidPredicate)243     void setUidPredicate(Predicate<Integer> uidPredicate) {
244         mUidPredicate = uidPredicate;
245     }
246 
247     /**
248      * Read all of the CPU usage statistics for each child thread of a process
249      *
250      * @param processPath the {@code /proc} path of the thread
251      * @param processId the ID of the process
252      * @param uid the ID of the user who owns the process
253      * @return process CPU usage containing usage of all child threads. Null if the process exited
254      *     and its {@code proc} directory was removed while collecting information
255      */
256     @Nullable
getProcessCpuUsage(Path processPath, int processId, int uid)257     private ProcessCpuUsage getProcessCpuUsage(Path processPath, int processId, int uid) {
258         if (DEBUG) {
259             Slog.d(
260                     TAG,
261                     "Reading CPU thread usages with directory "
262                             + processPath
263                             + " process ID "
264                             + processId
265                             + " and user ID "
266                             + uid);
267         }
268 
269         final Path allThreadsPath = processPath.resolve("task");
270         final ArrayList<ThreadCpuUsage> threadCpuUsages = new ArrayList<>();
271         try (DirectoryStream<Path> threadPaths = Files.newDirectoryStream(allThreadsPath)) {
272             for (Path threadDirectory : threadPaths) {
273                 ThreadCpuUsage threadCpuUsage = getThreadCpuUsage(threadDirectory);
274                 if (threadCpuUsage == null) {
275                     continue;
276                 }
277                 threadCpuUsages.add(threadCpuUsage);
278             }
279         } catch (IOException e) {
280             // Expected when a process finishes
281             return null;
282         }
283 
284         // If we found no threads, then the process has exited while we were reading from it
285         if (threadCpuUsages.isEmpty()) {
286             return null;
287         }
288         if (DEBUG) {
289             Slog.d(TAG, "Read CPU usage of " + threadCpuUsages.size() + " threads");
290         }
291         return new ProcessCpuUsage(processId, getProcessName(processPath), uid, threadCpuUsages);
292     }
293 
294     /**
295      * Get a thread's CPU usage
296      *
297      * @param threadDirectory the {@code /proc} directory of the thread
298      * @return thread CPU usage. Null if the thread exited and its {@code proc} directory was
299      *     removed while collecting information
300      */
301     @Nullable
getThreadCpuUsage(Path threadDirectory)302     private ThreadCpuUsage getThreadCpuUsage(Path threadDirectory) {
303         // Get the thread ID from the directory name
304         final int threadId;
305         try {
306             final String directoryName = threadDirectory.getFileName().toString();
307             threadId = Integer.parseInt(directoryName);
308         } catch (NumberFormatException e) {
309             Slog.w(TAG, "Failed to parse thread ID when iterating over /proc/*/task", e);
310             return null;
311         }
312 
313         // Get the thread name from the thread directory
314         final String threadName = getThreadName(threadDirectory);
315 
316         // Get the CPU statistics from the directory
317         final Path threadCpuStatPath = threadDirectory.resolve(CPU_STATISTICS_FILENAME);
318         final long[] cpuUsagesLong = mProcTimeInStateReader.getUsageTimesMillis(threadCpuStatPath);
319         if (cpuUsagesLong == null) {
320             return null;
321         }
322         int[] cpuUsages = mFrequencyBucketCreator.bucketValues(cpuUsagesLong);
323 
324         return new ThreadCpuUsage(threadId, threadName, cpuUsages);
325     }
326 
327     /** Get the command used to start a process */
getProcessName(Path processPath)328     private String getProcessName(Path processPath) {
329         final Path processNamePath = processPath.resolve(PROCESS_NAME_FILENAME);
330 
331         final String processName = ProcStatsUtil.readSingleLineProcFile(processNamePath.toString());
332         if (processName != null) {
333             return processName;
334         }
335         return DEFAULT_PROCESS_NAME;
336     }
337 
338     /** Get the name of a thread, given the {@code /proc} path of the thread */
getThreadName(Path threadPath)339     private String getThreadName(Path threadPath) {
340         final Path threadNamePath = threadPath.resolve(THREAD_NAME_FILENAME);
341         final String threadName = ProcStatsUtil.readNullSeparatedFile(threadNamePath.toString());
342         if (threadName == null) {
343             return DEFAULT_THREAD_NAME;
344         }
345         return threadName;
346     }
347 
348     /**
349      * Get the ID of a process from its path
350      *
351      * @param processPath {@code proc} path of the process
352      * @return the ID, {@link #ID_ERROR} if the path could not be parsed
353      */
getProcessId(Path processPath)354     private int getProcessId(Path processPath) {
355         String fileName = processPath.getFileName().toString();
356         try {
357             return Integer.parseInt(fileName);
358         } catch (NumberFormatException e) {
359             Slog.w(TAG, "Failed to parse " + fileName + " as process ID", e);
360             return ID_ERROR;
361         }
362     }
363 
364     /**
365      * Quantizes a list of N frequencies into a list of M frequencies (where M<=N)
366      *
367      * <p>In order to reduce data sent from the device, we discard precise frequency information for
368      * an approximation. This is done by putting groups of adjacent frequencies into the same
369      * bucket, and then reporting that bucket under the minimum frequency in that bucket.
370      *
371      * <p>Many devices have multiple core clusters. We do not want to report frequencies from
372      * different clusters under the same bucket, so some complication arises.
373      *
374      * <p>Buckets are allocated evenly across all core clusters, i.e. they all have the same number
375      * of buckets regardless of how many frequencies they contain. This is done to reduce code
376      * complexity, and in practice the number of frequencies doesn't vary too much between core
377      * clusters.
378      *
379      * <p>If the number of buckets is not a factor of the number of frequencies, the remainder of
380      * the frequencies are placed into the last bucket.
381      *
382      * <p>It is possible to have less buckets than asked for, so any calling code can't assume that
383      * initializing with N buckets will use return N values. This happens in two scenarios:
384      *
385      * <ul>
386      *   <li>There are less frequencies available than buckets asked for.
387      *   <li>There are less frequencies in a core cluster than buckets allocated to that core
388      *       cluster.
389      * </ul>
390      */
391     @VisibleForTesting
392     public static class FrequencyBucketCreator {
393         private final int mNumFrequencies;
394         private final int mNumBuckets;
395         private final int[] mBucketStartIndices;
396 
397         @VisibleForTesting
FrequencyBucketCreator(long[] frequencies, int targetNumBuckets)398         public FrequencyBucketCreator(long[] frequencies, int targetNumBuckets) {
399             mNumFrequencies = frequencies.length;
400             int[] clusterStartIndices = getClusterStartIndices(frequencies);
401             mBucketStartIndices =
402                     getBucketStartIndices(clusterStartIndices, targetNumBuckets, mNumFrequencies);
403             mNumBuckets = mBucketStartIndices.length;
404         }
405 
406         /**
407          * Put an array of values into buckets. This takes a {@code long[]} and returns {@code
408          * int[]} as everywhere this method is used will have to do the conversion anyway, so we
409          * save time by doing it here instead
410          *
411          * @param values the values to bucket
412          * @return the bucketed usage times
413          */
414         @VisibleForTesting
bucketValues(long[] values)415         public int[] bucketValues(long[] values) {
416             Preconditions.checkArgument(values.length == mNumFrequencies);
417             int[] buckets = new int[mNumBuckets];
418             for (int bucketIdx = 0; bucketIdx < mNumBuckets; bucketIdx++) {
419                 final int bucketStartIdx = getLowerBound(bucketIdx, mBucketStartIndices);
420                 final int bucketEndIdx =
421                         getUpperBound(bucketIdx, mBucketStartIndices, values.length);
422                 for (int valuesIdx = bucketStartIdx; valuesIdx < bucketEndIdx; valuesIdx++) {
423                     buckets[bucketIdx] += values[valuesIdx];
424                 }
425             }
426             return buckets;
427         }
428 
429         /** Get the minimum frequency in each bucket */
430         @VisibleForTesting
bucketFrequencies(long[] frequencies)431         public int[] bucketFrequencies(long[] frequencies) {
432             Preconditions.checkArgument(frequencies.length == mNumFrequencies);
433             int[] buckets = new int[mNumBuckets];
434             for (int i = 0; i < buckets.length; i++) {
435                 buckets[i] = (int) frequencies[mBucketStartIndices[i]];
436             }
437             return buckets;
438         }
439 
440         /**
441          * Get the index in frequencies where each core cluster starts
442          *
443          * <p>The frequencies for each cluster are given in ascending order, appended to each other.
444          * This means that every time there is a decrease in frequencies (instead of increase) a new
445          * cluster has started.
446          */
getClusterStartIndices(long[] frequencies)447         private static int[] getClusterStartIndices(long[] frequencies) {
448             ArrayList<Integer> indices = new ArrayList<>();
449             indices.add(0);
450             for (int i = 0; i < frequencies.length - 1; i++) {
451                 if (frequencies[i] >= frequencies[i + 1]) {
452                     indices.add(i + 1);
453                 }
454             }
455             return ArrayUtils.convertToIntArray(indices);
456         }
457 
458         /** Get the index in frequencies where each bucket starts */
getBucketStartIndices( int[] clusterStartIndices, int targetNumBuckets, int numFrequencies)459         private static int[] getBucketStartIndices(
460                 int[] clusterStartIndices, int targetNumBuckets, int numFrequencies) {
461             int numClusters = clusterStartIndices.length;
462 
463             // If we haven't got enough buckets for every cluster, we instead have one bucket per
464             // cluster, with the last bucket containing the remaining clusters
465             if (numClusters > targetNumBuckets) {
466                 return Arrays.copyOfRange(clusterStartIndices, 0, targetNumBuckets);
467             }
468 
469             ArrayList<Integer> bucketStartIndices = new ArrayList<>();
470             for (int clusterIdx = 0; clusterIdx < numClusters; clusterIdx++) {
471                 final int clusterStartIdx = getLowerBound(clusterIdx, clusterStartIndices);
472                 final int clusterEndIdx =
473                         getUpperBound(clusterIdx, clusterStartIndices, numFrequencies);
474 
475                 final int numBucketsInCluster;
476                 if (clusterIdx != numClusters - 1) {
477                     numBucketsInCluster = targetNumBuckets / numClusters;
478                 } else {
479                     // If we're in the last cluster, the bucket will contain the remainder of the
480                     // frequencies
481                     int previousBucketsInCluster = targetNumBuckets / numClusters;
482                     numBucketsInCluster =
483                             targetNumBuckets - (previousBucketsInCluster * (numClusters - 1));
484                 }
485 
486                 final int numFrequenciesInCluster = clusterEndIdx - clusterStartIdx;
487                 // If there are less frequencies than buckets in a cluster, we have one bucket per
488                 // frequency, and do not use the remaining buckets
489                 final int numFrequenciesInBucket =
490                         Math.max(1, numFrequenciesInCluster / numBucketsInCluster);
491                 for (int bucketIdx = 0; bucketIdx < numBucketsInCluster; bucketIdx++) {
492                     int bucketStartIdx = clusterStartIdx + bucketIdx * numFrequenciesInBucket;
493                     // If we've gone over the end index, ignore the rest of the buckets for this
494                     // cluster
495                     if (bucketStartIdx >= clusterEndIdx) {
496                         break;
497                     }
498                     bucketStartIndices.add(bucketStartIdx);
499                 }
500             }
501             return ArrayUtils.convertToIntArray(bucketStartIndices);
502         }
503 
getLowerBound(int index, int[] startIndices)504         private static int getLowerBound(int index, int[] startIndices) {
505             return startIndices[index];
506         }
507 
getUpperBound(int index, int[] startIndices, int max)508         private static int getUpperBound(int index, int[] startIndices, int max) {
509             if (index != startIndices.length - 1) {
510                 return startIndices[index + 1];
511             } else {
512                 return max;
513             }
514         }
515     }
516 
517     /** CPU usage of a process */
518     public static class ProcessCpuUsage {
519         public final int processId;
520         public final String processName;
521         public final int uid;
522         public ArrayList<ThreadCpuUsage> threadCpuUsages;
523 
524         @VisibleForTesting
ProcessCpuUsage( int processId, String processName, int uid, ArrayList<ThreadCpuUsage> threadCpuUsages)525         public ProcessCpuUsage(
526                 int processId,
527                 String processName,
528                 int uid,
529                 ArrayList<ThreadCpuUsage> threadCpuUsages) {
530             this.processId = processId;
531             this.processName = processName;
532             this.uid = uid;
533             this.threadCpuUsages = threadCpuUsages;
534         }
535     }
536 
537     /** CPU usage of a thread */
538     public static class ThreadCpuUsage {
539         public final int threadId;
540         public final String threadName;
541         public int[] usageTimesMillis;
542 
543         @VisibleForTesting
ThreadCpuUsage(int threadId, String threadName, int[] usageTimesMillis)544         public ThreadCpuUsage(int threadId, String threadName, int[] usageTimesMillis) {
545             this.threadId = threadId;
546             this.threadName = threadName;
547             this.usageTimesMillis = usageTimesMillis;
548         }
549     }
550 
551     /** Used to inject static methods from {@link Process} */
552     @VisibleForTesting
553     public static class Injector {
554         /** Get the UID for the process with ID {@code pid} */
getUidForPid(int pid)555         public int getUidForPid(int pid) {
556             return Process.getUidForPid(pid);
557         }
558     }
559 }
560