• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
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} */
247     @VisibleForTesting
setUidPredicate(Predicate<Integer> uidPredicate)248     public void setUidPredicate(Predicate<Integer> uidPredicate) {
249         mUidPredicate = uidPredicate;
250     }
251 
252     /**
253      * Read all of the CPU usage statistics for each child thread of a process
254      *
255      * @param processPath the {@code /proc} path of the thread
256      * @param processId the ID of the process
257      * @param uid the ID of the user who owns the process
258      * @return process CPU usage containing usage of all child threads. Null if the process exited
259      *     and its {@code proc} directory was removed while collecting information
260      */
261     @Nullable
getProcessCpuUsage(Path processPath, int processId, int uid)262     private ProcessCpuUsage getProcessCpuUsage(Path processPath, int processId, int uid) {
263         if (DEBUG) {
264             Slog.d(
265                     TAG,
266                     "Reading CPU thread usages with directory "
267                             + processPath
268                             + " process ID "
269                             + processId
270                             + " and user ID "
271                             + uid);
272         }
273 
274         final Path allThreadsPath = processPath.resolve("task");
275         final ArrayList<ThreadCpuUsage> threadCpuUsages = new ArrayList<>();
276         try (DirectoryStream<Path> threadPaths = Files.newDirectoryStream(allThreadsPath)) {
277             for (Path threadDirectory : threadPaths) {
278                 ThreadCpuUsage threadCpuUsage = getThreadCpuUsage(threadDirectory);
279                 if (threadCpuUsage == null) {
280                     continue;
281                 }
282                 threadCpuUsages.add(threadCpuUsage);
283             }
284         } catch (IOException | DirectoryIteratorException e) {
285             // Expected when a process finishes
286             return null;
287         }
288 
289         // If we found no threads, then the process has exited while we were reading from it
290         if (threadCpuUsages.isEmpty()) {
291             return null;
292         }
293         if (DEBUG) {
294             Slog.d(TAG, "Read CPU usage of " + threadCpuUsages.size() + " threads");
295         }
296         return new ProcessCpuUsage(processId, getProcessName(processPath), uid, threadCpuUsages);
297     }
298 
299     /**
300      * Get a thread's CPU usage
301      *
302      * @param threadDirectory the {@code /proc} directory of the thread
303      * @return thread CPU usage. Null if the thread exited and its {@code proc} directory was
304      *     removed while collecting information
305      */
306     @Nullable
getThreadCpuUsage(Path threadDirectory)307     private ThreadCpuUsage getThreadCpuUsage(Path threadDirectory) {
308         // Get the thread ID from the directory name
309         final int threadId;
310         try {
311             final String directoryName = threadDirectory.getFileName().toString();
312             threadId = Integer.parseInt(directoryName);
313         } catch (NumberFormatException e) {
314             Slog.w(TAG, "Failed to parse thread ID when iterating over /proc/*/task", e);
315             return null;
316         }
317 
318         // Get the thread name from the thread directory
319         final String threadName = getThreadName(threadDirectory);
320 
321         // Get the CPU statistics from the directory
322         final Path threadCpuStatPath = threadDirectory.resolve(CPU_STATISTICS_FILENAME);
323         final long[] cpuUsagesLong = mProcTimeInStateReader.getUsageTimesMillis(threadCpuStatPath);
324         if (cpuUsagesLong == null) {
325             return null;
326         }
327         final int[] cpuUsages;
328         if (mFrequencyBucketCreator != null) {
329             cpuUsages = mFrequencyBucketCreator.bucketValues(cpuUsagesLong);
330         } else {
331             cpuUsages = new int[cpuUsagesLong.length];
332             for (int i = 0; i < cpuUsagesLong.length; i++) {
333                 cpuUsages[i] = (int) cpuUsagesLong[i];
334             }
335         }
336         return new ThreadCpuUsage(threadId, threadName, cpuUsages);
337     }
338 
339     /** Get the command used to start a process */
getProcessName(Path processPath)340     private String getProcessName(Path processPath) {
341         final Path processNamePath = processPath.resolve(PROCESS_NAME_FILENAME);
342 
343         final String processName = ProcStatsUtil.readSingleLineProcFile(processNamePath.toString());
344         if (processName != null) {
345             return processName;
346         }
347         return DEFAULT_PROCESS_NAME;
348     }
349 
350     /** Get the name of a thread, given the {@code /proc} path of the thread */
getThreadName(Path threadPath)351     private String getThreadName(Path threadPath) {
352         final Path threadNamePath = threadPath.resolve(THREAD_NAME_FILENAME);
353         final String threadName = ProcStatsUtil.readNullSeparatedFile(threadNamePath.toString());
354         if (threadName == null) {
355             return DEFAULT_THREAD_NAME;
356         }
357         return threadName;
358     }
359 
360     /**
361      * Get the ID of a process from its path
362      *
363      * @param processPath {@code proc} path of the process
364      * @return the ID, {@link #ID_ERROR} if the path could not be parsed
365      */
getProcessId(Path processPath)366     private int getProcessId(Path processPath) {
367         String fileName = processPath.getFileName().toString();
368         try {
369             return Integer.parseInt(fileName);
370         } catch (NumberFormatException e) {
371             Slog.w(TAG, "Failed to parse " + fileName + " as process ID", e);
372             return ID_ERROR;
373         }
374     }
375 
376     /**
377      * Quantizes a list of N frequencies into a list of M frequencies (where M<=N)
378      *
379      * <p>In order to reduce data sent from the device, we discard precise frequency information for
380      * an approximation. This is done by putting groups of adjacent frequencies into the same
381      * bucket, and then reporting that bucket under the minimum frequency in that bucket.
382      *
383      * <p>Many devices have multiple core clusters. We do not want to report frequencies from
384      * different clusters under the same bucket, so some complication arises.
385      *
386      * <p>Buckets are allocated evenly across all core clusters, i.e. they all have the same number
387      * of buckets regardless of how many frequencies they contain. This is done to reduce code
388      * complexity, and in practice the number of frequencies doesn't vary too much between core
389      * clusters.
390      *
391      * <p>If the number of buckets is not a factor of the number of frequencies, the remainder of
392      * the frequencies are placed into the last bucket.
393      *
394      * <p>It is possible to have less buckets than asked for, so any calling code can't assume that
395      * initializing with N buckets will use return N values. This happens in two scenarios:
396      *
397      * <ul>
398      *   <li>There are less frequencies available than buckets asked for.
399      *   <li>There are less frequencies in a core cluster than buckets allocated to that core
400      *       cluster.
401      * </ul>
402      */
403     @VisibleForTesting
404     public static class FrequencyBucketCreator {
405         private final int mNumFrequencies;
406         private final int mNumBuckets;
407         private final int[] mBucketStartIndices;
408 
409         @VisibleForTesting
FrequencyBucketCreator(long[] frequencies, int targetNumBuckets)410         public FrequencyBucketCreator(long[] frequencies, int targetNumBuckets) {
411             mNumFrequencies = frequencies.length;
412             int[] clusterStartIndices = getClusterStartIndices(frequencies);
413             mBucketStartIndices =
414                     getBucketStartIndices(clusterStartIndices, targetNumBuckets, mNumFrequencies);
415             mNumBuckets = mBucketStartIndices.length;
416         }
417 
418         /**
419          * Put an array of values into buckets. This takes a {@code long[]} and returns {@code
420          * int[]} as everywhere this method is used will have to do the conversion anyway, so we
421          * save time by doing it here instead
422          *
423          * @param values the values to bucket
424          * @return the bucketed usage times
425          */
426         @VisibleForTesting
bucketValues(long[] values)427         public int[] bucketValues(long[] values) {
428             Preconditions.checkArgument(values.length == mNumFrequencies);
429             int[] buckets = new int[mNumBuckets];
430             for (int bucketIdx = 0; bucketIdx < mNumBuckets; bucketIdx++) {
431                 final int bucketStartIdx = getLowerBound(bucketIdx, mBucketStartIndices);
432                 final int bucketEndIdx =
433                         getUpperBound(bucketIdx, mBucketStartIndices, values.length);
434                 for (int valuesIdx = bucketStartIdx; valuesIdx < bucketEndIdx; valuesIdx++) {
435                     buckets[bucketIdx] += values[valuesIdx];
436                 }
437             }
438             return buckets;
439         }
440 
441         /** Get the minimum frequency in each bucket */
442         @VisibleForTesting
bucketFrequencies(long[] frequencies)443         public int[] bucketFrequencies(long[] frequencies) {
444             Preconditions.checkArgument(frequencies.length == mNumFrequencies);
445             int[] buckets = new int[mNumBuckets];
446             for (int i = 0; i < buckets.length; i++) {
447                 buckets[i] = (int) frequencies[mBucketStartIndices[i]];
448             }
449             return buckets;
450         }
451 
452         /**
453          * Get the index in frequencies where each core cluster starts
454          *
455          * <p>The frequencies for each cluster are given in ascending order, appended to each other.
456          * This means that every time there is a decrease in frequencies (instead of increase) a new
457          * cluster has started.
458          */
getClusterStartIndices(long[] frequencies)459         private static int[] getClusterStartIndices(long[] frequencies) {
460             IntArray indices = new IntArray();
461             indices.add(0);
462             for (int i = 0; i < frequencies.length - 1; i++) {
463                 if (frequencies[i] >= frequencies[i + 1]) {
464                     indices.add(i + 1);
465                 }
466             }
467             return indices.toArray();
468         }
469 
470         /** Get the index in frequencies where each bucket starts */
getBucketStartIndices( int[] clusterStartIndices, int targetNumBuckets, int numFrequencies)471         private static int[] getBucketStartIndices(
472                 int[] clusterStartIndices, int targetNumBuckets, int numFrequencies) {
473             int numClusters = clusterStartIndices.length;
474 
475             // If we haven't got enough buckets for every cluster, we instead have one bucket per
476             // cluster, with the last bucket containing the remaining clusters
477             if (numClusters > targetNumBuckets) {
478                 return Arrays.copyOfRange(clusterStartIndices, 0, targetNumBuckets);
479             }
480 
481             IntArray bucketStartIndices = new IntArray();
482             for (int clusterIdx = 0; clusterIdx < numClusters; clusterIdx++) {
483                 final int clusterStartIdx = getLowerBound(clusterIdx, clusterStartIndices);
484                 final int clusterEndIdx =
485                         getUpperBound(clusterIdx, clusterStartIndices, numFrequencies);
486 
487                 final int numBucketsInCluster;
488                 if (clusterIdx != numClusters - 1) {
489                     numBucketsInCluster = targetNumBuckets / numClusters;
490                 } else {
491                     // If we're in the last cluster, the bucket will contain the remainder of the
492                     // frequencies
493                     int previousBucketsInCluster = targetNumBuckets / numClusters;
494                     numBucketsInCluster =
495                             targetNumBuckets - (previousBucketsInCluster * (numClusters - 1));
496                 }
497 
498                 final int numFrequenciesInCluster = clusterEndIdx - clusterStartIdx;
499                 // If there are less frequencies than buckets in a cluster, we have one bucket per
500                 // frequency, and do not use the remaining buckets
501                 final int numFrequenciesInBucket =
502                         Math.max(1, numFrequenciesInCluster / numBucketsInCluster);
503                 for (int bucketIdx = 0; bucketIdx < numBucketsInCluster; bucketIdx++) {
504                     int bucketStartIdx = clusterStartIdx + bucketIdx * numFrequenciesInBucket;
505                     // If we've gone over the end index, ignore the rest of the buckets for this
506                     // cluster
507                     if (bucketStartIdx >= clusterEndIdx) {
508                         break;
509                     }
510                     bucketStartIndices.add(bucketStartIdx);
511                 }
512             }
513             return bucketStartIndices.toArray();
514         }
515 
getLowerBound(int index, int[] startIndices)516         private static int getLowerBound(int index, int[] startIndices) {
517             return startIndices[index];
518         }
519 
getUpperBound(int index, int[] startIndices, int max)520         private static int getUpperBound(int index, int[] startIndices, int max) {
521             if (index != startIndices.length - 1) {
522                 return startIndices[index + 1];
523             } else {
524                 return max;
525             }
526         }
527     }
528 
529     /** CPU usage of a process */
530     public static class ProcessCpuUsage {
531         public final int processId;
532         public final String processName;
533         public final int uid;
534         public ArrayList<ThreadCpuUsage> threadCpuUsages;
535 
536         @VisibleForTesting
ProcessCpuUsage( int processId, String processName, int uid, ArrayList<ThreadCpuUsage> threadCpuUsages)537         public ProcessCpuUsage(
538                 int processId,
539                 String processName,
540                 int uid,
541                 ArrayList<ThreadCpuUsage> threadCpuUsages) {
542             this.processId = processId;
543             this.processName = processName;
544             this.uid = uid;
545             this.threadCpuUsages = threadCpuUsages;
546         }
547     }
548 
549     /** CPU usage of a thread */
550     public static class ThreadCpuUsage {
551         public final int threadId;
552         public final String threadName;
553         public int[] usageTimesMillis;
554 
555         @VisibleForTesting
ThreadCpuUsage(int threadId, String threadName, int[] usageTimesMillis)556         public ThreadCpuUsage(int threadId, String threadName, int[] usageTimesMillis) {
557             this.threadId = threadId;
558             this.threadName = threadName;
559             this.usageTimesMillis = usageTimesMillis;
560         }
561     }
562 
563     /** Used to inject static methods from {@link Process} */
564     @VisibleForTesting
565     public static class Injector {
566         /** Get the UID for the process with ID {@code pid} */
getUidForPid(int pid)567         public int getUidForPid(int pid) {
568             return Process.getUidForPid(pid);
569         }
570     }
571 }
572