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